Bài viết kỹ thuật

Backend timestamp libcurl cho PDFium VCL trên FPC

PDFium VCL gửi các truy vấn timestamp RFC 3161 qua libcurl trên các target không phải Windows, bind động tới đúng tám symbol, soi gương hình dáng của backend Windows vốn bind vào WinHTTP. Hai setting quyết định transport có đáng tin dưới tải hay không, và cả unit được kiểm chứng trên một máy không thể compile nó cho platform đích của nó

Timestamping là thứ biến một chữ ký thành thứ sống sót qua hạn chứng chỉ, và nó là một phép toán mạng nằm trong lòng một phép toán ký số. Tổ hợp đó khiến lựa chọn transport nặng ký hơn thường lệ: nó chạy trên một worker thread, nó nói chuyện với một server bạn không kiểm soát, và một cú treo ở đó là cả pipeline ký số đứng hình chứ không phải một page load chậm

Vì sao libcurl thay vì HTTP client của FPC?

Vì phương án thay thế kéo nguyên một TLS stack vào repository rồi giao cho bạn việc bảo trì phần detect version của nó. Đường hiển nhiên trên Free Pascal là fphttpclient với socket layer OpenSSL, và nó gãy ở các chi tiết: FPC 3.2.2 binding OpenSSL phát hiện OpenSSL 3.x không đáng tin trên đa số distribution hiện tại, còn macOS đặt thêm các khác biệt LibreSSL lên trên. Một lời gọi HTTP nhỏ biến thành công việc bảo trì thường trực ABI TLS của người khác

libcurl tự resolve TLS backend của nó và validate chuỗi chứng chỉ theo platform trust store, nên phía Pascal không cần chuyện gì trong số đó. Tầng binding là tám symbol. Con số đó chính là lập luận: một bề mặt hẹp hơn giữa code của bạn và một dependency di động nghĩa là ít chỗ hơn cho một lần nâng cấp distribution làm bạn gãy, và nó khớp với backend Windows hiện có — thứ bind một nắm WinHTTP entry point đúng cách tương tự

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;

Khai báo một hàm C variadic bằng Pascal

curl_easy_setoptcurl_easy_getinfo là variadic phía C, và Object Pascal không có cách diễn đạt điều đó. Cách chạy được là khai báo vài fixed prototype, mỗi cái cho một lớp argument, tất cả trỏ vào cùng một exported symbol: một biến thể nhận long, một biến thể nhận pointer, vân vân, chọn tại call site theo thứ bạn thật sự truyền

Cách này an toàn vì một lý do cụ thể đáng hiểu thay vì chép máy móc. Mỗi loại argument trong số đó được truyền qua một integer register dưới các calling convention đang áp dụng của platform — đúng chỗ mà bản triển khai C va_arg đọc nó. Mẹo vì thế đúng với integer, pointer và handle, và không đúng với argument floating-point, thứ đi bằng những register khác. Đừng thêm một biến thể nhận double với giả định pattern này tổng quát được

// Một exported symbol, vài fixed prototype. Mỗi variant truyền
// argument của nó trong một integer register — đúng chỗ phía C đọc
// nó. Variant floating-point sẽ không chạy và không được thêm vào
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;

Hai setting quyết định request có hoàn tất hay không

Thứ nhất là một header Expect: rỗng tường minh. libcurl bật HTTP 100-continue handshake cho request body vượt cỡ một kilobyte, và một truy vấn timestamp kèm certificate request thường vượt ngưỡng đó. Vài TSA server không bao giờ trả lời phần continuation, nên client chờ hết nguyên một timeout trước khi gửi một body mà server lẽ ra đã nhận ngay lập tức. Gửi một header Expect: rỗng tắt handshake đi, và request đi trọn trong một round trip

Thứ hai là CURLOPT_NOSIGNAL, bắt buộc phải set. Không có nó, libcurl triển khai name-resolution timeout bằng SIGALRM, và cơ chế đó không thread-safe. Signing chạy trên một worker thread, nên hành vi mặc định là một crash ngầm chỉ xuất hiện dưới concurrency và chưa bao giờ trong một test đơn luồng. Set flag này tắt đường dựa trên signal đi, và cái giá chỉ là độ mịn của resolver timeout

Cả hai defect chung một chân dung khiến chúng đắt đỏ khi phải tìm về sau. Không cái nào hiện ra trong một functional test với một server ngoan ngoãn trên một luồng. Cả hai đều xuất hiện trên production, với đúng một TSA cụ thể, dưới tải. Khi bạn bind một networking library, hãy đọc xem các default của nó mặc định điều gì về process của bạn trước khi mặc định chúng khớp

