Teknisk artikel

Signer PDF-filer med PAdES B-B i Delphi ved hjælp af PDFium

PDFium-komponenten signerer en PDF med en digital PAdES B-B-signatur via sin SignPades-metode: Den indlæser dokumentet, hasher det signerede byte-område, opbygger en CAdES CMS-struktur og tilføjer signaturen som en trinvis opdatering (incremental update). Den kryptografiske backend er kun til Windows, så beskyt ethvert kald med PadesCryptoAvailable, før du signerer

Situationen er velkendt. En kontrakt-PDF lander på dit skrivebord, den juridiske afdeling vil have den digitalt signeret, før den sendes ud, og du rækker ud efter det samme PDFium-build, du allerede bruger til at rendere og inspicere dokumenter, blot for at opdage, at PDFium slet ikke kan skrive en signatur. Dets signatur-API er strengt skrivebeskyttet. PDFium-komponenten lukker dette hul by at eje hele signerings-pipelinen i Pascal, fra hash-funktionen helt op til injektionen på byte-niveau, og denne artikel gennemgår pipelinen fra start til slut

Hvorfor kan PDFium ikke skrive en digital signatur?

PDFium eksponerer signaturer som skrivebeskyttede objekter og tilbyder intet, der kan oprette en. FPDFSignatureObj_*-familien lader dig gennemgå en eksisterende signatur, læse dens /Contents og inspicere dens /ByteRange, men der er intet modstykke, der opbygger en signatur-ordbog, reserverer en /Contents-plads eller skriver et byte-område. Trinvis lagring eksisterer (FPDF_SaveAsCopy med FPDF_INCREMENTAL), men bærer ingen signerings-krog (hook). Enhver komponent, der signerer en PDF oven på PDFium, skal derfor selv generere hver eneste signatur-byte, og det er grunden til, at PDFium-komponenten bygger maskineriet af tre rene Pascal-units. FPC 3.2.2 leveres med md5 og sha1, men slet ingen SHA-2, og Delphis System.Hash SHA-256-API er ikke kildekompatibel med FPC, så FPdfSha256 er en selvstændig FIPS 180-4-implementering, der holder enhver CMS-kodesti på en enkelt TSHA256Digest-type uden compiler-forgreninger. FPdfAsn1 leverer den DER-koder og -læser, som CMS-strukturerne har brug for, og FPdfCms samler CAdES SignedData oven på begge

Hvordan signerer du en PDF digitalt i Delphi?

Indlæs dokumentet, og kald derefter SignPades med et certifikats fingeraftryk (thumbprint). PDFium-komponenten slår dette fingeraftryk op i den aktuelle brugers "MY"-certifikatlager, henter det matchende certifikat og dets private nøgle og skriver en signeret kopi til den sti, du angiver

uses
  PDFium, FPdfCrypto;

procedure SignContract(const AThumbprint: string);
var
  Pdf: TPdf;
begin
  if not PadesCryptoAvailable then
    raise Exception.Create('PAdES signing requires the Windows CNG backend');

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';   // the document to sign
    Pdf.Active := True;
    // Second argument: SHA-1 thumbprint of a certificate in the Current
    // User "MY" store. First argument: destination for the signed copy.
    if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

PadesCryptoAvailable er den kontrol, du tjekker først, hver gang. På Windows returnerer den True, og crypt32/ncrypt-backend'en er aktiv; på alle andre platforme returnerer den False, og et signeringskald vil udløser EPadesCrypto. Ved at behandle denne sikring som obligatorisk forhindres et Linux- eller macOS-build i at fejle under kørsel på en sti, der ikke kan fungere der. Selve fingeraftrykket er SHA-1-hashen for certifikatet, den samme værdi som Windows-certifikathåndteringen viser på fanen Detaljer, og det identificerer en specifik signatør uden nogensinde at placere nøgle-materiale i din kildekode

Hvad der indgår i CMS: Signerede attributter og RFC 5652

En PAdES-basissignatur er ikke en rå RSA-signatur over filen; det er en CAdES CMS SignedData-struktur, der bærer et påkrævet sæt af signerede attributter, og FPdfCms.BuildSignedData udsender præcis dette sæt: content-type, message-digest og signing-certificate-v2, som er ESS-attributten, der binder signaturen til signatørens certifikat ved hjælp af hash. Én detalje her spænder ben for næsten alle hjemmelavede CMS-implementeringer. RFC 5652 §5.4 kræver, at de signerede attributters digest beregnes over DER SET OF-kodningen, tag 0x31, mens de samme attributter transporteres inde i SignerInfo under tags som IMPLICIT [0], 0xA0. PDFium-komponenten koder attributsættet én gang, beregner digest for 0x31-formen og omskriver derefter kun det første tag-byte to 0xA0 før afsendelse, så én enkelt buffer udfylder begge roller uden endnu en gennemgang af træet

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;

    Opts := TPadesSignOptions.Default;
    Opts.CertificateThumbprint := 'a1b2c3d4e5f6...';  // signer in the MY store
    Opts.Reason := 'I approve this agreement';
    Opts.Location := 'Berlin, DE';
    Opts.ContentsSize := 16384;                        // hex width of /Contents

    if not Pdf.SignPades('contract-signed.pdf', Opts) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

