Teknisk artikel

Digitale PDF-signaturer og PAdES i Delphi med HotPDF

En PDF-signatur handler mest om byte-regnskab, og det er der, det går galt. Kryptografien kører på kode, der har været revideret i to årtier, og den del fejler næsten aldrig. Det, der fejler i produktion, er mere jordnært: en placeholder reserveret for lille til den rigtige signatur, en hash taget over det forkerte stykke af filen, eller en "gem", der efter signering stille omskrev bytes, som signaturen allerede havde frosset. Læg bytene korrekt, og det grønne flueben klarer sig selv

HotPDF dækker signering for Delphi og C++Builder på tre niveauer, og man vælger mellem dem ved at besvare ét spørgsmål: hvor bor den private nøgle? En PFX-fil på disk kræver et enkelt funktionskald. En nøgle låst inde i en HSM eller en fjern signeringstjeneste kræver reserve-hash-indsæt-sekvensen, fordi intet bibliotek kan række ind i et token og trække nøglen ud. En signatur, der skal opfylde europæisk regulering, kræver PAdES-baseline-strukturerne oven i det. Afsnittene nedenfor følger den progression

Beslutningsdiagram, der vælger mellem HotPDF PFX-signering i ét kald, reserve-hash-insert-stien, når nøglen ligger i en HSM eller remote service, og PAdES baseline-strukturer til reguleret europæisk signering
Vælg signeringsniveauet ved at spørge, hvor den private nøgle bor; en læsbar PFX-fil reducerer signeringen til ét kald, mens token-heldige nøgler tvinger omvejen på byte-niveau, og europæisk regulering tilføjer PAdES-laget

Hvordan /ByteRange fastlåser de signerede bytes

En signatur skal bo inde i den fil, den signerer, og den kan ikke signere sig selv. PDF omgår paradokset ved at efterlade et hul. Før signering reserverer skriveren en /Contents-post med fast størrelse fyldt med nuller og registrerer et /ByteRange-array for de to strækninger på hver side af det: alt før hullet, alt efter. Signeringen hasher de to strækninger og skriver den resulterende CMS-blob ind i hullet som hexadecimal. Fælden ligger i ordet fast. Man forpligter sig til hullets størrelse, før man ved, hvor stor den færdige signatur bliver, så reservationen skal være et selvsikkert overestimat. Otte kilobyte rummer bekvemt en frakoblet (detached) CMS-signatur med en kort certifikatkæde

HotPDF opdeler de to tilfælde i to kald, og at forveksle dem er en almindelig begynderfejl. AddSignatureField lægger et tomt, synligt felt til, som en person kan signere senere i en viser. AddSignedSignatureField opretter feltet og reserverer /Contents-hullet, hvilket er det, man vil have, når kode, snarere end et menneske, skal fuldføre signaturen. Giver man en ekstern signeringstjeneste et tomt felt, har den intet at udfylde

Den ét-kalds-vej: signering fra en PFX

Når certifikatet og dets private nøgle ligger i en PFX/PKCS#12-fil, som din proces kan læse, reduceres hele pipelinen til en klassefunktion:

if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
    'company-cert.pfx', 'pfx-password') then
  Writeln('Signed: invoice-signed.pdf')
else
  raise Exception.Create('PFX signing failed');

Når dette fejler, er PDF'en sjældent problemet. Det er PFX'en. HotPDF læser containere beskyttet med PBES2, det vil sige PBKDF2-nøgleudledning over AES-256-CBC. En PFX eksporteret af en ældre Windows-certifikatguide, eller af OpenSSL før 3.0, er som regel i stedet pakket ind i forældet RC2 eller 3DES, og den vil ganske enkelt ikke kunne parses. Løsningen er at genexportere containeren én gang med moderne beskyttelse; nutidens OpenSSL gør dette som standard, og det er ikke en kodeændring. Så når signeringen dør øjeblikkeligt på et certifikat, der "virker alle andre steder", så se på, hvordan PFX'en blev oprettet, før du mistænker din egen kode

Reserve-hash-indsæt-vejen for HSM'er og tokens

Ét-kalds-vejen forudsætter, at din proces kan læse nøglen som en fil. I stigende grad kan den ikke. Nøglen bor i en HSM, på et USB-token eller bag en signeringstjenestes API, og der er ingen måde, et bibliotek kan nå den direkte på. HotPDF håndterer det ved at opdele signeringen i byte-niveau-trin: skriv et placeholder-dokument, spørg biblioteket om hash-områderne, send hash-inputtet til hvad end der har nøglen, og splejs derefter den returnerede CMS tilbage ind i hullet

HotPDF: Firetrins reserve-hash-insert-pipeline over placeholder.pdf, der viser det reserverede /Contents-hul mellem de to ByteRange-intervaller og en HSM, der bytter digesten til CMS hex
HotPDF reserverer hullet og rapporterer begge ByteRange-span, din nøgleholder signerer dem udenfor, og den returnerede CMS skjoses tilbage byte for byte uden at røre én frossen byte
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Skriv dokumentet med et reserveret /Contents-hul
  Doc := THotPDF.Create(nil);
  try
    Doc.FileName := 'placeholder.pdf';
    Doc.BeginDoc;
    Doc.CurrentPage.AddSignedSignatureField('Sig1',
      Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
      'Contract approval', 'Boston, MA', 'legal@example.com');
    Doc.EndDoc;
  finally
    Doc.Free;
  end;

  // 2. Indlæs de gemte bytes; de returnerede offsets er 0-baserede
  Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
  try
    SetLength(PdfBytes, Fs.Size);
    Fs.ReadBuffer(PdfBytes[1], Fs.Size);
  finally
    Fs.Free;
  end;
  THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
    CStart, CLen);

  // 3. Hash begge strækninger, og signér eksternt (HSM, token, tjeneste)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // din integration: returnerer CMS som hex

  // 4. Splejs signaturen ind i det reserverede hul
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

