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
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
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