Technický článek

Inspekce podpisů PDF a úrovní PAdES v Delphi

Obdrželi jste podepsané PDF a potřebujete ve svém prohlížeči zobrazit, kdo jej podepsal, kdy bylo podepsáno, zda podpis pokrývá celý soubor a jak daleko sahá z hlediska dlouhodobé shody (compliance). Komponenta PDFium Component pro Delphi a Lazarus odpovídá na všechny čtyři otázky pomocí volání pouze pro čtení: FPDF_GetSignatureCount a rodina FPDFSignatureObj_* vystavují slovník podpisu a TPdf.ValidatePades klasifikuje základní úroveň PAdES. Toto je první ze tří článků o podpisech PDF s PDFium; dva následující pokrývají vytvoření podpisu B-B a přidání dlouhodobých časových razítek. Jedna hranice však patří hned na začátek: všechno zde je inspekce a čtení toho, co podpis tvrdí, je jiná práce než ověřování jeho kryptografie nebo rozhodování o tom, zda důvěřujete podepisující osobě

Proč slovník podpisu PDF není jen shluk bajtů

Podpis PDF je slovník, nikoli neprůhledná příloha, a jeho dva nejdůležitější záznamy vám řeknou, kolik ze souboru je skutečně chráněno. Specifikace ISO 32000-1 §12.8 definuje slovník podpisu s položkami /ByteRange a /Contents. /Contents obsahuje hexadecimálně kódovanou strukturu CMS SignedData (RFC 5652), což je kryptografická obálka, která nese certifikát podepisujícího, podepsané atributy a samotnou hodnotu podpisu. /ByteRange je část, kterou vývojáři podceňují: je to pole dvou rozsahů offset-délka, které dohromady pokrývají celý soubor kromě hexadecimálního řetězce /Contents. Tato mezera je přesně místem, kde leží bajty podpisu, a dva rozsahy na obou stranách jsou přesně tím, k čemu se podpis zavazuje

Návrh ByteRange je to, co činí inkrementální ukládání auditovatelným. Protože podepisující nemůže hashovat bajty podpisu, které ještě neexistují, soubor se rozdělí kolem zástupného symbolu /Contents a vše ostatní se zahashuje do podpisu. Podpis, hisž ByteRange nedosahuje na konec souboru, je varovným signálem: obsah připojený po pokrytém rozsahu prostřednictvím pozdější inkrementální aktualizace by podpis neporušil, i když mění to, co čtenář vidí. Takže první věc, kterou seriózní inspektor kontroluje, není to, kdo podepsal, ale zda podpis pokrývá bajty, které se zdá, že schvaluje

PDFium vystavuje podepsané PDF jako slovník podpisu, jehož dva úseky /ByteRange hašují celý soubor kolem hex řetězce CMS /Contents, zatímco bajty připojené po podpisu zůstávají nepokryté
Dva úseky /ByteRange hashují vše kolem hex řetězce /Contents, takže připojené bajty změní to, co čtečky vidí, aniž by zlomily podpis

Čtení slovníku podpisu pomocí rozhraní API PDFium pouze pro čtení

Komponenta PDFium zpřístupňuje slovník podpisu prostřednictvím dvou členů pouze pro čtení: SignatureCount a záznamu Signature[Index]. Pod kapotou volají FPDF_GetSignatureCount, FPDF_GetSignatureObject a přístupové rutiny FPDFSignatureObj_* pro /SubFilter, /ByteRange, /Contents, /Reason a čas podpisu. Fráze „pouze pro čtení“ je zde klíčová: PDFium dokáže podpisy vyjmenovat a číst, ale nemá žádné API pro jejich vytváření nebo zápis, což je důvod, proč je podepisující strana této série implementována samotnou knihovnou, a nikoli 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);            // řetězec data podepisujícího, např. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // nezpracovaná data DER SignedData převzatá z /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = bez certifikace, 1..3 = úroveň MDP
    end;
  finally
    Pdf.Free;
  end;
end;

