Technický článek

Podepisování PDF s PAdES B-B v Delphi s využitím PDFium

Komponenta PDFium Component podepisuje PDF digitálním podpisem PAdES B-B prostřednictvím své metody SignPades: načte dokument, zahashuje podepsaný rozsah bajtů, sestaví strukturu CAdES CMS a připojí podpis jako inkrementální aktualizaci. Kryptografický backend je určen pouze pro Windows, takže každý hovor před podpisem opatřete kontrolou PadesCryptoAvailable

Tato situace je známá. Smlouva v PDF přistane na vašem stole, právní oddělení požaduje její digitální podpis před odesláním a vy sáhnete po stejném sestavení PDFium, které již používáte k vykreslování a kontrole dokumentů, abyste zjistili, že PDFium podpis vůbec zapsat neumí. Jeho rozhraní API pro podpisy je striktně pouze pro čtení. Komponenta PDFium Component tuto mezeru uzavírá tím, že vlastní celé podepisovací potrubí (pipeline) v Pascalu, od hashovací funkce až po vkládání na úrovni bajtů, a tento článek toto potrubí prochází od začátku do konce

Proč PDFium neumí zapsat digitální podpis?

PDFium vystavuje podpisy jako objekty pouze pro čtení a nenabízí nic, co by je vytvořilo. Rodina FPDFSignatureObj_* vám umožňuje vyjmenovat stávající podpis, přečíst jeho /Contents a zkontrolovat jeho /ByteRange, ale neexistuje žádný protějšek, který by sestavil slovník podpisu, rezervoval slot /Contents nebo zapsal rozsah bajtů; inkrementální ukládání existuje (FPDF_SaveAsCopy s FPDF_INCREMENTAL), ale nenese žádný podepisovací háček. Jakkáli komponenta, která podepisuje PDF nad PDFium, proto musí vygenerovat každý bajt podpisu sama, což je důvod, proč komponenta PDFium Component sestavuje mechanismus ze tří čistě pascalských jednotek. FPC 3.2.2 obsahuje md5 a sha1, ale žádné SHA-2 vůbec, a API SHA-256 v delphijském System.Hash není zdrojově kompatibilní s FPC, takže FPdfSha256 je samostatná implementace FIPS 180-4, která udržuje každou kódovou cestu CMS na jednom typu TSHA256Digest bez jakéhokoli větvení překladače. FPdfAsn1 dodává kodér a čtečku DER, které struktury CMS potřebují, a FPdfCms sestavuje CAdES SignedData nad oběma

Jak digitálně podepsat PDF v Delphi?

Načtěte dokument a poté zavolejte SignPades s otiskem certifikátu (thumbprint). Komponenta PDFium Component vyhledá tento otisk v úložišti certifikátů Current User "MY", získá odpovídající certifikát a jeho soukromý klíč a zapíše podepsanou kopii na cestu, kterou zadáte

Diagram pipeline podepisování PAdES B-B v Delphi: PDFium Component vyzkoumá backend Windows CNG, hašuje podepsaný bajtový rozsah, postaví CAdES CMS a připojí inkrementální aktualizaci
PDFium Component vlastní celou pipeline PAdES B-B, od sondy platformy přes hash SHA-256 a stavbu CMS až po přírůstkové připojení
uses
  PDFium, FPdfCrypto;

procedure SignContract(const AThumbprint: string);
var
  Pdf: TPdf;
begin
  if not PadesCryptoAvailable then
    raise Exception.Create('Podepisovací backend PAdES je pouze pro Windows');

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';   // dokument k podpisu
    Pdf.Active := True;
    // Druhý argument: otisk SHA-1 certifikátu v úložišti Current
    // User "MY". První argument: cíl pro podepsanou kopii.
    if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

