PDFium VCL firma documenti PAdES con una chiave privata custodita nel Keychain di macOS attraverso un backend che risolve ogni simbolo Security e CoreFoundation a runtime con dlopen e dlsym. Niente è legato al linking, il che significa che un nome di simbolo sbagliato emerge come KeychainAvailable che restituisce False e KeychainMissingSymbols che nomina il colpevole, invece che come un errore del linker o un crash
Quella scelta è stata forzata da un vincolo scomodo, e il modo in cui è stata gestita si generalizza. L'unit è stata scritta su una macchina senza SDK macOS, quindi ogni nome di simbolo e ogni costante del framework viene dalla documentazione e nessuno poteva essere controllato contro un header. La risposta sbagliata a quella situazione è scrivere il codice con cura e sperare. Quella giusta è organizzare le cose perché gli inevitabili errori si annuncino nella forma più localizzabile possibile
Perché il binding dinamico è la scelta giusta anche sulla piattaforma target
Perché converte una classe di guasto che ferma il programma in una classe di guasto che si auto-riporta. Un riferimento staticamente linkato a un framework che è sbagliato fallisce al linking sulla target e non linka mai altrove. Uno legato dinamicamente che è sbagliato produce un backend non disponibile e una lista di nomi non risolti, e la prima esecuzione su un Mac trasforma la domanda da perché questo non è disponibile in una singola riga che nomina un refuso
C'è un secondo beneficio che paga ogni giorno invece che una volta. Poiché l'unit non linka framework, compila su ogni piattaforma, così la build Windows ordinaria continua a controllarne sintassi, tipi e uses clause. Un'unit che compila solo su una piattaforma che nessuno nel team possiede è un'unit senza alcun compilatore che la guardi, e decade in silenzio a ogni refactor di un tipo condiviso
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // installare come backend signer PAdES
ConfigureKeychainCmsVerifier; // e come backend di verifica
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, maiuscole o minuscole
Options.PaddingScheme := psRsaPss;
end;
Due tipi di simbolo esportato, due modi di leggerli
Questo è il dettaglio singolarmente più confondente di tutto il binding, e prenderlo al contrario compila pulito e fallisce a runtime. CoreFoundation e Security esportano due cose categoricamente diverse attraverso la stessa chiamata dlsym, e il codice deve sapere qual è quale
Le costanti nominate come le chiavi di classe degli item del keychain e i singleton booleani CoreFoundation sono variabili esportate il cui contenuto è il CFStringRef o CFBooleanRef che vuoi. dlsym restituisce l'indirizzo di quella variabile, quindi devi dereferenziare una volta per ottenere il valore. Le strutture a tabella di callback come le callback di chiave e valore del dizionario sono strutture esportate, e dlsym restituisce l'indirizzo della struttura, che è precisamente il puntatore che la funzione di creazione del dizionario si aspetta. Dereferenziare quella e passi la prima parola macchina della struttura come se fosse un puntatore
Nessuno dei due errori produce un errore di compilazione, e nessuno produce un errore runtime chiaro. Ottieni un puntatore spazzatura che fallisce da qualche parte a valle. Il modo per rendere la distinzione impossibile da sbagliare è smettere di contare sul ricordarsela: due funzioni helper, una che lega e dereferenzia e una che lega e non dereferenzia, così il call site dichiara quale tipo di simbolo sta chiedendo e l'helper impone il resto
// Variabile esportata: dlsym dà l'indirizzo di una variabile che contiene
// il CFTypeRef, quindi dereferenziare una volta
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Struttura esportata: dlsym dà l'indirizzo DELLA struttura, che è ciò
// che l'API vuole. Non dereferenziare
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Perché una firma RSA-PSS ha bisogno di due fallback separati?
Perché l'algoritmo può mancare in due modi indipendenti, e solo uno di loro è una questione di versione. La costante dell'algoritmo di firma del digest PSS è apparsa in macOS 10.13, quindi su un sistema più vecchio il simbolo semplicemente non c'è e il binding ottiene nil. Questo è il controllo di versione. Separatamente, su un sistema dove la costante esiste, una specifica chiave può comunque rifiutarla, e il framework risponde a quella domanda tramite SecKeyIsAlgorithmSupported per quella chiave. Una chiave supportata da hardware o una chiave con attributi restrittivi può declinare PSS mentre una chiave software sulla stessa macchina lo accetta
Entrambi i percorsi devono portare allo stesso fallback: passare a PKCS#1 v1.5. E la parte critica è che il fallback deve cambiare anche l'identificatore di algoritmo scritto nella struttura CMS, non solo la chiamata di firma. Emettere un identificatore di algoritmo PSS producendo davvero una firma v1.5 genera un documento che ogni verificatore respinge a occhi chiusi, il che è strettamente peggiore che riportare che PSS non è supportato. Un downgrade è accettabile, un disallineamento tra ciò che dichiari e ciò che hai fatto non lo è, ed è una regola generale per il codice di firma e non una bizzarria macOS. Le implicazioni a livello di firma sono esposte in firmare PDF con PAdES B-B
Codifica delle firme ECDSA, e un'inversione da notare
Il percorso a curve ellittiche non richiede alcuna conversione su macOS, e questo è l'opposto di ciò che richiede un binding PKCS#11. L'algoritmo di firma del digest del framework Security per ECDSA restituisce la firma già in forma X9.62 DER, che è esattamente ciò che CMS vuole. Un token PKCS#11 restituisce invece la coppia grezza a larghezza fissa P1363, che va ricodificata prima di entrare in una struttura di firma
Così due backend che implementano la stessa interfaccia hanno bisogno di trattamenti opposti per lo stesso algoritmo, e nessuno dei due è sbagliato. Questo è precisamente il tipo di differenza che un'astrazione deve assorbire invece di esporre: il layer PAdES chiede a un provider di firmare, e le convenzioni di codifica restano dentro il provider. Se trapelano verso l'alto, ogni chiamante finisce per portarsi dietro un condizionale per backend. La stessa forma compare nella storia della firma remota descritta in sessioni di firma PAdES remota contro un HSM
// L'interfaccia provider è la stessa su ogni piattaforma, quindi la
// selezione è una decisione di avvio e non per chiamata
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Il provider CNG di Windows è installato dall'unit di piattaforma
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Da qui il codice di firma è platform-neutral
Signer := ResolvePadesSigner(Options);
Regole di reference counting che stanno a tre righe di distanza
La gestione della memoria di Core Foundation segue convenzioni di nomenclatura, e la trappola qui è che funzioni con convenzioni diverse appaiono una accanto all'altra nello stesso breve blocco. Una funzione che ottiene un certificato da un trust object restituisce un riferimento prestato che non va rilasciato. Le funzioni che copiano un certificato del signer o copiano i suoi dati restituiscono riferimenti posseduti che vanno rilasciati. Tre chiamate in sequenza, due regole di proprietà, e rilasciare quello prestato non fallisce a quella riga. Corrompe un retain count e butta giù qualcosa di non correlato più tardi
La mitigazione è leggere il verbo in ogni nome di funzione del framework prima di scrivere la pulizia, ogni volta, senza eccezioni. È l'equivalente CoreFoundation del controllare se un'API restituisce una copia o una vista, e il costo di sbagliare è un crash intermittente invece di un errore
Cosa non dichiara questo backend
Non è mai girato su macOS al momento della scrittura, e dirlo senza giri di parole è più utile di una rassicurazione implicita. Ciò che è dimostrabilmente vero è più ristretto e resta comunque prezioso: l'unit compila su Windows come parte della build quotidiana, ogni simbolo del framework è legato per nome a runtime con i fallimenti enumerati, e la logica di selezione degli algoritmi compresi entrambi i fallback PSS è Pascal ordinario che si può revisionare e su cui ragionare. La prima esecuzione su un Mac funzionerà o produrrà una lista di nomi da correggere
La controparte di verifica, che usa il decoder CMS di livello più alto invece di assemblare a mano la struttura CMS, è trattata in verificare firme PDF su macOS con SecTrust, e condivide la stessa infrastruttura di binding e lo stesso approccio diagnostico
L'idea trasferibile qui riguarda il piazzamento del rischio piuttosto che macOS. Quando devi scrivere codice contro un'interfaccia che non puoi verificare, scegli la costruzione dove gli errori sono più economici da localizzare. Il binding dinamico con una lista esplicita di nomi non risolti trasforma venti assunzioni non verificabili in una riga diagnostica. Entrambi i backend vengono spediti come sorgente con il PDFium Delphi component, quindi se un nome di simbolo deve davvero essere corretto, è una modifica di una riga nel tuo albero invece che un ticket di supporto