Technický článek

Ověření digitálních podpisů PDF v Delphi s HotPDF

HotPDF ověřuje digitální podpisy v načtených dokumentech PDF třemi metodami THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature a VerifyLoadedSignatureEx, zavedenými ve verzi v2.259.0. Komponenta znovu zahashuje segmenty /ByteRange původního souboru, zkontroluje atribut CMS messageDigest a spustí ověření RSA PKCS#1 v1.5 vůči vloženému certifikátu podepisujícího, přičemž vrací svValid, když jsou bajty dokumentu neporušené

Scénář je všední, sázky nikoli. Protistrana vrátí podepsanou smlouvu, váš pracovní postup ji potřebuje založit a někdo položí jedinou otázku, na které záleží: je to dokument, který jsme poslali, bajt po bajtu, podepsaný certifikátem, který uvádí? Odpověď na to v kódu je ověřovací stranou příběhu o podpisech; podepisovací strana, tedy sestavení a vložení podpisů PAdES, je pokryta v doprovodném článku o vytváření digitálních podpisů PAdES pomocí HotPDF. Tento článek je o opačném směru: PDF dorazí už podepsané a vy chcete programový verdikt, nikoli snímek obrazovky zeleného zatržítka Acrobatu

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

Podpis PDF chrání konkrétní rozsahy bajtů souboru, ne abstraktní pojem „dokument“. ISO 32000-1 §12.8 definuje mechanismus: pole formuláře pro podpis nese slovník, jehož položka /Contents obsahuje kontejner CMS SignedData (RFC 5652) a jehož pole /ByteRange podle §12.8.1 pojmenovává přesné oblasti souboru, které podpis pokrývá. Pole je seznam dvojic offsetu a délky, v praxi dva segmenty: vše před hexadecimálním řetězcem /Contents a vše za ním. Hodnota podpisu nemůže pokrývat sama sebe, takže soubor se hashuje kolem této díry

Tento návrh má důsledek, který formuje celé API: ověření musí hashovat původní serializované bajty přesně tak, jak leží na disku. Parsovaný objektový model je k tomu nepoužitelný, protože opětovná serializace i nezměněného dokumentu vytvoří jiné bajty. HotPDF proto ověřuje vůči zdrojovému souboru, ze kterého byl dokument načten, nebo vůči TStream se surovými bajty, který dodáte, nikdy vůči své reprezentaci v paměti

Diagram HotPDF ověřujícího podepsané PDF v Delphi opětovným hashováním dvou segmentů ByteRange zdrojového souboru kolem díry Contents, zatímco parsovaný model v paměti se nikdy nehashuje, protože opětovná serializace mění bajty
Ověření hashuje dva segmenty ByteRange zdrojových bajtů přesně tak, jak byly serializovány; parsovaný model v paměti je nepoužitelný, protože opětovná serializace i nezměněného dokumentu vytvoří jiné bajty

Čtení metadat podpisu před jakýmkoli ověřováním

GetLoadedSignatureInfo parsuje slovník podpisu a jeho kontejner CMS, aniž by se dotkla jediného bajtu dokumentu, což z ní dělá správné první volání, když potřebujete jen zobrazit, kdo a kdy podepsal. Pole podpisů se indexují od 0 v pořadí polí formuláře a GetLoadedSignatureFieldCount vám řekne, kolik jich existuje. Vrácený záznam THPDFSignatureInfo nese název pole, /SubFilter, běžný název certifikátu podepisujícího, rozlišovací názvy 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 algoritmu digestu a řetězce /Reason, /Location a /ContactInfo. Jeho člen Status zůstává svNotVerified, což je poctivé označení pro „parsová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

VerifyLoadedSignatureEx provede úplné ověření dokumentu načteného ze souboru a v jednom volání vrátí naplněný informační záznam: znovu otevře zdrojový soubor, zahashuje segmenty /ByteRange algoritmem digestu ze SignerInfo, porovná výsledek s podepsaným atributem messageDigest (RFC 5652 §5.4) a poté ověří podpis RSA nad DER SET překódováním podepsaných atributů. Když podpis nenese žádné podepsané atributy, kontrola RSA běží místo toho přímo nad hashem dokumentu. Podporované podpisy jsou RSA PKCS#1 v1.5 s digesty SHA-1, SHA-256, SHA-384 nebo SHA-512, což pokrývá subfiltry adbe.pkcs7.detached a ETSI.CAdES.detached produkované běžnými podepisovacími nástroji

Pipeline VerifyLoadedSignatureEx v HotPDF pro Delphi: znovu otevřít zdrojový soubor, zahashovat segmenty ByteRange, porovnat s podepsaným atributem CMS messageDigest, poté ověřit RSA PKCS#1 v1.5 s výsledkem svValid, svDigestMismatch nebo svSignatureInvalid
VerifyLoadedSignatureEx znovu zahashuje segmenty ByteRange, porovná je s podepsaným atributem messageDigest a ověří RSA nad DER SET podepsaných atributů, než ohlásí svValid nebo konkrétní stav selhání
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 implementační detaily stojí za to znát, protože vysvětlují selhání, která zvenčí vypadají záhadně. Zaprvé, kontrola podepsaných atributů je vybíravá ohledně 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í tag přesně tak, jak vyžaduje RFC 5652 §5.4. Ručně napsaný ověřovatel, který hashuje bajty tak, jak se objevují v souboru, odmítne každý správně podepsaný dokument. Zadruhé, /Contents je konvenčně doplněn nulami na rezervovaný rozpočet bajtů, takže ověřovatel před parsováním ořízne blob DER na skutečnou délku jeho vnější SEQUENCE; koncové nuly vypadající jako smetí jsou normální, ne poškození. Stejná rodina rizik parsování ASN.1 na straně importu certifikátů je předmětem článku o bezpečnostním zpevnění PKCS#12 a ASN.1 v HotPDF

