PDFium VCL giờ có thể hoàn tất một chuỗi chữ ký PDF và kiểm tra thu hồi qua mạng trên backend OpenSSL của mình: khi OnlineRetrieval được bật, ConfigureSslCmsVerifier cài một verifier tải các certificate trung gian còn thiếu từ các URL AIA caIssuers và CRL từ các CRL distribution point, trong một ngân sách thời gian, số request và byte cố định cho mỗi lần gọi verify. Certificate được tải về chỉ là vật liệu chuỗi, không hơn. Trust vẫn đến độc quyền từ system store và các anchor bạn cấu hình
Khoảng hổng mà việc này lấp lộ diện ngay lần đầu bạn validate các PDF thực tế trên một server Linux. Phần lớn người ký chỉ nhúng certificate lá của chính họ vào CMS, nên OpenSSL không với tới được một root, TrustStatus quay về invalid, và thu hồi chẳng bao giờ chạy vì chuỗi chưa bao giờ trở nên đáng tin. Trước v3.121.0, backend OpenSSL mô tả trong verify chữ ký PDF với OpenSSL trong PDFium VCL thuần offline và OnlineRetrieval chẳng có tác dụng gì với nó. Một điều đáng nói trước: chính engine PDFium chẳng làm verification CMS nào cả, nên mọi luật dưới đây sống trong lớp PAdES của component và binding OpenSSL của nó, nơi bạn đọc được
Backend OpenSSL verify, fetch và kiểm tra theo thứ tự nào?
Integrity trước, rồi trust, rồi thu hồi, và mạng chỉ bị chạm tới giữa các bước cần nó. VerifyCmsWithSsl kiểm chữ ký CMS và signed attributes (RFC 5652) với việc đánh giá chuỗi bị tắt, và nếu điều đó gãy, nó trả về ngay lập tức, trước cả khi một fetch session kịp tồn tại, nên một tài liệu có byte hỏng không kích hoạt bất kỳ request đi ra ngoài nào. Chỉ khi chuỗi sau đó gãy và OnlineRetrieval đang bật, nó mới đi theo các link AIA và verify lại. Các CRL distribution point chỉ được fetch sau khi chuỗi được tin tưởng, vì một CRL treo trên một đường không đáng tin chẳng chứng minh gì. Ba phán quyết giữ riêng biệt xuyên suốt: một chữ ký hợp lệ với chuỗi chưa hoàn chỉnh vẫn được báo là một chữ ký hợp lệ
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER; nguồn trust bổ sung duy nhất
ConfigureSslCmsVerifier;
Probe := TPdfCmsVerifyOptions.Default;
Probe.OnlineRetrieval := True;
Probe.CheckRevocation := True;
Diags := SslVerifyOptionsDiagnostics(Probe);
if psvdOnlineRetrievalIgnored in Diags then
Log('no HTTP transport or CMS_add1_cert: validation stays offline');
Trust := TPadesTrustValidationOptions.Default; // ptnpOffline theo mặc định
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // mỗi lần gọi verify
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'signed-contract.pdf';
Pdf.Active := True;
Verdict := Pdf.ValidatePadesTrust(Trust);
for I := 0 to High(Verdict.Signatures) do
Log(Format('#%d trust=%d revocation=%d', [I,
Ord(Verdict.Signatures[I].CertificateTrustStatus),
Ord(Verdict.Signatures[I].RevocationStatus)]));
finally
Pdf.Free;
end;
end;
Vì sao certificate tải về chẳng bao giờ được thêm vào trust store?
Vì các URL đến từ certificate đang được validate, và người ký là người chọn chúng. Entry authorityInfoAccess caIssuers (RFC 5280 §4.2.2.1) chỉ là một gợi ý về nơi issuer cư trú, chẳng thêm gì nữa. Nếu bất cứ thứ gì trả lời URL đó được nhét vào trust-anchor store, bất kỳ ai cũng có thể ký bằng một key tự chế, trỏ AIA về server của chính họ, và nhận về một phán quyết xanh. Vì thế RetrieveIntermediates đưa từng certificate đã parse cho CMS_add1_cert, thứ đặt nó vào tập untrusted của đúng cấu trúc CMS này, và OpenSSL vẫn phải dựng một đường từ nó tới một anchor bạn cấu hình hay system store đã có. Còn một lý do êm thấm hơn: tham số certificate của CMS_verify không phải thứ thay thế thả-vào-được cho các certificate nhúng trong CMS, nên thêm thẳng vào CMS mới là con đường đáng tin
Vòng lặp truy xuất được cố ý thu hẹp. RetrieveIntermediates chạy tối đa 4 vòng, mỗi vòng thu các URL caIssuers từ mọi certificate đang có trong CMS, và dừng ngay khi một vòng chẳng thêm gì hay ngân sách thời gian đã cạn. Một response phải decode bằng d2i_X509 thành một certificate DER đơn lẻ ăn trọn body; các byte đuôi bị từ chối, và một bundle PKCS#7 chỉ-chứa-cert phục vụ từ một URL .p7c bị bỏ qua thay vì được giải nén. Phương thức truy cập OCSP trong cùng extension AIA bị bỏ qua, vì backend này chẳng biết nói OCSP. Phía thu hồi, RetrieveCrls chỉ đọc các URI fullName của mỗi DistributionPoint (RFC 5280 §4.2.1.13) từ các certificate trong CMS và các anchor đã cấu hình, và các CRL tải về đi vào một X509_STORE thứ hai, độc lập, với kiểm CRL trên toàn chuỗi, nên một CRL thiếu hay cũ kỹ đổi RevocationStatus mà chẳng bao giờ đụng tới TrustStatus
// Rút gọn từ VerifyCmsWithSsl (FPdfCryptoSsl.pas); phần dựng BIO bị bỏ qua.
// Mọi lần gọi _CMS_verify đều nhận một content BIO tươi
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // chữ ký gãy: chẳng chạm mạng chút nào
if Options.OnlineRetrieval and SslCapabilities.OnlineRetrieval then
FetchSession := TPdfCryptoFetchSession.Create(Options.UrlRetrievalTimeoutMs);
Store := BuildStore(False, nil, RevocationChecked, CrlsMalformed);
if (_CMS_verify(Cms, nil, Store, Bio, nil, CMS_BINARY) <> 1) and
(FetchSession <> nil) then
begin
RetrieveIntermediates(Cms, FetchSession); // CMS_add1_cert, chỉ untrusted
// verify lại chuỗi đối với cùng anchor store
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// store thứ hai, riêng biệt: các CRL đã cấu hình cộng các CRL truy xuất được
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Một lần gọi verify tốn tối đa bao nhiêu?
Một trần cố định, thực thi bởi một TPdfCryptoFetchSession duy nhất mà các bước AIA và CRL của một lần gọi verify dùng chung. Các giới hạn là hằng số trong FPdfCryptoHttp, không phải lời khuyên:
- Thời gian:
UrlRetrievalTimeoutMs, mặc định 15000 trong cảTPdfCmsVerifyOptions.DefaultlẫnTPadesTrustValidationOptions.Default; một session tạo với 0 rơi về 30000, và đồng hồ bắt đầu ngay khi chữ ký đã pass, phủ mọi request về sau - Request: tối đa 8 mỗi session, đếm trước khi transport được thử, nên một host chết vẫn tiêu tốn một slot
- Byte: 1 MiB mỗi response và 4 MiB tổng cộng, với các URL dài hơn 2048 ký tự bị từ chối trước mọi kết nối
Phép tính ngân sách nghiêm ngặt hơn cái nhìn đầu tiên. Byte nhận từ một response gãy vẫn bị trừ vào tổng, nên một server trả 404 kèm một trang lớn không thể cạn ngân sách một cách miễn phí. Phép đọc vượt giới hạn mỗi response hủy download thay vì chuyển một body cụt cho parser ASN.1, và một HTTP 200 với body rỗng bị từ chối thẳng, vì nếu không đường AIA sẽ index Data[0] của một mảng rỗng. Chỉ các URL http:// và https:// trơn được qua, không redirect, không cookie, không credential, không phát hiện proxy tự động, trong khi HTTPS giữ nguyên các phép kiểm certificate và hostname thường lệ. Việc dedup URL được giới hạn trong đúng một lần gọi một cách chủ ý: lần validate kế tiếp phải có khả năng nhìn thấy một CRL vừa công bố. Ngân sách cũng theo từng lần gọi chứ không theo tài liệu, và ValidatePadesTrust verify từng chữ ký lẫn từng timestamp token một cách riêng biệt, nên ca xấu nhất lớn dần theo số chữ ký
Vì sao một request WinHTTP hết giờ vẫn có thể ghi vào bộ nhớ của bạn?
Vì việc trả về khi hết giờ chẳng hủy được các callback đã cất cánh. Transport Windows lái WinHTTP bất đồng bộ và chờ trên một event với thời gian còn lại của session, và khi phép chờ đó bỏ cuộc, request vẫn có thể hoàn tất một phép đọc và báo hiệu sau đó. Trỏ phép đọc bất đồng bộ vào một buffer trên stack, và cái hoàn tất muộn màng đó sẽ ghi vào một frame khi đó đã thuộc về một hàm nào đó chẳng liên quan. Bản sửa là về quyền sở hữu, chứ không phải thời điểm: event và buffer đọc 16 KB sống trong một heap record với hai tham chiếu, một do caller giữ và một chỉ được giải phóng bởi callback cuối cùng HANDLE_CLOSING, nên bên nào kết thúc sau bên đó free bộ nhớ
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // caller + callback HANDLE_CLOSING cuối cùng
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // các phép đọc bất đồng bộ đáp xuống đây, không bao giờ trên stack
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// Trong status callback: HANDLE_CLOSING là thông báo cuối cùng WinHTTP
// gửi cho request, nên nó nhả tham chiếu thứ hai
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
libcurl phải cung cấp gì trên FPC Unix
Một resolver bất đồng bộ và một bản build thread-safe, nếu không truy xuất trực tuyến sẽ bị tắt. Trên FPC Unix, transport đi qua libcurl, cùng dependency đứng sau backend timestamp libcurl cho các target không phải Windows, và binding từ chối bất kỳ thư viện nào mà feature mask thiếu CURL_VERSION_ASYNCHDNS hay CURL_VERSION_THREADSAFE. Lý do là CURLOPT_NOSIGNAL — thứ mà một thư viện sống trong process của người khác phải đặt — kết hợp với một resolver đồng bộ nghĩa là một tra DNS có thể đơn giản sống dai hơn timeout. Cái bẫy thứ hai là shutdown: curl_global_cleanup không chờ các thread DNS bất đồng bộ, nên một khi libcurl đã được khởi tạo thì module cứ bị map cho tới khi process thoát, thay vì để một thread nền chạy vào code đã được unload. Khi một trong hai điều kiện gãy, SslCapabilities.OnlineRetrieval là False và SslVerifyOptionsDiagnostics báo psvdOnlineRetrievalIgnored thay vì giả vờ rằng mạng đã được hỏi
Kết quả đảm bảo và không đảm bảo điều gì
Một RevocationStatus hợp lệ từ backend này nghĩa là các CRL hiện hành phủ toàn bộ chuỗi đã được tìm thấy, cấu hình hay tải về, và chẳng cái nào liệt kê một certificate trong đó; không hơn. Không có OCSP, nên một CA chỉ công bố thu hồi qua OCSP sẽ để kết quả ở trạng thái unsupported, và một lỗi mạng nhìn hệt như một CA chẳng công bố gì. Cũng chú ý psvdNoCrlsConfigured chỉ mô tả các CRL bạn đã cấu hình, nên với truy xuất trực tuyến nó là một gợi ý, không phải một lời tiên đoán thất bại. Khi một audit trail phải tái lập được mà không cần mạng, hãy để NetworkPolicy ở mặc định ptnpOffline: chẳng fetch session nào được tạo và backend không bao giờ mở kết nối, khớp với hợp đồng offline phía CryptoAPI đã mô tả trong kiểm tra thu hồi chữ ký PDF offline trên Windows
Code truy xuất, các ngân sách và các binding transport được giao kèm dạng source với PDFium Delphi component, nên bạn có thể xác nhận chính xác một lần validate được phép liên hệ những URL nào và được phép tải bao nhiêu, trước khi bật ptnpOnline trên một server xử lý các tài liệu không đáng tin