PadesCryptoAvailable je test, který kontrolujete jako první, pokaždé. Na Windows vrací True a backend crypt32/ncrypt je aktivní; na jakékoli jiné platformě vrací False a volání podepisování by vyvolalo výjimku EPadesCrypto. Považování této ochrany za povinnou brání tomu, aby sestavení pro Linux nebo macOS selhalo za běhu na cestě, která tam nemůže fungovat. Samotný otisk je SHA-1 otisk certifikátu, což je stejná hodnota, jakou zobrazuje správce certifikátů Windows na své kartě Podrobnosti, a označuje konkrétního podepisujícího bez toho, aby do vašeho zdrojového kódu vkládal materiály klíče

Co patří do CMS: podepsané atributy a RFC 5652

Základní podpis PAdES není hrubý podpis RSA nad souborem; je to struktura CAdES CMS SignedData nesoucí povinnou sadu podepsaných atributů a FPdfCms.BuildSignedData emituje přesně tuto sadu: content-type, message-digest a signing-certificate-v2, což je atribut ESS, který váže podpis k podepisovacímu certifikátu pomocí hashe. Jeden detail zde maří téměř každou ručně psanou implementaci CMS. Specifikace RFC 5652 odstavce 5.4 vyžaduje, aby se digest podepsaných atributů počítal nad kódováním DER SET OF, tag 0x31, zatímco tytéž atributy cestují uvnitř SignerInfo pod tagem IMPLICIT [0], 0xA0. Komponenta PDFium Component kóduje sadu atributů jednou, spočítá digest formy 0x31 a poté pro emisi přepíše pouze úvodní bajt tagu na 0xA0, takže jedna vyrovnávací paměť slouží oběma rolím bez druhého průchodu stromem

Diagram tagování signed atributů v PAdES CMS postaveném PDFium Component v Delphi: forma 0x31 SET OF se zahašuje, pak jen úvodní bajt se stane 0xA0 uvnitř SignerInfo
RFC 5652 §5.4 digestuje kódování SET OF s tagem 0x31, zatímco tytéž bajty atributu cestují uvnitř SignerInfo pod IMPLICIT [0] tagem 0xA0
var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;

    Opts := TPadesSignOptions.Default;
    Opts.CertificateThumbprint := 'a1b2c3d4e5f6...';  // podepisující v úložišti MY
    Opts.Reason := 'I approve this agreement';
    Opts.Location := 'Berlin, DE';
    Opts.ContentsSize := 16384;                        // hexadecimální šířka /Contents

    if not Pdf.SignPades('contract-signed.pdf', Opts) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

Přetížení voleb přidává metadata slovníku podpisu definovaná specifikací ISO 32000-1 odstavce 12.8.1: Reason, Location, ContactInfo a Name, všechna volitelná a všechna zapsaná do slovníku hodnoty podpisu. Jedno omezení je snadné přehlédnout. Pokud nastavíte CommitmentTypeOid pro přidání podepsaného atributu commitment-type-indication v CAdES, nenastavujte také Reason; norma ETSI EN 319 142-1 odstavec 6.3 zakazuje přenášet obojí, protože obě vyjadřují stejný záměr různými prostředky

Jak do sebe ByteRange a slot /Contents zapadají?

Podpis musí pokrývat celý soubor kromě bajtů, které drží samotný podpis, a PAdES tuto cykličnost řeší zástupným symbolem s pevnou šířkou, který SignPadesBytes spravuje přesně. Rezervuje hexadecimální řetězec /Contents o velikosti ContentsSize bajtů (výchozí hodnota 16384, pohodlně větší než typická struktura CMS SignedData), serializuje inkrementální aktualizaci pro zjištění přesného offsetu slotu a poté vypočítá /ByteRange jako dva rozsahy, které slot ohraničují — vše před otevíracím oddělovačem hexadecimálního řetězce a vše po jeho uzavíracím oddělovači. SHA-256 běží pouze nad těmito dvěma rozsahy. Hotový CMS se zakóduje hexadecimálně do vyhrazeného slotu, doplní nulami na pevnou šířku a připojí se aktualizace křížových odkazů. Vzhledem k tomu, že je šířka pevně nastavena předem, vyplnění slotu neposune žádný navazující bajt, což je celý důvod, proč rozsah bajtů zůstává platný; původní bajty dokumentu jsou zachovány verbatim, takže dřívější podpis na stejném souboru přežije neporušený, přesně jak vyžaduje inkrementální podepisování ISO 32000-1 odstavce 12.8.1

