PDFium VCL kiểm tra revocation chữ ký PDF offline trên Windows bằng cách thêm CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY vào lượt revocation của CertGetCertificateChain, vì cờ cache-only dùng cho việc dựng chain chẳng phủ lấy retrieval CRL hay OCSP chút nào. Từ v3.119.1, một lần gọi ValidatePadesTrust offline giữ nguyên khỏi mạng, và một kết quả sạch đòi hỏi bằng chứng revocation thật sự cho từng chứng chỉ. Phần còn lại của bài là vì sao cả hai nửa của câu trên đều cần được sửa
Bối cảnh phơi bày vấn đề thì bình thường thôi. Một service validation chạy trên một host Windows bị khóa chặt, TPadesTrustValidationOptions.NetworkPolicy là ptnpOffline (cũng là mặc định), và người vận hành kỳ vọng mọi câu trả lời đến từ cache chứng chỉ local. Rồi có người để ý các request outbound tới một distribution point của CA trong log firewall, hay một batch job khựng hết UrlRetrievalTimeoutMs 15000 ms ở mỗi chữ ký. Chẳng gì trong code đòi hỏi mạng. Windows tự đi đó
Vì sao dựng chain offline vẫn fetch CRL trên Windows?
Vì CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL chỉ giới hạn phần URL retrieval mà việc dựng chain làm: fetch AIA issuer, cập nhật root và CTL. Tài liệu Microsoft cho CertGetCertificateChain nói rõ cờ này không áp dụng cho việc kiểm revocation. Revocation có công tắc riêng của nó, CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY ($80000000), và thiếu nó thì các revocation provider thoải mái tải một CRL hay gửi một request OCSP dù lời gọi xung quanh trông có vẻ offline. PDFium VCL giờ OR cờ đó vào lượt revocation bất cứ khi nào OnlineRetrieval là False, chồng lên các cờ chain, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT và CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT. Chuyện này quan trọng hơn cả độ trễ: một request OCSP nói cho responder biết bạn đang xem chứng chỉ nào, đúng thứ một validator air-gapped phải tránh
uses
PDFium, FPdfCrypto, FPdfPades;
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
begin
Options := TPadesTrustValidationOptions.Default; // ptnpOffline, 15000 ms
Options.CheckRevocation := True; // False theo mặc định
Options.CheckTimeStamps := True;
// Offline giờ nghĩa là offline cả với revocation: chỉ CRL và OCSP
// đã cache thôi, không checkpoint pcvstOnlineRetrieval nào được raise
Report := Pdf.ValidatePadesTrust(Options);
end;
Hai lần dựng chain, hai slot lỗi
Backend Windows dựng chain hai lần, và mỗi lần dựng giờ sở hữu một slot lỗi riêng. Lần gọi CertGetCertificateChain đầu chạy không có cờ revocation và đưa CertVerifyCertificateChainPolicy với chính sách cơ bản, thứ sinh ra TrustStatus và TrustError. Lần gọi thứ hai thêm các cờ revocation. Trước v3.119.1, một thất bại của lần gọi thứ hai ghi GetLastError vào TrustError, nên một chain vừa được xác nhận tin cậy có thể quay về trông như không tin cậy chỉ vì một revocation provider hiccup. Bản sửa đọc GetLastError ngay lập tức và cất nó vào TPdfCmsVerifyResult.RevocationError, để nguyên phán quyết lượt đầu. Và một giá trị True trả về từ lần gọi thứ hai cũng không bị coi là thành công; nó chỉ có nghĩa là Windows đã đưa lại một chain context đáng soi
Một mặt lỗi trust bằng 0 thật sự chứng minh điều gì?
Một mình nó, chẳng chứng minh gì. Một TrustStatus.dwErrorStatus tổng hợp bằng 0 sau lượt revocation nói rằng chẳng bit lỗi nào được raise, và một chain mà chẳng element nào mang thông tin revocation có thể cho ra đúng kết quả đó. Code trước đây ánh xạ thẳng “không bit revoked, không bit unknown, không bit offline” sang valid, kiểu kinh điển mà một validator báo một chứng chỉ chưa được kiểm như một chứng chỉ sạch. Routine mới ReadWinRevocationEvidence đi qua mọi simple chain và mọi element, chối các cấu trúc mà cbSize nhỏ quá để đọc an toàn, và chỉ báo thành công khi tồn tại ít nhất một element khác root và mọi element như vậy mang một CERT_REVOCATION_INFO mà dwRevocationResult của nó bằng 0
// Rút gọn từ evidence walk: một element chỉ được tính khi có
// một revocation provider thật sự trả lời cho nó
for J := 0 to ElementCount - 1 do
begin
Element := Elements[J];
ExcludedRoot := (J = ElementCount - 1) and
((Element^.TrustStatus.dwInfoStatus and
(CERT_TRUST_IS_SELF_SIGNED or CERT_TRUST_IS_CA_TRUSTED)) <> 0);
InfoPresent := (Element^.pRevocationInfo <> nil) and
(Element^.pRevocationInfo^.cbSize >= SizeOf(TCERT_REVOCATION_INFO));
if not ExcludedRoot then
begin
Inc(RequiredCount);
if not InfoPresent or
(Element^.pRevocationInfo^.dwRevocationResult <> 0) then
Complete := False;
end;
end;
Complete := Complete and (RequiredCount > 0); // một chain chỉ toàn root chẳng chứng minh gì
Kết quả của provider được giữ nguyên trạng. RevocationError giữ DWORD dwRevocationResult đúng như provider trả về, Ưu tiên lỗi từ element bị revoke khi có (CRYPT_E_REVOKED là $80092010), và bitmask trust không bao giờ được thay áo thành một mã lỗi native. Phép ánh xạ sang TPdfCmsRevocationReason là thô một cách có chủ ý: pcrrCertificateRevoked với pcvsInvalid cho revocation tường minh, pcrrChainUntrusted khi chain thất bại vì lý do chẳng liên quan revocation, và pcrrUnknown cho mọi thứ còn lại. Windows có thể đã thử OCSP thay vì CRL, nên một kết quả offline hay unknown không được dịch thành pcrrCrlExpired. Backend verify CMS OpenSSL có thể đưa ra các phân biệt riêng-CRL đó vì nó chỉ từng đánh giá những CRL bạn đưa cho nó, trong khi backend macOS SecTrust để các trường ở pcrrNone và 0, nghĩa là “không có chẩn đoán chi tiết”, chứ không phải “đã qua”
Việc loại root dừng ở đâu
CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT bỏ anchor một cách chính đáng, vì chẳng ai phát hành một CRL revoke một root chống lại chính nó. Cái bẫy là quyết định element nào là root. PDFium VCL loại element cuối của một simple chain chỉ khi dwInfoStatus của nó đánh dấu self-signed ($00000008) hay được CA tin cậy tường minh ($00004000). Một host offline thường không fetch nổi một issuer còn thiếu, nên chain kết thúc ở một intermediate; coi element cuối của chain một phần đó là root sẽ lặng lẽ đánh rơi đúng chứng chỉ mà trạng thái revocation nhiều khả năng vắng mặt trong cache nhất. Element đó vẫn nằm trong tập bắt buộc, không có câu trả lời của provider nào, và kết quả giữ nguyên pcvsIndeterminate
Kết quả revocation của chữ ký và timestamp được tách nhau thế nào?
Bằng các trường riêng chẳng bao giờ ghi đè nhau. Validator PAdES verify CMS detached của chữ ký tài liệu và CMS attached của token timestamp RFC 3161 trong hai lần gọi độc lập, và v3.119.0 cho mỗi cái một bộ chẩn đoán riêng trên TPadesSignatureValidation: RevocationReason và NativeRevocationError cho người ký, TimeStampRevocationReason và NativeTimeStampRevocationError cho TSA. Một chứng chỉ TSA bị revoke vì thế không thể giả làm một người ký bị revoke, và một lần timestamp thất bại không xóa được một kết quả toàn vẹn đã được thiết lập. Khi CheckRevocation là False, hay validation chưa từng chạm tới chặng đó, các trường giữ pcrrNone và 0, nên hãy luôn đọc chúng cạnh RevocationStatus và TimeStampRevocationStatus
for I := 0 to High(Report.Signatures) do
begin
S := Report.Signatures[I];
case S.RevocationStatus of
pcsInvalid:
Log(Format('sig %d: signer revoked, provider 0x%.8x',
[I, S.NativeRevocationError]));
pcsIndeterminate:
Log(Format('sig %d: revocation unknown, reason %d, provider 0x%.8x',
[I, Ord(S.RevocationReason), S.NativeRevocationError]));
pcsNotChecked:
Log(Format('sig %d: revocation not checked', [I]));
end;
if S.TimeStampRevocationStatus = pcsIndeterminate then
Log(Format('sig %d: TSA revocation unknown, provider 0x%.8x',
[I, S.NativeTimeStampRevocationError]));
end;
Báo cáo bằng chứng đi theo cùng luật đó. Bản export CSV nối thêm revocationReason, nativeRevocationError và các cột timestamp tới nativeTimeStampRevocationError ở cuối thứ tự cột hiện có, nên các parser cũ vẫn chạy tiếp, và bản export JSON thêm các trường tương ứng mà không đổi ý nghĩa của các trường cũ. Nếu validation offline cứ trả về indeterminate mãi, bản sửa tận gốc nằm ở thượng nguồn: thu thập vật liệu validation ngay lúc ký, như được mô tả trong chữ ký PDF dài hạn với timestamp RFC 3161 và DSS, thay vì hy vọng máy verify có một cache ấm
Ma trận test chứng minh điều gì và không chứng minh điều gì
Ma trận verify Windows đã qua 30 kịch bản chain API được kiểm soát và một lần smoke CMS offline thật trên mỗi đích Win32 và Win64 của cả Delphi lẫn FPC. Smoke thật verify một chữ ký hợp lệ dưới một CA riêng không được tin cậy, trong khi các kết quả sạch và bị revoke tường minh đến từ các response CertGetCertificateChain được stub thay vì các trust anchor cài sẵn hay retrieval sống. Đó là một biên trung thực đáng nói ra: việc xử lý cờ, việc cô lập lỗi và cuộc đi lấy bằng chứng đã được ghim chặt, nhưng cache revocation của một máy cụ thể chứa gì vào một ngày cụ thể vẫn là chuyện của Windows, và một cache rỗng giờ đúng đắn cho ra “unknown” thay vì một request mạng hay một “valid” giả
Phần xử lý revocation offline, các chẩn đoán theo từng trường và các bản export bằng chứng là một phần của API validation chữ ký PDF trong PDFium VCL for Delphi and C++Builder, cùng các backend OpenSSL và macOS cho các triển khai đa nền tảng