Articol tehnic

Un backend de timestamps libcurl pentru PDFium VCL pe FPC

PDFium VCL trimite cererile de timestamp RFC 3161 prin libcurl pe țintele non-Windows, legat dinamic la opt simboluri, oglindind forma backend-ului Windows care se leagă la WinHTTP. Două setări de opțiuni decid dacă transportul e fiabil sub sarcină, iar întreaga unitate a fost validată pe o mașină care nu putea să o compileze pentru platforma ei țintă

Timestamping-ul e ceea ce transformă o semnătură în ceva ce supraviețuiește expirării certificatului, și e o operație de rețea care stă în interiorul unei operații de semnare. Combinația aceea face alegerea transportului consecventă într-un mod în care de obicei nu e: rulează pe un fir de execuție worker, vorbește cu un server pe care nu îl controlați, iar un blocaj acolo oprește un pipeline de semnare, nu o încărcare de pagină

De ce libcurl și nu clientul HTTP al FPC?

Pentru că alternativa tragă un stack TLS în depozit și apoi vă pune să îi întrețineți detectarea versiunii. Calea evidentă pe Free Pascal e fphttpclient cu stratul de socket OpenSSL, și pică pe detalii: binding-urile OpenSSL din FPC 3.2.2 detectează nesigur OpenSSL 3.x pe majoritatea distribuțiilor actuale, iar macOS adaugă diferențele LibreSSL deasupra. Ceea ce începe ca un apel HTTP mic devine întreținere continuă a ABI-ului TLS al altcuiva

libcurl își rezolvă singur backend-ul TLS și validează lanțurile contra trust store-ului platformei, deci partea în Pascal n-are nevoie de niciunul. Stratul de binding e de opt simboluri. Contorul acela e argumentul: o suprafață mai mică între codul dumneavoastră și o dependență în mișcare înseamnă mai puține locuri în care un upgrade de distribuție să vă strice, și se potrivește cu backend-ul Windows existent, care leagă la fel o mână de puncte de intrare WinHTTP

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;

Declararea unei funcții C variadice în Pascal

curl_easy_setopt și curl_easy_getinfo sunt variadice pe partea C, iar Object Pascal nu are cum să exprime asta. Abordarea care funcționează e să declarați mai multe prototipuri fixe, câte unul per clasă de argumente, toate îndreptate spre același simbol exportat: o variantă care ia long, una care ia pointer și așa mai departe, alese la locul apelului după ce transmiteți efectiv

Asta e sigur dintr-un motiv care merită înțeles, nu copiat. Fiecare dintre acele tipuri de argumente e transmis într-un registru de întregi sub convențiile de apel ale platformei aflate în joc, exact locul de unde îl citește implementarea C cu va_arg. Trucul ține deci pentru întregi, pointeri și handle-uri, și nu ține pentru argumentele în virgulă mobilă, care călătoresc în registre diferite. Nu adăugați o variantă pentru double pe presupunerea că tiparul se generalizează

// Un simbol exportat, mai multe prototipuri fixe. Fiecare variantă își transmite
// argumentul într-un registru de întregi, adică exact locul de unde citește
// partea C. O variantă în virgulă mobilă nu ar funcționa și nu trebuie adăugată
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;

Două setări care decid dacă cererea se termină

Prima e un antet Expect: gol, explicit. libcurl activează strângerea de mână HTTP 100-continue pentru corpurile de cerere de peste un kilobyte cam așa, iar o interogare de timestamp cu o cerere de certificat depășește de obicei acel prag. Unele servere TSA nu răspund niciodată la continuare, deci clientul așteaptă un timeout complet înainte să trimită un corp pe care serverul l-ar fi acceptat imediat. Trimiterea unui antet Expect: gol suprimă strângerea de mână, iar cererea trece într-un singur du-te-vino

A doua e CURLOPT_NOSIGNAL, care trebuie stabilită. Fără ea libcurl implementează timeout-ul de rezolvare a numelor cu SIGALRM, iar mecanismul acela nu e thread-safe. Semnarea rulează pe un fir worker, deci comportamentul implicit e un crash latent care apare sub concurență și niciodată într-un test single-threaded. Stabilirea fanionului dezactivează calea bazată pe semnale și costă doar granularitatea timeout-ului resolver-ului

Ambele defecte partajează un profil care le face scumpe de găsit mai târziu. Niciuna nu apare într-un test funcțional contra unui server bine-crescut, pe un singur fir. Ambele apar în producție, contra unui anume TSA, sub sarcină. Când legați o bibliotecă de rețea, citiți ce presupun implicit-urile ei despre procesul dumneavoastră înainte să presupuneți că se potrivesc

