PDFium VCL отправляет запросы таймстампов RFC 3161 через libcurl на не-Windows таргетах, динамически связываясь с восемью символами, повторяя форму Windows-бэкенда, который связывается с WinHTTP. Две настройки опций решают, надёжен ли транспорт под нагрузкой, и весь юнит валидировался на машине, которая не могла компилировать его для целевой платформы
Таймстамп — то, что превращает подпись в нечто, переживающее истечение сертификата, и это сетевая операция внутри операции подписания. Эта комбинация делает выбор транспорта значимым так, как обычно не бывает: он работает на рабочем потоке, говорит с сервером, который вы не контролируете, и зависание там останавливает пайплайн подписания, а не загрузку страницы
Почему libcurl, а не FPC HTTP-клиент?
Потому что альтернатива тащит TLS-стек в репозиторий и заставляет вас поддерживать его определение версии. Очевидный путь на Free Pascal — fphttpclient с сокетным слоем OpenSSL, и он спотыкается на деталях: биндинги OpenSSL FPC 3.2.2 ненадёжно определяют OpenSSL 3.x на большинстве актуальных дистрибутивов, а macOS сверху добавляет различия LibreSSL. Маленький HTTP-вызов превращается в постоянную поддержку чужого TLS ABI
libcurl сама разрешает свой TLS-бэкенд и валидирует цепочки против платформенного хранилища доверия, поэтому Pascal-стороне ничего из этого не нужно. Слой биндинга — восемь символов. Это и есть аргумент: меньшая поверхность между вашим кодом и движущейся зависимостью означает меньше мест, где обновление дистрибутива может вас сломать, и это совпадает с существующим Windows-бэкендом, который так же связывает горстку точек входа 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-вариадик функции в Pascal
curl_easy_setopt и curl_easy_getinfo вариадичны на стороне C, и Object Pascal не имеет способа это выразить. Работающий подход — объявить несколько фиксированных прототипов, по одному на класс аргументов, все указывающих на один и тот же экспортированный символ: вариант, принимающий long, вариант, принимающий указатель, и так далее, выбираемых в месте вызова по тому, что вы реально передаёте
Это безопасно по конкретной причине, которую стоит понять, а не скопировать. Каждый из этих типов аргументов передаётся в целочисленном регистре при действующих на платформе соглашениях о вызовах, что ровно там, где C-реализация va_arg его читает. Трюк поэтому держится для целых, указателей и хендлов, и не держится для аргументов с плавающей точкой, которые едут в других регистрах. Не добавляйте вариант, принимающий double, в предположении, что паттерн обобщается
// Один экспортированный символ, несколько фиксированных прототипов.
// Каждый вариант передаёт аргумент в целочисленном регистре, откуда его
// и читает сторона C. Вариант для плавающей точки не сработает — не добавлять
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:. libcurl включает хендшейк HTTP 100-continue для тел запроса размером больше примерно килобайта, а запрос таймстампа с запросом сертификата обычно этот порог переходит. Некоторые TSA-серверы никогда не отвечают на продолжение, и клиент выжидает полный таймаут, прежде чем отправить тело, которое сервер принял бы немедленно. Отправка пустого заголовка Expect: подавляет хендшейк, и запрос проходит за один round trip
Вторая — CURLOPT_NOSIGNAL, которую выставить обязаны. Без неё libcurl реализует таймаут разрешения имён через SIGALRM, и этот механизм не потокобезопасен. Подписание работает на рабочем потоке, поэтому поведение по умолчанию — латентный краш, появляющийся под конкурентностью и никогда в однопоточном тесте. Выставление флага отключает путь на сигналах и стоит только гранулярности таймаута резолвера
Оба дефекта делят профиль, делающий их дорогими в обнаружении позже. Ни один не проявляется в функциональном тесте против воспитанного сервера в один поток. Оба проявляются в продакшене, против одного конкретного TSA, под нагрузкой. Связывая сетевую библиотеку, прочтите, что её дефолты предполагают о вашем процессе, прежде чем предполагать, что они совпадают
Как верифицировать код, который ваш компилятор никогда не увидит?
Тем, что компилятор всё же его увидит, — через контролируемую копию. Машина разработки здесь не имеет кросс-компиляторов Linux или macOS, поэтому не-Windows ветки юнита таймстампов никогда не доходят до генератора кода при обычной сборке. Код, который никогда не компилируется, — код, который тихо гниёт: переименование в общем типе, изменённый список параметров, добавленная зависимость юнита, и никто не замечает месяцами
Техника механическая. Скопируйте юнит во временный каталог, переименуйте и замените каждую Windows-условную конструкцию, и форму {$IFDEF MSWINDOWS}, и форму {$IF DEFINED(MSWINDOWS), на символ, который никогда не определён. Затем скомпилируйте копию. Когда все 3 828 строк компилируются, вы доказали, что не-Windows путь использует существующие юниты, зовёт функции бэкендов с совпадающими сигнатурами и ссылается на типы в области видимости. Это не доказательство, что транспорт работает, — это даст только целевая платформа. Это доказательство, что ветка не сломана уже сейчас, — а именно этот режим отказа реально накапливается
Сопутствующая привычка — держать сам юнит libcurl свободным от платформенных охран, чтобы он участвовал в обычной Windows-сборке, хотя бы там на него никто не ссылался. Ежедневная сборка тогда бесплатно продолжает проверять его синтаксис и типы. Юнит, который компилируется только на платформе, которой у вас нет, — юнит, который не проверяет ни один компилятор, и та же логика приложима к межкомпиляторной работе из ловушек кросс-компиляции Delphi и FPC
Ограничение того, что возвращается
Ответ таймстампа — маленькая DER-структура, и ничего в транспорте это не принуждает. Сервер скомпрометированный, неверно сконфигурированный или просто указанный на не тот URL может вернуть произвольный поток, и клиент, читающий до закрытия соединения, добросовестно его накопит. Поэтому оба транспорта ограничивают ответ, и это правильное место для лимита: отказ на транспорте не даёт чрезмерному телу вообще быть аллоцированным, тогда как проверка уровня парсера срабатывает уже после выделения памяти
Та же логика приложима к URL. Бэкенд принимает только схемы, на которых он умеет осмысленно говорить, поэтому ошибка конфигурации падает немедленно с ясным сообщением вместо того, чтобы достаться libcurl для интерпретации так, как позволяет её поддержка протоколов
Где транспорт сидит в истории подписания
Таймстамп — первый шаг истории долгосрочной валидации, а не вся она. Токен надо прикрепить к подписи, материал валидации записать в document security store документа, а архивные таймстампы обновлять, прежде чем текущий ослабнет. Вся эта арка разобрана в долгосрочных PDF-подписях с таймстампами RFC 3161 и DSS
Транспорт — также один кусок более широкой позиции переносимости: нативный загрузчик библиотек из загрузки нативной библиотеки на любом таргете решает тот же класс задач для самого бинарника PDFium. В обоих случаях паттерн идентичен: связать малое число символов динамически, точно репортить, что не связалось, и никогда не позволять отсутствующей зависимости превращаться в отказ на линковке, мешающий приложению стартовать
Windows и не-Windows бэкенды таймстампов оба поставляются с Delphi-компонентом PDFium, выбираемыми по таргету, а не по конфигурации, так что Lazarus-приложение на Linux и Delphi-приложение на Windows производят одну и ту же подпись с таймстампом через разную сантехнику