Technický článek

Ověřování digitálních podpisů PDF v Delphi pomocí HotPDF

Knihovna HotPDF ověřuje digitální podpisy v načtených dokumentech PDF prostřednictvím tří metod THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature a VerifyLoadedSignatureEx, představených ve verzi v2.259.0. Komponenta přepočítává hashe segmentů /ByteRange původního souboru, kontroluje atribut CMS messageDigest a spouští ověření RSA PKCS#1 v1.5 vůči vloženému certifikátu podepisující osoby. Pokud jsou bajty dokumentu neporušené, vrací hodnotu svValid

Scénář je to všední, ale sázka je vysoká. Druhá strana vrátí podepsanou smlouvu, váš systém ji potřebuje zaevidovat a někdo položí jedinou otázku, na které záleží: je to přesně ten dokument, který jsme odeslali, bajt po bajtu, podepsaný deklarovaným certifikátem? Odpověď v kódu představuje ověřovací stranu příběhu o podpisech; strana podepisování — tedy především vytváření a vkládání podpisů PAdES — je popsána v doprovodném článku o vytváření digitálních podpisů PAdES v HotPDF. Tento článek se zabývá opačným směrem: PDF přichází již podepsané a vy chcete programový verdikt namísto snímku zeleného zatržítka z programu Acrobat

Jak podepsané PDF prokazuje, že s ním nebylo manipulováno?

Podpis PDF chrání konkrétní rozsahy bajtů souboru, nikoli abstraktní pojem „dokument“. Norma ISO 32000-1 §12.8 definuje tento mechanismus: pole formuláře podpisu nese slovník, jehož položka /Contents obsahuje kontejner CMS SignedData (RFC 5652) a jehož pole /ByteRange definuje přesné oblasti souboru, které podpis pokrývá, podle §12.8.1. Toto pole je seznamem dvojic offsetů a délek, v praxi jde o dva segmenty: vše před hexadecimálním řetězcem /Contents a vše za ním. Hodnota podpisu nemůže pokrýt samu sebe, takže se soubor zahashuje okolo této mezery

Tento návrh má důsledek, který formuje celé rozhraní API: ověření musí zahashovat původní serializované bajty přesně tak, jak leží na disku. Analyzovaný objektový model je pro tento účel nepoužitelný, protože opětovná serializace i nezměněného dokumentu produkuje odlišné bajty. HotPDF proto provádí ověření vůči zdrojovému souboru, ze kterého byl dokument načten, nebo vůči streamu TStream surových bajtů, který dodáte, nikdy ne vůči jeho reprezentaci v paměti

Čtení metadat podpisu před samotným ověřením

Metoda GetLoadedSignatureInfo analyzuje slovník podpisu a jeho kontejner CMS bez nutnosti dotknout se byť jediného bajtu dokumentu, což z ní dělá správné první volání, pokud potřebujete pouze zobrazit, kdo a kdy dokument podepsal. Pole podpisů jsou indexována od 0 v pořadí polí formuláře a metoda GetLoadedSignatureFieldCount vrací jejich celkový počet. Vrácený záznam THPDFSignatureInfo nese název pole, /SubFilter, obecný název (common name) certifikátu podepisujícího, rozlišená jména (distinguished names) subjektu a vydavatele, sériové číslo, data platnosti, čas podpisu (z podepsaného atributu, je-li přítomen, jinak z položky /M slovníku), název hashovacího algoritmu a řetězce /Reason, /Location a /ContactInfo. Člen Status zůstává nastaven na svNotVerified, což je korektní označení pro „analyzováno, nezkontrolováno“

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Spuštění kryptografické kontroly

Metoda VerifyLoadedSignatureEx provádí kompletní ověření pro dokument načtený ze souboru a vrací vyplněný záznam informací v jediném volání: znovu otevře zdrojový soubor, zahashuje segmenty /ByteRange pomocí algoritmu otisku SignerInfo, porovná výsledek s podepsaným atributem messageDigest (RFC 5652 §5.4) a poté provede ověření RSA nad novým kódováním DER SET podepsaných atributů. Pokud podpis neobsahuje žádné podepsané atributy, spouští se kontrola RSA přímo nad hashem dokumentu. Podporovány jsou podpisy RSA PKCS#1 v1.5 s algoritmy SHA-1, SHA-256, SHA-384 nebo SHA-512, což pokrývá subfiltry adbe.pkcs7.detached a ETSI.CAdES.detached generované běžnými podpisovými nástroji

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Dva detaily implementace stojí za to znát, protože vysvětlují selhání, která zvenčí vypadají záhadně. Za prvé, kontrola podepsaných atributů je náročná na kódování: uvnitř souboru jsou atributy označeny jako [0] IMPLICIT, ale podpis byl vypočítán nad jejich formou DER SET OF, takže ověřovatel před hashováním změní značky přesně tak, jak vyžaduje norma RFC 5652 §5.4. Vlastnoručně napsaný ověřovatel, který hashují bajty tak, jak se objevují v souboru, odmítne každý správně podepsaný dokument. Za druhé, položka /Contents bývá konvenčně vyplněna nulami (zero-padded) do vyhrazené velikosti, takže ověřovatel před analýzou zkrátí blok DER na skutečnou délku jeho vnější SEQUENCE; jako poškození vypadající koncové nuly jsou normálním jevem, nikoli poškozením. Stejná skupina rizik při analýze ASN.1 na straně importu certifikátů je tématem článku o posílení bezpečnosti PKCS#12 a ASN.1 in HotPDF