Sơ đồ transport timestamp libcurl của PDFium VCL cho thấy curl_easy_setopt được khai báo dưới các prototype Pascal fixed dạng long và pointer truyền argument qua integer register, header Expect rỗng tắt HTTP 100-continue handshake, CURLOPT_NOSIGNAL gỡ bỏ đường SIGALRM trên worker thread, và cap mức response của transport
Hai setting quyết định request có hoàn tất hay không: một header Expect rỗng né những server không bao giờ trả lời continuation, và NOSIGNAL giữ name-resolution timeout xa con đường signal trong khi signing chạy trên worker thread

Kiểm chứng code mà compiler của bạn không bao giờ thấy?

Bằng cách bắt compiler thấy nó anyway, qua một bản sao có kiểm soát. Máy phát triển ở đây không có cross-compiler Linux hay macOS, nên các nhánh không phải Windows của unit timestamping không bao giờ tới tay code generator trong một lần build thường. Code không bao giờ được compile là code âm thầm mục rữa: một rename trong shared type, một parameter list đổi, một unit dependency thêm vào, và chẳng ai nhận ra trong nhiều tháng

Kỹ thuật thì thuần cơ học. Copy unit sang một thư mục tạm, đổi tên, rồi thay mọi Windows conditional — cả dạng {$IFDEF MSWINDOWS} lẫn dạng {$IF DEFINED(MSWINDOWS) — bằng một symbol không bao giờ được định nghĩa. Rồi compile bản copy. Khi cả 3.828 dòng compile sạch, bạn đã chứng minh nhánh không phải Windows dùng những unit tồn tại, gọi các backend function với signature khớp, và tham chiếu những type trong scope. Đó không phải bằng chứng transport chạy được, và chẳng có gì ngắn hơn platform đích cho nổi thứ đó. Nó là bằng chứng nhánh chưa gãy sẵn — đúng failure mode thực sự tích tụ theo thời gian

Thói quen đi cặp là giữ bản thân unit libcurl không dính bất kỳ platform guard nào, để nó tham gia build Windows thường ngày dù chẳng có gì ở đó reference tới nó. Bản build hằng ngày khi đó guard syntax và type của nó miễn phí. Một unit chỉ compile được trên một platform bạn không có là một unit không có compiler nào kiểm tra, và cùng lý lẽ đó áp dụng cho toàn bộ công việc đa compiler được kể trong các cái bẫy cross-compiler Delphi và FPC

Giới hạn những gì quay về

Một timestamp response là một cấu trúc DER nhỏ, và chẳng có gì ở transport ép chuyện đó. Một server bị chiếm quyền, cấu hình sai, hay đơn giản trỏ sai URL có thể trả về một stream tùy ý, và một client đọc cho tới khi kết nối đóng sẽ hạnh phúc gom hết về. Vì thế cả hai transport đều cap response, và đó là chỗ đúng cho giới hạn: từ chối ngay ở transport ngăn một body cỡ lớn kịp được cấp phát, trong khi một check tầng parser chỉ kích hoạt sau khi bộ nhớ đã bị chiếm

Cùng lý lẽ áp dụng cho URL. Backend chỉ chấp nhận những scheme nó nói nổi một cách có nghĩa, nên một lỗi cấu hình fail ngay lập tức với một message rõ ràng thay vì được trao cho libcurl diễn giải theo bất kỳ cách nào phần hỗ trợ protocol của nó cho phép

Transport đứng ở đâu trong câu chuyện ký số

Timestamping là bước đầu tiên của câu chuyện long-term validation chứ không phải toàn bộ nó. Token phải được gắn vào chữ ký, validation material phải được ghi vào document security store, và archive timestamp phải được gia hạn trước khi cái hiện tại yếu đi. Cả cung đó được kể trong long-term PDF signature với RFC 3161 timestamp và DSS

Sơ đồ PDFium VCL của một truy vấn timestamp RFC 3161 chảy từ DocumentDigest qua BuildTimeStampQuery và PostTimeStampQuery trên libcurl tới một TSA server, response DER bị cap ngay ở transport, rồi AttachTimeStampToken nạp vào DSS cùng việc gia hạn archive timestamp trong long-term validation
Timestamping là bước đầu của câu chuyện long-term validation: token phải được gắn, validation material phải được ghi vào document security store, và archive timestamp phải được gia hạn trước khi cái hiện tại yếu đi

Transport cũng là một mảnh trong thế đứng portability rộng hơn: native library loader được mô tả trong load native library trên mọi target xử lý cùng một lớp bài toán cho chính binary PDFium. Ở cả hai case, pattern là một: bind một số lượng nhỏ symbol một cách động, báo cáo chính xác thứ nào bind thất bại, và không bao giờ để một dependency bị thiếu biến thành link-time failure chặn application khởi động

Cả hai timestamp backend Windows và không Windows đều đi kèm PDFium Delphi component, được chọn theo target chứ không theo cấu hình, để một Lazarus application trên Linux và một Delphi application trên Windows cho ra cùng một chữ ký có timestamp qua hai hệ ống dẫn khác nhau