Техническа статия

PDFium VCL: libcurl TSA транспорт за не-Windows цели

PDFium VCL праща RFC 3161 timestamp заявки през libcurl на не-Windows цели, динамично свързан към осем символа, оглеждайки формата на Windows backend-а, който се свързва към WinHTTP. Две настройки на опции решават дали транспортът е надежден под товар, а целият unit беше верифициран на машина, която не можеше да го компилира за целевата му платформа

Timestamping-ът е това, което превръща подпис в нещо, оцеляващо изтичането на сертификата, и е мрежова операция, седяща в подписваща операция. Комбинацията прави избора на транспорт съществен по начин, по който обикновено не е: върви на работен thread, говори със сървър, който не контролирате, и заклинване там спира подписващ pipeline, а не зареждане на страница

Защо libcurl вместо FPC HTTP клиента?

Защото алтернативата влачи TLS stack в хранилището и после ви кара да поддържате откриването му на версия. Очевидният маршрут на Free Pascal е fphttpclient със OpenSSL socket слоя, и той се проваля на детайлите: FPC 3.2.2 OpenSSL bindings разпознават OpenSSL 3.x ненадеждно на повечето актуални дистрибуции, а macOS добавя LibreSSL разлики отгоре. Това, което започва като малко HTTP извикване, става постоянна поддръжка на чужд TLS ABI

libcurl разрежда собствените си TLS backend-и и валидира вериги срещу trust store-а на платформата, така че Pascal страната не се нуждае от нито едното. Binding слоят е осем символа. Това число е аргументът: по-малка повърхност между кода ви и движеща се зависимост значи по-малко места, където ъпгрейд на дистрибуция да ви счупи, и съответства на съществуващия Windows backend, който свързва шепа 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;

Обявяване на C variadic функция в Pascal

curl_easy_setopt и curl_easy_getinfo са variadic от C страната, а Object Pascal няма начин да го изрази. Работещият подход е да обявите няколко фиксирани прототипа, по един за клас аргументи, всички сочащи един и същ експортиран символ: вариант, взимащ long, вариант, взимащ указател, и така нататък, избирани на мястото на извикването според това, което реално подавате

Това е безопасно по конкретна причина, заслужаваща разбиране, а не копиране. Всяка от тези типове аргументи се подава в целочислен регистър под платформените calling conventions в игра, което е точно откъдето C имплементацията va_arg го чете. Трикът затова важи за integers, указатели и handle-и, и не важи за floating-point аргументи, които пътуват в различни регистри. Не добавяйте вариант, взимащ double, с допускането, че моделът се обобщава

// Един експортиран символ, няколко фиксирани прототипа. Всеки вариант подава
// аргумента си в целочислен регистър, откъдето C страната го чете.
// Floating-point вариант няма да работи и не бива да се добавя
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;

Две настройки, решаващи дали заявката завършва

Първата е изричен празен Expect: header. libcurl включва HTTP 100-continue ръкостискането за request тела над грубо един килобайт, а timestamp заявка с искане на сертификат обикновено минава този праг. Някои TSA сървъри никога не отговарят на continuation-а, така че клиентът изчаква пълен timeout, преди да изпрати тяло, което сървърът би приел мигновено. Изпращането на празен Expect: header потиска ръкостискането, а заявката минава в един round trip

Втората е CURLOPT_NOSIGNAL, която трябва да бъде зададена. Без нея libcurl имплементира своя timeout за разрешаване на имена чрез SIGALRM, а този механизъм не е thread-safe. Подписването върви на работен thread, така че поведението по подразбиране е латентен краш, появяващ се под конкурентност и никога в едно-thread тест. Задаването на флага изключва сигнално-базирания път и струва само гранулярността на resolver timeout-а

И двата дефекта споделят профил, правещ ги скъпи за откриване после. Нито един не се появява във функционален тест срещу добре държащ се сървър на един thread. И двата се появяват в продукция, срещу един конкретен TSA, под товар. Когато свързвате мрежова библиотека, прочетете какво нейните стойности по подразбиране допускат за процеса ви, преди да допускнете, че съвпадат

