Komponent PDFium Component podpisuje PDF pomocou digitálneho podpisu PAdES B-B prostredníctvom svojej metódy SignPades: načíta dokument, zahašuje podpísaný rozsah bajtov, zostaví štruktúru CAdES CMS a pripojí podpis ako prírastkovú aktualizáciu. Kryptografický backend je dostupný len pre Windows, preto pred podpisovaním zabezpečte každé volanie pomocou PadesCryptoAvailable
Situácia je známa. Na stole vám pristane zmluva vo formáte PDF, právne oddelenie ju chce pred odoslaním digitálne podpísať a vy siahnete po rovnakom builde PDFium, aký už používate na vykresľovanie a kontrolu dokumentov, len aby ste zistili, že PDFium podpis vôbec nedokáže zapísať. Jeho podpisové API je striktne len na čítanie. Komponent PDFium Component vypĺňa túto medzeru tým, že vlastní celú podpisovaciu pipeline v Pascale, od hašovacej funkcie až po vstrekovanie na úrovni bajtov, a tento článok prechádza touto pipeline od začiatku do konca
Prečo PDFium nedokáže zapísať digitálny podpis?
PDFium vystavuje podpisy ako objekty len na čítanie a neponúka nič na ich vytvorenie. Rodina funkcií FPDFSignatureObj_* vám umožňuje enumerovať existujúci podpis, čítať jeho /Contents a kontrolovať jeho /ByteRange, ale neexistuje žiadny náprotivok, ktorý by zostavil slovník podpisu, vyhradil slot /Contents alebo zapísal rozsah bajtov. Prírastkové ukladanie síce existuje (FPDF_SaveAsCopy s príznakom FPDF_INCREMENTAL), ale neobsahuje žiadny hook pre podpisovanie. Akýkoľvek komponent, ktorý podpisuje PDF nad nástrojom PDFium, preto musí každý bajt podpisu generovať sám, čo je dôvod, prečo PDFium Component stavia tento mechanizmus na troch čistých jednotkách v Pascale. FPC 3.2.2 sa dodáva s md5 a sha1, ale bez akejkoľvek podpory SHA-2, a rozhranie API SHA-256 z System.Hash v Delphi nie je zdrojovo kompatibilné s FPC. Preto je FPdfSha256 samostatnou implementáciou normy FIPS 180-4, ktorá udržiava každú cestu kódu CMS na jednom type TSHA256Digest bez vetvenia kompilátora. FPdfAsn1 poskytuje DER kodér a čítačku, ktoré štruktúry CMS potrebujú, a FPdfCms zostavuje CAdES SignedData nad oboma
Ako digitálne podpísať PDF v Delphi?
Načítajte dokument a potom zavolajte SignPades s odtlačkom certifikátu. Komponent PDFium Component vyhľadá tento odtlačok v úložisku certifikátov aktuálneho používateľa „MY“, získa zodpovedajúci certifikát a jeho súkromný kľúč a zapíše podpísanú kópiu na zadanú cestu
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('PAdES signing requires the Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // the document to sign
Pdf.Active := True;
// Second argument: SHA-1 thumbprint of a certificate in the Current
// User "MY" store. First argument: destination for the signed copy.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Vlastnosť PadesCryptoAvailable je test, ktorý kontrolujete ako prvý pri každom spustení. V systéme Windows vracia True a backend crypt32/ncrypt backend je aktívny; na akejkoľvek inej platforme vracia False a volanie podpísania by vyvolalo chybu EPadesCrypto. Považovanie tejto ochrany za povinnú zabraňuje zlyhaniu zostavenia pre Linux alebo macOS pri behu na ceste, ktorá tam nemôže fungovať. Samotný odtlačok je haš SHA-1 certifikátu, rovnaká hodnota, akú zobrazuje správca certifikátov Windows na karte Detaily, a pomenúva konkrétneho podpisovateľa bez toho, aby sa do zdrojového kódu vkladal samotný kľúč
Čo ide do CMS: podpísané atribúty a RFC 5652
Základný podpis PAdES nie je surový RSA podpis nad súborom; je to štruktúra CAdES CMS SignedData, ktorá nesie predpísanú sadu podpísaných atribútov, a metóda FPdfCms.BuildSignedData generuje presne túto sadu: content-type, message-digest a signing-certificate-v2, čo je atribút ESS, ktorý spája podpis s certifikátom podpisovateľa pomocou hašu. Jeden detail here defeats almost every hand-rolled CMS implementation. Norma RFC 5652 §5.4 vyžaduje, aby sa haš podpísaných atribútov počítal nad kódovaním DER SET OF, tag 0x31, zatiaľ čo rovnaké atribúty putujú v rámci SignerInfo pod tagom IMPLICIT [0], 0xA0. Komponent PDFium Component zakóduje sadu atribútov raz, vytvorí haš z formy 0x31 a potom prepíše iba úvodný bajt tagu na 0xA0 pre výstup, takže jeden vyrovnávací buffer slúži obom úlohám bez druhého prechodu cez strom
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // signer in the MY store
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // hex width of /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Preťaženie možností pridáva metadáta slovníka podpisu, ktoré definuje ISO 32000-1 §12.8.1: Reason (dôvod), Location (miesto), ContactInfo a Name (meno) — všetky sú voliteľné a zapisujú sa do slovníka hodnôt podpisu. Jedno obmedzenie je ľahké prehliadnuť. Ak nastavíte CommitmentTypeOid na pridanie podpísaného atribútu indikácie typu záväzku CAdES, nenastavujte súčasne Reason; norma ETSI EN 319 142-1 §6.3 zakazuje niesť obe hodnoty, pretože obe vyjadrujú rovnaký zámer rôznymi spôsobmi
Ako do seba zapadajú ByteRange a slot /Contents?
Podpis musí pokrývať celý súbor okrem bajtov, ktoré obsahujú samotný podpis, a PAdES rieši túto cyklickosť pomocou zástupného symbolu s pevnou šírkou, ktorý metóda SignPadesBytes presne spravuje. Vyhradí hexadecimálny reťazec /Contents s dĺžkou ContentsSize bajtov (predvolene 16384, čo je pohodlne viac ako typická štruktúra CMS SignedData), serializuje prírastkovú aktualizáciu na nájdenie presného offsetu slotu a potom vypočíta /ByteRange ako dva rozsahy ohraničujúce tento slot — všetko pred otváracím oddeľovačom hexadecimálneho reťazca a všetko za jeho zatváracím oddeľovačom. SHA-256 beží len nad týmito dvoma rozsahmi. Dokončená štruktúra CMS sa hexadecimálne zakóduje do vyhradeného slotu, doplní nulami na pevnú šírku a pripojí sa aktualizácia krížových odkazov (cross-reference). Keďže šírka je určená vopred, vyplnenie slotu neposunie žiadny nasledujúci bajt, čo je dôvodom, prečo rozsah bajtov zostáva platný. Pôvodné bajty dokumentu sú zachované nezmenené, takže starší podpis v rovnakom súbore prežije neporušený, presne tak, ako vyžaduje prírastkové podpisovanie podľa ISO 32000-1 §12.8.1
Backend Windows CNG a jeho limity
Komponent PDFium Component podpisuje iba v systéme Windows a táto hranica je zámerná. Jednotka FPdfCryptoWin dynamicky viaže knižnice crypt32.dll a ncrypt.dll, čím nepridáva žiadnu závislosť na DLL v čase kompilácie, a podpisovací reťazec je štandardný CNG: otvorí úložisko „MY“, nájde certifikát podľa hašu, získa handle jeho súkromného kľúča cez CryptAcquireCertificatePrivateKey a zavolá NCryptSignHash. Podporované sú algoritmy RSA s PKCS#1 v1.5, RSA-PSS a ECDSA. ECDSA vyžaduje jednu úpravu, ktorú ostatné nepotrebujú, pretože NCryptSignHash vracia surovú dvojicu r-and-s podľa IEEE P1363, zatiaľ čo CMS očakáva SEQUENCE DER ECDSA-Sig-Value, takže backend ju prekoduje podľa RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // no signing backend on this platform
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ôsledkom je, že súkromný kľúč musí žiť v úložisku certifikátov systému Windows. Certifikát uložený v súbore PFX funguje až po importe do úložiska aktuálneho používateľa (Current User), v tomto bode je jeho odtlačok hodnotou, ktorú odovzdáte metóde SignPades. Toto vydanie nemá žiadnu cestu pre PKCS#11 alebo HSM a ani žiadny softvérový backend pre kľúče zo súborov, takže keď vlastnosť PadesCryptoAvailable vracia False, na danom stroji jednoducho nie je možné podpisovať
Kde PAdES B-B končí
PAdES B-B je základná úroveň, minimum zo štyroch úrovní PAdES: dokazuje, kto dokument podpísal, a že bajty sa odvtedy nezmenili, nič viac. Podpis B-B nenesie žiadnu dôveryhodnú časovú pečiatku, takže nedokáže dokázať, kedy k podpísaniu došlo, a neobsahuje žiadne údaje o odvolaní, takže overovateľ o niekoľko rokov neskôr musí sám získať reťazec certifikátov a jeho stav. Tieto medzery uzatvárajú práve vyššie úrovne. Keď horúcim časom potrebujete čas podpísania, ktorý akceptuje audítor, pridanie časovej pečiatky RFC 3161 a DSS pre dlhodobé overenie posunie podpis na B-T a vyššie. Keď chcete prečítať hotový podpis a potvrdiť, ktorú úroveň dosiahol, sprievodným nástrojom je kontrola PDF podpisu a jeho úrovne PAdES, a pred samotným podpísaním čohokoľvek vám audit bezpečnostných rizík PDF povie, pod čo presne dávate svoje meno
Tu zobrazené metódy SignPades sa dodávajú s komponentom PDFium Component pre Delphi a C++Builder, spolu s kontrolou podpisov len na čítanie, ktorú PDFium poskytuje priamo po vybalení