Artikel Teknis

Backend Timestamp libcurl untuk PDFium VCL di FPC

PDFium VCL mengirim permintaan timestamp RFC 3161 lewat libcurl pada target non-Windows, terikat dinamis ke delapan simbol, menyingkung bentuk backend Windows yang terikat ke WinHTTP. Dua setting opsi menentukan apakah transportnya andal di bawah beban, dan satu unit penuh itu divalidasi di mesin yang tak bisa mengompilasinya untuk platform targetnya

Timestamping adalah yang mengubah tanda tangan menjadi sesuatu yang selamat dari kedaluwarsa sertifikat, dan itu operasi jaringan yang duduk di dalam operasi penandatanganan. Kombinasi itu membuat pilihan transportnya berkonsekuensi dengan cara yang biasanya tidak: ia berjalan di worker thread, berbicara dengan server yang tak Anda kendalikan, dan menggantung di sana mematikan pipeline penandatanganan alih-alih pemuatan halaman

Mengapa libcurl, bukan klien HTTP FPC?

Karena alternatifnya menyeret stack TLS ke dalam repositori lalu menyuruh Anda merawat deteksi versinya. Rute yang tampak jelas di Free Pascal adalah fphttpclient dengan lapisan socket OpenSSL, dan ia gagal di detailnya: binding OpenSSL FPC 3.2.2 mendeteksi OpenSSL 3.x secara tak andal di kebanyakan distribusi terkini, dan macOS menambahkan perbedaan LibreSSL di atasnya. Yang dimulai sebagai panggilan HTTP kecil berubah menjadi perawatan berkelanjutan atas ABI TLS milik orang lain

libcurl menyelesaikan backend TLS-nya sendiri dan memvalidasi chain terhadap trust store platform, sehingga sisi Pascal tidak butuh satu pun dari itu. Lapisan bindingnya delapan simbol. Hitungan itulah argumennya: permukaan yang lebih kecil antara kode Anda dan dependensi yang bergerak berarti lebih sedikit tempat upgrade distribusi merusak Anda, dan itu serasi dengan backend Windows yang ada, yang mengikat segelintir entry point WinHTTP dengan cara sama

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;

Mendeklarasikan fungsi variadic C di Pascal

curl_easy_setopt dan curl_easy_getinfo bersifat variadic di sisi C, dan Object Pascal tak punya cara mengekspresikannya. Pendekatan yang bekerja adalah mendeklarasikan beberapa prototipe tetap, satu per kelas argumen, semuanya menunjuk ke simbol terekspor yang sama: varian yang menerima long, varian yang menerima pointer, dan seterusnya, dipilih di call site sesuai apa yang benar-benar Anda berikan

Ini aman karena alasan spesifik yang layak dipahami, bukan sekadar disalin. Tiap tipe argumen itu diberikan di register integer di bawah calling convention platform yang berlaku, yang persis tempat implementasi C membacanya lewat va_arg. Triknya karenanya berlaku untuk integer, pointer, dan handle, dan tidak berlaku untuk argumen floating-point, yang bepergian di register berbeda. Jangan tambahkan varian penerima double dengan asumsi polanya menggeneralisasi

// Satu simbol terekspor, beberapa prototipe tetap. Tiap varian memberikan
// argumennya di register integer, tempat sisi C membacanya
// Varian floating-point tidak akan bekerja dan tidak boleh ditambahkan
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;

Dua setting yang menentukan apakah permintaan selesai

Yang pertama adalah header Expect: kosong yang eksplisit. libcurl menyalakan handshake HTTP 100-continue untuk body permintaan di atas kira-kira satu kilobyte, dan kueri timestamp dengan permintaan sertifikat biasanya melampaui threshold itu. Sebagian server TSA tak pernah menjawab continuation-nya, sehingga klien menunggu satu timeout penuh sebelum mengirim body yang sebenarnya langsung diterima server. Mengirim header Expect: kosong menekan handshake itu, dan permintaannya lewat dalam satu round trip

Yang kedua adalah CURLOPT_NOSIGNAL, yang wajib disetel. Tanpanya libcurl mengimplementasikan timeout resolusi namanya memakai SIGALRM, dan mekanisme itu tidak thread-safe. Penandatanganan berjalan di worker thread, jadi perilaku bawaannya adalah crash laten yang muncul di bawah konkurensi dan tak pernah muncul di test satu thread. Menyetel flag itu mematikan jalur berbasis sinyal dan hanya berbiaya granularitas timeout resolver

Kedua cacat itu berbagi profil yang membuatnya mahal untuk ditemukan belakangan. Tak satu pun muncul di test fungsional terhadap server yang sopan pada satu thread. Keduanya muncul di produksi, terhadap satu TSA tertentu, di bawah beban. Ketika Anda mengikat pustaka jaringan, baca dulu apa yang diasumsikan bawaannya tentang proses Anda sebelum berasumsi semuanya cocok

