Teknisk artikkel

Digitale PDF-signaturer og PAdES i Delphi med HotPDF

En PDF-signatur er for det meste byteregnskap, og det er der det går galt. Kryptografien kjører på kode som har blitt revidert i to tiår, og den delen svikter nesten aldri. Det som svikter i produksjon, er mer beskjedent: en plassholder reservert for liten til den virkelige signaturen, en hash tatt over feil strekning av filen, eller en «lagring» etter signering som stille skrev om byte signaturen allerede hadde frosset. Legg bytene riktig ut, så tar det grønne haketegnet seg av seg selv

HotPDF dekker signering for Delphi og C++Builder på tre nivåer, og du velger mellom dem ved å svare på ett spørsmål: hvor bor den private nøkkelen? En PFX-fil på disk trenger bare ett enkelt funksjonskall. En nøkkel låst i en HSM eller en ekstern signeringstjeneste trenger reserve-hash-insert-sekvensen, fordi intet bibliotek kan rekke inn i en token og hente ut nøkkelen. En signatur som må tilfredsstille europeisk regelverk, trenger PAdES-baselinestrukturene i tillegg til dette. Avsnittene nedenfor følger denne progresjonen

Beslutningsdiagram som velger mellom HotPDF PFX ét-kalls signering, reserve-hash-insert-banen når nøkkelen sitter i en HSM eller fjern tjeneste, og PAdES-basisstrukturer for regulert europeisk signering
Velg signeringsnivået ved å spørre hvor den private nøkkelen bor; en lesbar PFX-fil kollapser signering til ett enkelt kall, mens tokenholdte nøkler tvinger frem bytenivåomveien, og europeisk regulering legger til PAdES-laget

Hvordan /ByteRange fastlåser de signerte bytene

En signatur må bo inne i filen den signerer, og den kan ikke signere seg selv. PDF omgår paradokset ved å etterlate et hull. Før signering reserverer skriveren en /Contents-oppføring med fast størrelse, full av nuller, og registrerer et /ByteRange-array for de to strekningene på hver side av det: alt før hullet, alt etter. Signereren hasher disse to strekningene og skriver den resulterende CMS-blobben inn i hullet som heksadesimal. Fellen ligger i ordet fast. Du forplikter deg til størrelsen på det hullet før du vet hvor stor den ferdige signaturen blir, så reservasjonen må være et selvsikkert overslag i overkant. Åtte kilobyte holder komfortabelt en frittstående CMS-signatur med en kort sertifikatkjede

HotPDF deler de to tilfellene i to kall, og å blande dem sammen er en vanlig tidlig feil. AddSignatureField legger inn et tomt, synlig felt som et menneske kan signere senere i en viser. AddSignedSignatureField oppretter feltet og reserverer /Contents-hullet, som er den du vil ha når kode, ikke et menneske, skal fullføre signaturen. Gir du en ekstern signerer et tomt felt, har den ingenting å fylle ut

Ett-kall-stien: signering fra en PFX

Når sertifikatet og dets private nøkkel ligger i en PFX/PKCS#12-fil prosessen din kan lese, reduseres hele pipelinen til én klassefunksjon:

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 feiler, er det sjelden PDF-en som er problemet. Det er PFX-en. HotPDF leser containere beskyttet med PBES2, det vil si PBKDF2-nøkkelutledning over AES-256-CBC. En PFX eksportert av en eldre Windows-sertifikatveiviser, eller av OpenSSL før 3.0, er som regel pakket inn i gammeldags RC2 eller 3DES i stedet, og den vil rett og slett ikke la seg parse. Løsningen er å re-eksportere containeren én gang med moderne beskyttelse; dagens OpenSSL gjør dette som standard, og det er ingen kodeendring. Så når signeringen dør momentant på et sertifikat som «fungerer overalt ellers», bør du se på hvordan PFX-en ble opprettet, før du mistenker din egen kode

Reserve-hash-insert-stien for HSM-er og tokener

Ett-kall-stien forutsetter at prosessen din kan lese nøkkelen som en fil. Stadig oftere kan den ikke det. Nøkkelen sitter i en HSM, på en USB-token, eller bak et API for en signeringstjeneste, og det finnes ingen måte for et bibliotek å nå den direkte. HotPDF håndterer dette ved å bryte signeringen opp i byte-nivå-trinn: skriv et plassholderdokument, be biblioteket om hash-områdene, send hash-inndataen videre til det som holder nøkkelen, og sett så den returnerte CMS-en inn igjen i hullet

HotPDF: Firetrinns reserve-hash-insert-rørledning over placeholder.pdf som viser det reserverte /Contents-hullet mellom de to ByteRange-spennene og en HSM som bytter digesten mot CMS-heks
HotPDF reserverer hullet og rapporterer begge ByteRange-spennene, nøkkelholderen din signerer dem utenpå, og den returnerte CMS-en settes inn igjen byte for byte uten å røre en fastfrossen byte
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Skriv dokumentet med et reservert /Contents-hull
  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. Last inn de lagrede bytene; de returnerte offsetene er 0-baserte
  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 strekningene og signer eksternt (HSM, token, tjeneste)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // din integrasjon: returnerer CMS som hex

  // 4. Sett signaturen inn i det reserverte hullet
  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 sekvensen forårsaker de fleste av de sporadiske feilene. Den første er at PreparePDFForSigning arbeider på bytene til en ferdig fil. Plassholderen må være skrevet og lagret i sin helhet før offsetene betyr noe; beregner du dem mot en strøm som fortsatt bygges opp, vil de ikke stemme overens med bytene du til slutt hasher. Den andre er reservasjonsstørrelsen, igjen. De 8192 bytene du ba om, må romme den endelige CMS-en, og en signatur som bærer med seg mellomliggende sertifikater, eller en som en tjeneste dekorerer med signerte attributter, kan sprenge den. InsertSignatureHex vil ikke utvide hullet for å gi plass. Tegnet på dette er en pipeline som signerer fint med ett sertifikat og feiler med det neste; kuren er å regenerere plassholderen med en reservasjon målt fra en ekte signatur produsert av den faktiske signereren, ikke gjettet

