Teknisk artikel

En libcurl timestamp-backend til PDFium VCL på FPC

PDFium VCL sender RFC 3161 timestamp-forespørgsler gennem libcurl på ikke-Windows-targets, dynamisk bundet til otte symboler, i spejling af formen på Windows-backenden, der binder til WinHTTP. To option-indstillinger afgør, om transporten er pålidelig under belastning, og hele uniten blev valideret på en maskine, der ikke kunne kompilere den til sin target-platform

Timestamping er det, der forvandler en signatur til noget, der overlever certifikatudløb, og det er en netværksoperation, der sidder inde i en signeringsoperation. Den kombination gør transportvalget konsekvensbærende på en måde, det normalt ikke er: den kører på en worker-tråd, den taler med en server, du ikke styrer, og et hæng dér sætter en signeringspipeline i stå snarere end en sideindlæsning

Hvorfor libcurl i stedet for FPC HTTP-klienten?

Fordi alternativet slæber en TLS-stack ind i repositoryet og derefter får dig til at vedligeholde dens versionsdetektion. Den åbenlyse rute på Free Pascal er fphttpclient med OpenSSL socket-laget, og den fejler på detaljerne: FPC 3.2.2 OpenSSL-bindingerne detekterer OpenSSL 3.x upålideligt på de fleste aktuelle distributioner, og macOS lægger LibreSSL-forskelle ovenpå. Det, der starter som et lille HTTP-kald, bliver til løbende vedligeholdelse af en andens TLS ABI

libcurl resolver sin egen TLS-backend og validerer kæder mod platformens trust store, så Pascalsiden behøver intet af det. Bindinglaget er otte symboler. Det tal er argumentet: en mindre flade mellem din kode og en bevægelig afhængighed betyder færre steder, hvor en distributionsopgradering kan bryde dig, og det matcher den eksisterende Windows-backend, der binder en håndfuld WinHTTP entry points på samme måde

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;

At deklarere en C-variadic-funktion i Pascal

curl_easy_setopt og curl_easy_getinfo er variadiske på C-siden, og Object Pascal har ingen måde at udtrykke det på. Tilgangen, der virker, er at deklarere flere faste prototyper, én per argumentklasse, alle pegende på det samme eksporterede symbol: en variant, der tager en long, en variant, der tager en pointer, og så videre, valgt på call site efter, hvad du reelt sender

Dette er sikkert af en specifik grund, der er værd at forstå snarere end kopiere. Hver af disse argumenttyper sendes i et integer-register under de platform calling conventions, der er i spil, hvilket er præcis dér, hvor C-implementeringens va_arg læser den fra. Tricket holder derfor for integers, pointere og handles, og det holder ikke for floating-point-argumenter, som rejser i andre registre. Tilføj ikke en double-tagende variant i den antagelse, at mønsteret generaliserer

// Ét eksporteret symbol, flere faste prototyper. Hver variant sender
// sit argument i et integer-register, hvilket er, hvor C-siden læser
// det. En floating-point-variant ville ikke virke og må ikke tilføjes
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;

To indstillinger, der afgør, om forespørgslen fuldføres

Den første er en eksplicit tom Expect:-header. libcurl slår HTTP 100-continue-håndtrykket til for request bodies over omtrent én kilobyte, og en timestamp-forespørgsel med et certifikat request overskrider som regel den tærskel. Nogle TSA-servere svarer aldrig på continuationen, så klienten venter en fuld timeout ud, før den sender en body, serveren ville have accepteret med det samme. At sende en tom Expect:-header undertrykker håndtrykket, og forespørgslen går igennem i én round trip

Den anden er CURLOPT_NOSIGNAL, som skal sættes. Uden den implementerer libcurl sin name-resolution-timeout med SIGALRM, og den mekanisme er ikke thread-safe. Signering kører på en worker-tråd, så standardadfærden er et latent crash, der optræder under samtidighed og aldrig i en single-threaded test. At sætte flaget deaktiverer den signalbaserede vej og koster kun resolverens timeout-granularitet

Begge defekter deler en profil, der gør dem dyre at finde senere. Ingen af dem viser sig i en funktionel test mod en velopførende server på én tråd. Begge optræder i produktion, mod én bestemt TSA, under belastning. Når du binder et netværksbibliotek, så læs, hvad dets defaults antager om din proces, før du antager, at de matcher

