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
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
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
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