PDFium VCL tratta la verifica CMS come un backend sostituibile dietro l'interfaccia IPdfCmsVerifier, così il validatore PAdES può girare su Windows tramite CryptoAPI, su macOS tramite il Keychain, e ovunque sia presente OpenSSL tramite ConfigureSslCmsVerifier. L'interfaccia è piccola. Tre comportamenti OpenSSL sotto di essa producono risposte sicure di sé e sbagliate se la implementi ingenuamente
La motivazione è abbastanza chiara appena un'applicazione Delphi lascia Windows. La validazione delle firme è una delle poche aree in cui lo stack crittografico di piattaforma non è un dettaglio implementativo: decide quali certificati sono fidati, quali algoritmi esistono, e cosa significa revoca. Hard-codarne uno e il codice non si porta altrove. Astrarlo male e ogni piattaforma riporta una risposta di forma diversa che il chiamante non può confrontare
Cosa deve davvero portare l'astrazione
Due forme di verifica e tre verdetti indipendenti. Una firma PDF è detached: il contenuto firmato sono i due intervalli di byte ai lati del buco /Contents, quindi VerifyDetached prende due segmenti invece di un buffer. Un token di timestamp è attached, porta il proprio contenuto, quindi VerifyAttached prende solo il DER
Il risultato si divide in tre status perché rispondono a tre domande diverse e possono essere in disaccordo. SignatureStatus dice se i byte sono stati firmati dalla chiave nel certificato del signer. TrustStatus dice se quel certificato forma una catena fino a qualcosa in cui hai fiducia. RevocationStatus dice se il certificato era ancora valido al momento rilevante. Un documento con una firma matematicamente perfetta da un certificato di cui non hai mai sentito parlare è valido, non fidato e sconosciuto, e collassare ciò in un solo booleano è il modo in cui i validatori finiscono per mentire agli utenti
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, può essere vuoto
ConfigureSslCrls(LoadFreshCrls); // DER, può essere vuoto
ConfigureSslCmsVerifier; // installa il 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 sembra una curiosità e non lo è. Ogni codice errore OpenSSL e ogni flag di store attraversa il confine come un unsigned long C, che è quattro byte su Windows e otto su Linux e macOS. Dichiaraelo come tipo fisso a 32 bit e il codice funziona su Windows, per poi leggere in silenzio metà valore su LP64. Riportare le larghezze assunte come una stringa su cui asserire in un test trasforma un'intera classe di deriva ABI di piattaforma in un controllo di una riga. Chi ha affrontato lo stesso problema con CK_ULONG in un binding PKCS#11 lo riconoscerà all'istante; quella storia è in packing delle struct PKCS#11 e larghezza CK_ULONG
Perché la seconda passata di verifica vede contenuto vuoto?
Perché CMS_verify legge il BIO del contenuto detached fino a fine file, e un BIO già letto non viene riavvolto per te. Verificare in due passate è un design ragionevole, prima la sola firma crittografica con la valutazione della catena soppressa, poi la valutazione completa, e fallisce in un modo insolitamente ingannevole se entrambe le passate condividono un BIO
La seconda passata ottiene zero byte di contenuto. In modalità detached non è un errore, perché un buffer di contenuto vuoto è un input legale. Il digest semplicemente non combacia, e il fallimento emerge come un fallimento di costruzione della catena invece che un fallimento di contenuto, il che ti manda a ispezionare certificati e trust store mentre il problema vero è una posizione di stream. Ricostruisci il memory BIO con BIO_new_mem_buf a ogni passata. Costa un'allocazione e elimina del tutto la possibilità
Cosa sopprime e cosa no il flag no-verify
CMS_NO_SIGNER_CERT_VERIFY sopprime la valutazione della catena, non la ricerca del certificato del signer. Internamente OpenSSL risolve e allega i certificati del signer prima di consultare il flag, quindi dopo una prima passata che porta quel flag il signer è già disponibile e i suoi identificatori di algoritmo si possono leggere subito. Non serve eseguire una seconda verifica completa solo per ottenere il certificato del signer, che è ciò che il nome del flag ti tenta di presupporre
Una regola di proprietà va con questo. Il riferimento al signer appartiene alla struttura CMS e non va liberato indipendentemente. È valido finché lo è la struttura, e liberarlo produce una corruzione il cui sintomo compare da tutt'altra parte, di solito durante la pulizia di un oggetto non correlato
Perché attivare il controllo CRL respinge ogni firma?
Perché OpenSSL controlla le CRL solo contro ciò che lo store già custodisce e non va a prelevare nulla da sé. Non segue i CRL distribution point e non parla OCSP. Imposta X509_V_FLAG_CRL_CHECK su uno store senza CRL al suo interno e ogni catena fallisce con l'incapacità di ottenere una CRL del certificato. Il risultato sembra un controllo di revoca che funziona e trova problemi. È un controllo di revoca che non gira mai
Il backend quindi imposta il flag solo quando ConfigureSslCrls ha davvero fornito almeno una CRL. Senza, RevocationStatus torna come pcvsUnsupported, che è un'affermazione onesta che la domanda non ha ricevuto risposta. Per lo stesso motivo OnlineRetrieval non ha effetto su questo backend e nessun checkpoint pcvstOnlineRetrieval viene emesso: non c'è un percorso di fetch da cui riportare progresso
Questa è una posizione di design che vale la pena difendere in generale. Un validatore che non può controllare la revoca dovrebbe dirlo. Riportare un certificato non controllato come non revocato è il modo più comune in assoluto con cui gli strumenti di validazione delle firme disinformano i propri utenti, ed è esattamente la classe di confusione esplorata in perché i validatori respingono le firme PAdES
// I checkpoint permettono a una UI di mostrare quale stadio sta girando,
// e dicono quali stadi un backend esegue davvero
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;
// Leggi i tre verdetti separatamente; è permesso che siano in disaccordo
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');
Legarsi a una libreria che non puoi bloccare
OpenSSL ha rinominato i propri accessor dello stack tra 1.0 e 1.1, quindi la stessa funzione logica ha due possibili nomi di export a seconda della build che l'host si trova ad avere. Il binding risolve prima il nome più recente e ricade su quello più vecchio, e registra un simbolo mancante solo quando nessuno dei due si risolve. Quella è la forma giusta per qualunque binding dinamico verso una libreria che non spedisci: preferisci i nomi attuali, tollera quelli storici, e riporta solo l'assenza genuina
SslMissingSymbols è ciò che trasforma un caricamento fallito in un evento diagnosticabile. Un risultato non vuoto su un host che chiaramente ha libcrypto installato significa che la versione installata è più vecchia dell'API a cui questa build punta, che è una conversazione di supporto completamente diversa da una libreria assente. ConfigureSslLibraryPath copre l'altro caso comune, un host con diverse build OpenSSL dove quella sul percorso di ricerca predefinito non è quella che vuoi
Scegliere un backend per piattaforma
L'assetto pratico è selezionare all'avvio e registrare quale ha risposto. Su Windows, il backend di piattaforma si integra con gli archivi certificati che un'enterprise gestisce già, che è di solito ciò che vuoi. Su macOS il backend Keychain segue lo stesso ragionamento ed è descritto in verificare firme con SecTrust su macOS. OpenSSL è l'opzione portabile, ed è anche la scelta giusta quando ti serve una policy di validazione identica tra piattaforme invece che una che segua il trust store di ciascuna piattaforma
Qualunque tu installi, logga PadesCmsVerificationBackendName accanto a ogni verdetto che registri. Un risultato di validazione memorizzato senza il backend che lo ha prodotto non si può riprodurre dopo, perché i tre valori di status significano cose sottilmente diverse a seconda di quale stack ha risposto. Il layer di ispezione delle firme sopra tutto questo, compreso come vengono riportati i livelli PAdES, è trattato in ispezionare firme digitali PDF e livelli PAdES
Tutto ciò viene spedito come sorgente con il PDFium Delphi component, il che qui conta più del solito: per un validatore di firme, poter leggere esattamente quali flag imposta un backend e quali controlli salta non è un avere-di-più, è l'unico modo di sapere cosa dichiara davvero una spunta verde nella tua applicazione