Du har modtaget en signeret PDF, og du skal i din fremviser vise, hvem der har signeret den, hvornår den blev signeret, om signaturen dækker hele filen, og hvor langt den rækker mod langtidsoverholdelse. PDFium Component til Delphi og Lazarus besvarer alle fire spørgsmål med skrivebeskyttede kald: FPDF_GetSignatureCount og familien FPDFSignatureObj_* blotlægger signaturordbogen, og TPdf.ValidatePades klassificerer PAdES-baselineniveauet. Dette er den første af tre artikler om PDF-signaturer med PDFium; de to følgende dækker at oprette en B-B-signatur og at tilføje langtidstidsstempler. Én grænse hører dog til med det samme: alt her er inspektion, og at læse, hvad en signatur påstår, er en anden opgave end at verificere dens kryptografi eller at afgøre, om du stoler på underskriveren
Hvorfor en PDF-signaturordbog ikke bare er en klump bytes
En PDF-signatur er en ordbog, ikke en uigennemsigtig vedhæftning, og dens to vigtigste poster fortæller dig, hvor meget af filen der rent faktisk er beskyttet. ISO 32000-1 §12.8 definerer signaturordbogen med en /ByteRange- og en /Contents-post. /Contents rummer en hex-kodet CMS SignedData-struktur (RFC 5652), den kryptografiske konvolut, der bærer underskriverens certifikat, de signerede attributter og selve signaturværdien. /ByteRange er den del, udviklere undervurderer: det er et array af to spænd angivet som offset og længde, der tilsammen dækker hele filen bortset fra /Contents-hex-strengen. Netop det hul er der, hvor signaturbytene sidder, og de to spænd på hver side er præcis det, signaturen forpligter sig på
ByteRange-designet er det, der gør en inkrementel gemning revisionsegnet. Fordi en underskriver ikke kan hashe signaturbytes, der endnu ikke findes, deles filen omkring /Contents-pladsholderen, og alt andet hashes ind i signaturen. En signatur, hvis ByteRange ikke når filens slutning, er et advarselstegn: indhold tilføjet efter det dækkede område, gennem en senere inkrementel opdatering, ville ikke bryde signaturen, selv om det ændrer, hvad læseren ser. Så det første, en seriøs inspektør tjekker, er ikke hvem der signerede, men om signaturen dækker de bytes, den ser ud til at gå god for
Sådan læser du signaturordbogen med PDFiums skrivebeskyttede API
PDFium Component blotlægger signaturordbogen gennem to skrivebeskyttede medlemmer: SignatureCount og Signature[Index]-recorden. Under motorhjelmen kalder de FPDF_GetSignatureCount, FPDF_GetSignatureObject og FPDFSignatureObj_*-accessorerne til /SubFilter, /ByteRange, /Contents, /Reason og signeringstidspunktet. Skrivebeskyttet er nøgleordet her: PDFium kan opregne og læse signaturer, men har intet API til at oprette eller skrive en, og derfor er signeringssiden af denne serie implementeret af biblioteket selv snarere end af PDFium
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
Writeln('Signed at : ', Sig.Time); // underskriverens datostreng, f.eks. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // rå DER SignedData taget fra /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = ingen certificering, 1..3 = MDP-niveau
end;
finally
Pdf.Free;
end;
end;
Hver TPdfSignature-record afbilder direkte på ordbogen. Encoding er /SubFilter, det enkeltfelt der siger mest diagnostisk, fordi det navngiver signaturhåndteringen og straks adskiller en moderne ETSI.CAdES.detached-signatur fra en gammel eller forbudt. Time er det signeringstidspunkt, forfatteren har erklæret, som en PDF-datostreng, hvilket er en påstand fra underskriveren og ikke betroet tid. Content er den rå CMS SignedData, og Permission blotlægger DocMDP-certificeringsniveauet (0 for en almindelig godkendelsessignatur, 1 til 3 for en certificeringssignatur, der låser senere ændringer). Det ene felt, recorden ikke blotlægger, er den parsede ByteRange, og den udeladelse er bevidst, for ValidatePades laver dækningsberegningen på ByteRange for dig i stedet for at få dig til at lave den om i hånden
Hvad er forskellen på PAdES B-B, B-T, B-LT og B-LTA?
De fire PAdES-baselineniveauer danner en stige fra en minimalt gyldig signatur til en, der er bygget til at overleve årtiers arkivering, og hvert niveau indeholder strengt det under sig. ETSI EN 319 142-1 definerer dem som B-B, B-T, B-LT og B-LTA. B-B (Basic) er signaturen plus de obligatoriske signerede attributter og intet derudover. B-T (Timestamp) tilføjer et betroet RFC 3161-tidsstempel over signaturen, så signeringsøjeblikket attesteres af en tidsstemplingsmyndighed i stedet for at blive påstået af underskriverens ur. B-LT (Long-Term) indlejrer valideringsmaterialet — certifikatkæden og eventuelt OCSP- eller CRL-svar — inde i filen, så signaturen stadig kan valideres år senere, når den udstedende infrastruktur er væk. B-LTA (Long-Term with Archive timestamp) lægger et dokumenttidsstempel omkring det materiale, hvilket beskytter selve langtidsdataene og giver dig et punkt at tidsstemple på ny fra, før den underliggende kryptografi ældes
Den praktiske læsning handler om tidshorisont. En B-B-signatur besvarer "har nogen signeret dette". B-T besvarer "og hvornår, beviseligt". B-LT besvarer "og kan jeg stadig tjekke det, efter certifikaterne udløber". B-LTA besvarer "og holder det tjek stadig om tyve år". Regulatoriske profiler vælger et trin: mange sammenhænge inden for e-fakturering og eIDAS kræver mindst B-T, og arkiveringskrav rækker efter B-LT eller B-LTA. At vide, hvilket trin et dokument rent faktisk når, før du accepterer eller afviser det, er hele pointen med inspektionstrinnet
Sådan finder du baselineniveauet med TPdf.ValidatePades
PDFium Component reducerer hele niveauspørgsmålet til ét kald. TPdf.ValidatePades returnerer en TPadesValidationResult-record, hvis Level-felt er en TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT eller plB_LTA — sammen med et sæt problemer, et signaturantal og et antal dokumenttidsstempler. Niveauet udledes monotont: validatoren fastslår først B-B, forfremmer derefter til B-T, hvis et signaturtidsstempel eller et dokumenttidsstempel er til stede, til B-LT, hvis kataloget bærer en /DSS med certifikater og /Extensions /ESIC-markøren for niveau 1, og til B-LTA, hvis både et dokumenttidsstempel og ESIC-markøren for niveau 2 er til stede. To hjælpere gør resultatet handlingsegnet: IsCompliant er kun True, når niveauet når mindst B-B, og problemsættet er tomt, og IsCompliantAt lader dig hævde et politikgulv som for eksempel plB_T
var
Pdf: TPdf;
R: TPadesValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
case R.Level of
plNone: Writeln('No PAdES signature present');
plUnknown: Writeln('Signature present but level undeterminable');
plB_B: Writeln('PAdES B-B (basic)');
plB_T: Writeln('PAdES B-T (trusted timestamp)');
plB_LT: Writeln('PAdES B-LT (long-term material embedded)');
plB_LTA: Writeln('PAdES B-LTA (archive timestamp)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Meets the B-T policy floor')
else
Writeln('Below the required B-T level');
finally
Pdf.Free;
end;
end;
Hvorfor er adbe.pkcs7.sha1 en forbudt SubFilter?
Fordi SHA-1 er brudt, og adbe.pkcs7.sha1-håndteringen bager den ind. Den SubFilter forhåndshasher dokumentet med SHA-1, før det pakkes ind i PKCS#7, og SHA-1 har været sårbar over for kollisioner i årevis, så EN 319 142-1 punkt 6.3 forbyder den blankt for en baselinesignatur. ValidatePades rejser ppeiForbiddenSubFilter, når den ser adbe.pkcs7.sha1 eller adbe.x509.rsa_sha1, og den rejser ppeiBadDigestAlgorithm, når selve CMS-strukturen bruger MD5 eller SHA-1 som meddelelsesdigest (punkt 6.2.1). Det er to adskilte tjek, der fanger den samme klasse af svaghed i to forskellige lag
Problemsættet har 26 medlemmer i alt, og dem, du oftest møder, klumper sig omkring struktur og dækning. ppeiByteRangeNotCoveringFile er det dækningstjek, der er beskrevet tidligere. ppeiForbiddenCertKey udløses, når signaturordbogen bærer en /Cert-post, hvilket PAdES forbyder, fordi kæden i stedet skal bo inde i CMS-strukturens SignedData.certificates. ppeiMissingSigningCertificate, ppeiMissingContentType og ppeiMissingMessageDigest markerer obligatoriske signerede attributter, der mangler, og ppeiDetachedContentViolation fanger en signatur, der fejlagtigt indlejrer det signerede indhold i stedet for at løsrive det. At opregne sættet forvandler en nøgen afvisning til en diagnose, du kan logge
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
Hvad ValidatePades ikke tjekker
ValidatePades validerer struktur, ikke tillid, og at forveksle det ene med det andet er den farlige fejl. Et resultat på plB_LTA betyder, at dokumentet indeholder en velformet B-LTA-signatur med alle de påkrævede attributter, materialer og tidsstempler de rigtige steder — det betyder ikke, at signaturen er kryptografisk gyldig, at certifikatet kæder til en rod, du stoler på, eller at intet certifikat i kæden er tilbagekaldt. Validatoren udfører bevidst ingen kryptografisk verifikation: den genberegner ikke signaturen over ByteRange, den bygger eller evaluerer ikke tillidskæden, og den tjekker ikke tilbagekaldelsesstatus via OCSP eller CRL. Den opdeling er tilsigtet og nyttig, fordi strukturel inspektion er hurtig, fuldt deterministisk og hverken kræver nøgler, netværk eller platformskryptografi, så ValidatePades kører identisk på Windows, Linux og macOS som ren Pascal over filens bytes. Validering af tillidskæden er derimod uadskillelig fra politik — hvilke rødder du stoler på, hvordan du henter tilbagekaldelsesdata, hvor stram din tidsstempeltolerance er — og den hører til et senere trin, der afhænger af platformens certifikatlager, så behandl en bestået ValidatePades som den nødvendige port for, at en signatur er formet korrekt, og send derefter en strukturelt sund signatur videre til rigtig kryptografisk verifikation, før du læner dig op ad den
Det strukturelle gennemløb er det rigtige sted at starte, og det passer naturligt sammen med de bredere skrivebeskyttede tjek i audit af PDF-sikkerhedsrisici med PDFium Component og med arbejde omkring formatoverholdelse såsom validering af trykklare PDF/X-dokumenter. Når du kan læse og klassificere en signatur, er næste skridt at fremstille en: den anden artikel i denne serie dækker signering af PDF-filer med PAdES B-B, og den tredje udvider det til betroede tidsstempler og langtidssignaturer på B-LT og B-LTA. Den skrivebeskyttede signaturinspektion og ValidatePades-klassifikatoren, der er vist her, er en del af PDFium Component til Delphi, C++Builder og Lazarus