Teknisk artikel

PAdES digitale signaturer i Delphi: Signering og validering med PDF Library for Delphi

At validere én PAdES-signatur betyder at tjekke tre uafhængige ting, og et grønt flueben i en fremviser fortæller dig kun om den tredje. For det første skal /ByteRange-arrayet dække de rigtige bytes: de spænd, det navngiver, skal rekonstruere præcis det input, CMS-digestet blev taget over, uden at nogen signerede bytes ligger uden for dem. For det andet skal certifikatet inde i CMS'en kæde op til en rod, du stoler på, og bære den signerede signing-certificate-attribut, PAdES kræver. For det tredje skal et RFC 3161-token, hvis profilen påstår et tidsstempel, binde signaturværdien til et tidspunkt, før certifikatet udløb. Acrobat klapper alle tre sammen til ét ikon; en conformance checker holder dem adskilt, og det bør den kode, der producerer disse filer, også gøre. losLab PDF Library (PDF Library for Delphi) giver dig signeringssiden af det, genindlejringen af tidsstemplet og revisionskaldene til at inspicere en ByteRange, før du stoler på den

Én skelnen spænder ben for næsten enhver første PAdES-implementering, så den er værd at slå fast før al kode. En signatur skrevet med /SubFilter /adbe.pkcs7.detached er en fuldt ud gyldig ISO 32000-1 §12.8-signatur, som Acrobat vil melde som gyldig. Den er samtidig ikke en PAdES-signatur, for ETSI EN 319 142-1 kræver ETSI.CAdES.detached på hvert baseline-niveau. En eIDAS-conformance checker afviser den første og accepterer den anden, selv om kryptografien er identisk. Profilen er en påstand, dokumentet fremsætter om sig selv, og at få den påstand rigtig er ét kald i PDF Library for Delphi

Hvad der gør en PDF-signatur til en PAdES-signatur

ETSI EN 319 142-1 definerer fire baseline-niveauer stablet oven på CMS-formatet. PAdES-B-B er indgangen: en CAdES-signatur i et PDF-signaturfelt med SubFilteren ETSI.CAdES.detached og en signeret signing-certificate-attribut. PAdES-B-T tilføjer et RFC 3161-tidsstempel over signaturværdien og beviser, at signaturen fandtes før et tidspunkt, ingen kan antedatere. PAdES-B-LT indlejrer de certifikater, CRL'er og OCSP-svar, der skal bruges til validering, i et Document Security Store, så filen forbliver verificerbar, efter at den udstedende CA har pensioneret sin infrastruktur. PAdES-B-LTA sætter kronen på stakken med et dokumenttidsstempel, der genbeskytter det ophobede bevismateriale, efterhånden som algoritmerne svækkes

PDF Library for Delphi afbilder disse begreber på sit sign process-API. Profilmarkøren er SetSignProcessCustomSubFilter. Kræver din politik en angivelse af commitment-type (bevis for oprindelse, bevis for godkendelse eller en af de øvrige ETSI-identifikatorer nummereret 1 til 6), går det gennem SetSignProcessCommitmentType. En eksplicit signaturpolitik vedhæftes med SetSignProcessSignaturePolicy, der tager politikkens OID og dens digest. Ét standardvalg fortjener opmærksomhed: står digest-algoritmen på auto, vælger biblioteket SHA-256 til ETSI- og adbe.pkcs7.detached-signaturer og falder kun tilbage til SHA-1 på den ældre adbe.pkcs7.sha1-sti. Sæt den eksplicit alligevel. Revisorer spørger, hvilken hash du brugte, og en eksplicit værdi i koden er lettere at forsvare end et standardvalg, du skal slå op i manualen for at kunne forklare

Stige af PAdES baseline-niveauer B-B, B-T, B-LT og B-LTA bygget med PDF Library for Delphi, der viser, hvordan hvert niveau tilføjer tidsstempler, DSS-beviser eller et fornybart dokumenttidsstempel oven på ETSI.CAdES.detached-kernen
Hvert ETSI-basist niveau stabler én garanti mere oven på den samme CAdES-kerne, fra signerede attributter til et fornybart dokumenttidsstempel

At producere baseline-signaturen

