Tehnički članak

libcurl timestamp backend za PDFium VCL na FPC-u

PDFium VCL šalje RFC 3161 timestamp zahtjeve kroz libcurl na ne-Windows ciljevima, dinamički vezan na osam simbola, preslikavajući oblik Windows backenda koji se vezuje na WinHTTP. Dvije opcije postavke odlučuju je li transport pouzdan pod opterećenjem, i cijeli unit je validiran na mašini koja ga nije mogla kompajlirati za svoju ciljnu platformu

Timestamping je ono što potpis pretvara u nešto što preživljava istek certifikata, i to je mrežna operacija koja sjedi unutar potpisne operacije. Ta kombinacija čini izbor transporta sudbonosnim na način koji obično nije: trči na worker threadu, razgovara s poslužiteljem kojeg ne kontrolirate, i vješanje tamo zaustavlja signing pipeline umjesto učitavanja stranice

Zašto libcurl umjesto FPC HTTP klijenta?

Jer alternativa vuče TLS stack u repozitorij i onda vas natjera da održavate njegovu detekciju verzije. Očigledna ruta na Free Pascalu je fphttpclient s OpenSSL socket slojem, i pade na detaljima: FPC 3.2.2 OpenSSL bindingi nepouzdano detektiraju OpenSSL 3.x na većini trenutnih distribucija, a macOS doda LibreSSL razlike na vrh. Ono što počne kao mali HTTP poziv postaje stalno održavanje tuđeg TLS ABI-ja

libcurl razrješuje svoj vlastiti TLS backend i validira lance protiv platformskog trust storea, pa Pascal strana ne treba ništa od toga. Binding sloj je osam simbola. Taj broj je argument: manja površina između vašeg koda i pokretne dependencije znači manje mjesta gdje vas distribucijska nadogradnja može slomiti, i slaže se s postojećim Windows backendom, koji na isti način vezuje šaku WinHTTP entry pointova

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;

Deklariranje C variadic funkcije u Pascalu

curl_easy_setopt i curl_easy_getinfo su variadic na C strani, i Object Pascal nema načina to izraziti. Pristup koji radi je deklarirati nekoliko fiksnih prototipova, jedan po klasi argumenta, sve usmjerene na isti izvezeni simbol: varijanta koja uzima long, varijanta koja uzima pokazivač, i tako dalje, birana na call siteu prema onome što stvarno predajete

To je sigurno iz specifičnog razloga koji vrijedi razumjeti umjesto kopirati. Svaki od tih tipova argumenata predaje se u integer registru pod platformskim calling konvencijama koje su u igri, a to je točno odakle C implementacija va_arg čita. Trik zato drži za integere, pokazivače i handleove, i ne drži za floating-point argumente, koji putuju u drugim registrima. Ne dodajte varijantu koja uzima double u pretpostavci da se obrazac generalizira

// Jedan izvezeni simbol, nekoliko fiksnih prototipova. Svaka varijanta
// predaje argument u integer registru, odakle ga C strana čita.
// Floating-point varijanta ne bi radila i ne smije se dodati
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;

Dvije postavke koje odlučuju dovršava li se zahtjev

Prva je eksplicitno prazan Expect: header. libcurl uključuje HTTP 100-continue handshake za tijela zahtjeva iznad otprilike kilobajta, a timestamp upit s certifikatnim zahtjevem obično prijeđe taj prag. Neki TSA poslužitelji nikad ne odgovore na nastavak, pa klijent isčekuje puni timeout prije nego pošalje tijelo koje bi poslužitelj odmah prihvatio. Slanje praznog Expect: headera potiskuje handshake, i zahtjev prođe u jednom round tripu

Druga je CURLOPT_NOSIGNAL, koja mora biti postavljena. Bez nje libcurl implementira svoj name-resolution timeout pomoću SIGALRM-a, i taj mehanizam nije thread-safe. Signing trči na worker threadu, pa je defaultno ponašanje latentni crash koji se pojavi pod konkurencijom i nikad u jednothreadnom testu. Postavljanje zastavice isključuje signalni put i košta samo granularnost resolver timeouta

Oba defekta dijele profil koji ih kasnije skupo pronalazi. Nijedan se ne pojavi u funkcionalnom testu protiv dobro odgojenog poslužitelja na jednom threadu. Oba se pojave u produkciji, protiv jednog konkretnog TSA-e, pod opterećenjem. Kad vezujete networking biblioteku, pročitajte što njezini defaulti pretpostavljaju o vašem procesu prije nego pretpostavite da se poklapaju

