Teknisk artikel

Undersøg PDF-signaturer og PAdES-niveauer i Delphi

Du har modtaget en signeret PDF-fil, og du skal i din fremviser vise, hvem der signerede den, hvornår den blev signeret, om signaturen dækker hele filen, og hvor tæt den er på langsigtede overholdelseskrav. PDFium-komponenten til Delphi og Lazarus besvarer alle fire spørgsmål med skrivebeskyttede kald: FPDF_GetSignatureCount og FPDFSignatureObj_*-familien blotlægger signatur-ordbogen, og TPdf.ValidatePades klassificerer PAdES-basisniveauet (baseline level). Dette er den første af tre artikler om PDF-signaturer med PDFium; de to efterfølgende dækker oprettelsen af en B-B-signatur og tilføjelse af langsigtede tidsstempler. Én grænse skal dog trækkes på forhånd: Alt her handler om inspektion, og at læse, hvad en signatur påstår, er en anden opgave end at verificere dens kryptografi eller beslutte, om du stoler på signatøren

Hvorfor en PDF-signaturordbog ikke bare er en klump bytes

En PDF-signatur er en ordbog (dictionary), ikke en uigennemsigtig vedhæftning, og dens to vigtigste indgange fortæller dig, hvor meget af filen der reelt er beskyttet. ISO 32000-1 §12.8 definerer signaturordbogen med en /ByteRange- og en /Contents-indgang. /Contents indeholder en hex-kodet CMS SignedData-struktur (RFC 5652), den kryptografiske kuvert, der bærer signatørens certifikat, de signerede attributter og selve signaturværdien. /ByteRange er den del, udviklere undervurderer: Det er et array af to offset-længde-spænd, der tilsammen dækker hele filen undtagen hex-strengen for /Contents. Dette hul er præcis, hvor signatur-bytes sidder, og de to spænd på hver side er præcis det, signaturen forpligter sig til

ByteRange-designet er det, der gør en trinvis lagring (incremental save) auditerbar. Da en signatør ikke kan hashe signatur-bytes, der endnu ikke eksisterer, deles filen op omkring pladsholderen for /Contents, og alt andet hashes ind i signaturen. En signatur, hvis ByteRange ikke når slutningen af filen, er et advarselstegn: Indhold, der tilføjes efter det dækkede område via en senere trinvis opdatering, vil ikke bryde signaturen, selvom det ændrer, hvad læseren ser. Så det første, en seriøs inspektør tjekker, er ikke, hvem der har signeret, men om signaturen rent faktisk dækker de bytes, den ser ud til at godkende

Læsning af signaturordbogen med PDFiums skrivebeskyttede API

PDFium-komponenten eksponerer signaturordbogen via to skrivebeskyttede medlemmer: SignatureCount og recorden Signature[Index]. Under motorhjelmen kalder de FPDF_GetSignatureCount, FPDF_GetSignatureObject og FPDFSignatureObj_*-adgangsfunktionerne (accessors) for /SubFilter, /ByteRange, /Contents, /Reason samt signeringstidspunktet. Skrivebeskyttet (read-only) er nøgleordet her: PDFium kan tælle og læse signaturer, men har ingen API til at oprette eller skrive en, hvilket er grunden til, at signeringsdelen i denne serie implementeres af selve biblioteket frem for 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);            // 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;

Hvad er forskellen på PAdES B-B, B-T, B-LT og B-LTA?

De fire PAdES-basisniveauer (baseline levels) danner en stige fra en minimalt gyldig signatur til en, der er bygget til at overleve årtiers arkivering, og hvert niveau indeholder fuldt ud det foregående. 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 andet. B-T (Timestamp) tilføjer et betroet RFC 3161-tidsstempel over signaturen, så signeringsøjeblikket bekræftes af en tidsstempelmyndighed i stedet for at være erklæret af signatørens eget ur. B-LT (Long-Term) indlejrer valideringsmaterialet — certifikatkæden og eventuelt OCSP- eller CRL-svar — inde i filen, så signaturen stadig kan valideres år efter, at den udstedende infrastruktur er væk. B-LTA (Long-Term med arkivtidsstempel) pakker et dokumenttidsstempel omkring dette materiale, hvilket beskytter selve de langsigtede data og giver dig mulighed for at gen-tidsstemple, før den underliggende kryptografi forældes

