Du har tagit emot en signerad PDF och du behöver visa i din visare vem som signerade den, när den signerades, om signaturen täcker hela filen och hur långt den når mot långsiktig efterlevnad. PDFium Component för Delphi och Lazarus besvarar alla fyra frågorna med skrivskyddade anrop: FPDF_GetSignatureCount och FPDFSignatureObj_*-familjen exponerar signaturordboken, och TPdf.ValidatePades klassificerar PAdES-basnivån. Detta är den första av tre artiklar om PDF-signaturer med PDFium; de två som följer täcker skapandet av en B-B-signatur och tillägg av långsiktiga tidsstämplar. En gräns bör dock dras på en gång: allt här är inspektion, och att läsa vad en signatur gör anspråk på är ett annat jobb än att verifiera dess kryptografi eller avgöra om du litar på undertecknaren
Varför en PDF-signaturordbok inte bara är en klump byte
En PDF-signatur är en ordbok (dictionary), inte en ogenomskinlig bilaga, och dess två viktigaste poster talar om för dig hur mycket av filen som faktiskt är skyddad. ISO 32000-1 §12.8 definierar signaturordboken med en /ByteRange och en /Contents-post. /Contents innehåller en hex-kodad CMS SignedData-struktur (RFC 5652), det kryptografiska kuvertet som bär undertecknarens certifikat, de signerade attributen och själva signaturvärdet. /ByteRange är den del som utvecklare underskattar: det är en matris med två offset-längdsintervall som tillsammans täcker hela filen utom hex-strängen i /Contents. Det glappet är exakt där signaturbyten sitter, och de två intervallen på vardera sidan är exakt vad signaturen förbinder sig till
Designen med ByteRange är det som gör en inkrementell sparning granskningsbar. Eftersom en undertecknare inte kan hasha signaturbyte som inte finns ännu, delas filen runt platshållaren /Contents och allt annat hashas in i signaturen. En signatur vars ByteRange nu inte når slutet av filen är ett varningstecken: innehåll som läggs till efter det täckta intervallet, genom en senare inkrementell uppdatering, skulle inte bryta signaturen även om det ändrar vad läsaren ser. Så det första en seriös inspektör kontrollerar är inte vem som signerade, utan om signaturen täcker de byte som den verkar godkänna
Läsa signaturordboken med PDFiums skrivskyddade API
PDFium Component visar signaturordboken via två skrivskyddade medlemmar: SignatureCount och posten Signature[Index]. Under huven anropar de FPDF_GetSignatureCount, FPDF_GetSignatureObject och FPDFSignatureObj_*-åtkomstfunktionerna för /SubFilter, /ByteRange, /Contents, /Reason och signeringstid. Skrivskyddad är det operativa ordet här: PDFium kan räkna upp och läsa signaturer men har inget API för att skapa eller skriva en, vilket är anledningen till att signeringssidan i den här serien implementeras av själva biblioteket snarare än av 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); // signer date string, e.g. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = no certification, 1..3 = MDP level
end;
finally
Pdf.Free;
end;
end;
Varje TPdfSignature-post mappar direkt mot ordboken. Encoding is /SubFilter, det enskilt viktigaste fältet för diagnos, eftersom det namnger signaturhanteraren och omedelbart skiljer en modern ETSI.CAdES.detached-signatur från en äldre eller förbjuden. Time är den av undertecknaren deklarerade signeringstiden som en PDF-datumsträng, vilket är ett påstående från undertecknaren och inte betrodd tid. Content är rå CMS SignedData, och Permission exponerar DocMDP-certifieringsnivån (0 för en vanlig godkännandesignatur, 1 till 3 för en certifieringssignatur som låser senare ändringar). Det fält som posten inte visar är den tolkade ByteRange, och det utelämnandet är medvetet eftersom ValidatePades gör ByteRange-täckningsaritmetiken åt dig istället för att tvinga dig att göra om den för hand
Vad är skillnaden mellan PAdES B-B, B-T, B-LT och B-LTA?
De fyra PAdES-basnivåerna bildar en stege från en minimalt giltig signatur till en som är byggd för att överleva decennier av arkivering, och varje nivå innehåller strikt nivån under den. ETSI EN 319 142-1 definierar dem som B-B, B-T, B-LT och B-LTA. B-B (Basic) är signaturen plus de obligatoriska signerade attributen och inget mer. B-T (Timestamp) lägger till en betrodd RFC 3161-tidsstämpel över signaturen, så att signeringsögonblicket intygas av en tidsstämplingsmyndighet istället för undertecknarens egen klocka. B-LT (Long-Term) bäddar in valideringsmaterialet — certifikatkedjan och valfritt OCSP- eller CRL-svar — inuti filen, så att signaturen fortfarande kan valideras år senare när den utfärdande infrastrukturen är borta. B-LTA (Long-Term with Archive timestamp) omsluter dokumenttidsstämpeln runt det materialet, vilket skyddar de långsiktiga data i sig och ger dig en punkt att tidsstämpla igen innan den underliggande kryptografin åldras
Den praktiska tolkningen handlar om tidshorisont. En B-B-signatur svarar på "signerade någon detta". B-T svarar på "och när, bevisligen". B-LT svarar på "och kan jag fortfarande kontrollera det efter att certifikaten har löpt ut". B-LTA svarar på "och kommer den kontrollen fortfarande att gälla om tjugo år". Reglerande profiler väljer ett trappsteg: många e-fakturerings- och eIDAS-sammanhang kräver minst B-T, och arkiveringsmandat sträcker sig efter B-LT eller B-LTA. Att veta vilket trappsteg ett dokument faktiskt når, innan du accepterar eller avvisar det, är hela poängen med inspektionssteget
Identifiera basnivån med TPdf.ValidatePades
PDFium Component reducerar hela nivåfrågan till ett enda anrop. TPdf.ValidatePades returnerar en TPadesValidationResult-post vars Level-fält är en TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT eller plB_LTA — tillsammans med en uppsättning problem (issues), ett signaturantal och ett dokumenttidsstämpelantal. Nivån härleds monotont: valideraren etablerar B-B först, främjar sedan till B-T om en signaturtidsstämpel eller en dokumenttidsstämpel finns, till B-LT om katalogen innehåller en /DSS med certifikat och markören /Extensions /ESIC nivå 1, och till B-LTA om en dokumenttidsstämpel och markören ESIC nivå 2 båda finns. Två hjälpare gör resultatet användbart: IsCompliant är sant endast när nivån når minst B-B och uppsättningen av problem är tom, och IsCompliantAt låter dig ställa in en lägsta nivå för policy, såsom 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;
Varför är adbe.pkcs7.sha1 ett förbjudet SubFilter?
Eftersom SHA-1 är trasigt och hanteraren adbe.pkcs7.sha1 bakar in det. Denna SubFilter förhashar dokumentet med SHA-1 innan det omsluts i PKCS#7, och SHA-1 har varit sårbart för kollisioner i flera år, så EN 319 142-1 klausul 6.3 förbjuder det helt för en bassignatur. ValidatePades rapporterar ppeiForbiddenSubFilter när den ser adbe.pkcs7.sha1 eller adbe.x509.rsa_sha1, and den rapporterar ppeiBadDigestAlgorithm när CMS i sig använder MD5 eller SHA-1 som meddelandesammandrag (klausul 6.2.1). Det är två separata kontroller som fångar samma typ av svaghet i två olika lager
Uppsättningen med problem (issue set) har totalt 26 medlemmar, och de som du oftast stöter på kretsar kring struktur och täckning. ppeiByteRangeNotCoveringFile är den täckningskontroll som beskrevs tidigare. ppeiForbiddenCertKey utlöses när signaturordboken innehåller en /Cert-post, vilket PAdES förbjuder eftersom kedjan måste ligga inuti CMS SignedData.certificates istället. ppeiMissingSigningCertificate, ppeiMissingContentType och ppeiMissingMessageDigest flaggar obligatoriska signerade attribut som saknas, och ppeiDetachedContentViolation fångar en signatur som felaktigt bäddar in det signerade innehållet istället för att koppla loss det. Att räkna upp uppsättningen förvandlar ett enkelt avvisande till en diagnos som du kan logga
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;
Vad ValidatePades inte kontrollerar
ValidatePades validerar struktur, inte förtroende, och att missta det ena för det andra är det farliga misstaget. Ett resultat på plB_LTA betyder att dokumentet innehåller en välformad B-LTA-signatur med alla obligatoriska attribut, material och tidsstämplar på rätt plats — det betyder inte att signaturen är kryptografiskt giltig, att certifikatkedjan leder till en rot du litar på eller att inget certifikat i kedjan har återkallats. Valideraren utför medvetet ingen kryptografisk verifiering: den beräknar inte om signaturen över ByteRange, bygger eller utvärderar inte förtroendekedjan och kontrollerar inte OCSP- eller CRL-återkallningsstatus. Den uppdelningen är avsiktlig och användbar, eftersom strukturell inspektion är snabb, helt deterministisk och inte kräver några nycklar, nätverk eller plattformskryptografi, så ValidatePades körs identiskt på Windows, Linux och macOS som ren Pascal över filens byte. Verifiering av förtroendekedjan är däremot oskiljaktig från policy — vilka rötter du litar på, hur du hämtar återkallelser, hur strikt din tolerans för tidsstämplar är — och den hör hemma i ett senare skede som beror på plattformens certifikatlager. Betrakta därför ett godkänt ValidatePades som den nödvändiga grinden för att en signatur är formad korrekt, och lämna sedan en strukturellt sund signatur till verklig kryptografisk verifiering innan du förlitar dig på den
Den strukturella genomgången är rätt plats att börja på, och den parar sig naturligt med de bredare skrivskyddade kontrollerna i granskning av PDF-säkerhetsrisker med PDFium Component och med arbete för formatöverensstämmelse såsom validering av tryckfärdiga PDF/X-dokument. När du kan läsa och klassificera en signatur är nästa steg att producera en: den andra artikeln i den här serien täcker signering av PDF-filer med PAdES B-B, och den tredje utökar detta till betrodda tidsstämplar och långsiktiga B-LT- och B-LTA-signaturer. Den skrivskyddade signaturinspektionen och ValidatePades-klassificeraren som visas här är en part av PDFium Component för Delphi, C++Builder och Lazarus