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

Č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

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

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