PDFium VCL wysyła żądania znaczników czasu RFC 3161 przez libcurl na targetach innych niż Windows, bindowane dynamicznie do ośmiu symboli, w lustrzanym kształcie backendu Windows, który binduje się do WinHTTP. Dwa ustawienia opcji rozstrzygają, czy transport jest niezawodny pod obciążeniem, a cały unit był walidowany na maszynie, która nie umiała go skompilować dla jego platformy docelowej
Znaczniki czasu to to, co zamienia podpis w coś, co przeżywa wygaśnięcie certyfikatu, i to operacja sieciowa siedząca wewnątrz operacji podpisywania. Ta kombinacja czyni wybór transportu doniosłym w sposób, w jakim zwykle nie jest: chodzi na wątku roboczym, rozmawia z serwerem, którego nie kontrolujesz, i zawieszenie tam blokuje potok podpisywania, a nie ładowanie strony
Dlaczego libcurl zamiast klienta HTTP FPC?
Bo alternatywa wleci do repozytorium stos TLS, a potem każe ci utrzymywać jego detekcję wersji. Oczywista droga na Free Pascalu to fphttpclient z warstwą gniazd OpenSSL i polega na detalach: bindingi OpenSSL w FPC 3.2.2 wykrywają OpenSSL 3.x zawodnie na większości bieżących dystrybucji, a macOS dokłada różnice LibreSSL na wierzchu. To, co zaczyna się jako małe wywołanie HTTP, staje się ciągłym utrzymywaniem cudzego ABI TLS
libcurl rozwiązuje własny backend TLS i waliduje łańcuchy względem platformowego magazynu zaufania, więc strona Pascala nie potrzebuje niczego z tego. Warstwa bindingu to osiem symboli. Ta liczba jest argumentem: mniejsza powierzchnia między twoim kodem a ruchomą zależnością znaczy mniej miejsc, w których upgrade dystrybucji może cię złamać, i pasuje do istniejącego backendu Windows, który binduje garstkę punktów wejścia WinHTTP tak samo
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;
Deklarowanie funkcji wariadycznej C w Pascalu
curl_easy_setopt i curl_easy_getinfo są wariadyczne po stronie C, a Object Pascal nie ma jak tego wyrazić. Podejście, które działa, to zadeklarowanie kilku stałych prototypów, po jednym na klasę argumentów, wszystkie wskazujących ten sam eksportowany symbol: wariant biorący long, wariant biorący wskaźnik i tak dalej, wybieranych w miejscu wywołania według tego, co faktycznie przekazujesz
To jest bezpieczne z konkretnego powodu wartego zrozumienia, a nie skopiowania. Każdy z tych typów argumentów jest przekazywany w rejestrze całkowitoliczbowym przy obowiązujących konwencjach wywołań platformy, czyli dokładnie tam, skąd czyta go implementacja va_arg w C. Trik trzyma się więc dla liczb całkowitych, wskaźników i uchwytów i nie trzyma dla argumentów zmiennoprzecinkowych, które jadą innymi rejestrami. Nie dodawaj wariantu biorącego double w założeniu, że wzorzec się uogólnia
// Jeden eksportowany symbol, kilka stałych prototypów. Każdy wariant podaje
// argument w rejestrze całkowitoliczbowym, czyli tam, gdzie czyta go strona C.
// Wariant zmiennoprzecinkowy by nie zadziałał i nie wolno go dodawać
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;
Dwa ustawienia rozstrzygające, czy żądanie się kończy
Pierwsze to jawny pusty nagłówek Expect:. libcurl włącza handshake HTTP 100-continue dla ciał żądań ponad około jeden kilobajt, a zapytanie znacznika czasu z żądaniem certyfikatu zwykle przekracza ten próg. Niektóre serwery TSA nigdy nie odpowiadają na kontynuację, więc klient odczekuje pełny timeout, zanim wyśle ciało, które serwer przyjąłby natychmiast. Wysłanie pustego nagłówka Expect: wycisza handshake i żądanie przechodzi w jednej rundzie
Drugie to CURLOPT_NOSIGNAL, które musi być ustawione. Bez niego libcurl implementuje swój timeout rozwiązywania nazw przez SIGALRM, a ten mechanizm nie jest thread-safe. Podpisywanie chodzi na wątku roboczym, więc domyślne zachowanie to utajony crash, który pojawia się pod współbieżnością i nigdy w teście jednowątkowym. Ustawienie flagi wyłącza ścieżkę opartą na sygnałach i kosztuje tylko ziarnistość timeoutu resolvera
Oba defekty dzielą profil, który czyni je drogimi w znalezienie później. Żaden nie wychodzi w teście funkcjonalnym przeciw grzecznemu serwerowi na jednym wątku. Oba wychodzą na produkcji, przeciw jednemu konkretnemu TSA, pod obciążeniem. Gdy bindujesz bibliotekę sieciową, przeczytaj, co jej domyślne zakładają o twoim procesie, zanim założysz, że pasują
Jak zweryfikować kod, którego twój kompilator nigdy nie zobaczy?
Sprawiając, że kompilator i tak go zobaczy, przez kontrolowaną kopię. Maszyna developerska nie ma tu cross-kompilatora na Linuxa ani macOS, więc gałęzie inne niż Windows w unicie znaczników czasu nigdy nie docierają do generatora kodu przy normalnym buildzie. Kod, który nigdy nie jest kompilowany, to kod gnijący po cichu: rename we współdzielonym typie, zmieniona lista parametrów, dodana zależność unitu i nikt tego nie zauważa miesiącami
Technika jest mechaniczna. Skopiuj unit do tymczasowego katalogu, zmień mu nazwę i zastąp każdy warunek Windowsowy, zarówno formę {$IFDEF MSWINDOWS}, jak i formę {$IF DEFINED(MSWINDOWS), symbolem, który nigdy nie jest zdefiniowany. Potem skompiluj kopię. Gdy wszystkie 3 828 linii się kompiluje, udowodniłeś, że ścieżka inna niż Windows używa unitów, które istnieją, woła funkcje backendu o pasujących sygnaturach i referencjonuje typy, które są w zasięgu. To nie jest dowód, że transport działa, i nic krócej niż platforma docelowa ci tego nie da. To dowód, że gałąź już nie jest zepsuta, czyli ten tryb awarii, który faktycznie się kumuluje
Nawykiem towarzyszącym jest trzymać sam unit libcurl bez straż platformowych, żeby uczestniczył w zwykłym buildzie Windows, choć nic tam do niego nie referuje. Codzienny build pilnuje wtedy jego składni i typów za darmo. Unit kompilujący się wyłącznie na platformie, której nie masz, to unit bez żadnej kontroli kompilatora i to samo rozumowanie obowiązuje w całej pracy międzykompilatorowej opisanej w artykule pułapki cross-kompilacji Delphi i FPC
Ograniczanie tego, co wraca
Odpowiedź znacznika czasu to mała struktura DER i nic w transporcie tego nie egzekwuje. Serwer skompromitowany, źle skonfigurowany albo po prostu celujący w zły URL może zwrócić dowolny strumień, a klient czytający do zamknięcia połączenia z przyjemnością go zakumuluje. Oba transporty capują więc odpowiedź, co jest właściwym miejscem dla limitu: odmowa na poziomie transportu nie pozwala w ogóle zaalokować nadmiarowego ciała, a kontrola na poziomie parsera odpala dopiero po zarezerwowaniu pamięci
To samo rozumowanie dotyczy URL-a. Backend przyjmuje wyłącznie schematy, które umie mówić sensownie, więc błąd konfiguracji polegnie natychmiast z jasnym komunikatem, zamiast być oddany libcurlowi do zinterpretowania w dowolny sposób, na jaki pozwala jego wsparcie protokołów
Gdzie transport siedzi w historii podpisywania
Znaczniki czasu to pierwszy krok historii długoterminowej walidacji, a nie całość. Token musi zostać doczepiony do podpisu, materiał walidacyjny zapisany w document security store, a znaczniki archiwalne odnowione, zanim obecny osłabnie. Cały ten łuk opisuje długoterminowe podpisy PDF ze znacznikami RFC 3161 i DSS
Transport to też jeden kawałek szerszej pozycji przenośności: natywny loader bibliotek opisany w artykule ładowanie biblioteki natywnej na dowolnym targecie obsługuje tę samą klasę problemu dla samej binarki PDFium. W obu przypadkach wzorzec jest identyczny: zbinduj dynamicznie małą liczbę symboli, raportuj precyzyjnie, co się nie zbindowało, i nigdy nie pozwól, by brakująca zależność stała się błędem linkowania zatrzymującym aplikację przed startem
Backendy znaczników czasu dla Windows i innych systemów są obie częścią komponentu PDFium dla Delphi, wybierane według targetu, a nie konfiguracji, więc aplikacja Lazarus na Linuxie i aplikacja Delphi na Windows produkują ten sam podpis ze znacznikiem czasu przez różne zaplecze