Technisch artikel

PDF-handtekeningen en PAdES-niveaus inspecteren in Delphi

U heeft een ondertekende PDF ontvangen en u moet in uw viewer tonen wie deze heeft ondertekend, wanneer dit is gebeurd, of de handtekening het hele bestand dekt, en in hoeverre deze voldoet aan de vereisten voor langetermijnnaleving. De PDFium-component voor Delphi en Lazarus beantwoordt alle vier de vragen met alleen-lezen aanroepen: FPDF_GetSignatureCount en de FPDFSignatureObj_*-familie leggen het handtekening-dictionary bloot, en TPdf.ValidatePades classificeert het PAdES-baselineniveau. Dit is het eerste van drie artikelen over PDF-handtekeningen met PDFium; de twee die volgen behandelen het maken van een B-B-handtekening en het toevoegen van tijdstempels voor de lange termijn. Eén grens hoort echter vooraf te worden vermeld: alles hier is inspectie, en het lezen van wat een handtekening claimt is een andere taak dan het verifiëren van de cryptografie ervan of het beslissen of u de ondertekenaar vertrouwt

Waarom een PDF-handtekening-dictionary niet zomaar een blob met bytes is

Een PDF-handtekening is een dictionary, geen ondoorzichtige bijlage, en de twee belangrijkste ingangen vertellen u hoeveel van het bestand daadwerkelijk is beveiligd. ISO 32000-1 §12.8 definieert de handtekening-dictionary met een /ByteRange- en een /Contents-ingang. /Contents bevat een hex-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 offset-lengte-spannen die samen het hele bestand beslaan, behalve de hex-string /Contents. Dat gat is precies waar de bytes van de handtekening zich bevinden, en the twee spannen aan weerszijden zijn precies waaraan de handtekening zich committeert

Het ByteRange-ontwerp is wat een incrementele opslag controleerbaar maakt. Omdat een ondertekenaar geen handtekeningbytes kan hashen die nog niet bestaan, het bestand is gesplitst rond de tijdelijke aanduiding (placeholder) /Contents en wordt al het andere in de handtekening gehasht. Een handtekening waarvan de ByteRange niet tot het einde van het bestand reikt, is een waarschuwingssignaal: inhoud die na het gedekte bereik is toegevoegd via een latere incrementele update, zou de handtekening niet breken, hoewel het verandert wat de lezer ziet. Dus het eerste wat een serieuze inspecteur controleert, is niet wie er heeft ondertekend, maar of de handtekening de bytes dekt die deze lijkt te onderschrijven

De handtekening-dictionary lezen met PDFium's alleen-lezen API

De PDFium-component legt de handtekening-dictionary bloot via twee alleen-lezen leden: SignatureCount en het record Signature[Index]. Achter de schermen roepen ze FPDF_GetSignatureCount, FPDF_GetSignatureObject en de FPDFSignatureObj_*-accessors aan voor /SubFilter, /ByteRange, /Contents, /Reason en de ondertekeningstijd. Alleen-lezen is hier het sleutelwoord: PDFium kan handtekeningen opsommen en lezen, maar heeft geen API om er een te maken of te schrijven. Daarom wordt de ondertekeningskant van deze serie geïmplementeerd door de bibliotheek zelf 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);            // 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;

Elk TPdfSignature-record verwijst direct naar de dictionary. Encoding is de /SubFilter, het meest diagnostische veld, omdat dit de handtekeninghandler benoemt en direct een moderne ETSI.CAdES.detached-handtekening scheidt van een legacy of verboden handtekening. Time is de door de auteur aangegeven ondertekeningstijd als een PDF-datumstring, wat een claim van de ondertekenaar is en geen vertrouwde tijd. Content is de ruwe CMS SignedData, en Permission legt het DocMDP-certificeringsniveau bloot (0 voor een gewone goedkeuringshandtekening, 1 tot 3 voor een certificeringshandtekening die latere wijzigingen vergrendelt). Het enige veld dat het record niet blootlegt, is de geparseerde ByteRange, en die weglating is bewust, omdat ValidatePades de ByteRange-dekkingsberekening voor u uitvoert in plaats van dat u dit handmatig moet doen

Wat is het verschil tussen PAdES B-B, B-T, B-LT en B-LTA?

De vier PAdES-baselineniveaus vormen een ladder van een minimaal geldige handtekening naar een handtekening die is gebouwd om decennia van archivering te overleven, en elk niveau omvat strikt het onderliggende niveau. 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 toe over de handtekening, zodat het moment van ondertekening wordt geattesteerd door een tijdstempelautoriteit in plaats van te worden beweerd door de klok van de ondertekenaar. B-LT (Long-Term) embeds de validatie-materialen — de certificaatketen en optioneel OCSP- of CRL-reacties — in in het bestand, zodat de handtekening jaren later nog steeds kan worden gevalideerd wanneer de uitgevende infrastructuur is verdwenen. B-LTA (Long-Term met Archive-tijdstempel) wikkelt een documenttijdstempel om dat materiaal, waardoor de langetermijngegevens zelf worden beschermd en u een punt krijgt om opnieuw een tijdstempel aan te brengen voordat de onderliggende cryptografie veroudert

