Технічна стаття

Бекенд часових міток libcurl для PDFium VCL на FPC

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, під навантаженням. Коли ви підключаєте мережеву бібліотеку, прочитайте, що її дефолти припускують про ваш процес, перш ніж припускати, що вони збігаються

Діаграма транспорту часових міток libcurl у PDFium VCL: curl_easy_setopt, декларований як фіксовані паскалівські прототипи long і вказівника, що передають аргументи в цілочисельних регістрах, порожній заголовок Expect, що пригнічує рукостискання HTTP 100-continue, CURLOPT_NOSIGNAL, що знімає SIGALRM-шлях на робочих потоках, і транспортна стеля відповіді
Два налаштування вирішують, чи запит завершиться: порожній заголовок Expect оминає сервери, які ніколи не відповідають на продовження, а NOSIGNAL тримає тайм-аути розв'язання імен поза сигналним шляхом, поки підписування біжить на робочому потоці

Як верифікувати код, який ваш компілятор ніколи не побачить?

Зробити так, щоб компілятор його все ж побачив — через контрольовану копію. Машина розробки тут не має крос-компілятора 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 VCL запиту часової мітки RFC 3161, що тече від DocumentDigest крізь BuildTimeStampQuery і PostTimeStampQuery через libcurl до TSA-сервера, DER-відповідь, стелена на транспорті, потім AttachTimeStampToken, що годує DSS і оновлення архівних часових міток у довгостроковій валідації
Штампування часу — перший крок історії довгострокової валідації: токен мусить бути прикріплений, валідаційний матеріал записаний у document security store, а архівні часові мітки оновлені, перш ніж поточна ослабне

Транспорт — також один шматок ширшої позиції переносимості: нативний завантажувач бібліотек, описаний у завантаженні нативної бібліотеки на будь-якій цілі, обробляє той самий клас проблем для самого бінарника PDFium. В обох випадках патерн ідентичний: підключити малу кількість символів динамічно, точно повідомити, що не підключилося, і ніколи не дозволяти відсутній залежності ставати помилкою лінкування, що зупиняє застосунок від старту

Windows і не-Windows бекенди часових міток обидва постачаються з PDFium Delphi component, обрані за ціллю, а не за конфігурацією, тож застосунок Lazarus на Linux і застосунок Delphi на Windows продукують той самий підпис із часовою міткою через різні комунікації