Tehnički članak

Potpisivanje PDF-ova sa PAdES B-B u Delphi-ju koristeći PDFium

PDFium komponenta potpisuje PDF sa PAdES B-B digitalnim potpisom kroz metodu SignPades: ona učitava dokument, hešira potpisani opseg bajtova, gradi CAdES CMS strukturu i dodaje potpis kao inkrementalno ažuriranje. Kriptografski pozadinski program (backend) radi samo na Windows-u, tako da pre potpisivanja zaštitite svaki poziv proverom PadesCryptoAvailable

Situacija vam je verovatno poznata. PDF ugovora stiže na vaš sto, pravna služba želi da bude digitalno potpisan pre nego što se pošalje, i vi posežete za istom PDFium verzijom koju već koristite za renderovanje i inspekciju dokumenata, samo da biste otkrili da PDFium uopšte ne može da upiše potpis. Njegov API za potpise je strogo ograničen na čitanje. PDFium komponenta premošćuje taj jaz tako što kontroliše ceo proces potpisivanja u Pascal-u, od heš funkcije do ubacivanja na nivou bajtova, a ovaj članak prolazi kroz taj proces od početka do kraja

Zašto PDFium ne može da upiše digitalni potpis?

PDFium izlaže potpise kao objekte samo za čitanje i ne nudi ništa što bi moglo da kreira novi potpis. Familija funkcija FPDFSignatureObj_* vam omogućava da nabrojite postojeći potpis, pročitate njegov /Contents i inspicišete njegov /ByteRange, ali ne postoji pandan koji gradi rečnik potpisa, rezerviše slot /Contents ili upisuje opseg bajtova. Iako inkrementalno čuvanje postoji (FPDF_SaveAsCopy sa FPDF_INCREMENTAL), ono ne nosi nikakvu tačku za potpisivanje (hook). Bilo koja komponenta koja potpisuje PDF povrh PDFium-a mora sama da generiše svaki bajt potpisa, zbog čega PDFium komponenta gradi ovaj mehanizam iz tri čiste Pascal jedinice. FPC 3.2.2 se isporučuje sa md5 i sha1, ali bez SHA-2 podrške, a Delphi-jev System.Hash SHA-256 API nije izvorno kompatibilan sa FPC-om, pa je FPdfSha256 samostalna FIPS 180-4 implementacija koja sve CMS putanje koda drži na jednom tipu TSHA256Digest bez grananja kompajlera. FPdfAsn1 obezbeđuje DER enkoder i čitač koji su potrebni CMS strukturama, a FPdfCms sklapa CAdES SignedData povrh oba

Kako se digitalno potpisuje PDF u Delphi-ju?

Učitajte dokument, a zatim pozovite SignPades sa otiskom sertifikata (thumbprint). PDFium komponenta razrešava taj otisak u odnosu na skladište sertifikata "MY" trenutnog korisnika (Current User), preuzima odgovarajući sertifikat i njegov privatni ključ i upisuje 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 provera koju uvek prvu kontrolišete. Na Windows-u ona vraća True i crypt32/ncrypt pozadinski modul je aktivan; na bilo kojoj drugoj platformi ona vraća False i poziv za potpisivanje bi podigao izuzetak EPadesCrypto. Tretiranje ove zaštite kao obavezne sprečava da se izvršavanje na Linux-u ili macOS-u sruši u vreme pokretanja na putanji koja tamo ne može da radi. Sam otisak je SHA-1 heš sertifikata, ista vrednost koju Windows upravljač sertifikatima prikazuje na kartici Details, i on imenuje konkretnog potpisnika bez da ikada postavlja materijal ključa u vaš izvorni kod

Šta ide u CMS: potpisani atributi i RFC 5652

Osnovni PAdES potpis nije sirovi RSA potpis preko datoteke; to je CAdES CMS SignedData struktura koja nosi obavezni skup potpisanih atributa, a FPdfCms.BuildSignedData emituje upravo taj skup: content-type, message-digest i signing-certificate-v2, ESS atribut koji povezuje potpis sa sertifikatom potpisnika preko heša. Jedan detalj ovde ometa skoro svaku ručno napisanu CMS implementaciju. Standard RFC 5652 §5.4 zahteva da se sažetak potpisanih atributa računa preko DER SET OF kodiranja, oznaka 0x31, dok isti ti atributi putuju unutar SignerInfo pod IMPLICIT [0] oznakom, 0xA0. PDFium komponenta jednom kodira skup atributa, računa sažetak za oblik 0x31, a zatim prepisuje samo početni bajt oznake na 0xA0 radi emitovanja, tako da jedan bafer služi obema 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;

