Teknisk artikel

Inspektera PDF-signaturer och PAdES-nivåer i Delphi

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 TPadesLevelplNone, 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