Indstillings-overloadet (options) tilføjer de metadata for signaturordbogen, som ISO 32000-1 §12.8.1 definerer: Reason, Location, ContactInfo og Name, som alle er valgfrie og skrives ind i signaturværdiens ordbog. Én begrænsning er nem at falde over. Hvis du indstiller CommitmentTypeOid for at tilføje den signerede CAdES-attribut commitment-type-indication, må du ikke også angive Reason; ETSI EN 319 142-1 §6.3 forbyder at medtage begge, fordi de to udtrykker samme hensigt på forskellige måder

Hvordan hænger ByteRange og /Contents-pladsen sammen?

En signatur skal dække hele filen undtagen de bytes, der indeholder selve signaturen, og PAdES løser denne cirkulære afhængighed med en pladsholder med fast bredde, som SignPadesBytes styrer præcist. Den reserverer en hex-streng i /ContentsContentsSize bytes (16384 som standard, hvilket er rigeligt større end en typisk CMS SignedData), serialiserer den trinvise opdatering for at finde pladsens nøjagtige offset og beregner derefter /ByteRange som to spænd, der indrammer pladsen: Alt før hex-strengens startafgrænsning og alt efter dens slutafgrænsning. SHA-256 beregnes kun over disse to spænd. Det færdige CMS hex-kodes ind i den reserverede plads, udfyldes med nuller (zero-padding) til den faste bredde, og krydsreference-opdateringen tilføjes. Da bredden er fastlagt på forhånd, forskyder udfyldningen af pladsen ingen efterfølgende byte, hvilket er hele årsagen til, at byte-området forbliver gyldigt. De oprindelige dokument-bytes bevares uændret, så en tidligere signatur på samme fil overlever intakt, præcis som trinvise signeringer i ISO 32000-1 §12.8.1 kræver

Windows CNG-backend'en og dens grænser

PDFium-komponenten signerer kun på Windows, og denne afgrænsning er tilsigtet. FPdfCryptoWin binder crypt32.dll og ncrypt.dll dynamisk uden at tilføje DLL-afhængigheder under kompileringen, og signeringskæden er standard CNG: Åbn MY-lageret, find certifikatet ud fra dets hash, hent dets private nøglehåndtag via CryptAcquireCertificatePrivateKey og kald NCryptSignHash. RSA med PKCS#1 v1.5, RSA-PSS og ECDSA understøttes alle. ECDSA kræver én rettelse, som de andre ikke gør, fordi NCryptSignHash returnerer det rå IEEE P1363 r-og-s-par, mens CMS forventer en DER ECDSA-Sig-Value SEQUENCE, så backend'en koder det igen i henhold til RFC 5480

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
  Output: TFileStream;
begin
  if not PadesCryptoAvailable then
    Exit;   // no signing backend on this platform

  Opts := TPadesSignOptions.Default;
  Opts.CertificateThumbprint := ReadThumbprintFromConfig;

  Pdf := TPdf.Create(nil);
  Output := TFileStream.Create('contract-signed.pdf', fmCreate);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.SignPadesToStream(Output, Opts);
  finally
    Output.Free;
    Pdf.Free;
  end;
end;

Den praktiske konsekvens er, at den private nøgle skal ligge i Windows-certifikatlageret. Et certifikat i en PFX-fil fungerer kun efter, at du har importeret det til den aktuelle brugers lager, hvorefter dets fingeraftryk er den værdi, du sender til SignPades. Denne udgivelse har ingen PKCS#11- eller HSM-sti og ingen software-nøglefil-backend, så når PadesCryptoAvailable returnerer False, er der simpelthen ingen signeringsmulighed på den pågældende maskine

Hvor PAdES B-B stopper

PAdES B-B er basisniveauet, bunden af de fire PAdES-niveauer: Det beviser, hvem der signerede, og at bytes ikke er blevet ændret siden, og intet ud over det. En B-B-signatur bærer ikke et pålideligt tidsstempel, så den kan ikke bevise, hvornår signeringen fandt sted, og den indlejrer ingen tilbagekaldelsesdata, så en kontrollant skal år senere selv hente certifikatkæden og dens status. Disse huller er præcis det, de højere niveauer lukker. Når du har brug for et signerings-tidspunkt, som en auditor vil acceptere, flytter tilføjelse af et RFC 3161-tidsstempel og DSS til langsigtet validering signaturen til B-T og derover. Når du vil læse en færdig signatur og bekræfte, hvilket niveau den nåede, er inspektion af en PDF-signatur og dens PAdES-niveau ledsagerværktøjet, og før du signerer noget som helst, fortæller revision af en PDF for sikkerhedsrisici dig, hvad du er ved at skrive under på

De SignPades-metoder, der vises her, leveres med PDFium-komponenten til Delphi og C++Builder sammen med den skrivebeskyttede signaturinspektion, som PDFium tilbyder ud af boksen