Диаграма на libcurl timestamp транспорта в PDFium VCL, показваща curl_easy_setopt, обявен като фиксирани long и указателни Pascal прототипи, подаващи аргументи в целочислени регистри, празния Expect header, потискащ HTTP 100-continue ръкостискането, CURLOPT_NOSIGNAL, махаща SIGALRM пътя на работните threads, и тавана на отговора на транспортно ниво
Две настройки решават дали заявката завършва: празен Expect header избягва сървъри, никога не отговарящи на continuation-а, а NOSIGNAL държи timeout-ите за разрешаване на имена извън сигналния път, докато подписването върви на работен thread

Как верифицирате код, който компилаторът ви никога няма да види?

Като накарате компилатора пак да го види – чрез контролирано копие. Девелопърската машина тук няма Linux или macOS cross-компилатор, така че не-Windows клоновете на timestamp unit-а никога не стигат кодовия генератор при нормален build. Код, който никога не се компилира, е код, който тихо гние: преименуване в споделен тип, сменен списък с параметри, добавена unit зависимост, и никой не забелязва месеци наред

Техниката е механична. Копирайте unit-а във временна директория, преименувайте го и заменете всеки Windows conditional – и формата {$IFDEF MSWINDOWS}, и формата {$IF DEFINED(MSWINDOWS) – със символ, никога не дефиниран. После компилирайте копието. Когато всички 3,828 реда се компилират, сте доказали, че не-Windows пътят ползва unit-и, съществуващи, вика backend функции със съвпадащи сигнатури и сочи типове, в обхват. Това не е доказателство, че транспортът работи, и нищо по-малко от целевата платформа няма да ви го даде. Доказателство е, че клонът не е вече счупен, което е повредовият модел, реално натрупващ се

Спътникът навик е да оставите самия libcurl unit свободен от платформени guard-ове, така че той участва в обикновения Windows build, дори нищо там да не го сочи. Дневният build тогава пази синтаксиса и типовете му безплатно. Unit, компилиращ се само на платформа, която нямате, е unit без никаква компилаторна проверка изобщо, а същото разсъждение важи из цялата cross-компилаторна работа, описана в Delphi и FPC cross-компилаторни капани

Ограничаване на това, което се връща

Timestamp отговорът е малка DER структура и нищо в транспорта не налага това. Сървър, компрометиран, лошо конфигуриран или просто сочен към грешен URL, може да върне произволен stream, а клиент, четещ до затварянето на връзката, с охотно ще го натрупа. И двата транспорта затова поставят таван на отговора, което е правилното място за лимита: отказът на транспортното ниво пречи на свръхголямо тяло изобщо да бъде алокирано, докато проверка на ниво парсер светва едва след като паметта вече е ангажирана

Същото разсъждение важи и за URL. Backend-ът приема само схеми, които може да говори смислено, така че конфигурационна грешка се проваля мигновено с ясно съобщение, вместо да бъде подадена на libcurl да я тълкува по какъвто и да е начин, който поддръжката му за протоколи позволява

Къде седи транспортът в историята на подписването

Timestamping-ът е първата стъпка от историята на дългосрочната валидация, не цялата ѝ. Токенът трябва да бъде прикачен към подписа, валидационният материал да бъде записан в document security store, а архивните timestamp-и – подновявани, преди текущият да отслабне. Цялата тази дъга е обхваната в дългосрочни PDF подписи с RFC 3161 timestamp-и и DSS

Диаграма в PDFium VCL на RFC 3161 timestamp заявка, течаща от DocumentDigest през BuildTimeStampQuery и PostTimeStampQuery през libcurl до TSA сървър, DER отговорът с таван на транспорта, после AttachTimeStampToken, хранещ DSS и подновяването на архивни timestamp-и в дългосрочната валидация
Timestamping-ът е първата стъпка от историята на дългосрочната валидация: токенът трябва да бъде прикачен, валидационният материал – записан в document security store, а архивните timestamp-и – подновявани, преди текущият да отслабне

Транспортът е и едно парче от по-широка портабилност позиция: нативният library loader, описан в зареждане на нативната библиотека на всяка цел, обработва същия клас проблем за самия PDFium binary. И в двата случая моделът е идентичен – свързване на малък брой символи динамично, докладване прецизно кое не се е свързало, и никога да не позволите липсваща зависимост да стане link грешка, спираща приложението от стартиране

Windows и не-Windows timestamp backend-ите ship-ват и двата с PDFium Delphi компонента, избирани по цел, а не по конфигурация, така че Lazarus приложение на Linux и Delphi приложение на Windows произвеждат един и същ timestamp-нат подпис през различна водопроводка