Articolo tecnico

Un backend di timestamp libcurl per PDFium VCL su FPC

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

Diagramma del trasporto di timestamp libcurl di PDFium VCL che mostra curl_easy_setopt dichiarato come prototipi Pascal fissi long e puntatore che passano argomenti in registri interi, l'intestazione Expect vuota che sopprime l'handshake HTTP 100-continue, CURLOPT_NOSIGNAL che rimuove il percorso SIGALRM sui thread di lavoro, e il tetto della risposta a livello trasporto
Due impostazioni decidono se la richiesta si completa: un'intestazione Expect vuota evita i server che non rispondono mai alla continuazione, e NOSIGNAL tiene i timeout di risoluzione dei nomi fuori dal percorso dei segnali mentre la firma gira su un thread di lavoro

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

Diagramma PDFium VCL di una richiesta di timestamp RFC 3161 che fluisce da DocumentDigest attraverso BuildTimeStampQuery e PostTimeStampQuery su libcurl verso un server TSA, la risposta DER limitata al trasporto, poi AttachTimeStampToken che alimenta il DSS e il rinnovo dei timestamp di archivio nella validazione a lungo termine
Il timestamping è il primo passo della storia della validazione a lungo termine: il token va allegato, il materiale di validazione va registrato nel document security store, e i timestamp di archivio vanno rinnovati prima che quello corrente indebolisca

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