PDFium VCL posílá požadavky na časová razítka RFC 3161 přes libcurl na non-Windows cílech, dynamicky navázaný na osm symbolů, a zrcadlí podobu Windows backendu, který se váže na WinHTTP. Dvě nastavení option rozhodují, zda je transport spolehlivý pod zátěží, a celá unita se validovala na stroji, který ji nedokázal zkompilovat pro její cílovou platformu
Timestamping je to, co z podpisu udělá něco, co přežije vypršení certifikátu, a je to síťová operace sedící uvnitř podpisové operace. Tahle kombinace dělá volbu transportu důležitou způsobem, jakým obvykle není: běží na worker threadu, mluví se serverem, který nekontrolujete, a zatuhne-li tam, zastaví podpisovou pipeline, ne načítání stránky
Proč libcurl místo FPC HTTP klienta?
Protože alternativa vtáhne TLS stack do repozitáře a pak vás donutí udržovat jeho detekci verzí. Zjevná route na Free Pascalu je fphttpclient se socket vrstvou OpenSSL a selhává na detailech: FPC 3.2.2 OpenSSL bindingy detekují OpenSSL 3.x nespolehlivě na většině současných distribucí a macOS přidává navrch rozdíly LibreSSL. Co začíná jako malé HTTP volání, se stává průběžnou údržbou cizího TLS ABI
libcurl si resolveuje vlastní TLS backend a validuje chainy proti platformnímu trust store, takže Pascal strana nepotřebuje žádné z toho. Binding vrstva je osm symbolů. Tenhle počet je ten argument: menší povrch mezi vaším kódem a pohyblivou závislostí znamená míň míst, kde vás může rozbít upgrade distribuce, a sedí to ke stávajícímu Windows backendu, který stejným způsobem váže hrstku entry pointů 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;
Deklarace C variadic funkce v Pascalu
curl_easy_setopt a curl_easy_getinfo jsou na C straně variadic a Object Pascal nemá způsob, jak to vyjádřit. Fungující přístup je deklarovat několik fixních prototypů, jeden na třídu argumentů, všechny mířící na týž exportovaný symbol: variantu beroucí long, variantu beroucí pointer a tak dále, vybírané na místě volání podle toho, co doopravdy předáváte
Je to bezpečné z konkrétního důvodu, který stojí za pochopení, ne za okopírování. Každý z těch typů argumentů se předává v celočíselném registru za platných calling conventions platformy, což je přesně místo, kde ho čte C implementace va_arg. Trik tedy drží pro integery, pointery a handle a nedrží pro floating-point argumenty, které cestují v jiných registrech. Nepřidávejte variantu beroucí double v domnění, že se pattern zobecňuje
// Jeden exportovaný symbol, několik fixních prototypů. Každá varianta
// předává svůj argument v celočíselném registru, tedy tam, kde ho čte
// C strana. Floating-point varianta by nefungovala a přidávat se nesmí
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;
Dvě nastavení, která rozhodují, zda se požadavek dokončí
První je explicitní prázdný header Expect:. libcurl zapíná HTTP handshake 100-continue pro request body přes zhruba kilobajt a timestamp dotaz s requestem certifikátu tuhle hranici obvykle překračuje. Některé TSA servery na continuation nikdy neodpoví, takže klient vyčká plný timeout, než pošle body, který by server přijal okamžitě. Odeslání prázdného headeru Expect: handshake potlačí a požadavek projde jedním round tripem
Druhé je CURLOPT_NOSIGNAL, které se nastavit musí. Bez něj implementuje libcurl svůj timeout name resolution přes SIGALRM a tenhle mechanismus není thread-safe. Podepisování běží na worker threadu, takže defaultní chování je latentní crash, který se objeví pod konkurencí a nikdy v jednothreadovém testu. Nastavení flagu vypne signálovou cestu a stojí jen granularitu timeoutu resolveru
Oba defekty sdílejí profil, který je později dělá drahými na nalezení. Ani jeden se neukáže ve funkčním testu proti dobře vychovanému serveru na jednom threadu. Oba se objeví v produkci, proti jedné konkrétní TSA, pod zátěží. Když vážete networking knihovnu, přečtěte si, co její defaulty předpokládají o vašem procesu, než předpokladete, že sedí
Jak ověříte kód, který váš kompilátor nikdy neuvidí?
Tím, že ho kompilátoru stejně ukážete, přes kontrolovanou kopii. Vývojový stroj tady nemá žádný Linux ani macOS cross-compiler, takže non-Windows větve timestamping unity se při normálním buildu ke code generátoru nikdy nedostanou. Kód, který se nikdy nekompiluje, je kód, který tiše hnije: rename ve sdíleném typu, změněný seznam parametrů, přidaná závislost unity a nikdo si toho měsíce nevšimne
Technika je mechanická. Zkopírujte unitu do dočasného adresáře, přejmenujte ji a vyměňte každý Windows conditional, jak formu {$IFDEF MSWINDOWS}, tak formu {$IF DEFINED(MSWINDOWS), za symbol, který nikdy není definovaný. Pak kopii zkompilujte. Když se zkompiluje všech 3 828 řádků, dokázali jste, že non-Windows cesta používá unity, které existují, volá backend funkce se sedícími signaturami a referencuje typy, které jsou v scope. Není to důkaz, že transport funguje, to vám nedá nic kratšího než cílová platforma. Je to důkaz, že větev už není rozbitá, což je ten failure mód, který doopravdy akumuluje
Doprovodný zvyk je nechat unitu libcurl samotnou bez platformních guardů, takže se účastní obyčejného Windows buildu, i když se na ni tam nic nereferencuje. Denní build pak zdarma dál střeží její syntaxi a typy. Unitu, která se kompiluje jen na platformě, kterou nemáte, neprohlíží žádný kompilátor vůbec a tatáž úvaha platí napříč cross-compiler prací popsanou v článku pasti Delphi a FPC cross-compilátorů
Ohraničení toho, co se vrátí
Timestamp odpověď je malá DER struktura a transport to sám o sobě nevynucuje. Kompromitovaný, špatně nakonfigurovaný nebo prostě mířící na špatnou URL server může vrátit libovolný stream a klient, který čte, dokud se spojení nezavře, ho s chutí akumuluje. Oba transporty proto capují odezvu, což je správné místo pro ten limit: odmítnutí na transportu nedovolí, aby se oversized body vůbec alokoval, zatímco kontrola na úrovni parseru spustí až poté, co byla paměť commitnutá
Tatáž úvaha platí pro URL. Backend přijímá jen scheme, kterými umí smysluplně mluvit, takže chyba konfigurace selže okamžitě s jasnou hláškou, místo aby se předala libcurl k interpretaci jakýmkoli způsobem, jaký dovolí jeho podpora protokolů
Kde transport sedí v příběhu podepisování
Timestamping je první krok příběhu dlouhodobé validace, ne celý příběh. Token se musí připojit k podpisu, validační materiál se musí zaznamenat do document security store a archive timestampy se musí obnovovat, než současný oslabí. Celý tenhle oblouk pokrývá článek dlouhodobé PDF podpisy s RFC 3161 timestampy a DSS
Transport je taky jeden kus širší přenositelnostní pozice: native library loader popsaný v článku načítání native knihovny na jakémkoli cíli řeší tutéž třídu problému pro samotnou PDFium binárku. V obou případech je pattern identický: navázat dynamicky malý počet symbolů, přesně nahlásit, co se nepodařilo navázat, a nikdy nedovolit, aby se chybějící závislost změnila v link-time selhání zastavující start aplikace
Windows i non-Windows timestamp backendy se dodávají s PDFium Delphi komponentou, vybírané podle cíle, ne podle konfigurace, takže Lazarus aplikace na Linuxu a Delphi aplikace na Windows produkují tentýž timestampovaný podpis přes jiný plumbing