PDFium komponenta potpisuje PDF pomoću PAdES B-B digitalnog potpisa kroz svoju metodu SignPades: učitava dokument, sažima (hash) potpisani raspon bajtova, gradi CAdES CMS strukturu i dodaje potpis kao inkrementalno ažuriranje. Kriptografska pozadina (backend) radi samo na Windowsima, stoga zaštitite svaki poziv s PadesCryptoAvailable prije nego što potpišete
Situacija je poznata. Ugovor u PDF obliku stiže na vaš stol, pravna služba želi da bude digitalno potpisan prije slanja, a vi posežete za istom verzijom PDFiuma koju već koristite za iscrtavanje i provjeru dokumenata, samo kako biste otkrili da PDFium uopće ne može zapisati potpis. Njegov API za potpise je strogo samo za čitanje. PDFium komponenta premošćuje taj jaz tako što posjeduje cijeli cjevovod potpisivanja u Pascalu, od funkcije sažimanja do umetanja na razini bajtova, a ovaj članak prolazi kroz taj cjevovod od početka do kraja
Zašto PDFium ne može zapisati digitalni potpis?
PDFium izlaže potpise kao objekte samo za čitanje i ne nudi ništa što bi ih moglo stvoriti. Obitelj FPDFSignatureObj_* omogućuje vam nabrajanje postojećih potpisa, čitanje njihovih /Contents i provjeru njihovih /ByteRange, ali ne postoji pandan koji gradi rječnik potpisa, rezervira mjesto za /Contents ili zapisuje raspon bajtova; inkrementalno spremanje postoji (FPDF_SaveAsCopy s FPDF_INCREMENTAL), ali ne donosi nikakvu funkciju za potpisivanje. Svaka komponenta koja potpisuje PDF povrh PDFiuma stoga mora sama generirati svaki bajt potpisa, zbog čega PDFium komponenta gradi taj mehanizam od tri čiste Pascal jedinice. FPC 3.2.2 isporučuje md5 i sha1, ali uopće ne SHA-2, a Delphi System.Hash SHA-256 API nije kompatibilan na razini izvornog koda s FPC-om, pa je FPdfSha256 samostalna FIPS 180-4 implementacija koja drži sve CMS staze koda na jednom tipu TSHA256Digest bez grananja kompajlera. FPdfAsn1 isporučuje DER koder i čitač potreban za CMS strukture, a FPdfCms sastavlja CAdES SignedData na temelju oba
Kako digitalno potpisati PDF u Delphiju?
Učitajte dokument, a zatim pozovite SignPades s otiskom certifikata. PDFium komponenta razrješava taj otisak u odnosu na spremište certifikata Current User "MY", povlači odgovarajući certifikat i njegov privatni ključ te zapisuje potpisanu kopiju na putanju koju navedete
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;
PadesCryptoAvailable je provjera koju kontrolirate prvu, svaki put. Na Windowsima vraća True i crypt32/ncrypt pozadina je aktivna; na bilo kojoj drugoj platformi vraća False, a poziv potpisivanja bi podigao iznimku EPadesCrypto. Tretiranje ove provjere kao obavezne sprječava da Linux ili macOS verzija padne u vremenu izvršavanja na putanji koja tamo ne može raditi. Sam otisak je SHA-1 sažetak certifikata, ista vrijednost koju Windows upravitelj certifikata prikazuje na kartici Details, i on identificira određenog potpisnika bez stavljanja ključnog materijala u vaš izvorni kôd
Što ide u CMS: potpisani atributi i RFC 5652
PAdES osnovni potpis nije sirovi RSA potpis preko datoteke; to je CAdES CMS SignedData struktura koja prenosi propisani skup potpisanih atributa, a FPdfCms.BuildSignedData emitira točno taj skup: content-type, message-digest i signing-certificate-v2, odnosno ESS atribut koji povezuje potpis s certifikatom potpisnika putem sažetka. Jedan detalj ovdje ometa gotovo svaku ručno izrađenu CMS implementaciju. RFC 5652 §5.4 zahtijeva da se sažetak potpisanih atributa izračuna preko DER SET OF kodiranja, s oznakom 0x31, dok isti atributi putuju unutar SignerInfo pod oznakom IMPLICIT [0], 0xA0. PDFium komponenta jednom kodira skup atributa, izračunava sažetak oblika 0x31, a zatim prepisuje samo početni bajt oznake u 0xA0 za slanje, pa jedan međuspremnik služi objema ulogama bez potrebe za drugim prolaskom kroz stablo
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;
Overload s opcijama dodaje metapodatke rječnika potpisa koje definira ISO 32000-1 §12.8.1: Reason, Location, ContactInfo i Name, koji su svi opcionalni i svi zapisani u rječnik vrijednosti potpisa. Jedno ograničenje je lako previdjeti. Ako postavite CommitmentTypeOid kako biste dodali CAdES potpisani atribut commitment-type-indication, nemojte postaviti i Reason; ETSI EN 319 142-1 §6.3 zabranjuje nošenje obaju, jer ta dva izražavaju istu namjeru na različite načine
Kako ByteRange i mjesto za /Contents pristaju jedno uz drugo?
Potpis mora pokrivati cijelu datoteku osim bajtova koji sadrže sam potpis, a PAdES rješava tu kružnost s rezerviranim mjestom fiksne širine kojim SignPadesBytes precizno upravlja. Rezervira heksadecimalni niz /Contents od ContentsSize bajtova (16384 prema zadanim postavkama, što je ugodno veće od tipičnog CMS SignedData), serijalizira inkrementalno ažuriranje kako bi locirao točan pomak mjesta, a zatim izračunava /ByteRange kao dva raspona koji uokviruju to mjesto, odnosno sve prije otvarajućeg graničnika heksadecimalnog niza i sve nakon njegovog zatvarajućeg graničnika. SHA-256 se izvodi samo preko ta dva raspona. Dovršeni CMS se heksadecimalno kodira u rezervirano mjesto, nadopunjuje nulama do fiksne širine i dodaje se ažuriranje unakrsnih referenci. Budući da je širina unaprijed fiksirana, popunjavanje tog mjesta ne pomiče bajtove koji slijede, što je jedini razlog zašto raspon bajtova ostaje valjan; izvorni bajtovi dokumenta sačuvani su doslovno, tako da raniji potpis na istoj datoteci ostaje netaknut točno onako kako zahtijeva ISO 32000-1 §12.8.1 inkrementalno potpisivanje
Windows CNG pozadina i njezina ograničenja
PDFium komponenta potpisuje samo na Windowsima, i ta je granica namjerna. FPdfCryptoWin dinamički povezuje crypt32.dll i ncrypt.dll, ne dodajući ovisnost o DLL-u u vrijeme kompajliranja, a lanac potpisivanja je standardni CNG: otvori spremište MY, pronađi certifikat prema sažetku, dohvati njegov privatni ključ kroz CryptAcquireCertificatePrivateKey i pozovi NCryptSignHash. Podržani su RSA s PKCS#1 v1.5, RSA-PSS i ECDSA. ECDSA zahtijeva jedno prilagođavanje koje drugi ne trebaju, jer NCryptSignHash vraća sirovi IEEE P1363 r-i-s par, dok CMS očekuje DER ECDSA-Sig-Value SEQUENCE, pa ga pozadina ponovno kodira prema 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;
Praktična posljedica je da privatni ključ mora živjeti u spremištu certifikata sustava Windows. Certifikat koji se nalazi u PFX datoteci radi tek nakon što ga uvezete u spremište Current User, pri čemu je njegov otisak vrijednost koju prosljeđujete u SignPades. Ovo izdanje softvera nema PKCS#11 ili HSM putanju niti softversku datotečnu pozadinu za ključeve, pa kada PadesCryptoAvailable vraća False, potpisivanje na tom računalu jednostavno nije moguće
Gdje PAdES B-B staje
PAdES B-B je polazište, najniža od četiri PAdES razine: on dokazuje tko je potpisao i da se bajtovi od tada nisu promijenili, i ništa više od toga. B-B potpis ne nosi pouzdani vremenski žig, pa ne može dokazati kada se potpisivanje dogodilo, te ne ugrađuje podatke o opozivu, stoga provjeritelj godinama kasnije mora sam dohvatiti lanac certifikata i njegov status. Te praznine zatvaraju više razine. Kada vam je potrebno vrijeme potpisivanja koje će revizor prihvatiti, dodavanje vremenskog žiga RFC 3161 i DSS-a za dugoročnu provjeru podiže potpis na B-T i više; kada želite pročitati gotov potpis i potvrditi koju je razinu dosegao, provjera PDF potpisa i njegove PAdES razine je prateći alat; a prije nego što uopće išta potpišete, revizija PDF-a radi sigurnosnih rizika govori vam na što ćete točno staviti svoje ime
Metode SignPades prikazane ovdje dolaze s komponentom PDFium Component za Delphi i C++Builder, zajedno s provjerom potpisa samo za čitanje koju PDFium pruža odmah po instalaciji