Technisch artikel

Een libcurl timestamp backend voor PDFium VCL op FPC

PDFium VCL stuurt RFC 3161 timestamp-verzoeken via libcurl op niet-Windows-targets, dynamisch gebonden aan acht symbolen, als spiegelbeeld van de Windows-backend die aan WinHTTP bindt. Twee optie-instellingen bepalen of het transport onder belasting betrouwbaar is, en de hele unit werd gevalideerd op een machine die hem voor zijn targetplatform niet eens kon compileren

Timestamping is wat van een handtekening iets maakt dat certificaatverloop overleeft, en het is een netwerkoperatie die binnenin een ondertekeningsoperatie zit. Die combinatie maakt de transportkeuze consequent op een manier die hij gewoonlijk niet is: hij draait op een worker thread, hij praat met een server die u niet beheert, en een hang daar blokkeert een ondertekeningspipeline in plaats van een paginalading

Waarom libcurl in plaats van de FPC HTTP client?

Omdat het alternatief een TLS-stack de repository insleept en u vervolgens zijn versiedetectie laat onderhouden. De voor de hand liggende route op Free Pascal is fphttpclient met de OpenSSL-socketlaag, en die faalt op de details: de FPC 3.2.2 OpenSSL-bindings detecteren OpenSSL 3.x onbetrouwbaar op de meeste huidige distributies, en macOS voegt daar LibreSSL-verschillen bovenop. Wat begint als een kleine HTTP-aanroep wordt voortdurend onderhoud van het TLS ABI van iemand anders

libcurl resolved zijn eigen TLS-backend en valideert chains tegen de platform trust store, dus de Pascal-kant heeft dat alles niet nodig. De bindinglaag is acht symbolen. Dat aantal is het argument: een kleiner oppervlak tussen uw code en een bewegelijke dependency betekent minder plekken waar een distributie-upgrade u kan breken, en het matcht de bestaande Windows-backend, die op dezelfde manier een handjevol WinHTTP-entrypoints bindt

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;

Een C-variadische functie declareren in Pascal

curl_easy_setopt en curl_easy_getinfo zijn variadisch aan de C-kant, en Object Pascal heeft geen manier om dat uit te drukken. De aanpak die werkt, is meerdere vaste prototypes declareren, één per argumentklasse, allemaal wijzend naar hetzelfde geëxporteerde symbool: een long-variant, een pointer-variant, enzovoort, gekozen op de call site door wat u werkelijk doorgeeft

Dit is veilig om een specifieke reden die het begrijpen waard is in plaats van kopiëren. Elk van die argumenttypes wordt onder de geldende platform calling conventions doorgegeven in een integer-register, en dat is precies waar de C-implementatie va_arg hem vandaan leest. De truc geldt daardoor voor integers, pointers en handles, en hij geldt niet voor floating-point-argumenten, die in andere registers reizen. Voeg geen double-variant toe op de aanname dat het patroon veralgemeent

// Eén geëxporteerd symbool, meerdere vaste prototypes. Elke variant geeft zijn
// argument door in een integer-register, en dat is waar de C-kant hem leest.
// Een floating-point-variant zou niet werken en mag niet worden toegevoegd
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;

Twee instellingen die bepalen of het verzoek afloopt

De eerste is een expliciet lege Expect:-header. libcurl zet de HTTP 100-continue-handshake aan voor request bodies van ruwweg een kilobyte en daarboven, en een timestamp-query met een certificaatverzoek komt doorgaans over die drempel. Sommige TSA-servers antwoorden nooit op de continuation, dus de client wacht een volledige timeout uit voordat hij een body verstuurt die de server onmiddellijk geaccepteerd zou hebben. Een lege Expect:-header versturen onderdrukt de handshake, en het verzoek gaat in één round trip door

De tweede is CURLOPT_NOSIGNAL, die gezet moet worden. Zonder die implementeert libcurl zijn name-resolution timeout met SIGALRM, en dat mechanisme is niet thread-safe. Ondertekenen draait op een worker thread, dus het standaardgedrag is een latente crash die onder gelijktijdigheid verschijnt en nooit in een single-threaded test. De flag zetten schakelt de signaalgebaseerde route uit en kost alleen de resolutiegranulariteit van de resolver-timeout

Beide defecten delen een profiel dat ze later duur maakt om te vinden. Geen van beide verschijnt in een functionele test tegen een welwillende server op één thread. Beide verschijnen in productie, tegen één specifieke TSA, onder belasting. Als u een netwerklibrary bindt, lees dan wat zijn defaults over uw proces aannemen voordat u aanneemt dat ze matchen