Diagram transport timestamp libcurl PDFium VCL yang menampilkan curl_easy_setopt yang dideklarasikan sebagai prototipe Pascal long dan pointer tetap yang memberikan argumen di register integer, header Expect kosong yang menekan handshake HTTP 100-continue, CURLOPT_NOSIGNAL yang menghapus jalur SIGALRM di worker thread, dan cap respons di level transport
Dua setting menentukan apakah permintaan selesai: header Expect kosong menghindari server yang tak pernah menjawab continuation, dan NOSIGNAL menjaga timeout resolusi nama menjauh dari jalur sinyal sementara penandatanganan berjalan di worker thread

Bagaimana memverifikasi kode yang tak pernah dilihat kompilator Anda?

Dengan membuat kompilator tetap melihatnya, lewat salinan yang terkendali. Mesin pengembangan di sini tak punya cross-compiler Linux atau macOS, jadi cabang non-Windows unit timestamping tak pernah mencapai code generator selama build normal. Kode yang tak pernah terkompilasi adalah kode yang membusuk diam-diam: penggantian nama di tipe bersama, daftar parameter yang berubah, dependensi unit yang bertambah, dan tak ada yang menyadarinya berbulan-bulan

Tekniknya mekanis. Salin unitnya ke direktori sementara, ganti namanya, dan ganti setiap conditional Windows — baik bentuk {$IFDEF MSWINDOWS} maupun bentuk {$IF DEFINED(MSWINDOWS) — dengan simbol yang tak pernah didefinisikan. Lalu kompilasi salinannya. Ketika seluruh 3.828 baris terkompilasi, Anda telah membuktikan bahwa jalur non-Windows memakai unit yang ada, memanggil fungsi backend dengan signature yang cocok, dan mereferensikan tipe yang berada dalam scope. Itu bukan bukti transportnya bekerja, dan tak ada yang memberi Anda itu selain platform targetnya. Itu bukti bahwa cabangnya belum patah — mode kegagalan yang benar-benar menumpuk

Kebiasaan pendampingnya adalah membiarkan unit libcurl itu sendiri bebas dari guard platform, sehingga ia ikut serta dalam build Windows biasa meski tak ada di sana yang mereferensikannya. Build harian kemudian terus mengawasi sintaks dan tipe-nya secara gratis. Unit yang hanya terkompilasi di platform yang tidak Anda miliki adalah unit tanpa kompilator yang memeriksanya sama sekali, dan nalar yang sama berlaku di seluruh pekerjaan lintas-kompilator yang dibahas di jebakan lintas-kompilator Delphi dan FPC

Membatasi apa yang kembali

Respons timestamp adalah struktur DER kecil, dan tak ada apa pun di transportnya yang menegakkannya. Server yang terkompromi, salah konfigurasi, atau sekadar diarahkan ke URL yang salah bisa mengembalikan stream arbitrer, dan klien yang membaca sampai koneksi tertutup akan dengan senang hati mengakumulasinya. Karena itu kedua transport mem-cap responsnya, dan itulah tempat yang benar untuk batasnya: menolak di level transport mencegah body berukuran besar dialokasikan sama sekali, sedangkan pemeriksaan level parser baru menyala setelah memorinya sudah dikomit

Nalar yang sama berlaku untuk URL-nya. Backend hanya menerima scheme yang bisa dibicarakannya dengan bermakna, sehingga kesalahan konfigurasi gagal seketika dengan pesan yang jelas alih-alih diserahkan ke libcurl untuk ditafsirkan dengan cara apa pun yang diizinkan dukungan protokolnya

Di mana transport duduk dalam kisah penandatanganan

Timestamping adalah langkah pertama kisah validasi jangka panjang, bukan seluruhnya. Token harus dilekatkan ke tanda tangan, materi validasi harus dicatat di document security store, dan archive timestamp harus diperbarui sebelum yang sekarang melemah. Seluruh busur itu dibahas di tanda tangan PDF jangka panjang dengan timestamp RFC 3161 dan DSS

Diagram PDFium VCL atas permintaan timestamp RFC 3161 yang mengalir dari DocumentDigest melalui BuildTimeStampQuery dan PostTimeStampQuery lewat libcurl ke server TSA, respons DER di-cap di level transport, lalu AttachTimeStampToken menyuplai DSS dan pembaruan archive timestamp dalam validasi jangka panjang
Timestamping adalah langkah pertama kisah validasi jangka panjang: token harus dilekatkan, materi validasi dicatat di document security store, dan archive timestamp diperbarui sebelum yang sekarang melemah

Transport juga satu bagian dari posisi portabilitas yang lebih luas: native library loader yang dibahas di memuat native library di target mana pun menangani kelas masalah yang sama untuk biner PDFium itu sendiri. Di kedua kasus polanya identik — ikat segelintir simbol secara dinamis, laporkan dengan presisi apa yang gagal terikat, dan jangan pernah membiarkan dependensi yang hilang menjadi kegagalan link-time yang menghentikan aplikasi untuk berjalan

Backend timestamp Windows dan non-Windows keduanya tersedia dalam komponen Delphi PDFium, dipilih berdasarkan target alih-alih konfigurasi, sehingga aplikasi Lazarus di Linux dan aplikasi Delphi di Windows menghasilkan tanda tangan bertimestamp yang sama melalui pipa-pipa yang berbeda