Odborný článok

Backend časových pečiatok libcurl pre PDFium VCL na FPC

PDFium VCL posiela RFC 3161 požiadavky na časovú pečiatku cez libcurl na non-Windows cieľoch, dynamicky viazané na osem symbolov, v zrkadlovom tvare Windows backendu viažuceho na WinHTTP. Dve nastavenia volieb rozhodujú, či je transport spoľahlivý pod záťažou, a celá unita sa overila na stroji, ktorý ju nedokázal skompilovať pre jej cieľovú platformu

Časová pečiatka je to, čo z podpisu urobí niečo, čo prežije vypršanie certifikátu, a je to sieťová operácia siediaca vo vnútri podpisovej operácie. Táto kombinácia robí voľbu transportu závažnou spôsobom, akým obyčajne nie je: beží na worker vlákne, rozpráva so serverom, ktorý nekontrolujete, a zaseknutie tam zastaví podpisovú pipeline, nie načítanie strany

Prečo libcurl namiesto FPC HTTP klienta?

Pretože alternatíva vlečie TLS stack do repozitára a potom vám nadelí údržbu jeho detekcie verzie. Zjavná trasa na Free Pascal je fphttpclient s OpenSSL socket vrstvou a zlyháva na detailoch: FPC 3.2.2 OpenSSL bindingy detekujú OpenSSL 3.x nespoľahlivo na väčšine súčasných distribúcií a macOS pridáva LibreSSL rozdiely navyše. Čo začína ako malé HTTP volanie, sa stáva priebežnou údržbou cudzieho TLS ABI

libcurl si resolveuje vlastný TLS backend a validuje reťaze voči platformovému trust store, takže Pascal strana nepotrebuje nič z toho. Viazacia vrstva je osem symbolov. Toto číslo je argument: menšia plocha medzi vaším kódom a pohybujúcou sa závislosťou znamená menej miest, kde vás upgrade distribúcie rozbije, a sedí s existujúcim Windows backendom, ktorý viaže hrsť vstupných bodov WinHTTP rovnakým spôsobom

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;

Deklarovanie C variadic funkcie v Pascale

curl_easy_setopt a curl_easy_getinfo sú na C strane variadic a Object Pascal nemá spôsob, ako to vyjadriť. Prístup, ktorý funguje, je deklarovať niekoľko pevných prototypov, jeden na triedu argumentov, všetky ukazujúce na ten istý exportovaný symbol: variant berúci long, variant berúci pointer a tak ďalej, vyberaný na mieste volania podľa toho, čo reálne podávate

Toto je bezpečné z konkrétneho dôvodu, ktorý stojí za pochopenie, nie za kopírovanie. Každý z tých typov argumentov sa podáva v celočíselnom registri pod platformovými konvenciami volania, ktoré hrajú, presne odkiaľ ho číta C implementácia va_arg. Trik preto drží pre celé čísla, pointery a handly a nedrží pre argumenty s plávajúcou čiarkou, ktoré cestujú v iných registroch. Nepridávajte variant berúci double v domnienke, že sa vzor zobecňuje

// Jeden exportovaný symbol, niekoľko pevných prototypov. Každý variant
// podáva svoj argument v celočíselnom registri, presne odkiaľ ho číta
// C strana. Variant s plávajúcou čiarkou by nefungoval a nesmie sa pridať
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;

Dve nastavenia, ktoré rozhodnú, či sa požiadavka dokončí

Prvým je explicitne prázdna hlavička Expect:. libcurl zapína handshake HTTP 100-continue pre telá požiadaviek nad zhruba jeden kilobajt a dopyt po časovej pečiatke s certifikátovou požiadavkou tú prahovú hodnotu zvyčajne prekročí. Niektoré TSA servery nikdy neodpovedia na pokračovanie, takže klient vyčká plný timeout pred odoslaním tela, ktoré by server prijal okamžite. Odoslanie prázdnej hlavičky Expect: potlačí handshake a požiadavka prejde jedným round tripom

Druhým je CURLOPT_NOSIGNAL, ktorý sa musí nastaviť. Bez neho implementuje libcurl svoj timeout rozlíšenia mien cez SIGALRM a ten mechanizmus nie je thread-safe. Podpisovanie beží na worker vlákne, takže predvolené správanie je latentný pád, ktorý sa objaví pod konkurenciou a nikdy v jednovláknovom teste. Nastavenie príznaku vypne cestu založenú na signáloch a stojí len granularitu timeoutu resolvera

Oba defekty zdieľajú profil, ktorý ich robí drahými na neskoršie nájdenie. Ani jeden sa neukáže vo funkčnom teste proti dobre vychovanému serveru na jednom vlákne. Oba sa objavia vo výrobe, proti jednému konkrétnemu TSA, pod záťažou. Keď viažete sieťovú knižnicu, prečítajte si, čo jej defaulty predpokladajú o vašom procese, skôr než predpokladáte, že sedia

