U hebt een ondertekende PDF ontvangen en moet in uw viewer laten zien wie hem heeft ondertekend, wanneer dat gebeurde, of de handtekening het hele bestand dekt, en hoe ver hij gaat richting duurzame naleving. De PDFium Component voor Delphi en Lazarus beantwoordt alle vier de vragen met alleen-lezen aanroepen: FPDF_GetSignatureCount en de familie FPDFSignatureObj_* stellen de handtekeningdictionary bloot, en TPdf.ValidatePades classificeert het PAdES-basisniveau. Dit is het eerste van drie artikelen over PDF-handtekeningen met PDFium; de twee volgende behandelen het maken van een B-B-handtekening en het toevoegen van langetermijntijdstempels. Eén grens hoort vooraan te staan: alles hier is inspectie, en lezen wat een handtekening beweert, is een andere klus dan haar cryptografie verifiëren of beslissen of u de ondertekenaar vertrouwt
Waarom een PDF-handtekeningdictionary niet zomaar een klomp bytes is
Een PDF-handtekening is een dictionary, geen ondoorzichtige bijlage, en haar twee belangrijkste vermeldingen vertellen u hoeveel van het bestand werkelijk beschermd is. ISO 32000-1 §12.8 definieert de handtekeningdictionary met een vermelding /ByteRange en een vermelding /Contents. /Contents bevat een hexadecimaal gecodeerde CMS SignedData-structuur (RFC 5652), de cryptografische envelop die het certificaat van de ondertekenaar, de ondertekende attributen en de handtekeningwaarde zelf draagt. /ByteRange is het deel dat ontwikkelaars onderschatten: het is een array van twee spannen met offset en lengte die samen het hele bestand dekken behalve de hexadecimale tekenreeks van /Contents. Precies in dat gat zitten de handtekeningbytes, en de twee spannen aan weerszijden zijn exact waar de handtekening zich aan verbindt
Het ontwerp met ByteRange is wat een incrementele opslag controleerbaar maakt. Omdat een ondertekenaar geen handtekeningbytes kan hashen die nog niet bestaan, wordt het bestand rond de plaatshouder /Contents gesplitst en gaat al het overige gehasht de handtekening in. Een handtekening waarvan de ByteRange het einde van het bestand niet bereikt, is een waarschuwingssignaal: inhoud die na het gedekte bereik is toegevoegd, via een latere incrementele bijwerking, zou de handtekening niet breken hoewel zij verandert wat de lezer ziet. Het eerste wat een serieuze inspecteur controleert, is dus niet wie ondertekende, maar of de handtekening de bytes dekt die zij lijkt te onderschrijven
De handtekeningdictionary lezen met de alleen-lezen API van PDFium
De PDFium Component brengt de handtekeningdictionary naar boven via twee alleen-lezen leden: SignatureCount en het record Signature[Index]. Onder de motorkap roepen die FPDF_GetSignatureCount, FPDF_GetSignatureObject en de accessors FPDFSignatureObj_* aan voor /SubFilter, /ByteRange, /Contents, /Reason en het ondertekeningstijdstip. Alleen-lezen is hier het sleutelwoord: PDFium kan handtekeningen opsommen en lezen maar heeft geen API om er een te maken of te schrijven, en daarom wordt de ondertekenkant van deze reeks door de bibliotheek zelf geïmplementeerd en niet door 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); // datumtekenreeks van de ondertekenaar, bv. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // ruwe DER SignedData uit /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = geen certificering, 1..3 = MDP-niveau
end;
finally
Pdf.Free;
end;
end;
Elk record TPdfSignature beeldt rechtstreeks af op de dictionary. Encoding is de /SubFilter, het meest diagnostische veld van allemaal, want het benoemt de handtekeninghandler en scheidt meteen een moderne ETSI.CAdES.detached-handtekening van een verouderde of verboden variant. Time is het door de auteur verklaarde ondertekeningstijdstip als PDF-datumtekenreeks, wat een bewering van de ondertekenaar is en geen vertrouwde tijd. Content is de ruwe CMS SignedData, en Permission toont het DocMDP-certificeringsniveau (0 voor een gewone goedkeuringshandtekening, 1 tot 3 voor een certificeringshandtekening die latere wijzigingen vastzet). Het ene veld dat het record niet blootstelt, is de geparseerde ByteRange, en die weglating is bewust, want ValidatePades doet het dekkingsrekenwerk op de ByteRange voor u in plaats van u het met de hand te laten overdoen
Wat is het verschil tussen PAdES B-B, B-T, B-LT en B-LTA?
De vier PAdES-basisniveaus vormen een ladder van een minimaal geldige handtekening naar een die decennia archivering moet doorstaan, en elk niveau omvat het niveau eronder strikt. ETSI EN 319 142-1 definieert ze als B-B, B-T, B-LT en B-LTA. B-B (Basic) is de handtekening plus de verplichte ondertekende attributen en niets meer. B-T (Timestamp) voegt een vertrouwd RFC 3161-tijdstempel over de handtekening toe, zodat het moment van ondertekenen door een tijdstempelautoriteit wordt bevestigd in plaats van door de klok van de ondertekenaar beweerd. B-LT (Long-Term) sluit het validatiemateriaal in het bestand in — de certificaatketen, en optioneel OCSP- of CRL-antwoorden — zodat de handtekening jaren later nog te valideren is wanneer de uitgevende infrastructuur weg is. B-LTA (Long-Term met archieftijdstempel) wikkelt een documenttijdstempel om dat materiaal heen, wat de langetermijngegevens zelf beschermt en u een punt geeft om opnieuw te tijdstempelen voordat de onderliggende cryptografie veroudert
De praktische lezing draait om tijdshorizon. Een B-B-handtekening beantwoordt "heeft iemand dit ondertekend". B-T beantwoordt "en wanneer, aantoonbaar". B-LT beantwoordt "en kan ik het nog controleren nadat de certificaten zijn verlopen". B-LTA beantwoordt "en houdt die controle over twintig jaar nog stand". Regelgevende profielen kiezen een sport: veel contexten rond e-facturatie en eIDAS eisen ten minste B-T, en archiveringsvoorschriften grijpen naar B-LT of B-LTA. Weten welke sport een document werkelijk haalt, voordat u het aanvaardt of afwijst, is het hele doel van de inspectiestap
Het basisniveau herkennen met TPdf.ValidatePades
De PDFium Component brengt de hele niveaukwestie terug tot één aanroep. TPdf.ValidatePades geeft een record TPadesValidationResult terug waarvan het veld Level een TPadesLevel is — plNone, plUnknown, plB_B, plB_T, plB_LT of plB_LTA — naast een verzameling problemen, een aantal handtekeningen en een aantal documenttijdstempels. Het niveau wordt monotoon afgeleid: de validator stelt eerst B-B vast, promoveert daarna naar B-T als er een handtekeningtijdstempel of een documenttijdstempel aanwezig is, naar B-LT als de catalogus een /DSS met certificaten draagt plus de markering /Extensions /ESIC niveau 1, en naar B-LTA als zowel een documenttijdstempel als de ESIC-markering niveau 2 aanwezig is. Twee hulpfuncties maken het resultaat bruikbaar: IsCompliant is alleen True wanneer het niveau minstens B-B haalt en de probleemverzameling leeg is, en met IsCompliantAt legt u een beleidsondergrens vast zoals 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;
Waarom is adbe.pkcs7.sha1 een verboden SubFilter?
Omdat SHA-1 gebroken is en de handler adbe.pkcs7.sha1 hem inbakt. Die SubFilter hasht het document vooraf met SHA-1 voordat het in PKCS#7 wordt gewikkeld, en SHA-1 is al jaren kwetsbaar voor botsingen, dus EN 319 142-1 clausule 6.3 verbiedt hem ronduit voor een basishandtekening. ValidatePades werpt ppeiForbiddenSubFilter op wanneer hij adbe.pkcs7.sha1 of adbe.x509.rsa_sha1 ziet, en hij werpt ppeiBadDigestAlgorithm op wanneer de CMS zelf MD5 of SHA-1 als berichtdigest gebruikt (clausule 6.2.1). Dat zijn twee afzonderlijke controles die dezelfde klasse zwakte op twee verschillende lagen opvangen
De probleemverzameling telt in totaal 26 leden, en de leden die u het vaakst tegenkomt, clusteren rond structuur en dekking. ppeiByteRangeNotCoveringFile is de dekkingscontrole die eerder is beschreven. ppeiForbiddenCertKey gaat af wanneer de handtekeningdictionary een vermelding /Cert draagt, wat PAdES verbiedt omdat de keten in plaats daarvan binnen de CMS in SignedData.certificates moet wonen. ppeiMissingSigningCertificate, ppeiMissingContentType en ppeiMissingMessageDigest markeren verplichte ondertekende attributen die ontbreken, en ppeiDetachedContentViolation vangt een handtekening op die de ondertekende inhoud ten onrechte insluit in plaats van los te koppelen. De verzameling opsommen maakt van een kale afwijzing een diagnose die u kunt loggen
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;
Wat ValidatePades niet controleert
ValidatePades valideert structuur, geen vertrouwen, en het een voor het ander aanzien is de gevaarlijke fout. Een uitkomst plB_LTA betekent dat het document een welgevormde B-LTA-handtekening bevat met alle voorgeschreven attributen, materialen en tijdstempels op de juiste plaats — het betekent niet dat de handtekening cryptografisch geldig is, dat het certificaat naar een wortel leidt die u vertrouwt, of dat geen enkel certificaat in de keten is ingetrokken. De validator voert bewust geen cryptografische verificatie uit: hij herberekent de handtekening niet over de ByteRange, bouwt of beoordeelt de vertrouwensketen niet, en controleert de intrekkingsstatus via OCSP of CRL niet. Die scheiding is opzettelijk en nuttig, want structurele inspectie is snel, volledig deterministisch en heeft geen sleutels, geen netwerk en geen platformcryptografie nodig, zodat ValidatePades op Windows, Linux en macOS identiek draait als pure Pascal over de bestandsbytes. Ketenvertrouwen valideren is daarentegen onlosmakelijk met beleid verbonden — welke wortels u vertrouwt, hoe u intrekkingsinformatie ophaalt, hoe streng uw tijdstempeltolerantie is — en hoort bij een latere fase die van het certificaatarchief van het platform afhangt, dus behandel een geslaagde ValidatePades als de noodzakelijke poort dat een handtekening juist gevormd is, en geef een structureel gezonde handtekening daarna door aan echte cryptografische verificatie voordat u erop steunt
Die structurele doorloop is de juiste plek om te beginnen, en zij past van nature bij de bredere alleen-lezen controles in PDF-beveiligingsrisico's auditen met de PDFium Component en bij werk rond formaatconformiteit zoals drukklare PDF/X-documenten valideren. Zodra u een handtekening kunt lezen en classificeren, is de volgende stap er een produceren: het tweede artikel in deze reeks behandelt PDF-bestanden ondertekenen met PAdES B-B, en het derde breidt dat uit naar vertrouwde tijdstempels en langlevende B-LT- en B-LTA-handtekeningen. De hier getoonde alleen-lezen handtekeninginspectie en de classificatie met ValidatePades maken deel uit van de PDFium Component voor Delphi, C++Builder en Lazarus