Každý záznam TPdfSignature se mapuje přímo na slovník. Encoding je /SubFilter, což je jediné nejvíce diagnostické pole, protože pojmenovává obsluhu podpisu a okamžitě odděluje moderní podpis ETSI.CAdES.detached od zastaralého nebo zakázaného. Time je autorem deklarovaný čas podpisu jako řetězec data PDF, což je tvrzení podepisujícího a nikoli důvěryhodný čas. Content jsou nezpracovaná data CMS SignedData a Permission vystavuje úroveň certifikace DocMDP (0 pro běžný podpis schválení, 1 až 3 pro podpis certifikace, který uzamyká pozdější změny). Jediné pole, které záznam nezpřístupňuje, je analyzovaný ByteRange, a toto vynechání je záměrné, protože ValidatePades provádí aritmetiku pokrytí ByteRange za vás, místo aby vás nutilo dělat to ručně

Jaký je rozdíl mezi PAdES B-B, B-T, B-LT a B-LTA?

Čtyři základní úrovně PAdES tvoří žebříček od minimálně platného podpisu až po podpis vytvořený tak, aby přežil desetiletí archivace, přičemž každá úroveň striktně obsahuje úroveň pod ní. Specifikace ETSI EN 319 142-1 je definuje jako B-B, B-T, B-LT a B-LTA. B-B (Basic) je podpis plus povinné podepsané atributy a nic víc. B-T (Timestamp) přidává důvěryhodné časové razítko RFC 3161 nad podpis, takže okamžik podepsání je potvrzen autoritou časového razítka, místo aby byl deklarován hodinami podepisujícího. B-LT (Long-Term) integruje validační materiály — řetězec certifikátů a volitelně odpovědi OCSP nebo CRL — do souboru, takže podpis lze stále validovat i po letech, kdy je vydávající infrastruktura pryč. B-LTA (Long-Term s archivním časovým razítkem) obaluje tyto materiály časovým razítkem dokumentu, čímž chrání samotná dlouhodobá data a dává vám bod pro opětovné orazítkování dříve, než podkladová kryptografie zastará

Praktický výklad je o časovém horizontu. Podpis B-B odpovídá na otázku „podepsal to někdo“. B-T odpovídá „a kdy, prokazatelně“. B-LT odpovídá „a mohu to stále zkontrolovat po vypršení platnosti certifikátů“. B-LTA odpovídá „a bude tato kontrola platit i za dvacet let“. Regulační profily si vybírají úroveň: mnoho kontextů elektronické fakturace a eIDAS vyžaduje alespoň B-T a archivační mandáty sahají po B-LT nebo B-LTA. Vědět, na kterou úroveň dokument skutečně dosáhne předtím, než jej přijmete nebo odmítnete, je celým smyslem kroku inspekce

Žebříček základních úrovní PAdES stoupá od B-B přes B-T a B-LT po B-LTA, každá úroveň přidává časová razítka nebo materiál dlouhodobého ověřování, které TPdf.ValidatePades dokáže v Delphi odhalit
Každá úroveň PAdES baseline striktně obsahuje tu pod ní a ValidatePades hlásí, na kterou příčku dokument skutečně dosáhne

Detekce základní úrovně pomocí TPdf.ValidatePades

Komponenta PDFium redukuje celou otázku úrovně na jediné volání. TPdf.ValidatePades vrací záznam TPadesValidationResult, jehož pole Level je výčtového typu TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT nebo plB_LTA — spolu se sadou problémů (issues), počtem podpisů a počtem časových razítek dokumentu. Úroveň se odvozuje monotónně: validátor nejprve stanoví B-B, poté povýší na B-T, pokud je přítomno časové razítko podpisu nebo dokumentu, na B-LT, pokud katalog obsahuje /DSS s certifikáty a identifikátor úrovně 1 /Extensions /ESIC, a na B-LTA, pokud je přítomno časové razítko dokumentu i identifikátor úrovně 2 ESIC. Dva pomocníci činí výsledek použitelným: IsCompliant je True pouze tehdy, když úroveň dosáhne alespoň B-B a sada problémů je prázdná, a IsCompliantAt vám umožňuje prosadit minimální úroveň zásad (policy floor), jako je 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   (základní)');
      plB_T:     Writeln('PAdES B-T   (důvěryhodné časové razítko)');
      plB_LT:    Writeln('PAdES B-LT  (integrovaný dlouhodobý materiál)');
      plB_LTA:   Writeln('PAdES B-LTA (archivní časové razítko)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Splňuje minimální úroveň B-T')
    else
      Writeln('Pod požadovanou úrovní B-T');
  finally
    Pdf.Free;
  end;
end;

Proč je adbe.pkcs7.sha1 zakázaným SubFilterem?