Diagram transportu časových pečiatok libcurl v PDFium VCL ukazujúci curl_easy_setopt deklarovaný ako pevné long a pointer Pascal prototypy podávajúce argumenty v celočíselných registroch, prázdnu hlavičku Expect potlačujúcu handshake HTTP 100-continue, CURLOPT_NOSIGNAL odstraňujúci cestu SIGALRM na worker vláknach a strop odpovede na úrovni transportu
Dve nastavenia rozhodnú, či sa požiadavka dokončí: prázdna hlavička Expect sa vyhne serverom, ktoré nikdy neodpovedia na pokračovanie, a NOSIGNAL drží timeouty rozlíšenia mien mimo signálovej cesty, kým podpisovanie beží na worker vlákne

Ako overíte kód, ktorý váš prekladač nikdy nevidí?

Tým, že prekladač ho predsa len uvidí, cez kontrolovanú kópiu. Vývojový stroj tu nemá žiadny Linux ani macOS cross-compiler, takže non-Windows vetvy unitu časových pečiatok sa za normálneho buildu nikdy nedostanú ku generátoru kódu. Kód, ktorý sa nikdy nekompiluje, je kód, ktorý poticho hniezdi: premenovanie v zdieľanom type, zmenený zoznam parametrov, pridaná unit závislosť a nikto si nič nevšimne mesiace

Technika je mechanická. Skopírujte unitu do dočasného adresára, premenujte ju a nahraďte každý Windows podmienený riadok, formu {$IFDEF MSWINDOWS} aj formu {$IF DEFINED(MSWINDOWS), symbolom, ktorý nie je nikdy definovaný. Potom skompilujte kópiu. Keď sa skompiluje všetkých 3 828 riadkov, dokázali ste, že non-Windows cesta používa unitá, ktoré existujú, volá backendové funkcie so sediacimi signatúrami a referencuje typy, ktoré sú v dosahu. To nie je dôkaz, že transport funguje, a nič kratšie než cieľová platforma vám to nedá. Je to dôkaz, že vetva nie je už vopred rozbitá, čo je režim zlyhania, ktorý sa reálne hromadí

Sprievodným zvykom je nechať samotnú unitu libcurl bez platformových stráží, takže sa zúčastňuje obyčajného Windows buildu, hoci sa tam na nič neodkazuje. Denný build potom zadarmo ďalej stráži jej syntax a typy. Unita, ktorá sa kompiluje len na platforme, ktorú nemáte, je unita bez jediného prekladača, ktorý by ju kontroloval, a rovnaké uvažovanie platí naprieč prácou s cross-compiler popísanou v článku pasce cross-compilera Delphi a FPC

Ohraničenie toho, čo sa vráti

Odpoveď časovej pečiatky je malá DER štruktúra a transport to nevynucuje ničím. Server, ktorý je skompromitovaný, zle nakonfigurovaný alebo jednoducho namierený na zlé URL, môže vrátiť ľubovoľný stream a klient čítajúci do zavretia spojenia ho s radosťou nazbiera. Oba transporty preto nastavujú strop odpovede, čo je správne miesto pre limit: zamietnutie na transportu zabráni tomu, aby sa nadmerné telo kedykoľvek alokovalo, kým kontrola na úrovni parsera vystrelí až potom, čo sa pamäť už zaviazala

Rovnaké uvažovanie platí pre URL. Backend akceptuje len schémy, ktoré dokáže zmysluplne rozprávať, takže chyba konfigurácie zlyhá okamžite s jasnou správou namiesto toho, aby sa podala libcurl na interpretáciu akýmkoľvek spôsobom, ako dovolí jeho podpora protokolov

Kde transport sedí v podpisovom príbehu

Časová pečiatka je prvý krok príbehu dlhodobej validácie, nie celý ten príbeh. Token sa musí pripojiť k podpisu, validačný materiál sa musí zaznamenať do document security store a archívne pečiatky sa musia obnoviť skôr, než súčasná oslabne. Celý ten oblúk kryje článok dlhodobé PDF podpisy s RFC 3161 pečiatkami a DSS

Diagram PDFium VCL požiadavky RFC 3161 na časovú pečiatku tečúcej z DocumentDigest cez BuildTimeStampQuery a PostTimeStampQuery po libcurl k TSA serveru, DER odpoveď orezaná na transporte, potom AttachTimeStampToken živiaca DSS a obnovovanie archívnych pečiatok vo dlhodobej validácii
Časová pečiatka je prvý krok príbehu dlhodobej validácie: token sa musí pripojiť, validačný materiál zaznamenať do document security store a archívne pečiatky obnoviť skôr, než súčasná oslabne

Transport je aj jeden kúsok širšej pozície prenositeľnosti: natívny loader knižnice popísaný v článku načítanie natívnej knižnice na akomkoľvek cieli rieši tú istú triedu problému pre samotnú binárku PDFium. V oboch prípadoch je vzor identický: viazať malý počet symbolov dynamicky, hlásiť presne, čo sa nepodarilo viazať, a nikdy nedovoliť, aby sa chýbajúca závislosť stala linkovým zlyhaním zastavujúcim štart aplikácie

Windows aj non-Windows backendy časových pečiatok prichádzajú s PDFium Delphi komponentom, vyberané podľa cieľa, nie podľa konfigurácie, takže Lazarus aplikácia na Linuxe a Delphi aplikácia na Windows vyprodukujú ten istý podpis s časovou pečiatkou cez odlišnú inštalatérsku prácu