Teknisk artikel

Digitala PAdES-signaturer i Delphi: Signering och validering med PDFlibPas

Att validera en PAdES-signatur innebär att man kontrollerar tre oberoende saker, och en grön bock (green check mark) i en visare (viewer) berättar bara om den tredje. Först måste /ByteRange-arrayen täcka (cover) rätt byte: de spann (spans) den namnger måste rekonstruera den exakta inmatning (exact input) som CMS-sammanfattningen (CMS digest) togs över, med inga signerade byte lämnade utanför dem. För det andra måste certifikatet inuti CMS:en kedja sig (chain) till en rot du litar på, och bära med sig det signerade attributet för signeringscertifikat (signed signing-certificate attribute) som PAdES kräver. För det tredje, om profilen gör anspråk på en tidsstämpel (claims a timestamp), måste en RFC 3161-token binda signaturvärdet till en tidpunkt (point in time) innan certifikatet gick ut (expired). Acrobat slår ihop (collapses) alla tre till en enda ikon; en efterlevnadskontroll (conformance checker) håller dem separerade, och det borde också den kod som producerar dessa filer göra. losLab PDF Library (PDFlibPas) ger dig signeringssidan av detta, återinbäddningen av tidsstämplar (timestamp re-embedding) och granskningsanropen (audit calls) för att inspektera en ByteRange innan du litar på den

En distinktion får nästan varje första PAdES-implementering att snubbla (trips up), så den är värd att konstatera (stating) innan någon kod. En signatur skriven med /SubFilter /adbe.pkcs7.detached är en fullkomligt sund (sound) ISO 32000-1 §12.8-signatur som Acrobat kommer att rapportera som giltig. Det är dock inte en PAdES-signatur, eftersom ETSI EN 319 142-1 kräver ETSI.CAdES.detached vid varje baslinje-nivå (baseline level). En eIDAS-efterlevnadskontroll (eIDAS conformance checker) avvisar (rejects) den första och accepterar den andra, även om (even though) kryptografin är identisk. Profilen är ett anspråk (claim) som dokumentet gör om sig självt, och att få det anspråket rätt är ett anrop i PDFlibPas

Vad som gör en PDF-signatur till en PAdES-signatur

ETSI EN 319 142-1 definierar fyra baslinje-nivåer staplade ovanpå (stacked on) CMS-formatet. PAdES-B-B är ingångspunkten: en CAdES-signatur i ett PDF-signaturfält med SubFilter ETSI.CAdES.detached och ett signerat attribut för signeringscertifikat. PAdES-B-T adderar en RFC 3161-tidsstämpel över signaturvärdet, vilket bevisar att signaturen existerade innan en tidpunkt som ingen kan bakåtdatera (backdate). PAdES-B-LT inbäddar (embeds) de certifikat, CRL:er och OCSP-svar som behövs för validering i ett Document Security Store (DSS), så att filen förblir (remains) verifierbar efter att den utfärdande (issuing) CA:n drar tillbaka (retires) sin infrastruktur. PAdES-B-LTA toppar (caps) stapeln med en dokumenttidsstämpel som ombeskyddar (re-protects) det ackumulerade beviset allt eftersom algoritmer försvagas

PDFlibPas mappar dessa koncept mot (onto) sitt signeringsprocess-API (sign-process API). Profil-markören (The profile marker) är SetSignProcessCustomSubFilter. Om din policy behöver en indikation om åtagandetyp (commitment-type indication, det vill säga bevis på ursprung, bevis på godkännande, eller någon av de andra ETSI-identifierarna numrerade 1 till 6), går det genom SetSignProcessCommitmentType. En explicit signaturpolicy fästs (attaches) med SetSignProcessSignaturePolicy, vilken tar policyns OID och dess sammanfattning (digest). Ett standardval förtjänar uppmärksamhet: med sammanfattnings-algoritmen (digest algorithm) lämnad på auto väljer biblioteket SHA-256 för ETSI- och adbe.pkcs7.detached-signaturer, och faller tillbaka till SHA-1 enbart på det gamla (legacy) adbe.pkcs7.sha1-spåret (path). Sätt den explicit (explicitly) oavsett. Granskare (Auditors) frågar vilken hash du använde, och ett explicit värde i koden är lättare att försvara än ett standardval du måste gå och läsa manualen för att förklara

Att producera baslinje-signaturen