PDFium VCL libcurl timestamp-transportdiagram met curl_easy_setopt gedeclareerd als vaste long- en pointer-Pascal-prototypes die argumenten in integer-registers doorgeven, de lege Expect-header die de HTTP 100-continue-handshake onderdrukt, CURLOPT_NOSIGNAL dat de SIGALRM-route op worker threads wegneemt, en de responslimiet op transportniveau
Twee instellingen bepalen of het verzoek afloopt: een lege Expect-header vermijdt servers die nooit op de continuation antwoorden, en NOSIGNAL houdt name-resolution timeouts van het signaalpad terwijl ondertekenen op een worker thread draait

Hoe verifieert u code die uw compiler nooit zal zien?

Door de compiler hem toch te laten zien, via een gecontroleerde kopie. De ontwikkel-machine hier heeft geen Linux- of macOS-cross-compiler, dus de niet-Windows-takken van de timestamping-unit bereiken de codegenerator nooit tijdens een normale build. Code die nooit wordt gecompileerd is code die stilletjes rot: een hernoeming in een gedeeld type, een veranderde parameterlijst, een toegevoegde unit-afhankelijkheid, en niemand merkt het maandenlang

De techniek is mechanisch. Kopieer de unit naar een tijdelijke directory, hernoem hem en vervang elke Windows-conditional, zowel de {$IFDEF MSWINDOWS}-vorm als de {$IF DEFINED(MSWINDOWS)-vorm, door een symbool dat nooit gedefinieerd is. Compileer daarna de kopie. Als alle 3.828 regels compileren, heeft u bewezen dat het niet-Windows-pad units gebruikt die bestaan, backendfuncties aanroept met matchende signaturen en types raakt die in scope zijn. Dat is geen bewijs dat het transport werkt, en niets minder dan het targetplatform geeft u dat. Het is bewijs dat de tak niet al kapot is, en dat is de faalmodus die werkelijk accumuleert

De begeleidende gewoonte is de libcurl-unit zelf vrij van platformguards te houden, zodat hij deelneemt aan de gewone Windows-build zelfs als daar niets naar hem verwijst. De dagelijkse build bewaakt dan gratis zijn syntaxis en types. Een unit die alleen compileert op een platform dat u niet heeft, is een unit waar helemaal geen compiler op kijkt, en dezelfde redenering geldt in het hele cross-compilerwerk dat staat beschreven in Delphi- en FPC-cross-compiler-valkuilen

Begrenzen van wat er terugkomt

Een timestamp-respons is een kleine DER-structuur, en niets aan het transport dwingt dat af. Een server die is gecompromitteerd, verkeerd geconfigureerd of simpelweg naar de verkeerde URL wijzend, kan een willekeurige stream teruggeven, en een client die leest tot de verbinding sluit, stapelt hem braaf op. Beide transporten begrenzen daarom de respons, en dat is de juiste plek voor de limiet: weigeren op transportniveau voorkomt dat een te grote body ooit wordt gealloceerd, terwijl een check op parserniveau pas afgaat nadat het geheugen is vastgelegd

Dezelfde redenering geldt voor de URL. De backend accepteert alleen schemes die hij zinvol kan spreken, dus een configuratiefout faalt onmiddellijk met een duidelijke melding in plaats van aan libcurl te worden overgelaten om te interpreteren op welke manier zijn protocolondersteuning maar toelaat

Waar het transport zit in het ondertekeningsverhaal

Timestamping is de eerste stap van het langetermijnvalidatieverhaal in plaats van het hele verhaal. Het token moet aan de handtekening worden gehangen, het validatiemateriaal moet in de document security store worden vastgelegd, en archive timestamps moeten worden vernieuwd voordat de huidige verzwakt. Die hele boog staat in langetermijn-PDF-handtekeningen met RFC 3161 timestamps en de DSS

PDFium VCL-diagram van een RFC 3161 timestamp-verzoek dat van DocumentDigest via BuildTimeStampQuery en PostTimeStampQuery over libcurl naar een TSA-server stroomt, de DER-respons begrensd op transportniveau, en daarna AttachTimeStampToken dat de DSS en de vernieuwing van archive timestamps voedt in langetermijnvalidatie
Timestamping is de eerste stap van het langetermijnvalidatieverhaal: het token moet worden gehangen, validatiemateriaal vastgelegd in de document security store, en archive timestamps vernieuwd voordat de huidige verzwakt

Het transport is ook één stuk van een bredere portabiliteitspositie: de native library loader beschreven in de native library laden op elke target behandelt dezelfde probleemklasse voor de PDFium-binary zelf. In beide gevallen is het patroon identiek: een klein aantal symbolen dynamisch binden, precies rapporteren wat niet bindt, en nooit toestaan dat een ontbrekende dependency een link-time failure wordt die de applicatie verhindert te starten

De Windows- en niet-Windows-timestamp-backends worden beide geleverd met de PDFium Delphi component, gekozen per target in plaats van per configuratie, zodat een Lazarus-applicatie op Linux en een Delphi-applicatie op Windows dezelfde timestamp-handtekening produceren via verschillende leidingen