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
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
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
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