PDFium VCL надсилає запити часових міток RFC 3161 через libcurl на цілях без Windows, динамічно підключаючись до восьми символів, дзеркалячи форму Windows-бекенду, що підключається до WinHTTP. Два налаштування опцій вирішують, чи транспорт надійний під навантаженням, а весь юніт був валідований на машині, яка не могла його навіть скомпілювати для своєї цільової платформи
Штампування часу — це те, що перетворює підпис на щось, що переживає сплив сертифіката, і це мережева операція, що сидить всередині операції підписування. Та комбінація робить вибір транспорту важливим так, як він зазвичай не є: він біжить на робочому потоці, розмовляє з сервером, якого ви не контролюєте, і зависання там зупиняє конвеєр підписування, а не завантаження сторінки
Чому libcurl, а не HTTP-клієнт FPC?
Бо альтернатива затягує TLS-стек у репозиторій, а потім змушує вас підтримувати його виявлення версії. Очевидний маршрут на Free Pascal — fphttpclient із шаром сокетів OpenSSL, і він падає на деталях: прив'язки OpenSSL у FPC 3.2.2 ненадійно виявляють OpenSSL 3.x на більшості поточних дистрибутивів, а macOS додає зверху відмінності LibreSSL. Те, що починалося як маленький HTTP-виклик, стає постійною підтримкою чужого TLS ABI
libcurl розв'язує власний TLS-бекенд і валідує ланцюги проти платформового сховища довіри, тож паскалівському бокові не потрібно нічого з цього. Шар прив'язки — вісім символів. Та кількість і є аргументом: менша поверхня між вашим кодом і рухомою залежністю означає менше місць, де оновлення дистрибутива може вас зламати, і вона збігається з наявним 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-варіадичної функції в Паскалі
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: пригнічує рукостискання, і запит проходить за один рейс туди й назад
Друге — CURLOPT_NOSIGNAL, який мусить бути виставлений. Без нього libcurl реалізує тайм-аут розв'язання імен через SIGALRM, а той механізм не потокобезпечний. Підписування біжить на робочому потоці, тож дефолтна поведінка — латентне падіння, яке з'являється під конкурентністю і ніколи в однопоточному тесті. Виставлення прапорця вимикає сигналний шлях і коштує лише гранулярності тайм-аута резолвера
Обидва дефекти ділять профіль, що робить їх дорогими в пошуку потім. Жоден не з'являється у функціональному тесті проти чемного сервера на одному потоці. Обидва з'являються в проді, проти одного конкретного TSA, під навантаженням. Коли ви підключаєте мережеву бібліотеку, прочитайте, що її дефолти припускують про ваш процес, перш ніж припускати, що вони збігаються
Як верифікувати код, який ваш компілятор ніколи не побачить?
Зробити так, щоб компілятор його все ж побачив — через контрольовану копію. Машина розробки тут не має крос-компілятора Linux чи macOS, тож не-Windows гілки юніту штампування ніколи не досягають генератора коду під час звичайної збірки. Код, який ніколи не компілюється, — це код, який мовчки гниє: перейменування в спільному типі, змінений список параметрів, додана залежність юніта — і ніхто не помічає місяцями
Техніка механічна. Скопіюйте юніт у тимчасовий каталог, перейменуйте його і замініть кожен Windows-умовний — і форму {$IFDEF MSWINDOWS}, і форму {$IF DEFINED(MSWINDOWS) — на символ, який ніколи не визначений. Тоді скомпілюйте копію. Коли всі 3 828 рядків компілюються, ви довели, що не-Windows шлях використовує юніти, які існують, викликає функції бекенду зі збіжними сигнатурами і посилається на типи, що в області видимості. Це не доказ, що транспорт працює, і нічого, крім цільової платформи, не дасть вам цього. Це доказ, що гілка не зламана заздалегідь, — а це той режим відмови, який справді накопичується
Супутня звичка — лишити сам юніт libcurl вільним від платформених guard'ів, тож він бере участь у звичайній Windows-збірці, хоча там ніщо на нього не посилається. Щоденна збірка тоді дарма стереже його синтаксис і типи. Юніт, який компілюється лише на платформі, якої у вас немає, — юніт, який узагалі ніхто компілятором не перевіряє, і ті самі міркування застосовуються в межах крос-компіляторної роботи, описаної в пастках крос-компілятора Delphi і FPC
Обмеження того, що повертається
Відповідь часової мітки — мала DER-структура, і нічого в транспорті того не вимагає. Сервер, скомпрометований, неправильно налаштований чи просто наведений на неправильний URL, може повернути довільний потік, і клієнт, який читає до закриття з'єднання, охоче його накопичить. Тому обидва транспорти стелять відповідь, і це правильне місце для ліміту: відмова на транспорті не дає надмірному тілу коли-небудь бути виділеним, тоді як перевірка на рівні парсера спрацьовує лише після того, як пам'ять уже виділено
Ті самі міркування стосуються URL. Бекенд приймає лише схеми, які може осмислено вимовляти, тож помилка конфігурації падає негайно з ясним повідомленням, замість того щоб бути відданою libcurl на інтерпретацію тим способом, який дозволяє його підтримка протоколів
Де транспорт сидить в історії підписування
Штампування часу — перший крок історії довгострокової валідації, а не вся вона. Токен мусить бути прикріплений до підпису, валідаційний матеріал мусить бути записаний у document security store, а архівні часові мітки мусять оновлюватися, перш ніж поточна ослабне. Та ціла арка покрита в довгострокових PDF-підписах з часовими мітками RFC 3161 і DSS
Транспорт — також один шматок ширшої позиції переносимості: нативний завантажувач бібліотек, описаний у завантаженні нативної бібліотеки на будь-якій цілі, обробляє той самий клас проблем для самого бінарника PDFium. В обох випадках патерн ідентичний: підключити малу кількість символів динамічно, точно повідомити, що не підключилося, і ніколи не дозволяти відсутній залежності ставати помилкою лінкування, що зупиняє застосунок від старту
Windows і не-Windows бекенди часових міток обидва постачаються з PDFium Delphi component, обрані за ціллю, а не за конфігурацією, тож застосунок Lazarus на Linux і застосунок Delphi на Windows продукують той самий підпис із часовою міткою через різні комунікації