Diagrama transportului de timestamp libcurl din PDFium VCL, arătând curl_easy_setopt declarat ca prototipuri Pascal fixe pentru long și pointer care transmit argumentele în registre de întregi, antetul Expect gol care suprimă strângerea de mână HTTP 100-continue, CURLOPT_NOSIGNAL care înlătură calea SIGALRM pe firele worker și plafonul de răspuns la nivel de transport
Două setări decid dacă cererea se termină: un antet Expect gol evită serverele care nu răspund niciodată la continuare, iar NOSIGNAL ține timeout-urile de rezolvare a numelor în afara căii de semnale cât timp semnarea rulează pe un fir worker

Cum verificați cod pe care compilatorul dumneavoastră nu îl va vedea niciodată?

Făcând ca compilatorul să îl vadă oricum, printr-o copie controlată. Mașina de dezvoltare de aici nu are cross-compiler Linux sau macOS, deci ramurile non-Windows ale unității de timestamping nu ajung niciodată la generatorul de cod în timpul unui build normal. Codul care nu se compilează niciodată e cod care putrezește pe furiș: o redenumire într-un tip partajat, o listă de parametri schimbată, o dependență de unitate adăugată, și nimeni nu observă luni de zile

Tehnica e mecanică. Copiați unitatea într-un director temporar, redenumiți-o și înlocuiți fiecare condiționată Windows, atât forma {$IFDEF MSWINDOWS} cât și forma {$IF DEFINED(MSWINDOWS), cu un simbol care nu e niciodată definit. Apoi compilați copia. Când toate cele 3.828 de linii se compilează, ați dovedit că calea non-Windows folosește unități care există, apelează funcțiile backend-ului cu semnături potrivite și referențiază tipuri aflate în scop. Asta nu e dovada că transportul funcționează, iar nimic mai puțin decât platforma țintă nu vi-o va da. E dovada că ramura nu e deja stricată, care e modul de eșec care chiar se acumulează

Obiceiul însoțitor e să lăsați unitatea libcurl în sine liberă de gărzi de platformă, astfel încât ea să participe la build-ul obișnuit Windows chiar dacă nimic acolo nu o referențiază. Build-ul zilnic o păstrează apoi verificată la sintaxă și tipuri gratis. O unitate care se compilează doar pe o platformă pe care nu o aveți e o unitate fără niciun compilator care s-o verifice, iar același raționament se aplică în toată lucrarea cross-compiler descrisă în capcanele cross-compiler Delphi și FPC

Limitarea a ceea ce se întoarce

Un răspuns de timestamp e o structură DER mică, iar nimic din transport nu impune asta. Un server compromis, configurat greșit sau pur și simplu îndreptat spre URL-ul greșit poate întoarce un stream arbitrar, iar un client care citește până se închide conexiunea îl va acumula fericit. Ambele transporturi plafonează deci răspunsul, iar acela e locul corect pentru limită: refuzul la transport împiedică un corp supradimensionat să fie vreodată alocat, pe când o verificare la nivel de parser se declanșează doar după ce memoria a fost deja angajată

Același raționament se aplică URL-ului. Backend-ul acceptă doar scheme pe care le poate vorbi cu sens, astfel încât o greșeală de configurare eșuează imediat, cu un mesaj clar, în loc să fie predată libcurl spre interpretare în orice fel permite suportul său de protocoale

Unde stă transportul în povestea semnării

Timestamping-ul e primul pas al poveștii de validare de lungă durată, nu totul. Tokenul trebuie atașat semnăturii, materialul de validare trebuie înregistrat în document security store, iar timestamp-urile de arhivă trebuie reînnoite înainte ca cel curent să slăbească. Întregul arc acela e acoperit în semnăturile PDF de lungă durată cu timestamps RFC 3161 și DSS

Diagramă PDFium VCL a unei cereri de timestamp RFC 3161 care curge din DocumentDigest prin BuildTimeStampQuery și PostTimeStampQuery peste libcurl spre un server TSA, răspunsul DER plafonat la transport, apoi AttachTimeStampToken hrănind DSS-ul și reînnoirea timestamp-urilor de arhivă în validarea de lungă durată
Timestamping-ul e primul pas al poveștii de validare de lungă durată: tokenul trebuie atașat, materialul de validare înregistrat în document security store, iar timestamp-urile de arhivă reînnoite înainte ca cel curent să slăbească

Transportul e și o piesă a unei poziții mai largi de portabilitate: loader-ul de bibliotecă nativă descris în încărcarea bibliotecii native pe orice țintă tratează aceeași clasă de problemă pentru binarul PDFium. În ambele cazuri tiparul e identic: legați dinamic un număr mic de simboluri, raportați precis ce a eșuat la legare și nu lăsați niciodată o dependență lipsă să devină un eșec la linkare care împiedică aplicația să pornească

Backend-urile de timestamp Windows și non-Windows se livrează ambele cu PDFium Delphi component, selectate după țintă, nu după configurație, astfel încât o aplicație Lazarus pe Linux și o aplicație Delphi pe Windows produc aceeași semnătură cu timestamp prin plumburi diferite