To detaljer i denne sekvens forårsager de fleste af de intermitterende fejl. Den første er, at PreparePDFForSigning arbejder på bytene i en færdig fil. Placeholderen skal være skrevet og gemt i sin helhed, før offsettene betyder noget; beregner man dem mod en stream, der stadig samles, vil de ikke stemme overens med de bytes, man i sidste ende hasher. Den anden er reservationsstørrelsen, igen. De 8192 bytes, man bad om, skal rumme den endelige CMS, og en signatur, der bærer mellemliggende certifikater, eller en, som en tjeneste udsmykker med signerede attributter, kan overskride den. InsertSignatureHex vil ikke udvide hullet for at skabe plads. Kendetegnet er en pipeline, der signerer fint med ét certifikat og fejler med det næste; kuren er at gendanne placeholderen med en reservation målt fra en rigtig signatur produceret af den faktiske signeringstjeneste, ikke gættet

PAdES-baselines, og de tidsstempler, der holder en signatur i live

Hvis man signerer under europæiske regler, er standarden i spil ETSI EN 319 142-1, som stabler fire PAdES-baseline-niveauer. B-B er den rene signatur. B-T tilføjer et betroet tidsstempel, der beviser, hvornår den blev lavet. B-LT indlejrer valideringsmaterialet, certifikaterne og tilbagekaldelsesdataene, inde i dokumentet, så det stadig kan tjekkes år senere. B-LTA lægger periodiske dokumenttidsstempler oven på det, så beviset overlever de algoritmer, det blev bygget på. HotPDF udsender dokumentsidens strukturer for hvert niveau:

HotPDF: Stakkede PAdES baseline-niveauer fra B-B gennem B-T og B-LT til B-LTA, med en fornyelsestidslinje, der viser periodiske dokumenttidsstempler holde en signatur verificerbar årtier senere
Hvert niveau stabler ny beskyttelse oven på det forrige; B-LTA vedbliver med at genanvende dokumenttidsstempler, så beviset overlever de algoritmer, det først blev bygget på
// PAdES-baseline-signaturfelt (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Dokumenttidsstempel: større reservation til TSA-tokenet og kæden
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Reservationen på 16384 byte til tidsstemplet er bevidst. En tidsstempelmyndighed returnerer et token, der trækker sin egen certifikatkæde med sig, så den rutinemæssigt har brug for mere plads end de 8 KB, en almindelig signatur er tilfreds med. De dokumenttidsstempler er også maskineriet bag B-LTA: at gentidsstemple en arkiveret signatur hvert par år, med algoritmer, der stadig er aktuelle, er det, der holder et dokument, du signerede i 2026, verificerbart i 2040

Et par ord om de reason-, location- og contact-strenge, som begge feltkald accepterer: de er bekvemmelighedsmetadata og intet mere. HotPDF gemmer dem som almindelige dictionary-poster og maler dem ind i det synlige signaturudseende, men ingen validator tjekker dem mod noget som helst. Udfyld dem konsekvent fra dine workflow-data, da revisorer rent faktisk læser dem, og tag dem så aldrig fejl for bevis. Den egentlige kryptografiske påstand bor udelukkende i CMS'en og dens certifikatkæde, og en verificering ignorerer den synlige tekst fuldstændigt

Efter signering må filen kun vokse

I det øjeblik en signatur eksisterer, er bytene inden for dens områder frosset. Den eneste legitime måde at ændre filen på bagefter er en ISO 32000-1 §7.5.6-inkrementel opdatering, som tilføjer nye og ændrede objekter efter de oprindelige bytes og kæder et frisk cross-reference-afsnit tilbage til dem. Gjort på den måde forbliver signaturen gyldig for sin revision, og en viser rapporterer den ærlige tilstand: den signerede revision er intakt, dokumentet blev udvidet bagefter. Reserialiserer man i stedet hele filen, omskriver man de signerede strækninger, hvilket ødelægger signaturen, selv når intet synligt ændrede sig. Den samme revisionsmekanisme er også, hvordan ét dokument bærer flere signaturer: hver ny signatur havner i sin egen inkrementelle opdatering, og dens områder dækker alt forud for den, inklusive de tidligere signaturer. Append-only-mekanikken, og hvornår det er sikkert at komprimere dem, dækkes i artiklen om objektstrømme og inkrementelle opdateringer

To grænser er værd at holde sig for øje, mens man designer. HotPDF's PDF/A-outputtilstand afviser signaturfelter fuldstændigt, så arkivkonformitet og en indlejret signatur må leveres som separate filer. Og signering siger intet om hemmeligholdelse: den beviser, hvem der har produceret et dokument, og at det ikke er ændret siden, men alle kan stadig læse det. At skjule indholdet er en separat opgave, håndteret af AES-256-kryptering og tilladelsespolitik

Hvad end du bygger, så test det med noget andet end den kode, der skrev filen. Åbn outputtet i Acrobats signaturpanel, og bekræft tre ting: signaturen er gyldig, identiteten kæder til den rod, du forventede, og panelet rapporterer ingen ændringer siden signeringen. Vend derefter en enkelt byte inde i det signerede område på en engangskopi, og bekræft, at panelet nu kalder dokumentet ændret. En signeringspipeline, man aldrig har set afvise en manipuleret fil, er én, hvis verifikation ikke rigtig er blevet testet

Alle tre signeringsniveauer leveres med HotPDF Delphi Component til Delphi og C++Builder; produktsiden linker til den fulde signatur-API-reference