Det platta API:et driver signeringen som en envägs-tillståndsmaskin (one-shot state machine): öppna en process på källfilen, konfigurera den, slutför (finish) till en utdatafil (output file), läs av resultatkoden. Sekvensen nedan producerar en PAdES-B-B-signatur med SHA-256. Den rad som spelar störst roll (matters most) har ingenting att göra med signaturen i sig. Det är den medvetet överdimensionerade (deliberately oversized) /Contents-reservationen, eftersom det är den enda sak du inte kan ändra senare ifall en tidsstämpel någon gång (ever) måste läggas till denna signatur

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // room for a timestamp later
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

NewSignProcessFromFile returnerar 0 när källan (the source) inte kan öppnas över huvud taget. Därefter separerar GetSignProcessResult de felfall (failure modes) som faktiskt inträffar i produktion: 4 betyder fel PDF-lösenord, 7 ett felaktigt PFX-lösenord, 9 en certifikatfil utan någon privat nyckel, 10 en o-skrivbar (unwritable) utdatasökväg, 11 ett fel vid appliceringen av signatur-byten. Att logga den numeriska koden bredvid (next to) inmatningsfilens (input file) namn omvandlar ett vagt (vague) supportärende till en enminuts-diagnos

Att addera den RFC 3161-tidsstämpel biblioteket inte kommer att hämta (fetch) åt dig

PDFlibPas skeppar (ships) ingen TSA-klient, och det är en avsiktlig gräns (deliberate boundary) snarare än en lucka (gap). Biblioteket beräknar den hash som tidsstämpel-auktoriteten (timestamp authority) måste medsigenra (countersign) och åter-inbäddar (re-embeds) den utökade (augmented) CMS:en efteråt; HTTP-utbytet (HTTP exchange) och CMS-kirurgin (CMS surgery) däremellan (in between) tillhör anroparen. Det finns ett hårt tekniskt skäl för den uppdelningen (split). Windows CryptoAPI-kontrollen som nominellt lägger till osignerade attribut, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, fallerar (fails) med CRYPT_E_INVALID_INDEX på den fristående (detached) SignedData-layout som PAdES använder. Så den förstärkta (enhanced) CMS:en måste komma från en CMS-kodare (encoder) under din egen kontroll. Inget bibliotek kan i tysthet vika in token (fold the token in) med ett enda systemanrop, och de som påstår det (claims to) utför kirurgin någonstans (somewhere) där du inte kan se den

var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // both calls below are application code: an HTTP POST to your TSA,
    // and a CMS re-encode that attaches the token as an unsigned attribute
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

Håll ett öga på (Watch) resultatkoderna här: 12 betyder att det namngivna signaturfältet inte existerar, 11 att den befintliga (existing) CMS:en inte kunde tolkas (parsed), och 13 att den utökade CMS:en inte längre ryms i (fits) den reserverade /Contents-platshållaren (placeholder). Kod 13 är den som svider (hurts), eftersom den enda lösningen är en om-signering (re-signing): en typisk tidsstämpel-token med dess certifikatkedja löper på (runs) 4 till 6 KB, och den 8192-bytes-reservation som gjordes under B-B-steget existerar precis för att (precisely so) det här steget ska ha utrymme (room) att landa på

Validering börjar vid (at) ByteRange, inte vid certifikatkedjan

En grön bock (green check mark) i en visare är ett tillitsbeslut (trust decision) gentemot den maskinens certifikatlager (certificate store), inte ett strukturellt omdöme (structural verdict) om filen. Programmatisk validering (Programmatic validation) borde börja lägre ner, med frågan inkrementella uppdateringar (incremental updates) gör subtil: vilka byte täcker varje signatur faktiskt? Varje förbättring (enhancement) som diskuterats här, vare sig (whether) det är en andra signatur, ett DSS-lexikon eller en dokumenttidsstämpel, anländer via en inkrementell uppdatering, och varje uppdatering infogar (appends) byte utanför den tidigare signaturens /ByteRange. De inlagda byten är legitima. En validerare (validator) måste likväl (still) klassificera dem gentemot dokumentets modifieringspolicy, och den per-fält-DocMDP-nivå (per-field DocMDP level) som policyn lever i är läsbar med GetSignatureDocMDPLevelByName

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a share lock
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 means actually signed
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

Två fällor (traps) lever i det här granskningsspåret (audit path). TPDFlibSignDoc.Open håller (holds) filen med ett exklusivt delat lås (exclusive share lock), så en validerare som också vill hasha de råa fil-byten för CMS-verifiering måste läsa in filen i minnet innan den öppnas för granskning (audit). Kasta om (Reverse) den ordningen och inläsningen fallerar på ett lås du satt själv. Den andra fällan är tyst (silent) snarare än högljudd (loud): motsvarigheten (counterpart) i det platta API:et, GetSignProcessByteRange, returnerar Integer medan de bakomliggande (underlying) offsetsen är Int64, så förbi 2 GB trunkerar det platta anropet utan klagomål (without complaint), vilket är varför det här exemplet drar (pulls) offsetsen genom granskningsklassen i stället. En frånvaro (absence) är värd att namnge också. Det platta lagret (flat layer) har ingen VerifySignature-omslutare (wrapper) alls. Kryptografiska omdömen (Cryptographic verdicts) kommer från den klass-baserade (class-level) TPDFlibSignatureVerifier, vilken returnerar vsValid, vsInvalid eller vsUnknown, eller från en extern validerare din efterlevnadspolicy (compliance policy) redan litar på

