PDFium VCL invia le richieste di timestamp RFC 3161 attraverso libcurl sui target non Windows, con binding dinamico a otto simboli, rispecchiando la forma del backend Windows che si lega a WinHTTP. Due impostazioni di opzione decidono se il trasporto è affidabile sotto carico, e l'intera unit è stata validata su una macchina che non poteva compilarla per la sua piattaforma target
Il timestamping è ciò che trasforma una firma in qualcosa che sopravvive alla scadenza del certificato, ed è un'operazione di rete seduta dentro un'operazione di firma. Quella combinazione rende la scelta del trasporto consequenziale in un modo in cui di solito non lo è: gira su un thread di lavoro, parla con un server che non controlli, e un blocco lì stalla una pipeline di firma invece che il caricamento di una pagina
Perché libcurl invece del client HTTP di FPC?
Perché l'alternativa trascina uno stack TLS nel repository e poi ti fa mantenere il suo rilevamento di versione. La via ovvia su Free Pascal è fphttpclient con il socket layer OpenSSL, e fallisce sui dettagli: i binding OpenSSL di FPC 3.2.2 rilevano OpenSSL 3.x in modo inaffidabile sulla maggior parte delle distribuzioni attuali, e macOS aggiunge differenze LibreSSL sopra. Ciò che comincia come una piccola chiamata HTTP diventa manutenzione continuativa dell'ABI TLS di qualcun altro
libcurl risolve il proprio backend TLS e valida le catene contro il trust store della piattaforma, quindi il lato Pascal non ha bisogno di niente di ciò. Il layer di binding sono otto simboli. Quel conteggio è l'argomento: una superficie più piccola tra il tuo codice e una dipendenza in movimento significa meno posti in cui un aggiornamento di distribuzione ti rompe, e rispecchia l'esistente backend Windows, che lega una manciata di punti di ingresso WinHTTP allo stesso modo
uses
FPdfTsaFpc;
var
ReqDer, RespDer: TBytes;
begin
if not TsaHttpAvailable then
raise Exception.Create('no HTTP transport for timestamping');
Writeln('TSA transport: ', TsaHttpBackendName);
ReqDer := BuildTimeStampQuery(DocumentDigest);
if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
AttachTimeStampToken(RespDer)
else
raise Exception.Create('timestamp request failed');
end;
Dichiarare una funzione C variadica in Pascal
curl_easy_setopt e curl_easy_getinfo sono variadiche sul lato C, e Object Pascal non ha modo di esprimerlo. L'approccio che funziona è dichiarare diversi prototipi fissi, uno per classe di argomenti, tutti puntanti allo stesso simbolo esportato: una variante che prende un long, una che prende un puntatore, e così via, scelta al call site in base a ciò che stai davvero passando
Questo è sicuro per una ragione specifica che vale la pena capire invece di copiare. Ognuno di quei tipi di argomento viene passato in un registro intero sotto le convenzioni di chiamata di piattaforma in gioco, che è esattamente da dove il va_arg dell'implementazione C lo legge. Il trucco quindi regge per interi, puntatori e handle, e non regge per gli argomenti in virgola mobile, che viaggiano in registri diversi. Non aggiungere una variante che prende un double partendo dal presupposto che lo schema si generalizzi
// Un solo simbolo esportato, diversi prototipi fissi. Ogni variante passa
// il proprio argomento in un registro intero, che è dove il lato C lo
// legge. Una variante in virgola mobile non funzionerebbe e non va aggiunta
type
TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
Value: NativeInt): Integer; cdecl;
TCurlSetOptPtr = function(Handle: Pointer; Option: Integer;
Value: Pointer): Integer; cdecl;
var
curl_easy_setopt_long: TCurlSetOptLong;
curl_easy_setopt_ptr: TCurlSetOptPtr;
Due impostazioni che decidono se la richiesta si completa
La prima è un'intestazione Expect: vuota esplicita. libcurl accende l'handshake HTTP 100-continue per i corpi di richiesta sopra circa un kilobyte, e una query di timestamp con una richiesta di certificato di solito supera quella soglia. Alcuni server TSA non rispondono mai alla continuazione, quindi il client aspetta un timeout intero prima di inviare un corpo che il server avrebbe accettato immediatamente. Inviare un'intestazione Expect: vuota sopprime l'handshake, e la richiesta passa in un solo round trip
La seconda è CURLOPT_NOSIGNAL, che va impostata. Senza di essa libcurl implementa il proprio timeout di risoluzione dei nomi usando SIGALRM, e quel meccanismo non è thread-safe. La firma gira su un thread di lavoro, quindi il comportamento predefinito è un crash latente che compare sotto concorrenza e mai in un test single-threaded. Impostare il flag disabilita il percorso basato su segnali e costa solo la granularità del timeout del resolver
Entrambi i difetti condividono un profilo che li rende costosi da trovare dopo. Nessuno dei due compare in un test funzionale contro un server ben educato su un solo thread. Entrambi compaiono in produzione, contro un particolare TSA, sotto carico. Quando leghi una libreria di rete, leggi cosa i suoi default assumono del tuo processo prima di dare per scontato che combacino
Come verifichi codice che il tuo compilatore non vedrà mai?
Facendo in modo che il compilatore lo veda comunque, attraverso una copia controllata. La macchina di sviluppo qui non ha un cross-compiler Linux o macOS, quindi i rami non Windows dell'unit di timestamping non raggiungono mai il generatore di codice durante una build normale. Codice mai compilato è codice che marcesce in silenzio: un rename in un tipo condiviso, una lista di parametri cambiata, una dipendenza di unit aggiunta, e nessuno se ne accorge per mesi
La tecnica è meccanica. Copia l'unit in una directory temporanea, rinominala, e sostituisci ogni condizionale Windows, sia la forma {$IFDEF MSWINDOWS} sia la forma {$IF DEFINED(MSWINDOWS), con un simbolo mai definito. Poi compila la copia. Quando tutte le 3.828 righe compilano, hai provato che il percorso non Windows usa unit che esistono, chiama funzioni backend con firme corrispondenti, e referenzia tipi in scope. Non è la prova che il trasporto funzioni, e solo la piattaforma target te la darà. È la prova che il ramo non è già rotto, che è la modalità di guasto che davvero si accumula
L'abitudine compagna è lasciare l'unit libcurl stessa libera da guardie di piattaforma, così partecipa alla build Windows ordinaria anche se niente lì la referenzia. La build quotidiana quindi continua a controllarne gratis sintassi e tipi. Un'unit che compila solo su una piattaforma che non hai è un'unit senza alcun compilatore che la controlli, e lo stesso ragionamento vale attraverso il lavoro cross-compiler descritto in insidie cross-compiler di Delphi e FPC
Limitare ciò che torna
Una risposta di timestamp è una piccola struttura DER, e niente nel trasporto lo impone. Un server compromesso, mal configurato o semplicemente puntato all'URL sbagliato può restituire uno stream arbitrario, e un client che legge finché la connessione si chiude lo accumulerà allegramente. Entrambi i trasporti quindi pongono un tetto alla risposta, che è il posto giusto per il limite: rifiutare al trasporto impedisce che un corpo sovradimensionato venga mai allocato, mentre un controllo a livello di parser scatta solo dopo che la memoria è stata impegnata
Lo stesso ragionamento vale per l'URL. Il backend accetta solo scheme che sa parlare in modo sensato, così un errore di configurazione fallisce immediatamente con un messaggio chiaro invece di essere consegnato a libcurl da interpretare nel modo che il suo supporto di protocollo permette
Dove sta il trasporto nella storia della firma
Il timestamping è il primo passo della storia della validazione a lungo termine piuttosto che tutta la storia. Il token va allegato alla firma, il materiale di validazione va registrato nel document security store, e i timestamp di archivio vanno rinnovati prima che quello corrente indebolisca. Tutto quell'arco è trattato in firme PDF a lungo termine con timestamp RFC 3161 e DSS
Il trasporto è anche un pezzo di una posizione di portabilità più ampia: il loader di libreria nativa descritto in caricare la libreria nativa su qualsiasi target gestisce la stessa classe di problema per il binario PDFium stesso. In entrambi i casi lo schema è identico, legare dinamicamente un piccolo numero di simboli, riportare precisamente cosa non si è legato, e non lasciare mai che una dipendenza mancante diventi un fallimento al linking che impedisce all'applicazione di partire
I backend di timestamp Windows e non Windows vengono spediti entrambi con il PDFium Delphi component, selezionati per target e non per configurazione, così un'applicazione Lazarus su Linux e un'applicazione Delphi su Windows producono la stessa firma con timestamp attraverso plumbing diversi