Preopterećena verzija sa opcijama dodaje metapodatke rečnika potpisa koje definiše ISO 32000-1 §12.8.1: Reason, Location, ContactInfo i Name, svi opcioni i svi upisani u rečnik vrednosti potpisa. Na jedno ograničenje je lako naići. Ako postavite CommitmentTypeOid da biste dodali CAdES potpisani atribut commitment-type-indication, nemojte takođe postavljati Reason; standard ETSI EN 319 142-1 §6.3 zabranjuje nošenje oba, jer ta dva izražavaju istu nameru na različite načine

Kako se uklapaju ByteRange i slot /Contents?

Potpis mora pokrivati celu datoteku osim bajtova koji drže sam potpis, a PAdES rešava tu kružnu zavisnost pomoću rezervisanog mesta fiksne širine kojim SignPadesBytes precizno upravlja. On rezerviše heksadecimalni string /Contents od ContentsSize bajtova (podrazumevano 16384, što je komotno veće od tipičnog CMS SignedData-a), serijalizuje inkrementalno ažuriranje da bi locirao tačan pomeraj slota, a zatim računa /ByteRange kao dva raspona koji uokviruju slot — sve pre otvarajućeg graničnika heksadecimalnog stringa i sve nakon njegovog zatvarajućeg graničnika. SHA-256 se pokreće samo preko ta dva raspona. Završeni CMS se heksadecimalno kodira u rezervisani slot, dopunjuje nulama do fiksne širine, a zatim se dodaje ažuriranje unakrsnih referenci. Pošto je širina fiksirana na samom početku, popunjavanje slota ne pomera nijedan nizvodni bajt, što je jedini razlog zašto opseg bajtova ostaje validan. Originalni bajtovi dokumenta su sačuvani doslovno, tako da raniji potpis na istoj datoteci opstaje netaknut, baš onako kako zahteva ISO 32000-1 §12.8.1 za inkrementalno potpisivanje

Windows CNG pozadinski modul i njegova ograničenja

PDFium komponenta potpisuje dokumente samo na Windows-u, i ta granica je namerna. FPdfCryptoWin dinamički povezuje crypt32.dll i ncrypt.dll, ne dodajući zavisnost o DLL-u u vreme kompajliranja, a lanac potpisivanja je standardni CNG: otvaranje MY skladišta, pronalaženje sertifikata preko heša, pribavljanje njegove ručke privatnog ključa kroz CryptAcquireCertificatePrivateKey i pozivanje NCryptSignHash. Podržani su RSA sa PKCS#1 v1.5, RSA-PSS i ECDSA. ECDSA zahteva jedno prilagođavanje koje drugi ne trebaju, jer NCryptSignHash vraća sirovi IEEE P1363 par r-i-s dok CMS očekuje DER ECDSA-Sig-Value SEQUENCE, pa ga pozadinski program ponovo kodira prema standardu 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 posledica je da privatni ključ mora da živi u Windows skladištu sertifikata. Sertifikat koji se nalazi u PFX datoteci radi tek nakon što ga uvezete u Current User skladište, nakon čega je njegov otisak vrednost koju prosleđujete u SignPades. Ovo izdanje nema PKCS#11 ili HSM putanju, niti pozadinski modul sa datotekama softverskih ključeva, tako da kada PadesCryptoAvailable vrati False, na toj mašini jednostavno nema potpisivanja

Gde se PAdES B-B zaustavlja

PAdES B-B je osnovni nivo, minimum od četiri PAdES nivoa: on dokazuje ko je potpisao i da se bajtovi od tada nisu promenili, i ništa više od toga. B-B potpis ne nosi provereni vremenski žig, pa ne može dokazati kada se potpisivanje desilo, i ne ugrađuje podatke o opozivu, tako da verifikator godinama kasnije mora sam da pribavi lanac sertifikata i njihov status. Te nedostatke zatvaraju viši nivoi. Kada vam je potrebno vreme potpisivanja koje će revizor prihvatiti, dodavanje RFC 3161 vremenskog žiga i DSS-a za dugoročnu validaciju pomera potpis na B-T i više; kada želite ponovo da pročitate gotov potpis i potvrdite koji je nivo dostigao, inspekcija PDF potpisa i njegovog PAdES nivoa je prateći alat; a pre nego što bilo šta potpišete, revizija PDF-a radi bezbednosnih rizika govori vam na šta zapravo stavljate svoje ime

Metodi SignPades prikazani ovde dolaze sa komponentom PDFium Component za Delphi i C++Builder, zajedno sa inspekcijom potpisa samo za čitanje koju PDFium pruža iz kutije