Co platný podpis skutečně garantuje?

Hodnota svValid znamená přesně toto: bajty definované v /ByteRange dávají při hashování hodnotu, kterou podepisující podepsal, a podpis se ověří pod veřejným klíčem certifikátu vloženého v kontejneru CMS. Jedná se o integritu bajtů a vazbu na klíč, nic víc. Ověřování řetězce certifikátů a důvěryhodnosti je výslovně mimo rozsah ověřovatele HotPDF: neprochází řetězec ke kořenovému certifikátu, nekontroluje odvolání (revokaci) ani nekonzultuje žádné úložiště důvěryhodných certifikátů. Certifikát s vlastním podpisem (self-signed) od útočníka, který znovu podepsal upravený dokument, se ověří jako svValid, protože matematika je interně konzistentní. To, zda je podepisující tím, za koho se vydává, a zda by mu měl kdekoli důvěrvat, je politické rozhodnutí, které patří do samostatné vrstvy, ať už je to seznam povolených certifikátů vaší organizace, úložiště certifikátů systému Windows nebo validační autorita

Příznak CoversWholeDocument hlídá jemnější mezeru. Podpis vždy pokrývá pouze své /ByteRange a mechanismus inkrementálních aktualizací PDF umožňuje připojovat obsah za podpis bez jeho zneplatnění, což je záměrné a umožňuje to fungování pracovních postupů s více podpisy. Příznak se počítá během ověřování a je pravdivý pouze tehdy, když oba segmenty plus mezera /Contents pokrývají celý soubor. Pokud se vrátí svValid s hodnotou CoversWholeDocument nastavenou na false, podepsaná revize je neporušená, ale soubor obsahuje pozdější dodatky, a zda tyto dodatky tolerovat, by měl rozhodnout váš pracovní postup

Dokumenty načtené ze streamu a šifrované dokumenty potřebují vlastní zdrojové bajty

Bezparametrické metody VerifyLoadedSignature a VerifyLoadedSignatureEx závisejí na tom, zda si komponenta pamatuje soubor, ze kterého dokument pochází. Pokud načtete dokument ze streamu, neexistuje žádný název souboru, který by se dal znovu otevřít; totéž platí po cestě opětovného načtení s heslem používané pro šifrované dokumenty, což je postup popsaný v článku o šifrování PDF pomocí AES-256 v HotPDF. V obou případech přetížené verze pracující se soubory vracejí hodnotu svSourceUnavailable namísto hádání. Řešením je přetížená verze s TStream, která vám umožní předat původní surové bajty odtud, kde je uchováváte — ze souboru, který stále máte, z paměťového bufferu nebo databázového blobu

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Stream-loaded document: the component holds no source
  // file name, so supply the original bytes yourself.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Hlášení toho, co nelze ověřit

Ověřovatel, který zná pouze stavy „platný“ a „neplatný“, bude nesprávně hlásit dokumenty, kterým pouze nerozumí, proto výčet stavů odděluje případy, které by mělo vaše uživatelské rozhraní rozlišovat. svDigestMismatch znamená, že se bajty dokumentu po podepsání změnily, což je klasický signál neoprávněné manipulace. svSignatureInvalid znamená, že hashe bajtů jsou správné, ale selhala kontrola RSA, což ukazuje na poškozenou nebo padělanou hodnotu podpisu. svUnsupportedAlgorithm je korektní odpověď pro klíče ECDSA a neznámé algoritmy otisků: podpis může být zcela v pořádku, HotPDF jej pouze nedokáže zkontrolovat a jeho nahlášení jako „neplatného“ by poškodilo bezvadný dokument. svMalformed označuje kontejner CMS, který nebylo možné vůbec analyzovat. Pro kontroly typu brány (gate-style) vrací VerifyAllLoadedSignatures hodnotu true pouze tehdy, když existuje alespoň jedno podpisové pole a každé z nich se ověří jako svValid, což představuje pohodlnou jedinou logickou hodnotu pro linku příjmu archivů, která odmítá cokoli menšího

Ověřování podpisů, podepisování PAdES, šifrování AES-256 a API pro úpravu načtených dokumentů jsou dodávány ve stejné nativní knihovně VCL pro Delphi a C++Builder bez externích závislostí na DLL; kompletní seznam funkcí a podporovaných verzí IDE naleznete na stránce produktu HotPDF Component