PDFium VCL coi CMS verification như một backend có thể hoán đổi sau interface IPdfCmsVerifier, để PAdES validator chạy trên Windows qua CryptoAPI, trên macOS qua Keychain, và ở bất cứ đâu có OpenSSL qua ConfigureSslCmsVerifier. Interface thì nhỏ. Ba hành vi OpenSSL nằm dưới nó sẽ cho những câu trả lời sai đầy tự tin nếu bạn triển khai một cách ngây thơ
Động cơ thì rõ ràng ngay khi một application Delphi rời Windows. Signature validation là một trong ít lĩnh vực mà crypto stack của platform không phải một chi tiết triển khai: nó quyết định chứng chỉ nào được tin, thuật toán nào tồn tại, và revocation nghĩa là gì. Hard-code một cái thì code không port được. Abstract kém thì mỗi platform báo một hình dạng câu trả lời khác nhau mà caller không so sánh nổi
Abstraction thực sự phải mang theo những gì
Hai hình dạng verification và ba phán quyết độc lập. Một PDF signature là detached: nội dung được ký là hai dải byte ở hai bên lỗ /Contents, nên VerifyDetached nhận hai segment thay vì một buffer. Một timestamp token là attached, tự mang content theo, nên VerifyAttached chỉ nhận DER
Kết quả tách thành ba status vì chúng trả lời ba câu hỏi khác nhau và có thể không đồng tình với nhau. SignatureStatus nói bytes có được ký bởi key trong signer certificate hay không. TrustStatus nói certificate đó có nối tới thứ bạn tin hay không. RevocationStatus nói certificate có còn hiệu lực tại thời điểm liên quan hay không. Một tài liệu với chữ ký hoàn hảo về mặt toán học từ một certificate bạn chưa từng nghe tên là valid, untrusted và unknown, và việc gộp cả ba thành một boolean duy nhất là cách các validator kết thúc bằng việc nói dối người dùng
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, có thể rỗng
ConfigureSslCrls(LoadFreshCrls); // DER, có thể rỗng
ConfigureSslCmsVerifier; // cài đặt backend
Writeln('backend : ', PadesCmsVerificationBackendName);
Writeln('library : ', SslLibraryPath, ' ', SslLibraryVersion);
Writeln('ABI : ', SslAbiLayout); // ulong=<n> long=<n>
Options := TPdfCmsVerifyOptions.Default;
Options.CheckRevocation := True;
Options.CollectChainCertificates := True;
end;
SslAbiLayout trông như một món tò mò và không phải vậy. Mỗi OpenSSL error code và mọi store flag vượt qua ranh giới dưới dạng C unsigned long, vốn là bốn byte trên Windows và tám byte trên Linux cùng macOS. Khai báo nó thành một type 32-bit cố định thì code chạy trên Windows, rồi âm thầm đọc nửa giá trị trên LP64. Báo các width đang được giả định dưới dạng một chuỗi assert được trong test biến cả một hạng platform ABI drift thành một phép check một dòng. Ai từng vật lộn với cùng vấn đề qua CK_ULONG trong một binding PKCS#11 sẽ nhận ra ngay; câu chuyện đó nằm trong PKCS#11 struct packing và độ rộng CK_ULONG
Vì sao pass verify thứ hai lại thấy content rỗng?
Vì CMS_verify đọc content BIO detached tới cuối file, và một BIO đã bị đọc thì không tự rewind giùm bạn. Verify thành hai pass là một thiết kế hợp lý — pass đầu chỉ chữ ký thuần với chain evaluation bị tắt, pass sau đánh giá trọn vẹn — và nó fail theo một cách lừa người một cách bất thường nếu cả hai pass dùng chung một BIO
Pass thứ hai nhận đúng không byte content nào. Ở chế độ detached, đó không phải một error, vì một content buffer rỗng là input hợp lệ. Digest đơn giản không khớp, và failure lộ diện dưới dạng một chain-building failure thay vì một content failure — thứ đẩy bạn đi soi certificate và trust store trong khi vấn đề thật chỉ là một stream position. Hãy dựng lại memory BIO bằng BIO_new_mem_buf cho từng pass. Cái giá là một lần cấp phát, và khả năng xảy ra biến mất hoàn toàn
Flag no-verify tắt được gì và không tắt gì
CMS_NO_SIGNER_CERT_VERIFY tắt chain evaluation, không tắt signer certificate lookup. Bên trong, OpenSSL resolve và gắn các signer certificate trước khi tham khảo flag, nên sau một pass đầu mang flag đó, signer đã có sẵn và các algorithm identifier của nó đọc được ngay. Không cần chạy một lần verify đầy đủ thứ hai chỉ để lấy signer certificate — đúng điều mà cái tên flag gợi ý cho bạn làm theo
Một luật sở hữu đi kèm. Reference signer thuộc về cấu trúc CMS và không được free độc lập. Nó hợp lệ chừng nào cấu trúc còn hợp lệ, và việc free nó tạo ra một corruption mà triệu chứng xuất hiện ở một nơi hoàn toàn khác, thường là lúc dọn dẹp một object không liên quan
Vì sao bật CRL checking lại từ chối mọi chữ ký?
Vì OpenSSL chỉ check CRL với những gì store đang giữ và không tự đi lấy gì cả. Nó không theo CRL distribution point và không nói OCSP. Set X509_V_FLAG_CRL_CHECK lên một store không có CRL nào và mọi chuỗi fail với một bất khả thu thập certificate CRL. Kết quả trông như revocation checking đang chạy và tìm ra vấn đề. Thực ra là revocation checking chưa từng chạy
Vì thế backend chỉ set flag khi ConfigureSslCrls thực sự được cung cấp ít nhất một CRL. Không có CRL nào, RevocationStatus quay về pcvsUnsupported — một lời phát biểu trung thực rằng câu hỏi chưa được trả lời. Vì cùng lý do, OnlineRetrieval không tác dụng gì với backend này và không checkpoint pcvstOnlineRetrieval nào được emit: chẳng có đường fetching nào để báo tiến độ
Đây là một thế đứng thiết kế đáng bảo vệ nói chung. Một validator không check được revocation nên nói vậy. Báo một certificate chưa được kiểm tra là chưa bị thu hồi là cách phổ biến nhất khiến các công cụ signature validation đánh lừa người dùng, và đúng hạng nhầm lẫn được mổ xẻ trong vì sao validator từ chối PAdES signature
// Checkpoint để một UI hiển thị đang chạy stage nào, và cho biết
// backend thật sự thực hiện những stage nào
type
TSignatureProbe = class
procedure Checkpoint(Stage: TPdfCmsVerifyStage);
end;
procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
case Stage of
pcvstCryptographicSignature: Status('checking the signature');
pcvstChainBuild: Status('building the certificate chain');
pcvstOnlineRetrieval: Status('fetching validation data');
pcvstRevocationCheck: Status('checking revocation');
end;
end;
// Đọc ba phán quyết tách biệt; chúng được phép không đồng tình
if Result.SignatureStatus = pcvsValid then
case Result.TrustStatus of
pcvsValid: Report('signed and trusted');
pcvsInvalid: Report('signed, chain rejected');
pcvsUnsupported,
pcvsIndeterminate: Report('signed, trust not established');
end;
if Result.RevocationStatus = pcvsUnsupported then
Report('revocation was not checked on this backend');
Bind vào một library bạn không ghim được
OpenSSL đổi tên các stack accessor giữa 1.0 và 1.1, nên cùng một hàm logic có hai tên export khả dĩ tùy bản build máy chủ tình cờ có. Phần binding resolve tên mới trước rồi fallback về tên cũ, và chỉ ghi một missing symbol khi cả hai đều không resolve được. Đó là hình dáng đúng cho bất kỳ dynamic binding nào vào một library bạn không ship kèm: ưu tiên tên hiện hành, khoan dung tên lịch sử, và chỉ báo cáo sự vắng mặt thật
SslMissingSymbols là thứ biến một lần load thất bại thành một sự kiện chẩn đoán được. Một kết quả không rỗng trên một máy rõ ràng đã cài libcrypto nghĩa là version đang cài cũ hơn API mà bản build này nhắm tới — một câu chuyện support hoàn toàn khác với trường hợp library vắng mặt. ConfigureSslLibraryPath phủ nốt case phổ biến còn lại: một máy có vài bản build OpenSSL mà bản nằm trên default search path không phải bản bạn muốn
Chọn backend theo từng platform
Bố cục thực dụng là chọn lúc khởi động và ghi lại bên nào đã trả lời. Trên Windows, platform backend tích hợp với các certificate store mà doanh nghiệp đã quản lý sẵn — thường là điều bạn muốn. Trên macOS, Keychain backend khớp cùng lý lẽ và được mô tả trong verify chữ ký bằng SecTrust trên macOS. OpenSSL là phương án portable, và cũng là lựa chọn đúng khi bạn cần một validation policy đồng nhất giữa các platform thay vì một policy đi theo trust store của từng platform
Cài bên nào đi nữa, hãy log PadesCmsVerificationBackendName cạnh mọi phán quyết bạn ghi lại. Một kết quả validation đã lưu thiếu backend đã sinh ra nó thì không tái hiện được về sau, vì ba status mang những sắc thái khác nhau tùy stack nào trả lời. Tầng soi chữ ký đứng trên tất cả những thứ này, gồm cả cách báo PAdES level, được nói trong soi PDF digital signature và PAdES level
Toàn bộ đi kèm dạng source với PDFium Delphi component, và ở đây điều đó có ý nghĩa hơn thường lệ: với một signature validator, đọc được chính xác backend set những flag nào và bỏ qua những check nào không phải một thứ có cũng được — đó là cách duy nhất biết được một dấu check xanh trong application của bạn thật sự tuyên bố điều gì