Tehnični članak

Zaledje časovnih žigov libcurl za PDFium VCL na FPC

PDFium VCL pošilja zahteve po časovnem žigu RFC 3161 skozi libcurl na ne-Windows ciljih, dinamično vezane na osem simbolov, v zrcaljenju oblike zaledja Windows, ki se veže na WinHTTP. Dve nastavitvi možnosti odločita, ali je prenos zanesljiv pod obremenitvijo, celotna enota pa je bila validirana na stroju, ki je za svojo ciljno platformo ni mogel prevesti

Časovno žigosanje je tisto, ki podpis spremeni v nekaj, kar preživi potek veljavnosti certifikata, in je omrežna operacija, ki sede znotraj podpisne operacije. Ta kombinacija naredi izbiro prenosa odločilno na način, kot običajno ni: teče na delovni niti, govori s strežnikom, ki ga ne nadzorujete, in zasek tam zablokira podpisni cevovod in ne nalaganje strani

Zakaj libcurl in ne odjemalec HTTP FPC?

Ker alternativa v repozitorij povleče sklad TLS in vas potem prisili, da vzdržujete njegovo zaznavanje verzij. Očitna pot na Free Pascal je fphttpclient s plastjo vtičnic OpenSSL in spodleti na podrobnostih: vezi OpenSSL FPC 3.2.2 nezanesljivo zaznavajo OpenSSL 3.x na večini trenutnih distribucij, macOS pa doda še razlike LibreSSL. Kar se začne kot majhen klic HTTP, postane nenehno vzdrževanje tujega TLS ABI

libcurl razreši svoje lastno zaledje TLS in validira verige proti shrambi zaupanja platforme, tako da Pascal stran tega ne potrebuje. Vezalna plast je osem simbolov. To štetje je argument: manjša površina med vašo kodo in gibljivo odvisnostjo pomeni manj mest, kjer vas lahko nadgradnja distribucije zlomi, in se ujema z obstoječim zaledjem Windows, ki na enak način veže peščico vstopnih točk 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;

Razglašanje variadične funkcije C v Pascalu

curl_easy_setopt in curl_easy_getinfo sta na strani C variadična in Object Pascal nima načina, kako to izraziti. Pristop, ki deluje, je razglasiti več fiksnih prototipov, enega na razred argumentov, vse kazajoče na isti izvoženi simbol: različico, ki jemlje long, različico, ki jemlje kazalec, in tako naprej — izbrano na mestu klica glede na to, kaj dejansko podajate

To je varno iz določenega razloga, ki ga vredi razumeti in ne kopirati. Vsak od teh tipov argumentov se poda v celoštevilskem registru pod klicnimi konvencijami platforme v igri — natanko tam, kjer ga implementacija C va_arg prebere. Trik zato velja za cela števila, kazalce in ročaje in ne velja za plavajoče argumente, ki potujejo v drugih registrih. Različice, ki jemlje double, ne dodajajte v prepričanju, da se vzorec posplošuje

// En izvoženi simbol, več fiksnih prototipov. Vsaka različica poda
// svoj argument v celoštevilskem registru, kjer ga bere stran C.
// Plavajoča različica ne bi delovala in je ne smete 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;

Dve nastavitvi, ki odločita, ali zahteva zaključi

Prva je izrecna prazna glava Expect:. libcurl vklopi izmenjavo HTTP 100-continue za telesa zahtev čez približno en kilobajt, poizvedba po časovnem žigu z zahtevo po certifikatu pa ta prag običajno preseže. Nekateri strežniki TSA nikoli ne odgovorijo na nadaljevanje, tako da odjemalec izčrpa poln časovni zamik, preden pošlje telo, ki bi ga strežnik sprejel takoj. Pošiljanje prazne glave Expect: zatre izmenjavo in zahteva gre skozi v enem povratnem skoku

Druga je CURLOPT_NOSIGNAL, ki jo je treba nastaviti. Brez nje libcurl implementira svoj časovni zamik razreševanja imen z SIGALRM in ta mehanizem ni thread-safe. Podpisovanje teče na delovni niti, tako da je privzeto vedenje latentno sesutje, ki se pojavi pod sočasnostjo in nikoli v enonitnem testu. Nastavitev zastavice onemogoči pot na osnovi signalov in stane le granularnost časovnega zamika razreševalnika

Oba pomanjkljivosti si delita profil, ki ju naredi drago najti kasneje. Nobena se ne pokaže v funkcionalnem testu proti dobro utečenemu strežniku na eni niti. Obe se pojavita v produkciji, proti enemu konkretnemu TSA, pod obremenitvijo. Ko vežete omrežno knjižnico, preberite, kaj njeni privzetki predpostavljajo o vašem procesu, preden predpostavite, da se ujemajo

Diagram prenosa časovnih žigov libcurl v PDFium VCL, ki prikazuje curl_easy_setopt, razglašen kot fiksni prototipi Pascal za long in kazalec, ki podata argumenta v celoštevilskih registrih, prazno glavo Expect, ki zatre izmenjavo HTTP 100-continue, CURLOPT_NOSIGNAL, ki odstrani pot SIGALRM na delovnih nitih, in strop odgovora na ravni prenosa
Dve nastavitvi odločita, ali zahteva zaključi: prazna glava Expect se izogne strežnikom, ki nikoli ne odgovorijo na nadaljevanje, NOSIGNAL pa drži časovne zamike razreševanja imen proč od poti signalov, medtem ko podpisovanje teče na delovni niti