PAdES-baselinjer, og tidsstemplene som holder en signatur i live

Hvis du signerer under europeisk regelverk, er standarden i spill ETSI EN 319 142-1, som stabler fire PAdES-baselinenivåer. B-B er den enkle signaturen. B-T legger til et pålitelig tidsstempel som beviser når den ble laget. B-LT innebygger valideringsmaterialet, sertifikatene og tilbakekallingsdataene, inne i dokumentet, slik at det fortsatt kan kontrolleres år senere. B-LTA legger periodiske dokumenttidsstempler oppå det igjen, slik at beviset overlever algoritmene det ble bygget på. HotPDF genererer de dokumentsidestrukturene som trengs for hvert nivå:

HotPDF: Stablede PAdES-basenivåer fra B-B via B-T og B-LT til B-LTA, med en fornyelsestidslinje som viser periodiske dokumenttidsstempler som holder en signatur verifiserbar tiår senere
Hvert nivå stabler ny beskyttelse oppå den forrige; B-LTA fortsetter å påføre dokumenttidsstempler på nytt slik at beviset overlever algoritmene det først ble 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 reservasjon for TSA-token og -kjede
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Reservasjonen på 16384 byte for tidsstempelet er bevisst. En tidsstempelmyndighet returnerer en token som drar med seg sin egen sertifikatkjede, så den trenger rutinemessig mer plass enn de 8 KB en enkel signatur klarer seg med. De dokumenttidsstemplene er også maskineriet bak B-LTA: å re-tidsstemple en arkivert signatur hvert par år, med algoritmer som fortsatt er aktuelle, er det som holder et dokument du signerte i 2026, verifiserbart i 2040

Et ord om reason-, location- og contact-strengene begge feltkallene tar imot: de er bekvemmelighetsmetadata og ikke noe mer. HotPDF lagrer dem som vanlige dictionary-oppføringer og maler dem inn i det synlige signaturutseendet, men ingen validator sjekker dem mot noe som helst. Fyll dem ut konsistent fra arbeidsflytdataene dine, siden revisorer faktisk leser dem, men forveksle dem aldri med bevis. Den faktiske kryptografiske påstanden ligger utelukkende i CMS-en og sertifikatkjeden dens, og en verifiserer ignorerer den synlige teksten fullstendig

Etter signering kan filen bare vokse

I det øyeblikket en signatur eksisterer, er bytene innenfor dens områder frosset. Den eneste legitime måten å endre filen på etterpå, er en ISO 32000-1 §7.5.6 inkrementell oppdatering, som legger nye og endrede objekter til etter de opprinnelige bytene og kjeder en fersk kryssreferanseseksjon tilbake til dem. Gjort på den måten forblir signaturen gyldig for sin revisjon, og en viser rapporterer den ærlige tilstanden: den signerte revisjonen er intakt, dokumentet ble utvidet etterpå. Reserialiser hele filen i stedet, og du skriver om de signerte strekningene, noe som ødelegger signaturen selv når ingenting synlig er endret. Den samme revisjonsmekanismen er også hvordan ett dokument kan bære flere signaturer: hver nye signatur havner i sin egen inkrementelle oppdatering, og dens områder dekker alt før den, inkludert de tidligere signaturene. Den kun-tillegg-mekanikken, og når det er trygt å komprimere dem, dekkes i artikkelen om objektstrømmer og inkrementelle oppdateringer

To grenser er verdt å ha i bakhodet mens du designer. HotPDFs PDF/A-outputmodus avviser signaturfelt rett ut, så arkivsamsvar og en innebygd signatur må sendes som separate filer. Og signering sier ingenting om hemmelighold: den beviser hvem som produserte et dokument, og at det ikke har endret seg siden, men hvem som helst kan fortsatt lese det. Å skjule innholdet er en egen jobb, håndtert av AES-256-kryptering og tillatelsespolicy

Uansett hva du bygger, test det med noe annet enn koden som skrev filen. Åpne outputen i Acrobats signaturpanel og bekreft tre ting: signaturen er gyldig, identiteten kjeder seg til roten du forventet, og panelet rapporterer ingen endringer siden signering. Vend deretter en enkelt byte inne i det signerte området på en kopi du kan kaste, og bekreft at panelet nå kaller dokumentet endret. En signeringspipeline du aldri har sett avvise en manipulert fil, er en hvis verifisering egentlig ikke har blitt testet

Alle tre signeringsnivåene følger med HotPDF Delphi Component for Delphi og C++Builder; produktsiden lenker til den komplette signatur-API-referansen