Långtidsvalidering: DSS, VRI och dokumenttidsstämpeln

PAdES-B-LT existerar eftersom tillbakadragnings-infrastruktur (revocation infrastructure) är dödlig (mortal). ETSI EN 319 142-1 §5.4.2.2 specificerar Document Security Store: ett lexikon på dokumentnivå som bär certifikat, CRL:er och OCSP-svar, valfritt (optionally) indexerat per signatur genom VRI-poster (VRI entries) nycklade (keyed) efter hashen av varje signaturs /Contents. PDFlibPas-flödet speglar tidsstämpel-designen. NewPAdESDSSProcessFromFile öppnar processen; AddPAdESDSSCertificate, AddPAdESDSSCRL och AddPAdESDSSOCSP accepterar DER-blobbar; AddPAdESDSSVRI binder (binds) utvalt material till en signatur; EndPAdESDSSProcessToFile skriver allt som en inkrementell uppdatering. Den svåra delen (hard part) ligger kvar (stays) på din sida. Att hämta återkallelse-materialet (revocation material), och bedöma (judging) huruvida det är tillräckligt färskt (fresh enough) för att vara värt att inbädda, är anroparens (caller's) jobb. Biblioteket garanterar (guarantees) att lexikonen är strukturellt överensstämmande (structurally conformant); det kan inte garantera att din OCSP-svarare (responder) talade sanning

Arkiv-slutpunkten (archival endpoint), B-LTA, lägger till en dokumenttidsstämpel (document timestamp): ett separat signaturfält vars typ är DocTimeStamp i stället för Sig, producerat genom SetSignProcessDocTimeStamp med en reserverad signaturlängd (signature length). Den ersätter (replace) inte signaturtidsstämpeln (signature timestamp) från B-T-steget. Signaturtidsstämpeln bevisar när en viss signatur existerade; dokumenttidsstämpeln skyddar (protects) hela filen, DSS-bevis inkluderat, och är det element ett långtidsarkiv förnyar (renews) vartefter (every few years as) algoritmer försvagas. En mogen (mature) arkivprofil bär (carries) båda. För läsare som härrör från (predate) innan dessa strukturer, registrerar TPDFlibSignDoc.EnsurePAdESExtensions ESIC-utvecklartillägget (ESIC developer extension) i dokumentkatalogen (document catalog), och annonserar (announcing) att filen använder ETSI-definierade funktioner (features)

En reaktion på (reaction to) allt detta är värd att avvärja (heading off), eftersom det ser ut som en bugg (bug) och inte är det. En visare (viewer) rapporterar ofta "okänd giltighet" (validity unknown) för en fil vars PAdES-struktur är fullkomligt (entirely) korrekt. Tillit (Trust) och struktur (structure) är oberoende axlar (axes). Visaren kan helt enkelt (simply) inte kedja signeraren till en rot den litar på på den maskinen, vilket är rutinförfarande (routine) med privata CA:er (private CAs) och testcertifikat, även medan ByteRange-granskningen och CMS-verifieringen båda blir godkända (pass). Lösningen (fix) är att distribuera rotcertifikatet (root certificate) korrekt, eller att utvärdera mot EU:s betrodda listor (EU trusted lists) när kvalificerad eIDAS-status är det faktiska målet, i stället för att röra vid (touch) signeringskoden

För granskningssidans perspektiv (audit-side perspective), vilket innebär (meaning) att räkna upp (enumerating) signaturfält tvärs över en korpus, dumpa ByteRange-layouter och läsa in DocMDP-nivåer i omgångar (in bulk), se tilläggsartikeln om (companion piece on) arbetsbänken för efterlevnad och signering (compliance and signing workbench). Signerade dokument som också måste tillfredsställa (satisfy) arkiv-policy (archival policy) hör hemma i det arbetsflöde som beskrivs i PDF/A- och PDF/UA-preflight (preflight) i Delphi. Fullständig API-dokumentation och utvärderingsnedladdningar (evaluation downloads) finns på produktsidan för losLab PDF Library för Delphi