PDFium VCL libcurl timestamp-transportdiagram, der viser curl_easy_setopt deklareret som faste long- og pointer-Pascal-prototyper, der sender argumenter i integer-registre, den tomme Expect-header, der undertrykker HTTP 100-continue-håndtrykket, CURLOPT_NOSIGNAL, der fjerner SIGALRM-vejen på worker-tråde, og transportniveau-responsloftet
To indstillinger afgør, om forespørgslen fuldføres: en tom Expect-header undgår servere, der aldrig svarer på continuationen, og NOSIGNAL holder name-resolution-timeouts væk fra signalvejen, mens signering kører på en worker-tråd

Hvordan verificerer du kode, din compiler aldrig ser?

Ved alligevel at få compileren til at se den, gennem en kontrolleret kopi. Udviklingsmaskinen her har ingen Linux- eller macOS-cross-compiler, så ikke-Windows-grenene af timestamping-uniten når aldrig kodegeneratoren under et normalt build. Kode, der aldrig kompileres, er kode, der rådner lydløst: et rename i en delt type, en ændret parameterliste, en tilføjet unit-afhængighed, og ingen lægger mærke til det i måneder

Teknikken er mekanisk. Kopiér uniten til en midlertidig mappe, omdøb den, og erstat enhver Windows-betinget, både {$IFDEF MSWINDOWS}-formen og {$IF DEFINED(MSWINDOWS)-formen, med et symbol, der aldrig er defineret. Kompilér så kopien. Når alle 3.828 linjer kompilerer, har du bevist, at ikke-Windows-vejen bruger unitter, der findes, kalder backend-funktioner med matchende signaturer og refererer typer, der er i scope. Det er ikke bevis på, at transporten virker, og intet kortere end target-platformen giver dig det. Det er bevis på, at grenen ikke allerede er i stykker, hvilket er den fejlmåde, der reelt akkumulerer

Den ledsagende vane er at holde libcurl-uniten selv fri for platform-guards, så den deltager i det almindelige Windows-build, selvom intet dér refererer den. Det daglige build holder så gratis vagt over dens syntaks og typer. En unit, der kun kompilerer på en platform, du ikke har, er en unit uden nogen compiler, der tjekker den overhovedet, og samme ræsonnement gælder på tværs af cross-compiler-arbejdet beskrevet i Delphi- og FPC cross-compiler-fælder

Sætte loft over det, der kommer tilbage

Et timestamp-respons er en lille DER-struktur, og intet ved transporten håndhæver det. En server, der er kompromitteret, forkert konfigureret eller blot peger på den forkerte URL, kan returnere en vilkårlig stream, og en klient, der læser, til forbindelsen lukker, akkumulerer den gerne. Begge transporter lofter derfor responsen, hvilket er det rigtige sted for grænsen: at nægte ved transporten forhindrer en overdimensioneret body i nogensinde at blive allokeret, mens et tjek på parser-niveau først slår til, efter hukommelsen er tildelt

Samme ræsonnement gælder URL'en. Backend accepterer kun schemes, den kan tale meningsfuldt, så en konfigurationsfejl fejler straks med en klar melding i stedet for at blive givet til libcurl at fortolke på hvilken som helst måde, dens protokolunderstøttelse tillader

Hvor transporten sidder i signeringshistorien

Timestamping er det første skridt i long-term validation-historien snarere end hele den. Tokenet skal vedhæftes signaturen, valideringsmaterialet skal registreres i document security store, og arkiv-timestamps skal fornys, før den aktuelle svækkes. Hele den bue er dækket i long-term PDF-signaturer med RFC 3161-timestamps og DSS

PDFium VCL-diagram over en RFC 3161 timestamp-forespørgsel, der flyder fra DocumentDigest gennem BuildTimeStampQuery og PostTimeStampQuery over libcurl til en TSA-server, DER-responsen loftet ved transporten, derefter AttachTimeStampToken, der fodrer DSS og arkiv-timestamp-fornyelse i long-term validation
Timestamping er det første skridt i long-term validation-historien: tokenet skal vedhæftes, valideringsmateriale registreres i document security store, og arkiv-timestamps fornys, før den aktuelle svækkes

Transporten er også én brik i en bredere portabilitetsposition: den native library loader beskrevet i indlæsning af det native bibliotek på ethvert target håndterer samme klasse af problem for selve PDFium-binaries. I begge tilfælde er mønsteret identisk: bind et lille antal symboler dynamisk, meld præcist, hvad der fejlede at binde, og lad aldrig en manglende afhængighed blive til en link-time-fejl, der stopper applikationen i at starte

Windows- og ikke-Windows-timestamp-backendene følger begge med PDFium Delphi-komponenten, valgt efter target snarere end efter konfiguration, så en Lazarus-applikation på Linux og en Delphi-applikation på Windows producerer samme tidsstemplede signatur gennem forskellige rørføringer