Dijagram libcurl timestamp transporta PDFium VCL-a pokazujući curl_easy_setopt deklariran kao fiksni long i pointer Pascal prototipovi koji predaju argumente u integer registrima, prazan Expect header koji potiskuje HTTP 100-continue handshake, CURLOPT_NOSIGNAL koji uklanja SIGALRM put na worker threadovima, i transportni strop odgovora
Dvije postavke odlučuju dovršava li se zahtjev: prazan Expect header izbjegava poslužitelje koji nikad ne odgovore na nastavak, a NOSIGNAL drži name-resolution timeoutove dalje od signalnog puta dok signing trči na worker threadu

Kako verificirati kod koji vaš kompajler nikad neće vidjeti?

Tako da kompajler ipak vidi kod, kroz kontroliranu kopiju. Razvojna mašina ovdje nema Linux ili macOS cross-compiler, pa ne-Windows grane timestamping unita nikad ne stignu do generatora koda tijekom normalnog builda. Kod koji se nikad ne kompajlira je kod koji tiho trune: preimenovanje u dijeljenom tipu, promijenjen popis parametara, dodana unit dependencija, i nitko ne primijeti mjesece

Tehnika je mehanička. Kopirajte unit u privremeni direktorij, preimenujte ga, i zamijenite svaki Windows uvjetni, i {$IFDEF MSWINDOWS} oblik i {$IF DEFINED(MSWINDOWS) oblik, simbolom koji nikad nije definiran. Zatim kompajlirajte kopiju. Kad se svih 3.828 linija kompajlira, dokazali ste da ne-Windows put koristi unite koji postoje, zove backend funkcije s odgovarajućim significama i referencira tipove koji su u dosegu. To nije dokaz da transport radi, i ništa kraće od ciljne platforme vam to neće dati. To je dokaz da grana već nije slomljena, a to je failure mod koji se stvarno nakuplja

Prateća navika je ostaviti libcurl unit samog bez platformskih guardova, pa sudjeluje u običnom Windows buildu iako ga tamo ništa ne referencira. Dnevni build onda besplatno i dalje čuva njegovu sintaksu i tipove. Unit koji se kompajlira samo na platformi koju nemate je unit bez ijednog kompajlera koji ga provjerava, i isto se razmišljanje primjenjuje na cijeli cross-compiler posao opisan u Delphi i FPC cross-compiler zamkama

Ograničavanje onoga što se vraća

Timestamp odgovor je mala DER struktura, i ništa oko transporta to ne nameće. Poslužitelj koji je kompromitiran, pogrešno konfiguriran ili jednostavno usmjeren na krivi URL može vratiti proizvoljan stream, i klijent koji čita do zatvaranja veze će ga slobodno nagomilati. Oba transporta zato postavljaju strop na odgovor, što je pravo mjesto za limit: odbijanje na transportu sprječava da pretjerano veliko tijelo ikad bude alocirano, dok parser-level provjera puca tek nakon što je memorija obračunana

Isto razmišljanje vrijedi i za URL. Backend prima samo sheme koje može smisleno govoriti, pa konfiguracijska greška pade odmah s jasnom porukom umjesto da se preda libcurlu da je protumači na koji god način njegova podrška protokola dopušta

Gdje transport sjedi u priči o potpisivanju

Timestamping je prvi korak priče o dugoročnoj validaciji umjesto cijele priče. Token se mora prikopčati na potpis, validacijski materijal mora se zabilježiti u document security store, i archive timestampovi se moraju obnavljati prije nego trenutni oslabi. Cijeli je taj luk pokriven u dugoročnim PDF potpisima s RFC 3161 timestampovima i DSS-om

Dijagram PDFium VCL-a RFC 3161 timestamp zahtjeva koji teče od DocumentDigest kroz BuildTimeStampQuery i PostTimeStampQuery preko libcurla do TSA poslužitelja, DER odgovor ograničen na transportu, pa AttachTimeStampToken hrani DSS i obnovu archive timestampova u dugoročnoj validaciji
Timestamping je prvi korak priče o dugoročnoj validaciji: token se mora prikopčati, validacijski materijal zabilježiti u document security store, i archive timestampovi obnoviti prije nego trenutni oslabi

Transport je također jedan dio šire pozicije prenosivosti: native library loader opisan u učitavanju native biblioteke na bilo kojem cilju rješava istu klasu problema za sam PDFium binary. U oba je slučaja obrazac identičan, vezati mali broj simbola dinamički, izvještati precizno što se nije vezalo, i nikad dopustiti da nedostajuća dependencija postane link-time neuspjeh koji sprječava aplikaciju da krene

Windows i ne-Windows timestamp backendi oboje isporučuju uz PDFium Delphi komponentu, birani po cilju umjesto po konfiguraciji, pa Lazarus aplikacija na Linuxu i Delphi aplikacija na Windowsima proizvode isti timestampirani potpis kroz različitu pozadinsku infrastrukturu