Det flade API driver signering som en engangs-tilstandsmaskine: åbn en proces på kildefilen, konfigurér den, afslut til en outputfil, læs resultatkoden. Sekvensen nedenfor producerer en PAdES-B-B-signatur med SHA-256. Den linje, der betyder mest, har intet med selve signaturen at gøre. Det er den bevidst overdimensionerede /Contents-reservation, for den er den ene ting, du ikke kan ændre senere, hvis der nogensinde skal tilføjes et tidsstempel til denne 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);  // plads til et tidsstempel senere
    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 returnerer 0, når kilden slet ikke kan åbnes. Derefter skiller GetSignProcessResult de fejltilstande ad, der rent faktisk optræder i produktion: 4 betyder en forkert PDF-adgangskode, 7 en forkert PFX-adgangskode, 9 en certifikatfil uden privat nøgle, 10 en outputsti, der ikke kan skrives til, 11 en fejl, mens signaturbytes blev påført. At logge den numeriske kode ved siden af inputfilens navn forvandler en vag supportsag til en diagnose på et minut

At tilføje det RFC 3161-tidsstempel, biblioteket ikke henter for dig

PDF Library for Delphi leverer ingen TSA-klient, og det er en bevidst grænse snarere end et hul. Biblioteket beregner den hash, tidsstempelmyndigheden skal medsignere, og genindlejrer bagefter den udvidede CMS; HTTP-udvekslingen og CMS-kirurgien derimellem tilhører kalderen. Der er en hård teknisk grund til opdelingen. Den Windows CryptoAPI-kontrol, der nominelt tilføjer usignerede attributter, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, fejler med CRYPT_E_INVALID_INDEX på det detached SignedData-layout, PAdES bruger. Så den forbedrede CMS må komme fra en CMS-encoder under din egen kontrol. Intet bibliotek kan stille og roligt folde tokenet ind med ét systemkald, og ethvert, der påstår det, udfører kirurgien et sted, du ikke kan se

Pipeline til at tilføje et RFC 3161-tidsstempel til en PAdES-signatur i Delphi, der adskiller PDF Library for Delphi hashing og indlejring fra kalderens TSA-forespørgsel og CMS-rekodning inde i det reserverede /Contents-rum
Biblioteket hasher og genindlejrer, mens din kode henter tokenet og udfører CMS-operationen, og resultatet skal lande inden for /Contents-reservationen på 8192 bytes
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);
    // begge kald nedenfor er applikationskode: en HTTP POST til din TSA,
    // og en CMS-genkodning, der vedhæfter tokenet som en usigneret attribut
    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;

Hold øje med resultatkoderne her: 12 betyder, at det navngivne signaturfelt ikke findes, 11 at den eksisterende CMS ikke kunne parses, og 13 at den udvidede CMS ikke længere passer i den reserverede /Contents-pladsholder. Kode 13 er den, der gør ondt, for den eneste udvej er at signere igen: et typisk tidsstempeltoken med sin certifikatkæde fylder 4 til 6 KB, og reservationen på 8192 byte fra B-B-trinnet findes netop, for at dette trin har plads at lande på

Validering starter ved ByteRange, ikke ved certifikatkæden

Et grønt flueben i en fremviser er en tillidsbeslutning truffet mod den maskines certifikatlager, ikke en strukturel dom over filen. Programmatisk validering bør begynde længere nede, med det spørgsmål, inkrementelle opdateringer gør subtilt: hvilke bytes dækker hver signatur egentlig? Hver forbedring, der omtales her, hvad enten det er en anden signatur, en DSS-ordbog eller et dokumenttidsstempel, ankommer via inkrementel opdatering, og hver opdatering tilføjer bytes uden for den tidligere signaturs /ByteRange. De tilføjede bytes er legitime. En validator skal stadig klassificere dem op mod dokumentets ændringspolitik, og det DocMDP-niveau per felt, den politik bor i, kan læses med GetSignatureDocMDPLevelByName

Byte-layout-audit af en signeret PDF i Delphi, der viser de ByteRange-dækkede intervaller, de udelukkede /Contents-bytes, tilføjede inkrementelle opdateringer uden for range og dækningsdommen mod filstørrelsen
To dækkede span med signaturens egne bytes udeladt fortæller den sande dækkningshistorie, og tilføjede opdateringer klassificeres efter DocMDP-politikken frem for at frygtes
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // før Open: SignDoc holder en share-lås
  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 betyder faktisk signeret
        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;