De praktische interpretatie gaat over de 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 zal die controle over twintig jaar nog standhouden". Regelgevende profielen kiezen een sport: veel e-facturatie- en eIDAS-contexten vereisen ten minste B-T, en archiveringsmandaten grijpen naar B-LT of B-LTA. Weten welke sport een document daadwerkelijk bereikt, voordat u het accepteert of weigert, is het hele doel van de inspectiestap

Het baselineniveau detecteren met TPdf.ValidatePades

De PDFium-component reduceert de hele niveaukwestie tot één aanroep. TPdf.ValidatePades retourneert een TPadesValidationResult-record waarvan het veld Level een TPadesLevel is — plNone, plUnknown, plB_B, plB_T, plB_LT of plB_LTA — samen met een set problemen, een aantal handtekeningen en een aantal documenttijdstempels. Het niveau wordt monotoon afgeleid: de validator stelt eerst B-B vast, promoveert vervolgens naar B-T als er een handtekeningtijdstempel of een documenttijdstempel aanwezig is, naar B-LT als de catalogus een /DSS met certificaten en de marker /Extensions /ESIC Niveau 1 bevat, en naar B-LTA als er zowel een documenttijdstempel als de Niveau 2-marker aanwezig zijn. Twee helpers maken het resultaat bruikbaar: IsCompliant is alleen True wanneer het niveau ten minste B-B bereikt en de probleemset leeg is, en IsCompliantAt stelt u in staat een beleidsgrens zoals plB_T af te dwingen

Waarom is adbe.pkcs7.sha1 een verboden SubFilter?

Omdat SHA-1 is gebroken en de handler adbe.pkcs7.sha1 dit hard ingebakken heeft. Die SubFilter pre-hasht het document met SHA-1 voordat het in PKCS#7 wordt verpakt, en SHA-1 is al jaren kwetsbaar voor botsingen (collisions). Daarom verbiedt EN 319 142-1 clausule 6.3 het ronduit voor een baseline-handtekening. ValidatePades werpt ppeiForbiddenSubFilter op wanneer het adbe.pkcs7.sha1 of adbe.x509.rsa_sha1 ziet, en het werpt ppeiBadDigestAlgorithm op wanneer de CMS zelf MD5 of SHA-1 gebruikt als de message digest (clausule 6.2.1). Dat zijn twee afzonderlijke controles die dezelfde klasse van zwakte op twee verschillende niveaus opvangen

De probleemset bevat in totaal 26 leden, en degene die u het meest zult tegenkomen hebben betrekking op de structuur en de dekking. ppeiByteRangeNotCoveringFile is de eerder beschreven dekkingscontrole. ppeiForbiddenCertKey vuurt wanneer de handtekening-dictionary een /Cert-ingang bevat, wat PAdES verbiedt omdat de keten in de CMS SignedData.certificates moet staan. ppeiMissingSigningCertificate, ppeiMissingContentType en ppeiMissingMessageDigest vlaggen verplichte ondertekende attributen die ontbreken, en ppeiDetachedContentViolation vangt een handtekening op die ten onrechte de ondertekende inhoud insluit in plaats van deze los te koppelen. Het opsommen van de set verandert een kale afwijzing in 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 die twee met elkaar verwarren is een gevaarlijke fout. Een resultaat van plB_LTA betekent dat het document een goedgevormde B-LTA-handtekening bevat met alle verplichte attributen, materialen en tijdstempels op de juiste plaats — het betekent niet dat de handtekening cryptografisch geldig is, dat het certificaat leidt naar een root die u vertrouwt, of dat er geen certificaat in de keten is ingetrokken. De validator voert bewust geen cryptografische verificatie uit: hij herberekent de handtekening niet over de ByteRange, bouwt of evalueert de vertrouwensketen niet, en controleert de OCSP- of CRL-intrekkingstatus niet. Die splitsing is bewust en nuttig, omdat structurele inspectie snel, volledig deterministisch is, en geen sleutels, geen netwerk en geen platformcryptografie nodig heeft. Hierdoor draait ValidatePades identiek op Windows, Linux en macOS als pure Pascal over de bestandsbytes. Verificatie van de vertrouwensketen daarentegen is onafscheidelijk van beleid — welke roots u vertrouwt, hoe u intrekkingen ophaalt, hoe strikt uw tolerantie voor tijdstempels is — en dit hoort thuis in een latere fase die afhankelijk is van het certificatenarchief van het platform. Behandel een passerende ValidatePades dus als de noodzakelijke poort om te controleren of een handtekening correct is gevormd, en bied een structureel gezonde handtekening vervolgens aan voor echte cryptografische verificatie voordat u erop vertrouwt

Die structurele controle is de juiste plek om te beginnen, en deze sluit natuurlijk aan bij de bredere alleen-lezen controles in het auditeren van PDF-veiligheidsrisico's met de PDFium-component en bij werkzaamheden rond formaatconformiteit zoals het valideren van printklare PDF/X-documenten. Zodra u een handtekening kunt lezen en classificeren, is de volgende stap het produceren ervan: het tweede artikel in deze serie behandelt het ondertekenen van PDF's met PAdES B-B, en het derde breidt dat uit naar vertrouwde tijdstempels en langetermijn-B-LT- en B-LTA-handtekeningen. De alleen-lezen handtekeninginspectie en de ValidatePades-classificator die hier worden getoond, maken deel uit van de PDFium-component voor Delphi, C++Builder en Lazarus