Tehnički članak

Verificiranje PDF potpisa s OpenSSL-om u PDFium VCL-u

PDFium VCL tretira CMS verifikaciju kao zamjenjivi backend iza IPdfCmsVerifier interfacea, pa PAdES validator može trčati na Windowsima kroz CryptoAPI, na macOS-u kroz Keychain, i bilo gdje gdje je OpenSSL prisutan kroz ConfigureSslCmsVerifier. Interface je malen. Tri OpenSSL ponašanja ispod njega proizvode samouvjerene, krive odgovore ako ga implementirate naivno

Motivacija je jasna čim Delphi aplikacija napusti Windows. Validacija potpisa je jedno od područja gdje platformski crypto stack nije implementacijski detalj: odlučuje koji su certifikati vjerodostojni, koji algoritmi postoje, i što opoziv znači. Hardkodirajte jedan i kod se ne portira. Apstrahirajte ga loše i svaka platforma javlja drugačije oblikovan odgovor koji pozivatelj ne može usporediti

Što apstrakcija stvarno mora nositi

Dva oblika verifikacije i tri neovisne presude. PDF potpis je odvojen: potpisani sadržaj su dva byte raspona s obje strane /Contents rupe, pa VerifyDetached uzima dva segmenta umjesto jednog buffera. Timestamp token je priložen, noseći vlastiti sadržaj, pa VerifyAttached uzima samo DER

Rezultat se dijeli u tri statusa jer odgovaraju na tri različita pitanja i mogu se razilaziti. SignatureStatus kaže jesu li bajtove potpisali ključem u certifikatu potpisnika. TrustStatus kaže se taj certifikat lančano veže na nešto čemu vjerujete. RevocationStatus kaže je li certifikat još bio važeći u relevantno vrijeme. Dokument s matematički savršenim potpisom od certifikata za koji nikad niste čuli je valjan, nepouzdan i nepoznat, i sažeti to u jedan boolean je način na koji validatori završe lažući korisnike

uses
  FPdfCrypto, FPdfCryptoSsl;

var
  Options: TPdfCmsVerifyOptions;
begin
  if not SslAvailable then
    raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);

  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER, smije biti prazno
  ConfigureSslCrls(LoadFreshCrls);                // DER, smije biti prazno
  ConfigureSslCmsVerifier;                        // instalira backend

  Writeln('backend  : ', PadesCmsVerificationBackendName);
  Writeln('library  : ', SslLibraryPath, ' ', SslLibraryVersion);
  Writeln('ABI      : ', SslAbiLayout);           // ulong=<n> long=<n>

  Options := TPdfCmsVerifyOptions.Default;
  Options.CheckRevocation := True;
  Options.CollectChainCertificates := True;
end;

SslAbiLayout izgleda kao kuriozitet i nije. Svaki OpenSSL error code i svaka store zastavica prelazi granicu kao C unsigned long, koji je četiri bajta na Windowsima i osam na Linuxu i macOS-u. Deklarirajte ga kao fiksni 32-bit tip i kod radi na Windowsima, pa tiho čita pola vrijednosti na LP64. Javiti pretpostavljene širine kao string na kojem možete assertati u testu pretvara cijelu klasu platformskog ABI drifta u provjeru jedne linije. Tko je radio kroz isti problem s CK_ULONGom u PKCS#11 vezivanju prepoznat će ga odmah; ta je priča u PKCS#11 struct pakiranju i širini CK_ULONG-a

Zašto drugi prolaz verifikacije vidi prazan sadržaj?

Jer CMS_verify čita odvojeni content BIO do kraja datoteke, i BIO koji je pročitan nije namotan natrag za vas. Verificirati u dva prolaza je razumna konstrukcija, najprije samo kriptografski potpis s potisnutom evaluacijom lanca, pa puna evaluacija, i pade na neobično obmanjujući način ako oba prolaza dijele jedan BIO

Drugi prolaz dobiva nula bajtova sadržaja. U odvojenom modu to nije greška, jer je prazan content buffer legalan ulaz. Digest jednostavno ne odgovara, i neuspjeh ispliva kao neuspjeh gradnje lanca umjesto kao neuspjeh sadržaja, što vas pošalje pregledavati certifikate i trust storeove dok je stvarni problem pozicija u streamu. Ponovno izgradite memory BIO s BIO_new_mem_buf za svaki prolaz. Košta jednu alokaciju i uklanja mogućnost u potpunosti

Što no-verify zastavica potiskuje a što ne

CMS_NO_SIGNER_CERT_VERIFY potiskuje evaluaciju lanca, a ne traženje certifikata potpisnika. Iznutra OpenSSL razrješuje i prikopčava certifikate potpisnika prije nego konzultira zastavicu, pa nakon prvog prolaza koji nosi tu zastavicu potpisnik je već dostupan i njegovi identifikatori algoritma mogu se čitati odmah. Nema potrebe trčati drugu punu verifikaciju samo da dobijete certifikat potpisnika, a to je ono što ime zastavice mami da pretpostavite

Jedno pravilo vlasništva ide uz to. Referenca potpisnika pripada CMS strukturi i ne smije se oslobođati neovisno. Važeća je dok je struktura, i oslobađanje je proizvodi korupciju čiji se simptom pojavi negdje sasvim drugdje, obično tijekom čišćenja nepovezanog objekta

Zašto uključivanje CRL provjere odbija svaki potpis?

Jer OpenSSL provjerava CRL-ove samo protiv onoga što store već drži i ništa ne dohvaća sam. Ne slijedi CRL distribution pointove i ne govori OCSP. Postavite X509_V_FLAG_CRL_CHECK na store bez ijednog CRL-a i svaki lanac pade s nemogućnošću dobaviti certifikat CRL. Rezultat izgleda kao provjera opoziva koja radi i nalazi probleme. To je provjera opoziva koja nikad nije ni trčala