Diagram rozložení ByteRange PDF podepsaného PDFium Component v Delphi: dva hašované úseky sevřou vyhrazený hex slot Contents a připojenou aktualizaci křížových odkazů
Dva úseky ByteRange ohraničí hex slot pevné šířky, takže vyplnění podpisu nikdy neposune bajt, který digest už spočítal

Windows CNG backend a jeho limity

Komponenta PDFium Component podepisuje pouze ve Windows, a tato hranice je záměrná. FPdfCryptoWin dynamicky váže knihovny crypt32.dll a ncrypt.dll, čímž nepřidává žádnou závislost na DLL při kompilaci, a podepisovací řetězec je standardní CNG: otevřít úložiště MY, najít certifikát podle hashe, získat handle jeho soukromého klíče přes CryptAcquireCertificatePrivateKey a zavolat NCryptSignHash. Podporovány jsou RSA s PKCS#1 v1.5, RSA-PSS a ECDSA. ECDSA vyžaduje jednu úpravu, kterou ostatní nepotřebují, protože NCryptSignHash vrací nezpracovanou dvojici r-a-s dle IEEE P1363, zatímco CMS očekává DER SEQUENCE ECDSA-Sig-Value, takže backend ji překóduje podle normy RFC 5480

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
  Output: TFileStream;
begin
  if not PadesCryptoAvailable then
    Exit;   // na této platformě není podepisovací backend

  Opts := TPadesSignOptions.Default;
  Opts.CertificateThumbprint := ReadThumbprintFromConfig;

  Pdf := TPdf.Create(nil);
  Output := TFileStream.Create('contract-signed.pdf', fmCreate);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.SignPadesToStream(Output, Opts);
  finally
    Output.Free;
    Pdf.Free;
  end;
end;

Praktickým důsledkem je, že soukromý klíč musí žít v úložišti certifikátů Windows. Certifikát uložený v souboru PFX funguje až po jeho importu do úložiště Current User, v tomto bodě je jeho otisk hodnotou, kterou předáváte metodě SignPades. Toto vydání nemá žádnou cestu pro PKCS#11 nebo HSM a žádný backend pro soubory softwarových klíčů, takže když PadesCryptoAvailable vrací False, na tomto počítači prostě žádné podepisování k dispozici není

Kde PAdES B-B končí

PAdES B-B je základní linie, nejnižší úroveň ze čtyř úrovní PAdES: dokazuje, kdo podepsal, a že se bajty od té doby nezměnily, a nic dalšího. Podpis B-B nenese důvěryhodné časové razítko, takže nemůže dokázat, kdy k podepsání došlo, a neobsahuje žádná data o odvolání, takže validátor po letech musí sám získat řetězec certifikátů a jejich stav. Tyto mezery jsou přesně tím, co vyšší úrovně uzavírají. Když potřebujete čas podepsání, který auditor přijme, přidání časového razítka RFC 3161 a DSS pro dlouhodobé ověření posune podpis na B-T a výše; když chcete hotový podpis přečíst zpět a potvrdit, jaké úrovně dosáhl, inspekce podpisu PDF a jeho úrovně PAdES je doprovodným nástrojem; a před samotným podepsáním čehokoli vám audit PDF z hlediska bezpečnostních rizik řekne, pod co se chystáte podepsat

Metody SignPades zde ukázané se dodávají s komponentou PDFium Component pro Delphi a C++Builder, spolu s inspekcí podpisů pouze pro čtení, kterou PDFium poskytuje po vybalení z krabice