Den praktiske tolkning handler om tidshorisonten. En B-B-signatur svarer på: "Har nogen signeret dette?". B-T svarer på: "Og hvornår, beviseligt?". B-LT svarer på: "Og kan jeg stadig kontrollere det, efter certifikaterne er udløbet?". B-LTA svarer på: "Og vil denne kontrol stadig holde om tyve år?". Lovgivningsmæssige profiler vælger et trin: Mange e-fakturerings- og eIDAS-kontekster kræver mindst B-T, og arkiveringsmandater rækker ud efter B-LT eller B-LTA. At vide, hvilket trin et dokument reelt når, før du accepterer eller afviser det, er hele formålet med inspektionstrinnet

Detektering af basisniveauet med TPdf.ValidatePades

PDFium-komponenten reducerer hele spørgsmålet om niveau til et enkelt kald. TPdf.ValidatePades returnerer en TPadesValidationResult-record, hvis Level-felt er en TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT eller plB_LTA — sammen med et sæt af problemer (issues), et signaturantal og et dokumenttidsstempelantal. Niveauet udledes monotont: Valideringen etablerer B-B først og forfremmer derefter til B-T, hvis et signaturtidsstempel eller et dokumenttidsstempel er til stede, til B-LT, hvis kataloget indeholder en /DSS med certifikater og Level 1-markøren /Extensions /ESIC, og til B-LTA, hvis et dokumenttidsstempel og ESIC Level 2-markøren begge er til stede. To hjælpere gør resultatet anvendeligt: IsCompliant er sand, når niveauet når mindst B-B og issues-sættet er tomt, og IsCompliantAt lader dig håndhæve en minimumspolitik som f.eks. plB_T

Hvorfor er adbe.pkcs7.sha1 et forbudt SubFilter?

Fordi SHA-1 er usikker, og adbe.pkcs7.sha1-handleren indbygger den direkte. Dette SubFilter pre-hasher dokumentet med SHA-1, før det pakkes ind i PKCS#7, og da SHA-1 har været sårbar over for kollisioner i årevis, forbyder EN 319 142-1 klausul 6.3 det fuldstændigt til en basissignatur. ValidatePades udløser ppeiForbiddenSubFilter, når den ser adbe.pkcs7.sha1 eller adbe.x509.rsa_sha1, og den udløser ppeiBadDigestAlgorithm, når selve CMS'en bruger MD5 eller SHA-1 som besked-digest (klausul 6.2.1). Dette er to adskilte kontroller, der fanger samme klasse af svagheder på to forskellige niveauer

Sættet af problemer har i alt 26 medlemmer, og dem, du oftest vil møde, klynger sig omkring struktur og dækning. ppeiByteRangeNotCoveringFile er dækningskontrollen beskrevet tidligere. ppeiForbiddenCertKey udløses, når signaturordbogen indeholder en /Cert-indgang, hvilket PAdES forbyder, fordi kæden i stedet skal ligge inde i CMS'ens 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 (detache) det. Gennemgang af sættet forvandler en simpel 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 kontrollerer

ValidatePades validerer struktur, ikke tillid, og at forveksle de to er en farlig 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 på de rigtige steder — det betyder ikke, at signaturen er kryptografisk gyldig, at certifikatet fører tilbage til en rod, du stoler på, eller at intet certifikat i kæden er blevet tilbagekaldt. Valideringen udfører bevidst ingen kryptografisk verifikation: Den genberegner ikke signaturen over ByteRange, opbygger eller evaluerer ikke tillidskæden og kontrollerer ikke OCSP- eller CRL-tilbagekaldelsestilstande. Denne opdeling er tilsigtet og nyttig, da strukturel inspektion er hurtig, fuldstændig deterministisk og ikke kræver nøgler, netværk eller platformkryptografi, 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 tilbagekaldelser, hvor streng din tolerance for tidsstempler er — og den hører hjemme på et senere stadie, der afhænger af platformens certifikatlager. Betragt derfor en bestået ValidatePades som den nødvendige indgangsport til, at en signatur er formet korrekt, og overlad derefter en strukturelt sund signatur til egentlig kryptografisk verifikation, før du stoler på den

Denne strukturelle kontrol er det rigtige sted at starte, og den passer naturligt sammen med de bredere skrivebeskyttede tjek i revision af PDF-sikkerhedsrisici med PDFium-komponenten og med format-overholdelsesarbejde såsom validering af printklare PDF/X-dokumenter. Når du kan læse og klassificere en signatur, er næste trin at producere en: Den anden artikel i denne serie dækker signering af PDF-filer med PAdES B-B, og den tredje udvider dette til betroede tidsstempler og langsigtede B-LT- og B-LTA-signaturer. Den skrivebeskyttede signaturinspektion og ValidatePades-klassificeringen vist her er en del af PDFium-komponenten til Delphi, C++Builder og Lazarus