PDFium-komponentti allekirjoittaa PDF-tiedoston PAdES B-B -digitaalisella allekirjoituksella SignPades-metodinsa kautta: se lataa asiakirjan, tiivistää (hash) allekirjoitetun tavualueen, rakentaa CAdES CMS -rakenteen ja lisää allekirjoituksen inkrementaalisena päivityksenä. Kryptografinen taustaosa toimii vain Windowsissa, joten suojaa jokainen kutsu PadesCryptoAvailable-ominaisuudella ennen allekirjoittamista
Tilanne on tuttu. Sopimus-PDF saapuu työpöydällesi, lakiosasto haluaa sen digitaalisesti allekirjoitettuna ennen kuin se lähetetään ulos, ja tartut samaan PDFium-koontiversioon, jota jo käytät asiakirjojen renderöintiin ja tarkasteluun, vain huomataksesi, ettei PDFium pysty lainkaan kirjoittamaan allekirjoitusta. Sen allekirjoitus-API on tiukasti vain luku -tyyppinen. PDFium-komponentti sulkee tuon aukon omistamalla koko allekirjoitusputken Pascal-koodissa, aina tiiviste-funktiosta aina tavutason injektioon asti, ja tässä artikkelissa käydään tuo putki läpi alusta loppuun
Miksi PDFium ei voi kirjoittaa digitaalista allekirjoitusta?
PDFium tuo allekirjoitukset esille vain luku -objekteina eikä tarjoa mitään sen luomiseen. FPDFSignatureObj_*-perhe mahdollistaa olemassa olevan allekirjoituksen luetteloinnin, sen /Contents-kentän lukemisen ja sen /ByteRange-alueen tarkastelun, mutta sille ei ole vastinetta, joka rakentaisi allekirjoitussanakirjan, varaisi /Contents-paikan tai kirjoittaisi tavualueen. Inkrementaalinen tallennus on olemassa (FPDF_SaveAsCopy lipulla FPDF_INCREMENTAL), mutta se ei sisällä allekirjoituskytkintä. Kaikkien PDFiumin päälle allekirjoittavien komponenttien on siksi luotava jokainen allekirjoitustavu itse. Tämän vuoksi PDFium-komponentti rakentaa koneiston kolmesta puhtaasta Pascal-yksiköstä. FPC 3.2.2 toimittaa md5- ja sha1-algoritmit, mutta ei lainkaan SHA-2-tukea, ja Delphin System.Hash SHA-256-API ei ole lähdekoodiyhteensopiva FPC:n kanssa. Siksi FPdfSha256 on itsenäinen FIPS 180-4 -toteutus, joka pitää jokaisen CMS-koodipolun yhdellä TSHA256Digest-tyypillä ilman kääntäjän haarautumista. FPdfAsn1 toimittaa DER-koodaimen ja -lukijan, joita CMS-rakenteet tarvitsevat, ja FPdfCms kokoaa CAdES SignedData -rakenteen molempien päälle
Miten PDF allekirjoitetaan digitaalisesti Delphissä?
Lataa asiakirja ja kutsu sitten SignPades-metodia varmenteen sormenjäljen (thumbprint) kanssa. PDFium-komponentti selvittää kyseisen sormenjäljen Current User "MY" -varmennesäilöä vasten, hakee vastaavan varmenteen ja sen yksityisen avaimen kahvan ja kirjoittaa allekirjoitetun kopion nimeämääsi polkuun
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'; // allekirjoitettava asiakirja
Pdf.Active := True;
// Toinen argumentti: Current User "MY" -säilössä olevan varmenteen SHA-1-sormenjälki.
// Ensimmäinen argumentti: allekirjoitetun kopion kohdepolku.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
PadesCryptoAvailable on se tarkistus, joka tehdään aina ensimmäisenä. Windowsissa se palauttaa True-arvon ja crypt32/ncrypt-taustaosa on käytössä. Kaikilla muilla alustoilla se palauttaa False-arvon ja allekirjoituskutsu nostaa poikkeuksen EPadesCrypto. Tämän suojauksen käsitteleminen pakollisena estää Linux- tai macOS-koodia epäonnistumasta ajoympäristössä polulla, joka ei voi toimia siellä. Itse sormenjälki on varmenteen SHA-1-tiiviste, eli sama arvo, jonka Windowsin varmennehallinta näyttää Tiedot-välilehdellään. Se nimeää tietyn allekirjoittajan tuomatta koskaan avainmateriaalia lähdekoodiisi
Mitä CMS:ään menee: allekirjoitetut määritteet ja RFC 5652
PAdES-perusallekirjoitus ei ole raaka RSA-allekirjoitus tiedoston yli. Se on CAdES CMS SignedData -rakenne, joka kantaa vaaditut allekirjoitetut määritteet, ja FPdfCms.BuildSignedData tuottaa juuri tuon joukon: content-type, message-digest ja signing-certificate-v2, eli ESS-määritteen, joka sitoo allekirjoituksen allekirjoittajan varmenteeseen tiivisteen (hash) avulla. Yksi yksityiskohta tässä kaataa melkein jokaisen käsin rullatun CMS-toteutuksen. RFC 5652 §5.4 vaatii allekirjoitettujen määritteiden tiivisteen laskettavaksi DER SET OF -koodauksesta (tunniste 0x31), kun taas samat määritteet kulkevat SignerInfo-osassa tunnisteen IMPLICIT [0] alla (0xA0). PDFium-komponentti koodaa määritejoukon kerran, tiivistää 0x31-muodon ja kirjoittaa sitten vain johtavan tunnistetavun muotoon 0xA0 lähetystä varten, joten yksi puskuri palvelee molempia rooleja ilman toista läpikäyntiä puun yli
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // allekirjoittaja MY-säilössä
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // /Contents-kentän heksaleveys
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Miten ByteRange ja /Contents-paikka sopivat yhteen?
Allekirjoituksen on katettava koko tiedosto lukuun ottamatta tavuja, jotka pitävät sisällään itse allekirjoituksen, ja PAdES ratkaisee tämän kehämäisyyden kiinteäleveyksisellä paikkamerkillä, jota SignPadesBytes hallitsee tarkasti. Se varaa /Contents-heksamerkkijonon, jonka koko on ContentsSize tavua (oletusarvoisesti 16384, selvästi suurempi kuin tyypillinen CMS SignedData), sarjoittaa inkrementaalisen päivityksen paikantaakseen tarkan paikan siirtymän, ja laskee sitten /ByteRange-alueen kahtena välinä, jotka ympäröivät paikan: kaiken ennen heksamerkkijonon avaavaa erotinta ja kaiken sen sulkevan erottimen jälkeen. SHA-256 lasketaan vain näiden kahden välin yli. Valmis CMS heksakoodataan varattuun paikkaan nollatäytettynä kiinteään leveyteen, ja ristiinviitepäivitys (cross-reference update) lisätään loppuun. Koska leveys on kiinteä etukäteen, paikan täyttäminen ei siirrä mitään alavirran tavua, minkä vuoksi tavualue pysyy validina. Alkuperäiset asiakirjan tavut säilyvät muuttumattomina, joten tiedostossa oleva aiempi allekirjoitus säilyy ehjänä juuri siten kuin ISO 32000-1 §12.8.1 inkrementaalinen allekirjoitus vaatii
Windows CNG -taustaosa ja sen rajat
PDFium-komponentti allekirjoittaa vain Windowsissa, ja tämä raja on tietoinen. FPdfCryptoWin sitoo crypt32.dll- ja ncrypt.dll-kirjastot dynaamisesti tuomatta kääntöaikaista DLL-riippuvuutta, ja allekirjoitusketju on standardia CNG:tä: avaa MY-säilö, etsi varmenne tiivisteen perusteella, hae sen yksityisen avaimen kahva CryptAcquireCertificatePrivateKey-kutsulla ja kutsu NCryptSignHash-metodia. RSA PKCS#1 v1.5-, RSA-PSS- ja ECDSA-algoritmit ovat kaikki tuettuja. ECDSA vaatii yhden korjauksen, jota muut eivät tarvitse, koska NCryptSignHash palauttaa raa'an IEEE P1363 r-ja-s-parin, kun taas CMS odottaa DER-koodattua ECDSA-Sig-Value SEQUENCE-rakennetta, joten taustaosa koodaa sen uudelleen RFC 5480 -standardin mukaisesti
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // ei allekirjoituksen taustaosaa tällä alustalla
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;
Käytännön seuraus on, että yksityisen avaimen on sijaittava Windowsin varmennesäilössä. PFX-tiedostossa oleva varmenne toimii vasta, kun tuot sen Current User -säilöön, jolloin sen sormenjälki on se arvo, jonka välität SignPades-metodille. Tässä julkaisussa ei ole PKCS#11- tai HSM-polkua eikä ohjelmistoavaintiedostotaustaa (software-key file backend), joten kun PadesCryptoAvailable palauttaa False-arvon, kyseisellä koneella ei yksinkertaisesti ole allekirjoitusmahdollisuutta
Mihin PAdES B-B pysähtyy
PAdES B-B on perustaso, neljän PAdES-tason alin taso: se todistaa kuka allekirjoitti ja ettei tavuja ole muutettu sen jälkeen, eikä mitään muuta sen ulkopuolella. B-B-allekirjoitus ei kanna luotettua aikaleimaa, joten se ei voi todesta, milloin allekirjoittaminen tapahtui, eikä se upota peruutustietoja, joten todentajan on vuosia myöhemmin haettava varmenneketju ja sen tila itse. Nämä aukot ovat juuri niitä, jotka korkeammat tasot sulkevat. Kun tarvitset allekirjoitusajan, jonka tarkastaja hyväksyy, artikkeli RFC 3161 -aikaleiman ja DSS-säilön lisääminen pitkäaikaista todentamista varten siirtää allekirjoituksen B-T-tasolle ja sen yli. Kun haluat lukea valmiin allekirjoituksen takaisin ja vahvistaa, minkä tason se saavutti, artikkeli PDF-allekirjoituksen ja sen PAdES-tason tarkastelu on kumppanityökalu. Ja ennen minkään allekirjoittamista, artikkeli PDF-turvallisuusriskien auditointi kertoo, mihin olet nimesi laittamassa
Tässä esitetyt SignPades-metodit toimitetaan Delphille ja C++Builderille tarkoitetun PDFium-komponentin mukana, yhdessä vain luku -allekirjoitustarkastuksen kanssa, jonka PDFium tarjoaa valmiina