PDFium VCL behandler CMS-verifisering som en utskiftbar backend bak IPdfCmsVerifier-grensesnittet, så PAdES-validatoren kan kjøre på Windows gjennom CryptoAPI, på macOS gjennom Keychain, og hvor som helst OpenSSL finnes, gjennom ConfigureSslCmsVerifier. Grensesnittet er lite. Tre OpenSSL-atferder under det gir selvsikre, feil svar hvis du implementerer det naivt
Motivasjonen er grei nok så snart en Delphi-applikasjon forlater Windows. Signaturvalidering er et av de få områdene der plattformens crypto-stakk ikke er en implementasjonsdetalj: den avgjør hvilke sertifikater som stoles på, hvilke algoritmer som finnes, og hva tilbakekalling betyr. Hardkoder du én, porterer ikke koden. Abstraherer du den dårlig, rapporterer hver plattform et svar med en form kalleren ikke kan sammenligne
Hva abstraksjonen faktisk må bære
To verifikasjonsformer og tre uavhengige kjennelser. En PDF-signatur er detached: det signerte innholdet er de to byte-rekkene på hver side av /Contents-hullet, så VerifyDetached tar to segmenter i stedet for én buffer. Et tidsstempel-token er attached og bærer sitt eget innhold, så VerifyAttached tar bare DER-en
Resultatet deles i tre statuser fordi de svarer på tre forskjellige spørsmål og kan være uenige. SignatureStatus sier om bytene ble signert med nøkkelen i signeringssertifikatet. TrustStatus sier om det sertifikatet kjeder til noe du stoler på. RevocationStatus sier om sertifikatet fortsatt var gyldig på det relevante tidspunktet. Et dokument med en matematisk perfekt signatur fra et sertifikat du aldri har hørt om, er gyldig, uten tillit og ukjent, og å slå det sammen til én boolsk verdi er hvordan validatorer ender med å lyve til brukerne
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, kan være tom
ConfigureSslCrls(LoadFreshCrls); // DER, kan være tom
ConfigureSslCmsVerifier; // installerer backend-en
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 ser ut som en kuriositet og er det ikke. Hver OpenSSL-feilkode og hvert store-flagg krysser grensen som en C unsigned long, som er fire byte på Windows og åtte på Linux og macOS. Deklarerer du den som en fast 32-bitstype, fungerer koden på Windows og leser deretter stille halve verdier på LP64. Å rapportere de antatte bredde som en streng du kan asserte på i en test, gjør en hel klasse plattform-ABI-drift om til en sjekk på én linje. Alle som har jobbet gjennom samme problem med CK_ULONG i en PKCS#11-binding, kjenner det igjen umiddelbart; den historien er i PKCS#11 struct packing og CK_ULONG-bredde
Hvorfor ser andre verifiseringspass tomt innhold?
Fordi CMS_verify leser den detached-innholds-BIO-en til filslutt, og en BIO som er lest, blir ikke spolt tilbake for deg. Å verifisere i to pass er et fornuftig design, først den kryptografiske signaturen alene med kjede-evaluering undertrykt, så den fulle evalueringen, og det feiler på en usedvanlig villedende måte hvis begge passene deler én BIO
Andre passet får null byte med innhold. I detached-modus er ikke det en feil, for en tom innholdsbuffer er en gyldig inngangsverdi. Digest-en stemmer rett og slett ikke, og feilen viser seg som en kjedebyggingsfeil snarere enn en innholdsfeil, noe som sender deg av gårde for å inspisere sertifikater og trust stores mens det faktiske problemet er en strømposisjon. Bygg minne-BIO-en på nytt med BIO_new_mem_buf for hvert pass. Det koster én allokering og fjerner muligheten helt
Hva no-verify-flagget undertrykker og ikke undertrykker
CMS_NO_SIGNER_CERT_VERIFY undertrykker kjede-evalueringen, ikke oppslaget etter signeringssertifikatet. Internt løser og fester OpenSSL signeringssertifikatene før det ser på flagget, så etter et første pass med det flagget er signeren allerede tilgjengelig, og algoritmeidentifikatorene dens kan leses med én gang. Det er ikke nødvendig å kjøre en andre full verifisering bare for å få tak i signeringssertifikatet, noe som er det flaggnavnet frister deg til å anta
Én eierskapsregel følger med det. Signeringsreferansen eies av CMS-strukturen og må ikke frigjøres uavhengig. Den er gyldig like lenge som strukturen, og å frigjøre den forårsaker en korrupsjon hvis symptom viser seg et helt annet sted, vanligvis under opprydding av et urelatert objekt
Hvorfor avviser CRL-sjekken hver signatur når den slås på?
Fordi OpenSSL sjekker CRL-er bare mot det store allerede har og henter ingenting på egen hånd. Den følger ikke CRL distribution points og snakker ikke OCSP. Setter du X509_V_FLAG_CRL_CHECK på et store uten CRL-er, feiler hver kjede med manglende mulighet til å skaffe et sertifikat-CRL. Resultatet ser ut som tilbakekallingssjekk som fungerer og finner problemer. Det er tilbakekallingssjekk som aldri kjører i det hele tatt
Backend-en setter derfor flagget bare når ConfigureSslCrls faktisk har levert minst én CRL. Uten det kommer RevocationStatus tilbake som pcvsUnsupported, som er en ærlig melding om at spørsmålet ikke ble besvart. Av samme grunn har OnlineRetrieval ingen effekt på denne backend-en, og et pcvstOnlineRetrieval-checkpoint sendes aldri ut: det finnes ingen hentevei å rapportere fremdrift fra
Dette er en designposisjon verdt å forsvare generelt. En validator som ikke kan sjekke tilbakekalling, bør si det. Å rapportere et usjekket sertifikat som ikke tilbakekalt, er den aller vanligste måten signaturvalideringsverktøy villeder brukerne sine på, og det er presis den typen forvirring som utforskes i hvorfor validatorer avviser PAdES-signaturer
// Checkpoints lar et UI vise hvilket stadium som kjører, og forteller
// deg hvilke stadium en backend faktisk utfø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;
// Les de tre kjennelsene hver for seg; de har lov til å 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');
Å binde seg til et bibliotek du ikke kan låse
OpenSSL omdøpte sine stack-aksessorer mellom 1.0 og 1.1, så samme logiske funksjon har to mulige eksportnavn avhengig av bygget verten tilfeldigvis har. Bindingen slår opp det nyere navnet først og faller tilbake på det eldre, og registrerer bare et manglende symbol når ingen av dem slår ut. Det er riktig form for enhver dynamisk binding mot et bibliotek du ikke selv leverer: foretrekk aktuelle navn, tolerer historiske, og rapporter bare reelt fravær
SslMissingSymbols er det som gjør en feilet lasting til en hendelse du kan diagnostisere. Et ikke-tomt resultat på en vert som åpenbart har libcrypto installert, betyr at den installerte versjonen er eldre enn API-et dette bygget sikter mot, noe som er et helt annet support-samtaleemne enn et bibliotek som mangler. ConfigureSslLibraryPath dekker det andre vanlige tilfellet, en vert med flere OpenSSL-bygg der den på standardsøkestien ikke er den du vil ha
Å velge backend per plattform
Den praktiske ordningen er å velge ved oppstart og registrere hvilken som svarte. På Windows integrerer plattform-backend-en med sertifikatbutikkene en virksomhet allerede administrerer, noe som normalt er det du vil ha. På macOS passer Keychain-backend-en til samme resonnement og er beskrevet i verifisering av signaturer med SecTrust på macOS. OpenSSL er det portable alternativet, og det er også riktig valg når du trenger en valideringspolicy som er identisk på tvers av plattformer, snarere enn én som følger hver plattforms trust store
Hvilken du enn installerer, logg PadesCmsVerificationBackendName ved siden av hver kjennelse du registrerer. Et lagret valideringsresultat uten backend-en som produserte det, kan ikke reproduseres senere, fordi de tre statusverdiene betyr subtilt forskjellige ting avhengig av hvilken stakk som svarte. Signaturinspeksjonslaget oppå alt dette, inkludert hvordan PAdES-nivåer rapporteres, er dekket i inspeksjon av digitale PDF-signaturer og PAdES-nivåer
Alt sammen følger med som kildekode med PDFium Delphi-komponenten, noe som betyr mer her enn vanlig: for en signaturvalidator er det å kunne lese nøyaktig hvilke flagg en backend setter og hvilke sjekker den hopper over, ikke et nice-to-have, det er den eneste måten å vite hva en grønn hake i applikasjonen din faktisk hevder