Backend zato postavlja zastavicu samo kad ConfigureSslCrls doista isporuči barem jedan CRL. Bez ijednog, RevocationStatus vraća se kao pcvsUnsupported, što je pošteno kazivo da pitanje nije odgovoreno. Iz istog razloga OnlineRetrieval nema učinka na ovaj backend i nijedan pcvstOnlineRetrieval checkpoint se ne emitira: nema dohvatnog puta iz kojega bi se javljao napredak

Dijagram triju zamki OpenSSL CMS verifiera u PDFium VCL-u: dijeljeni content BIO pročitan do kraja datoteke ostavlja drugi prolaz verifikacije s nula bajtova, CMS_NO_SIGNER_CERT_VERIFY potiskuje evaluaciju lanca a ne traženje potpisnika, i CRL provjera na praznom storeu odbija svaki lanac bez da se opoziv ikad provjeravao
Svaka zamka daje samouvjerenu krivu presudu: pozicija u streamu glumi neuspjeh vjerodostojnosti, no-verify zastavica potiskuje manje nego što njeno ime sugerira, i nikad pokrenut opoziv izgleda kao opoziv koji je našao probleme

Ovo je dizajnerska pozicija koju vrijedi braniti općenito. Validator koji ne može provjeriti opoziv treba to reći. Javiti neprovjeren certifikat kao neopozvan je pojedinačno najčešći način na koji alati za validaciju potpisa zavode svoje korisnike, i to je točno klasa zabune istražena u zašto validatori odbijaju PAdES potpise

// Checkpointovi puste UI da prikaže koji stupanj trči, i kažu vam koje
// stupnjeve backend stvarno izvodi
type
  TSignatureProbe = class
    procedure Checkpoint(Stage: TPdfCmsVerifyStage);
  end;

procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
  case Stage of
    pcvstCryptographicSignature: Status('checking the signature');
    pcvstChainBuild:             Status('building the certificate chain');
    pcvstOnlineRetrieval:        Status('fetching validation data');
    pcvstRevocationCheck:        Status('checking revocation');
  end;
end;

// Čitajte tri presude odvojeno; smiju se razilaziti
if Result.SignatureStatus = pcvsValid then
  case Result.TrustStatus of
    pcvsValid:         Report('signed and trusted');
    pcvsInvalid:       Report('signed, chain rejected');
    pcvsUnsupported,
    pcvsIndeterminate: Report('signed, trust not established');
  end;
if Result.RevocationStatus = pcvsUnsupported then
  Report('revocation was not checked on this backend');

Vezivanje na biblioteku čiju verziju ne kontrolirate

OpenSSL je preimenovao svoje stack accessore između 1.0 i 1.1, pa ista logička funkcija ima dva moguća imena izvoza ovisno o buildu koji domaćin slučajno ima. Vezivanje prvo razrješuje novije ime i pada natrag na starije, i zabilježi nedostajući simbol tek kad se ni jedno ne razriješi. To je pravi oblik za bilo koje dinamičko vezivanje na biblioteku koju ne isporučujete: preferirajte trenutna imena, trpite povijesna, i javljajte samo pravi nedostatak

SslMissingSymbols je ono što neuspjelo učitavanje pretvara u dijagnostificirajući događaj. Neprazan rezultat na domaćinu koji očito ima libcrypto instaliran znači da je instalirana verzija starija od API-ja kojega ovaj build cilja, što je posve drugačiji support razgovor od biblioteke koja nedostaje. ConfigureSslLibraryPath pokriva drugi uobičajeni slučaj, domaćin s više OpenSSL buildova gdje onaj na defaultnoj search putanji nije onaj koji želite

Birati backend po platformi

Praktičan aranžman je birati pri startanju i zabilježiti koji je odgovorio. Na Windowsima se platformski backend integrira s certificate storeovima kojima poduzeće već upravlja, što je obično ono što želite. Na macOS-u se Keychain backend uklapa u isto razmišljanje i opisan je u verificiranju potpisa sa SecTrustom na macOS-u. OpenSSL je prenosiva opcija, i to je također pravi izbor kad trebate politiku validacije koja je identična preko platformi umjesto one koja slijedi trust store svake platforme

Dijagram IPdfCmsVerifier apstrakcije u PDFium VCL-u koja nosi VerifyDetached preko dva byte raspona oko Contents rupe i VerifyAttached za timestamp tokene, tri neovisne presude SignatureStatus, TrustStatus i RevocationStatus, i per-platform backende birane pri startanju kroz CryptoAPI, SecTrust ili ConfigureSslCmsVerifier
Interface nosi dva oblika verifikacije i tri presude jer odgovaraju na različita pitanja i mogu se razilaziti, a instalirani backend se bilježi pokraj svake presude da se pohranjeni rezultati mogu reproducirati

Koji god instalirate, logirajte PadesCmsVerificationBackendName pokraj svake presude koju zabilježite. Pohranjen rezultat validacije bez backenda koji ga je proizveo ne može se kasnije reproducirati, jer tri statusne vrijednosti znače suptilno različite stvari ovisno o tome koji je stack odgovorio. Sloj pregleda potpisa iznad svega ovoga, uključujući kako se PAdES razine javljaju, pokriven je u pregledu PDF digitalnih potpisa i PAdES razina

Sve se to isporučuje kao source uz PDFium Delphi komponentu, što je tu važnije nego obično: za validator potpisa, moći pročitati točno koje zastavice backend postavlja i koje provjere preskače nije ukras, to je jedini način da znate što zelena kvačica u vašoj aplikaciji stvarno tvrdi