Kako preverite kodo, ki je vaš prevajalnik nikoli ne bo videl?

Tako, da prevajalnik vseeno vidi, skozi nadzorovan izvod. Razvojni stroj tukaj nima prečnega prevajalnika za Linux ali macOS, tako da veje ne-Windows enote za časovno žigosanje med normalno gradnjo nikoli ne pridejo do generatorja kode. Koda, ki se nikoli ne prevede, je koda, ki tiho gni: preimenovanje v deljenem tipu, spremenjen seznam parametrov, dodana odvisnost enote — in nihče ne opazi mesece

Tehnika je mehanična. Kopirajte enoto v začasni imenik, jo preimenujte in zamenjajte vsak Windows pogoj — tako obliko {$IFDEF MSWINDOWS} kot obliko {$IF DEFINED(MSWINDOWS) — s simbolom, ki nikoli ni definiran. Nato prevedite izvod. Ko se vseh 3.828 vrstic prevede, ste dokazali, da pot ne-Windows uporablja enote, ki obstajajo, kliče funkcije zaledja z ujemajočimi se podpisi in se sklicuje na tipe, ki so v obsegu. To ni dokaz, da prenos deluje, in nič krajšega od ciljne platforme vam tega ne da. Je dokaz, da veja še ni pokvarjena — to pa je način odpovedi, ki se dejansko kopiči

Spremna navada je pustiti samo enoto libcurl brez platformskih stražarjev, tako da sodeluje v običajni gradnji Windows, čeprav se nanjo tam nič ne sklicuje. Dnevna gradnja potem zastonj še naprej straži njeno skladnjo in tipe. Enota, ki se prevede le na platformi, ki je nimate, je enota brez nobenega prevajalnika, ki bi jo preverjal, isto razmišljanje pa velja čez delo čez prevajalnike, opisano v pasteh prečnih prevajalnikov Delphi in FPC

Omejevanje tega, kar se vrne

Odgovor časovnega žiga je majhna struktura DER in nič pri prenosu tega ne uveljavi. Strežnik, ki je ogrožen, napačno nastavljen ali pa preprosto usmerjen na napačen URL, lahko vrne poljuben tok in odjemalec, ki bere do zaprtja povezave, ga bo z veseljem nakopičil. Oba prenosa zato odgovor odrežeta, kar je pravo mesto za mejo: zavrnitev na prenosu prepreči, da bi se predimenzionirano telo sploh kadarkoli dodelilo, preverjanje na ravni razčlenjevalnika pa se sproži šele, ko je pomnilnik že zavezan

Isto razmišljanje velja za URL. Zaledje sprejema le sheme, ki jih zna smiselno govoriti, tako da napaka konfiguracije takoj spodleti z jasnim sporočilom, namesto da bi bila predana libcurlu, naj jo razlaga na kakršen koli način, ki mu njegova podpora protokolov dovoli

Kje prenos sede v zgodbi podpisovanja

Časovno žigosanje je prvi korak zgodbe dolgoročne validacije in ne celotna zgodba. Žeton se mora pripeti podpisu, validacijsko gradivo se mora zabeležiti v shrambo varnosti dokumenta, arhivski časovni žigi pa se morajo obnoviti, preden trenutni oslabi. Celoten lok je pokrit v dolgoročnih podpisih PDF s časovnimi žigi RFC 3161 in DSS

Diagram PDFium VCL zahteve po časovnem žigu RFC 3161, ki teče od DocumentDigest skozi BuildTimeStampQuery in PostTimeStampQuery čez libcurl do strežnika TSA, odgovor DER odrezan na prenosu, nato AttachTimeStampToken, ki hrani DSS in obnovo arhivskih časovnih žigov pri dolgoročni validaciji
Časovno žigosanje je prvi korak zgodbe dolgoročne validacije: žeton se mora pripeti, validacijsko gradivo zabeležiti v shrambo varnosti dokumenta, arhivski časovni žigi pa obnoviti, preden trenutni oslabi

Prenos je tudi en kos širšega položaja prenosljivosti: nalagalnik nativnih knjižnic, opisan v nalaganju nativne knjižnice na katerem koli cilju, obravnava isti razred težav za samo binarko PDFium. V obeh primerih je vzorec identičen — vezati majhno število simbolov dinamično, točno sporočiti, kaj se ni vezalo, in nikoli ne dovoliti, da bi manjkajoča odvisnost postala spodletel ob povezovanju, ki prepreči aplikaciji zagon

Zaledji časovnih žigov Windows in ne-Windows oba prideta z PDFium Delphi component, izbran po cilju in ne po konfiguraciji, tako da Lazarus aplikacija na Linuxu in Delphi aplikacija na Windows proizvedeta isti podpis s časovnim žigom skozi različne instalacije