Protože SHA-1 je prolomený a obsluha adbe.pkcs7.sha1 jej obsahuje v sobě. Tento SubFilter předběžně zahashuje dokument pomocí SHA-1 před jeho zabalením do PKCS#7 a SHA-1 je již léta zranitelný vůči kolizím, takže norma EN 319 142-1 odstavec 6.3 jej pro základní podpis přímo zakazuje. ValidatePades vyvolá ppeiForbiddenSubFilter, když uvidí adbe.pkcs7.sha1 nebo adbe.x509.rsa_sha1, a vyvolá ppeiBadDigestAlgorithm, když samotný CMS používá MD5 nebo SHA-1 jako otisk zprávy (odstavec 6.2.1). Jedná se o dvě odlišné kontroly, které zachycují stejnou třídu slabin ve dvou různých vrstvách

Sada problémů (issues) má celkem 26 členů a ty, se kterými se setkáte nejčastěji, se točí kolem struktury a pokrytí. ppeiByteRangeNotCoveringFile je dříve popsaná kontrola pokrytí. ppeiForbiddenCertKey se spustí, když slovník podpisu obsahuje položku /Cert, kterou PAdES zakazuje, protože řetězec musí místo toho žít uvnitř CMS SignedData.certificates. ppeiMissingSigningCertificate, ppeiMissingContentType a ppeiMissingMessageDigest označují chybějící povinné podepsané atributy a ppeiDetachedContentViolation zachycuje podpis, který nesprávně integruje podepsaný obsah, místo aby jej oddělil. Vyjmenování této sady promění pouhé odmítnutí v diagnózu, kterou můžete zaznamenat do logu

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;

Co ValidatePades nekontroluje

ValidatePades validuje strukturu, nikoli důvěryhodnost, a záměna jednoho za druhé je nebezpečnou chybou. Výsledek plB_LTA znamená, že dokument obsahuje správně formátovaný podpis B-LTA se všemi povinnými atributy, materiály a časovými razítky na správných místech — neznamená to však, že podpis je kryptograficky platný, že se certifikát řetězí k rootu, kterému důvěřujete, nebo že žádný certifikát v řetězci nebyl odvolán. Validátor záměrně neprovádí žádné kryptografické ověřování: nepřepočítává podpis nad ByteRange, nesestavuje ani nevyhodnocuje řetězec důvěry a nekontroluje stav odvolání OCSP nebo CRL. Toto rozdělení je záměrné a užitečné, protože strukturální inspekce je rychlá, plně deterministická a nevyžaduje žádné klíče, síť ani kryptografii platformy, takže ValidatePades běží identicky na Windows, Linuxu a macOS jako čistý Pascal nad bajty souboru. Validace řetězce důvěry je naproti tomu neoddělitelná od zásad (policy) — kterým rootům důvěřujete, jak získáváte odvolání, jak přísná je vaše tolerance časových razítek — a patří do pozdější fáze, která závisí na úložišti certifikátů platformy, takže berte úspěšný průchod ValidatePades jako nezbytnou bránu potvrzující, že podpis má správný tvar, a poté structurally bezvadný podpis předejte ke skutečnému kryptografickému ověření, než se na něj spolehnete

TPdf.ValidatePades zkontroluje v Delphi strukturu PAdES jako SubFilter, pokrytí ByteRange a časová razítka a pak podá strukturně zdravý podpis oddělené kryptografické verifikaci důvěry
Strukturální prohlídka je rychlá a deterministická, zatímco důvěra v řetěz a revokace patří do pozdější fáze ověření

Tento strukturální průchod je tím správným místem, kde začít, a přirozeně se doplňuje s širšími kontrolami pouze pro čtení v auditu bezpečnostních rizik PDF s komponentou PDFium a s prací na shodě formátů, jako je validace dokumentů PDF/X připravených k tisku. Jakmile dokážete podpis přečíst a klasifikovat, dalším krokem je jeho vytvoření: druhý článek v této sérii se zabývá podepisováním PDF pomocí PAdES B-B a třetí to rozšiřuje na důvěryhodná časová razítka a dlouhodobé podpisy B-LT a B-LTA. Inspekce podpisů pouze pro čtení a klasifikátor ValidatePades zde ukázané jsou součástí komponenty PDFium Component pro Delphi, C++Builder a Lazarus