De PDFium Component ondertekent een PDF met een PAdES B-B digitale handtekening via de methode SignPades: deze laadt het document, hasht het ondertekende bytebereik, bouwt een CAdES CMS-structuur op en voegt de handtekening toe als een incrementele update. De cryptografische backend is alleen voor Windows, dus beveilig elke aanroep met PadesCryptoAvailable voordat u ondertekent
De situatie is bekend. Er ligt een contract-PDF op uw bureau, de juridische afdeling wil dat deze digitaal wordt ondertekend voordat deze de deur uitgaat, en u grijpt naar dezelfde PDFium-build die u al gebruikt om documenten te renderen en te inspecteren, om er vervolgens achter te komen dat PDFium helemaal geen handtekening kan schrijven. De handtekening-API is strikt alleen-lezen. De PDFium Component overbrugt die kloof door de volledige ondertekeningspipeline in Pascal te beheren, van de hash-functie tot aan de injectie op byteniveau, en dit artikel doorloopt die pipeline van begin tot eind
Waarom kan PDFium geen digitale handtekening schrijven?
PDFium stelt handtekeningen beschikbaar als alleen-lezen objecten en biedt niets om er een aan te maken. De FPDFSignatureObj_* familie stelt u in staat om een bestaande handtekening op te sommen, de /Contents ervan te lezen en de /ByteRange ervan te inspecteren, maar er is geen tegenhanger die een handtekeningdictionary opbouwt, een /Contents-slot reserveert of een bytebereik schrijft; incrementeel opslaan bestaat (FPDF_SaveAsCopy met FPDF_INCREMENTAL) maar bevat geen ondertekeningshook. Elke component die een PDF ondertekent bovenop PDFium moet daarom elke handtekeningbyte zelf genereren, wat de reden is waarom de PDFium Component de machinerie opbouwt uit drie pure Pascal-units. FPC 3.2.2 wordt geleverd met md5 en sha1 maar helemaal geen SHA-2, en de Delphi System.Hash SHA-256 API is qua broncode niet compatibel met FPC, dus is FPdfSha256 een zelfstandige FIPS 180-4 implementatie die elk CMS-codepad op één TSHA256Digest type houdt zonder compiler-vertakkingen. FPdfAsn1 levert de DER-encoder en -lezer die de CMS-structuren nodig hebben, en FPdfCms assembleert de CAdES SignedData bovenop beide
Hoe ondertekent u een PDF digitaal in Delphi?
Laad het document en roep vervolgens SignPades aan met een certificaat-thumbprint. De PDFium Component zoekt die thumbprint op in de "MY"-certificaatstore van de Current User, haalt het bijbehorende certificaat en de privésleutel op, en schrijft een ondertekende kopie naar het door u opgegeven pad
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 is de controle die u altijd als eerste uitvoert. Op Windows retourneert deze True en is de crypt32/ncrypt-backend actief; op elk ander platform retourneert deze False en zou een ondertekeningsaanroep een EPadesCrypto-fout genereren. Door deze controle als verplicht te behandelen, wordt voorkomen dat een Linux- of macOS-build tijdens de runtime faalt op een pad dat daar niet kan werken. De thumbprint zelf is the SHA-1 hash van het certificaat, dezelfde waarde die de Windows-certificaatbeheerder op het tabblad Details toont, en deze identificeert een specifieke ondertekenaar zonder dat er ooit sleutelmateriaal in uw broncode wordt geplaatst
Wat gaat er in de CMS: ondertekende attributen en RFC 5652
Een PAdES-basishandtekening is geen ruwe RSA-handtekening over het bestand; het is een CAdES CMS SignedData-structuur die een verplichte set ondertekende attributen bevat, en FPdfCms.BuildSignedData verzendt exact die set: content-type, message-digest en signing-certificate-v2, het ESS-attribuut dat de handtekening via een hash aan het certificaat van de ondertekenaar koppelt. Eén detail hierin maakt het bijna elke handmatig gemaakte CMS-implementatie moeilijk. RFC 5652 §5.4 vereist dat de signed-attributes digest wordt berekend over de DER SET OF-codering, tag 0x31, terwijl dezelfde attributen binnen SignerInfo worden getransporteerd onder de IMPLICIT [0]-tag, 0xA0. De PDFium Component codeert de attributenset eenmalig, berekent de digest van de 0x31-vorm, en herschrijft vervolgens alleen de leidende tag-byte naar 0xA0 voor verzending, zodat één buffer beide rollen vervult zonder een tweede doorloop van de boomstructuur
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;
De overload met opties voegt de metadata voor de handtekeningdictionary toe die ISO 32000-1 §12.8.1 definieert: Reason, Location, ContactInfo en Name, allemaal optioneel en allemaal geschreven in de dictionary met handtekeningwaarden. Er is één beperking waar u gemakkelijk tegenaan loopt. Als u CommitmentTypeOid instelt om het CAdES-ondertekende attribuut commitment-type-indication toe te voegen, stel dan niet ook Reason in; ETSI EN 319 142-1 §6.3 verbiedt het dragen van beide, omdat de twee dezelfde intentie op verschillende manieren uitdrukken
Hoe passen de ByteRange en het /Contents-slot in elkaar?
Een handtekening moet het gehele bestand beslaan, behalve de bytes die de handtekening zelf bevatten. PAdES lost die circulariteit op met een aanduiding met vaste breedte die SignPadesBytes nauwkeurig beheert. Het reserveert een hex-string /Contents van ContentsSize bytes (standaard 16384, ruim groter dan een typische CMS SignedData), serialiseert de incrementele update om de exacte offset van het slot te bepalen, en berekent vervolgens de /ByteRange als twee bereiken die het slot omsluiten: alles voor het openingsteken van de hex-string en alles na het sluitingsteken ervan. De SHA-256 wordt alleen over die twee bereiken berekend. De voltooide CMS wordt hex-gecodeerd in het gereserveerde slot geplaatst, met nullen opgevuld tot de vaste breedte, en de update van de kruisverwijzing wordt toegevoegd. Omdat de breedte vooraf vaststaat, verschuift het vullen van het slot geen downstream bytes, wat de reden is waarom het bytebereik geldig blijft; de originele documentbytes blijven letterlijk behouden, zodat een eerdere handtekening op hetzelfde bestand intact overleeft, precies zoals ISO 32000-1 §12.8.1 incrementeel ondertekenen vereist
De Windows CNG-backend en zijn beperkingen
De PDFium Component ondertekent alleen op Windows, en die grens is bewust gekozen. FPdfCryptoWin koppelt crypt32.dll en ncrypt.dll dynamisch, zonder compilatiedependency's voor DLL's toe te voegen, en de ondertekeningsketen is standaard CNG: open the MY-store, zoek het certificaat op via de hash, verkrijg de privésleutelhandgreep via CryptAcquireCertificatePrivateKey en roep NCryptSignHash aan. RSA met PKCS#1 v1.5, RSA-PSS en ECDSA worden allemaal ondersteund. ECDSA vereist één aanpassing die de andere niet nodig hebben, omdat NCryptSignHash het ruwe IEEE P1363 r-en-s-paar retourneert, terwijl CMS een DER ECDSA-Sig-Value SEQUENCE verwacht, dus codeert de backend dit opnieuw conform 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;
De praktische consequentie is dat de privésleutel in de Windows-certificaatstore moet staan. Een certificaat in een PFX-bestand werkt pas nadat u het in de Current User-store hebt geïmporteerd, waarna de bijbehorende thumbprint de waarde is die u aan SignPades doorgeeft. Deze release heeft geen PKCS#11- of HSM-pad en geen backend voor software-sleutelbestanden, dus als PadesCryptoAvailable False retourneert, kan er op die machine simpelweg niet worden ondertekend
Waar PAdES B-B ophoudt
PAdES B-B is de basis, de ondergrens van de vier PAdES-niveaus: het bewijst wie er heeft ondertekend en dat de bytes sindsdien niet zijn gewijzigd, en niets meer dan dat. Een B-B handtekening bevat geen vertrouwde timestamp, dus ze kan niet bewijzen wanneer de ondertekening heeft plaatsgevonden, en ze sluit geen intrekkingsgegevens in, zodat een validator jaren later zelf de certificaatketen en de status ervan moet ophalen. Deze hiaten zijn precies wat de hogere niveaus dichten. Wanneer u een ondertekeningstijd nodig hebt die een auditor accepteert, brengt het toevoegen van een RFC 3161 timestamp en DSS voor langdurige validatie de handtekening naar B-T en hoger; wanneer u een voltooide handtekening wilt teruglezen en wilt bevestigen welk niveau is bereikt, is een PDF-handtekening en het bijbehorende PAdES-niveau inspecteren het begeleidende hulpmiddel; en voordat u überhaupt iets ondertekent, vertelt een PDF controleren op veiligheidsrisico's u waar u uw naam onder gaat zetten
De hier getoonde SignPades methoden worden geleverd met de PDFium Component voor Delphi en C++Builder, naast de alleen-lezen handtekeninginspectie die PDFium standaard biedt