PDFium VCL behandler CMS-verifikation som en udskiftelig backend bag IPdfCmsVerifier-interfacet, så PAdES-validatoren kan køre på Windows gennem CryptoAPI, på macOS gennem Keychain og alle steder, hvor OpenSSL findes, gennem ConfigureSslCmsVerifier. Interfacet er lille. Tre OpenSSL-adfærdsmønstre under det giver selvsikre, forkerte svar, hvis du implementerer interfacet naivt
Motivationen er indlysende nok, så snart en Delphi-applikation forlader Windows. Signaturvalidering er et af de få områder, hvor platformens crypto stack ikke er en implementeringsdetalje: den afgør, hvilke certifikater der stoles på, hvilke algoritmer der findes, og hvad revocation betyder. Hardkoder du én, porteres koden ikke. Abstraherer du den dårligt, rapporterer hver platform et svar i sin egen form, som kalderen ikke kan sammenligne
Hvad abstraktionen reelt skal bære
To verifikationsformer og tre uafhængige domme. En PDF-signatur er detached: det signerede indhold er de to byte ranges på hver side af /Contents-hullet, så VerifyDetached tager to segmenter i stedet for én buffer. En tidsstempel-token er attached og bærer sit eget indhold, så VerifyAttached tager kun DER
Resultatet splittes i tre statusser, fordi de besvarer tre forskellige spørgsmål og kan være uenige. SignatureStatus siger, om bytes blev signeret med nøglen i signeringscertifikatet. TrustStatus siger, om certifikatet kæder op til noget, du stoler på. RevocationStatus siger, om certifikatet stadig var gyldigt på det relevante tidspunkt. Et dokument med en matematisk perfekt signatur fra et certifikat, du aldrig har hørt om, er valid, untrusted og unknown, og at tvinge det hele ned i én enkelt boolean er måden, validators ender med at lyve over for brugerne på
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, må være tom
ConfigureSslCrls(LoadFreshCrls); // DER, må være tom
ConfigureSslCmsVerifier; // installerer backenden
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 ligner en kuriositet og er det ikke. Hver OpenSSL-fejlkode og hver store-flag krydser grænsen som en C unsigned long, som er fire bytes på Windows og otte på Linux og macOS. Erklærer du den som en fast 32-bit type, virker koden på Windows og læser derefter lydløst halvdelen af en værdi på LP64. At rapportere de antagne bredder som en streng, du kan asserte på i en test, forvandler en hel klasse af platform-ABI-drift til et tjek på én linje. Enhver, der har arbejdet sig gennem samme problem med CK_ULONG i en PKCS#11-binding, genkender det med det samme; den historie står i PKCS#11 struct packing og CK_ULONG-bredde
Hvorfor ser det andet verifikationspas tomt indhold?
Fordi CMS_verify læser den detached content-BIO til EOF, og en BIO, der er læst, bliver ikke spolt tilbage for dig. At verificere i to pas er et fornuftigt design, først den kryptografiske signatur alene med kædeevaluering slået fra, derefter den fulde evaluering, og det fejler på en usædvanligt vildledende måde, hvis de to pas deler én BIO
Det andet pas får nul bytes af indhold. I detached mode er det ikke en fejl, for en tom content-buffer er et lovligt input. Digest matcher bare ikke, og fejlen viser sig som en kædeopbygningsfejl i stedet for en indholdsfejl, hvilket sender dig ud at kigge på certifikater og trust stores, mens det egentlige problem er en stream position. Byg memory BIO op igen med BIO_new_mem_buf til hvert pas. Det koster én allokering og fjerner muligheden helt
Hvad no-verify-flaget undertrykker, og hvad det ikke undertrykker
CMS_NO_SIGNER_CERT_VERIFY undertrykker kædeevalueringen, ikke opslaget af signeringscertifikatet. Internt resolver og knytter OpenSSL signeringscertifikaterne, inden den kigger på flaget, så efter et første pas med det flag er signereren allerede tilgængelig, og dens algoritme-identifikatorer kan læses med det samme. Der er ingen grund til at køre en anden fuld verifikation bare for at få signeringscertifikatet, hvilket er det, flagnavnet frister dig til at antage
Én ejerskabsregel hører med. Signer-referencen ejes af CMS-strukturen og må ikke frigives selvstændigt. Den er gyldig, så længe strukturen er det, og frigiver du den, får du en corruption, hvis symptom viser sig et helt andet sted, typisk under oprydningen af et urelateret objekt
Hvorfor afvises hver signatur, når man slår CRL-tjek til?
Fordi OpenSSL kun tjekker CRL'er op mod det, store allerede holder, og selv henter ingenting. Den følger ikke CRL distribution points, og den taler ikke OCSP. Sæt X509_V_FLAG_CRL_CHECK på en store uden CRL'er i, og hver kæde fejler, fordi et certifikat-CRL ikke kan skaffes. Resultatet ligner revocationstjek, der virker og finder problemer. Det er revocationstjek, der slet aldrig bliver kørt
Backenden sætter derfor kun flaget, når ConfigureSslCrls faktisk har leveret mindst én CRL. Uden en kommer RevocationStatus tilbage som pcvsUnsupported, hvilket er en ærlig melding om, at spørgsmålet ikke blev besvaret. Af samme grund har OnlineRetrieval ingen effekt på denne backend, og der udsendes intet pcvstOnlineRetrieval-checkpoint: der er ingen hentesti at rapportere fremdrift fra
Det er en designholdning, der er værd at forsvare i generel forstand. En validator, der ikke kan tjekke revocation, bør sige det. At rapportere et utjekket certifikat som ikke-tilbagekaldt er den alleralmindeligste måde, signaturvalideringsværktøjer vildleder deres brugere på, og det er præcis den slags forvirring, der behandles i hvorfor validators afviser PAdES-signaturer
// Checkpoints lader en UI vise, hvilket stadie der kører, og fortæller
// dig, hvilke stadier en backend faktisk udfører
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;
// Læs de tre domme hver for sig; de må gerne være uenige
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');
Binding til et library, du ikke kan låse fast
OpenSSL omdøbte sine stack-accessors mellem 1.0 og 1.1, så den samme logiske funktion har to mulige eksportnavne afhængigt af, hvilken build hosten tilfældigvis har. Bindingen resolver det nyere navn først og falder tilbage til det ældre, og den registrerer kun et manglende symbol, når ingen af dem resolver. Det er den rigtige form for enhver dynamisk binding mod et library, du ikke selv distribuerer: foretræk aktuelle navne, tolerér historiske, og rapportér kun reelt fravær
SslMissingSymbols er det, der forvandler en fejlet indlæsning til en begivenhed, man kan diagnosticere. Et ikke-tomt resultat på en host, der åbenlyst har libcrypto installeret, betyder, at den installerede version er ældre end den API, denne build sigter mod, og det er en helt anden supportsamtale end et library, der mangler. ConfigureSslLibraryPath dækker det andet almindelige tilfælde, en host med flere OpenSSL-builds, hvor den på default search path ikke er den, du vil have
Valg af backend pr. platform
Det praktiske arrangement er at vælge ved opstart og notere, hvilken der svarede. På Windows integrerer platform-backend sig med de certifikat stores, en virksomhed allerede administrerer, hvilket normalt er det, du vil have. På macOS passer Keychain-backend til samme ræsonnement og er beskrevet i verificering af PDF-signaturer på macOS med SecTrust. OpenSSL er den portable mulighed, og det er også det rigtige valg, når du behøver en valideringspolitik, der er identisk på tværs af platforme, i stedet for én, der følger hver platforms trust store
Uanset hvilken du installerer, så log PadesCmsVerificationBackendName ved siden af hver dom, du registrerer. Et gemt valideringsresultat uden den backend, der producerede det, kan ikke reproduceres senere, fordi de tre statusværdier betyder subtilt forskellige ting afhængigt af, hvilken stack der svarede. Signaturinspektionslaget oven på det hele, inklusive hvordan PAdES-niveauer rapporteres, er dækket i inspektion af digitale PDF-signaturer og PAdES-niveauer
Det hele leveres som source med PDFium Delphi-komponenten, hvilket betyder mere her end normalt: for en signaturvalidator er evnen til at læse præcis, hvilke flags en backend sætter, og hvilke tjek den springer over, ikke et nice-to-have, det er den eneste måde at vide, hvad et grønt flueben i din applikation faktisk hævder