Co platný podpis ve skutečnosti zaručuje?

svValid znamená přesně toto: bajty pojmenované v /ByteRange se hashují na hodnotu, kterou podepisující podepsal, a podpis se ověří pod veřejným klíčem certifikátu vloženého v kontejneru CMS. To je integrita bajtů plus vazba na klíč a nic víc. Validace řetězce certifikátů a důvěry je explicitně mimo rozsah ověřovatele HotPDF: neprochází řetězec ke kořeni, nekontroluje odvolání ani nekonzultuje žádné úložiště důvěry. Vlastnoručně podepsaný certifikát útočníka, který znovu podepsal upravený dokument, se ověří jako svValid, protože matematika je vnitřně konzistentní. Zda je podepisující tím, za koho se vydává, a zda mu má kdokoli důvěřovat, je rozhodnutí o politice, které patří do samostatné vrstvy, ať už je to whitelist certifikátů vaší organizace, úložiště certifikátů Windows nebo validační autorita

Příznak CoversWholeDocument hlídá jemnější mezeru. Podpis vždy pokrývá jen svůj /ByteRange a mechanismus inkrementálních aktualizací PDF umožňuje připojit obsah za podpis, aniž by jej zneplatnil, což je záměr a takto fungují pracovní postupy s více podpisy. Příznak se počítá během ověření a je pravdivý jen tehdy, když dva segmenty plus mezera /Contents pokrývají celý soubor. Když svValid dorazí s CoversWholeDocument nastaveným na false, podepsaná revize je neporušená, ale soubor obsahuje pozdější doplnění, a co tato doplnění změnila, je něco, o čem by měl váš pracovní postup rozhodnout, zda to bude tolerovat

Diagram rozsahu toho, co svValid zaručuje při ověření podpisu v HotPDF: integrita bajtů ByteRange a vazba na klíč, zatímco procházení řetězce, odvolání a úložiště důvěry zůstávají mimo rozsah a CoversWholeDocument označuje připojené inkrementální aktualizace
svValid znamená integritu bajtů plus vazbu na klíč a nic víc; procházení řetězce, odvolání a úložiště důvěry patří do samostatné vrstvy politiky a CoversWholeDocument=false signalizuje bajty připojené po podpisu

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

Bezparametrické VerifyLoadedSignature a VerifyLoadedSignatureEx závisí na tom, že si komponenta pamatuje, ze kterého souboru dokument pochází. Načtěte dokument ze streamu a není žádný název souboru, který by šlo znovu otevřít; totéž platí po cestě opětovného načtení s heslem používané pro šifrované dokumenty, tedy pracovním postupu popsaném v článku o šifrování PDF pomocí AES-256 s HotPDF. V obou případech přetížení opírající se o soubor vrátí svSourceUnavailable, místo aby hádalo. Řešením je přetížení s TStream, které vám umožní předat původní surové bajty odkudkoli, kde jste je uchovali, ze souboru, který stále máte, z paměťového bufferu, z blobu v databázi

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Dokument načtený ze streamu: komponenta nedrží žádný název
  // zdrojového souboru, proto původní bajty dodejte sami.
  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á jen „platný“ a „neplatný“, bude chybně hlásit dokumenty, kterým pouze nerozumí, proto výčet stavů odděluje případy, které by vaše uživatelské rozhraní mělo rozlišovat. svDigestMismatch znamená, že se bajty dokumentu po podpisu změnily, klasický signál manipulace. svSignatureInvalid znamená, že se bajty hashují správně, ale kontrola RSA selhala, což ukazuje na poškozenou nebo padělanou hodnotu podpisu. svUnsupportedAlgorithm je poctivá odpověď pro klíče ECDSA a nerozpoznané digesty: podpis může být naprosto v pořádku, HotPDF jej prostě nedokáže zkontrolovat, a hlásit to jako „neplatný“ by pomlouvalo zdravý dokument. svMalformed označuje kontejner CMS, který se vůbec nepodařilo parsovat. Pro kontroly typu brány vrací VerifyAllLoadedSignatures true jen tehdy, když existuje alespoň jedno pole podpisu a každé z nich se ověří jako svValid, což je pohodlný jediný boolean pro pipeline příjmu do archivu, který odmítne cokoli menšího

Ověřování podpisů, podepisování PAdES, šifrování AES-256 a API pro úpravy načtených dokumentů jsou součástí téže nativní knihovny VCL pro Delphi a C++Builder, bez závislostí na externích DLL; úplný seznam funkcí a podporovaných verzí IDE najdete na produktové stránce komponenty HotPDF pro Delphi