PDFium VCL tratează verificarea CMS ca pe un backend înlocuibil, în spatele interfeței IPdfCmsVerifier, astfel încât validator-ul PAdES poate rula pe Windows prin CryptoAPI, pe macOS prin Keychain și oriunde există OpenSSL prin ConfigureSslCmsVerifier. Interfața e mică. Trei comportamente OpenSSL de dedesubtul ei produc răspunsuri încrezătoare și greșite dacă o implementați naiv
Motivația e suficient de clară imediat ce o aplicație Delphi părăsește Windows. Validarea semnăturilor e una dintre puținele arii în care stack-ul criptografic al platformei nu e un detaliu de implementare: el decide ce certificate sunt de încredere, ce algoritmi există și ce înseamnă revocarea. Hard-codați unul și codul nu se portă. Abstrageți-l prost și fiecare platformă raportează un răspuns de altă formă, pe care apelantul nu îl poate compara
Ce trebuie să poarte efectiv abstracția
Două forme de verificare și trei verdicte independente. O semnătură PDF e detașată: conținutul semnat sunt cele două intervale de octeți de o parte și de alta a găurii /Contents, deci VerifyDetached primește două segmente, nu un buffer. Un token de timestamp e atașat, purtându-și propriul conținut, deci VerifyAttached primește doar DER-ul
Rezultatul se desparte în trei stări pentru că ele răspund la trei întrebări diferite și pot să nu fie de acord. SignatureStatus spune dacă octeții au fost semnați de cheia din certificatul semnatarului. TrustStatus spune dacă certificatul acela se lanțează spre ceva în care aveți încredere. RevocationStatus spune dacă certificatul era încă valid la momentul relevant. Un document cu o semnătură matematic perfectă, dintr-un certificat de care nu ați auzit niciodată, e valid, neverificabil și necunoscut, iar a restrânge asta într-un singur boolean e felul în care validator-ele ajung să își mintă utilizatorii
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, poate fi gol
ConfigureSslCrls(LoadFreshCrls); // DER, poate fi gol
ConfigureSslCmsVerifier; // instalează backend-ul
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 pare o curiozitate și nu e. Fiecare cod de eroare OpenSSL și fiecare fanion de store traversează granița ca un unsigned long C, adică patru octeți pe Windows și opt pe Linux și macOS. Declarați-l ca un tip fix pe 32 de biți și codul funcționează pe Windows, apoi citește tăcut jumătate de valoare pe LP64. Raportarea lățimilor presupuse ca un șir pe care puteți afirma într-un test transformă toată clasa de derive de ABI de platformă într-o verificare de o linie. Oricine a lucrat aceeași problemă cu CK_ULONG într-un binding PKCS#11 o va recunoaște imediat; povestea aceea stă în împachetarea structurilor PKCS#11 și lățimea CK_ULONG
De ce a doua trecere de verificare vede conținut gol?
Pentru că CMS_verify citește BIO-ul de conținut detașat până la sfârșitul fișierului, iar un BIO care a fost citit nu e rulât înapoi pentru dumneavoastră. Verificarea în două treceri e un design rezonabil: mai întâi semnătura criptografică singură, cu evaluarea lanțului suprimată, apoi evaluarea completă, și ea eșuează într-un fel neobișnuit de înșelător dacă ambele treceri partajează un singur BIO
A doua trecere primește zero octeți de conținut. În modul detașat asta nu e o eroare, pentru că un buffer de conținut gol e o intrare legală. Digestul pur și simplu nu se potrivește, iar eșecul iese la suprafață ca un eșec de construire a lanțului, nu ca unul de conținut, ceea ce vă trimite să inspectați certificate și trust store-uri în timp ce problema reală e o poziție în stream. Reconstruiți BIO-ul de memorie cu BIO_new_mem_buf pentru fiecare trecere. Costă o alocare și înlătură complet posibilitatea
Ce suprimă și ce nu suprimă fanionul no-verify
CMS_NO_SIGNER_CERT_VERIFY suprimă evaluarea lanțului, nu căutarea certificatului semnatarului. Intern, OpenSSL rezolvă și atașează certificatele semnatarului înainte să consulte fanionul, deci după o primă trecere care poartă fanionul acela semnatarul e deja disponibil, iar identificatorii lui de algoritm pot fi citiți direct. Nu e nevoie să rulați o a doua verificare completă doar ca să obțineți certificatul semnatarului, exact ceea ce numele fanionului te isca să presupui
O regulă de proprietate vine cu asta. Referința semnatarului aparține structurii CMS și nu trebuie eliberată independent. E valabilă atâta timp cât structura, iar eliberarea ei produce o corupere al cărei simptom apare cu totul în altă parte, de obicei în timpul curățeniei unui obiect fără legătură
De ce activarea verificării CRL respinge fiecare semnătură?
Pentru că OpenSSL verifică CRL-urile doar contra a ceea ce store-ul ține deja și nu aduce nimic singur. Nu urmărește punctele de distribuție CRL și nu vorbește OCSP. Stabiliți X509_V_FLAG_CRL_CHECK pe un store fără CRL-uri în el și fiecare lanț pică cu o incapacitate de a obține un CRL de certificat. Rezultatul arată ca o verificare de revocare care funcționează și găsește probleme. E o verificare de revocare care nu rulează deloc
Backend-ul stabilește deci fanionul doar când ConfigureSslCrls a furnizat efectiv cel puțin un CRL. Fără unul, RevocationStatus revine ca pcvsUnsupported, o declarație onestă că întrebarea n-a primit răspuns. Din același motiv OnlineRetrieval nu are niciun efect pe acest backend și niciun checkpoint pcvstOnlineRetrieval nu e emis: nu există o cale de aducere din care să raporteze progres
Asta e o poziție de design care merită apărată în general. Un validator care nu poate verifica revocarea ar trebui să spună asta. Raportarea unui certificat neverificat drept nerevocat e cel mai comun mod singular în care uneltele de validare a semnăturilor își înșală utilizatorii, și e exact clasa de confuzie explorată în de ce resping validator-ele semnăturile PAdES
// Checkpoint-urile lasă o interfață să arate ce etapă rulează și îți
// spun ce etape efectuează efectiv un backend
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;
// Citiți cele trei verdicte separat; li se permite să nu fie de acord
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');
Legarea la o bibliotecă pe care nu o puteți fixa
OpenSSL și-a redenumit accesatoarele stack-ului între 1.0 și 1.1, deci aceeași funcție logică are două nume de export posibile, în funcție de build-ul pe care gazda se întâmplă să îl aibă. Binding-ul rezolvă mai întâi numele mai nou și cade pe cel mai vechi, și înregistrează un simbol lipsă doar când niciunul nu se rezolvă. Asta e forma corectă pentru orice binding dinamic spre o bibliotecă pe care nu o livrați: preferați numele actuale, tolerați-le pe cele istorice și raportați doar absența reală
SslMissingSymbols e ceea ce transformă o încărcare eșuată într-un eveniment diagnosticabil. Un rezultat non-gol pe o gazdă care are clar libcrypto instalat înseamnă că versiunea instalată e mai veche decât API-ul țintit de acest build, ceea ce e o conversație de suport cu totul diferită de o bibliotecă lipsă. ConfigureSslLibraryPath acoperă celălalt caz comun, o gazdă cu mai multe build-uri OpenSSL, unde cel de pe calea de căutare implicită nu e cel pe care îl vreți
Alegerea unui backend per platformă
Aranjamentul practic e să selectați la pornire și să înregistrați care a răspuns. Pe Windows, backend-ul platformei se integrează cu store-urile de certificate pe care o întreprindere le administrează deja, ceea ce e de regulă ce vreți. Pe macOS backend-ul Keychain se potrivește aceluiași raționament și e descris în verificarea semnăturilor cu SecTrust pe macOS. OpenSSL e opțiunea portabilă, iar el e și alegerea corectă când aveți nevoie de o politică de validare identică între platforme, nu de una care urmează trust store-ul fiecărei platforme
Oricare l-ați instala, înregistrați în jurnal PadesCmsVerificationBackendName lângă fiecare verdict pe care îl stocați. Un rezultat de validare stocat, fără backend-ul care l-a produs, nu poate fi reprodus mai târziu, pentru că cele trei valori de stare înseamnă lucruri subtil diferite în funcție de ce stack a răspuns. Stratul de inspecție a semnăturilor de deasupra tuturor acestora, inclusiv cum sunt raportate nivelurile PAdES, e acoperit în inspectarea semnăturilor digitale PDF și a nivelurilor PAdES
Totul se livrează ca sursă cu PDFium Delphi component, ceea ce contează aici mai mult decât de obicei: pentru un validator de semnături, să poți citi exact ce fanioane stabilește un backend și ce verificări sare nu e un moft, ci singurul mod de a ști ce pretinde de fapt un bifa verde în aplicația dumneavoastră