To fælder bor i denne revisionssti. TPDFlibSignDoc.Open holder filen med en eksklusiv share-lås, så en validator, der også vil hashe filens rå bytes til CMS-verifikation, er nødt til at læse filen ind i hukommelsen, før den åbnes til revision. Vend den rækkefølge om, og læsningen fejler på en lås, du selv har sat. Den anden fælde er tavs frem for larmende: modparten i det flade API, GetSignProcessByteRange, returnerer Integer, mens de underliggende offsets er Int64, så ud over 2 GB afkorter det flade kald uden at kny, og det er derfor, dette eksempel trækker offsets gennem revisionsklassen i stedet. Ét fravær er også værd at nævne. Det flade lag har slet ingen VerifySignature-wrapper. Kryptografiske domme kommer fra klasseniveauets TPDFlibSignatureVerifier, der returnerer vsValid, vsInvalid eller vsUnknown, eller fra en ekstern validator, din compliance-politik allerede stoler på

Langtidsvalidering: DSS, VRI og dokumenttidsstemplet

PAdES-B-LT findes, fordi tilbagekaldelsesinfrastruktur er dødelig. ETSI EN 319 142-1 §5.4.2.2 specificerer Document Security Store: en ordbog på dokumentniveau, der bærer certifikater, CRL'er og OCSP-svar, valgfrit indekseret per signatur gennem VRI-poster nøglet på hashen af hver signaturs /Contents. Flowet i PDF Library for Delphi afspejler tidsstempeldesignet. NewPAdESDSSProcessFromFile åbner processen; AddPAdESDSSCertificate, AddPAdESDSSCRL og AddPAdESDSSOCSP tager imod DER-blobs; AddPAdESDSSVRI binder udvalgt materiale til én signatur; EndPAdESDSSProcessToFile skriver det hele som en inkrementel opdatering. Det svære forbliver på din side. At hente tilbagekaldelsesmaterialet og at bedømme, om det er friskt nok til at være værd at indlejre, er kalderens opgave. Biblioteket garanterer, at ordbøgerne er strukturelt konforme; det kan ikke garantere, at din OCSP-responder talte sandt

Arkivendepunktet, B-LTA, tilføjer et dokumenttidsstempel: et separat signaturfelt, hvis type er DocTimeStamp frem for Sig, produceret gennem SetSignProcessDocTimeStamp med en reserveret signaturlængde. Det erstatter ikke signaturtidsstemplet fra B-T-trinnet. Signaturtidsstemplet beviser, hvornår én bestemt signatur fandtes; dokumenttidsstemplet beskytter hele filen, DSS-bevismaterialet inklusive, og er det element, et langtidsarkiv fornyer med nogle års mellemrum, efterhånden som algoritmerne svækkes. En moden arkivprofil bærer begge. Til læsere, der er ældre end disse strukturer, registrerer TPDFlibSignDoc.EnsurePAdESExtensions ESIC-udviklerudvidelsen i dokumentkataloget og bekendtgør dermed, at filen bruger ETSI-definerede features

Én reaktion på alt dette er værd at komme i forkøbet, for den ligner en fejl og er det ikke. En fremviser melder ofte "validity unknown" på en fil, hvis PAdES-struktur er helt korrekt. Tillid og struktur er uafhængige akser. Fremviseren kan ganske enkelt ikke kæde underskriveren op til en rod, den stoler på, på den maskine, hvilket er rutine med private CA'er og testcertifikater, selv når både ByteRange-revisionen og CMS-verifikationen består. Løsningen er at distribuere rodcertifikatet ordentligt eller at evaluere mod EU's trusted lists, når kvalificeret eIDAS-status er det egentlige mål, frem for at røre ved signeringskoden

For perspektivet fra revisionssiden, altså at optælle signaturfelter på tværs af et korpus, dumpe ByteRange-layouts og læse DocMDP-niveauer i store mængder, se følgeartiklen om arbejdsbænken til compliance og signering. Signerede dokumenter, der også skal opfylde arkivpolitik, hører hjemme i den arbejdsgang, der beskrives i PDF/A- og PDF/UA-preflight i Delphi. Fuld API-dokumentation og evalueringsdownloads findes på produktsiden for losLab PDF Library til Delphi