Bạn đã nhận được một tệp PDF được ký số và bạn cần hiển thị trong trình xem của mình ai đã ký, ký khi nào, liệu chữ ký có bao phủ toàn bộ tệp hay không, và mức độ tuân thủ lâu dài của nó đến đâu. PDFium Component dành cho Delphi và Lazarus trả lời tất cả bốn câu hỏi bằng các cuộc gọi chỉ đọc: FPDF_GetSignatureCount và họ hàm FPDFSignatureObj_* phơi bày từ điển chữ ký, và TPdf.ValidatePades phân loại cấp độ cơ sở PAdES. Đây là bài viết đầu tiên trong ba bài viết về chữ ký PDF với PDFium; hai bài tiếp theo bao quát việc tạo chữ ký B-B và thêm dấu thời gian dài hạn. Tuy nhiên, có một ranh giới cần nêu trước: mọi thứ ở đây đều là kiểm tra (inspection), và việc đọc những gì một chữ ký tuyên bố là một công việc hoàn toàn khác với việc xác minh mật mã của nó hoặc quyết định xem bạn có tin tưởng người ký hay không
Tại sao từ điển chữ ký PDF không chỉ là một khối các byte
Một chữ ký PDF là một từ điển, chứ không phải là một tệp đính kèm mờ đục, và hai mục nhập quan trọng nhất của nó cho bạn biết bao nhiêu phần của tệp thực sự được bảo vệ. Tiêu chuẩn ISO 32000-1 §12.8 định nghĩa từ điển chữ ký với khóa /ByteRange và một mục nhập /Contents. Khóa /Contents chứa cấu trúc CMS SignedData mã hóa thập lục phân (RFC 5652), phong bì mật mã mang chứng thư của người ký, các thuộc tính được ký và chính giá trị chữ ký. Khóa /ByteRange là phần mà các nhà phát triển thường đánh giá thấp: nó là một mảng gồm hai khoảng độ lệch-độ dài (offset-length spans) cùng nhau bao phủ toàn bộ tệp ngoại trừ chuỗi thập lục phân /Contents. Khoảng trống đó chính xác là nơi các byte chữ ký tọa lạc, và hai khoảng ở hai bên chính xác là những gì chữ ký cam kết bảo vệ
Thiết kế ByteRange là thứ giúp cho một lượt lưu lũy tiến (incremental save) có thể kiểm toán được. Bởi vì người ký không thể băm các byte chữ ký vốn chưa tồn tại, tệp tin được chia tách xung quanh trình giữ chỗ /Contents và mọi thứ khác được băm vào chữ ký. Một chữ ký có ByteRange không chạm tới cuối tệp là một dấu hiệu cảnh báo: nội dung được thêm vào sau phạm vi được bảo phủ, thông qua một bản cập nhật lũy tiến sau đó, sẽ không làm hỏng chữ ký ngay cả khi nó làm thay đổi những gì người đọc nhìn thấy. Vì thế, điều đầu tiên một người kiểm tra nghiêm túc xác minh không phải là ai đã ký, mà là liệu chữ ký đó có bao phủ các byte mà nó có vẻ như đang chứng thực hay không
Đọc từ điển chữ ký bằng API chỉ đọc của PDFium
Thành phần PDFium Component phơi bày từ điển chữ ký thông qua hai thành viên chỉ đọc: SignatureCount và bản ghi Signature[Index]. Ở bên dưới, chúng gọi FPDF_GetSignatureCount, FPDF_GetSignatureObject và các trình truy cập FPDFSignatureObj_* cho các khóa /SubFilter, /ByteRange, /Contents, /Reason và thời gian ký. Chỉ đọc là cụm từ hoạt động ở đây: PDFium có thể liệt kê và đọc chữ ký nhưng không có API để tạo hoặc ghi một chữ ký mới, đó là lý do tại sao phần ký số của loạt bài này được triển khai bởi chính thư viện chứ không phải bởi PDFium
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
Writeln('Signed at : ', Sig.Time); // signer date string, e.g. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = no certification, 1..3 = MDP level
end;
finally
Pdf.Free;
end;
end;
Mỗi bản ghi TPdfSignature ánh xạ trực tiếp vào từ điển chữ ký. Encoding đại diện cho khóa /SubFilter, trường có tính chẩn đoán duy nhất nhất, vì nó đặt tên cho trình xử lý chữ ký và ngay lập tức phân biệt một chữ ký ETSI.CAdES.detached hiện đại với một chữ ký cũ hoặc bị cấm. Time là thời gian ký do tác giả tự khai báo dưới dạng một chuỗi ngày tháng PDF, đây là một tuyên bố của người ký chứ không phải thời gian đáng tin cậy. Content là dữ liệu CMS SignedData thô, và Permission phơi bày cấp độ chứng nhận DocMDP (0 cho một chữ ký phê duyệt thông thường, từ 1 đến 3 cho một chữ ký chứng nhận khóa các thay đổi sau đó). Một trường mà bản ghi không hiển thị là ByteRange đã được phân tích cú pháp, và việc bỏ sót đó là có chủ ý, bởi vì ValidatePades thực hiện phép tính bao phủ ByteRange cho bạn thay vì bắt bạn phải tính toán lại bằng tay
Sự khác biệt giữa PAdES B-B, B-T, B-LT và B-LTA là gì?
Bốn cấp độ cơ sở PAdES tạo thành một chiếc thang từ một chữ ký có giá trị tối thiểu đến một chữ ký được xây dựng để tồn tại hàng thập kỷ lưu trữ, và mỗi cấp độ chứa đựng nghiêm ngặt cấp độ bên dưới nó. Tiêu chuẩn ETSI EN 319 142-1 định nghĩa chúng là B-B, B-T, B-LT và B-LTA. B-B (Cơ bản) là chữ ký cộng với các thuộc tính được ký bắt buộc và không có gì hơn. B-T (Dấu thời gian) thêm một dấu thời gian RFC 3161 đáng tin cậy đè lên chữ ký, do đó thời điểm ký được chứng thực bởi một tổ chức cấp dấu thời gian (time-stamping authority) thay vì được tự khẳng định bởi đồng hồ của người ký. B-LT (Dài hạn) nhúng tài liệu xác thực — chuỗi chứng thư, và tùy chọn các phản hồi OCSP hoặc CRL — vào bên trong tệp tin, giúp chữ ký vẫn có thể được xác thực nhiều năm sau đó khi cơ sở hạ tầng phát hành đã biến mất. B-LTA (Dài hạn với dấu thời gian lưu trữ) bao bọc một dấu thời gian tài liệu xung quanh tài liệu đó, bảo vệ chính dữ liệu dài hạn và cung cấp cho bạn một điểm để đóng dấu thời gian lại trước khi thuật toán mật mã bên dưới bị lỗi thời
Cách hiểu thực tế là về khoảng thời gian. Một chữ ký B-B trả lời cho câu hỏi "ai đó có ký cái này không". B-T trả lời "và ký khi nào, có thể chứng minh được". B-LT trả lời "và tôi có thể tiếp tục kiểm tra nó sau khi các chứng thư hết hạn hay không". B-LTA trả lời "và việc kiểm tra đó có còn giá trị sau hai mươi năm nữa không". Các cấu hình quy định chọn một bậc: nhiều bối cảnh hóa đơn điện tử và eIDAS yêu cầu tối thiểu là B-T, và các yêu cầu lưu trữ hướng tới B-LT hoặc B-LTA. Biết được tài liệu thực sự đạt đến bậc nào trước khi bạn chấp nhận hoặc từ chối nó, chính là toàn bộ mục đích của bước kiểm tra
Phát hiện cấp độ cơ sở bằng TPdf.ValidatePades
Thành phần PDFium Component thu gọn toàn bộ câu hỏi về cấp độ vào một cuộc gọi duy nhất. TPdf.ValidatePades trả về một bản ghi TPadesValidationResult có trường Level là kiểu TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT hoặc plB_LTA — cùng với một tập hợp các vấn đề (issues), một số lượng chữ ký và một số lượng dấu thời gian tài liệu. Cấp độ được suy luận một cách đơn điệu: bộ xác thực thiết lập B-B trước, sau đó thăng cấp lên B-T nếu có dấu thời gian chữ ký hoặc dấu thời gian tài liệu hiện diện, lên B-LT nếu danh mục chứa một khóa /DSS với các chứng thư và dấu hiệu /Extensions /ESIC Cấp độ 1, và lên B-LTA nếu cả dấu thời gian tài liệu và dấu hiệu ESIC Cấp độ 2 cùng hiện diện. Hai hàm bổ trợ làm cho kết quả có thể hành động được: IsCompliant chỉ bằng True khi cấp độ đạt ít nhất B-B và tập hợp vấn đề là trống rỗng, và IsCompliantAt cho phép bạn khẳng định một mức sàn chính sách chẳng hạn như plB_T
var
Pdf: TPdf;
R: TPadesValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
case R.Level of
plNone: Writeln('No PAdES signature present');
plUnknown: Writeln('Signature present but level undeterminable');
plB_B: Writeln('PAdES B-B (basic)');
plB_T: Writeln('PAdES B-T (trusted timestamp)');
plB_LT: Writeln('PAdES B-LT (long-term material embedded)');
plB_LTA: Writeln('PAdES B-LTA (archive timestamp)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Meets the B-T policy floor')
else
Writeln('Below the required B-T level');
finally
Pdf.Free;
end;
end;
Tại sao adbe.pkcs7.sha1 lại là một SubFilter bị cấm?
Bởi vì SHA-1 đã bị phá vỡ và trình xử lý adbe.pkcs7.sha1 tích hợp sẵn nó bên trong. SubFilter đó băm trước tài liệu bằng SHA-1 trước khi bao bọc nó trong PKCS#7, và SHA-1 đã dễ bị tấn công va chạm (collision-vulnerable) từ nhiều năm nay, do đó điều khoản 6.3 của tiêu chuẩn EN 319 142-1 nghiêm cấm hoàn toàn đối với một chữ ký cơ sở. ValidatePades nâng lỗi ppeiForbiddenSubFilter khi phát hiện adbe.pkcs7.sha1 hoặc adbe.x509.rsa_sha1, và nâng lỗi ppeiBadDigestAlgorithm khi bản thân CMS sử dụng MD5 hoặc SHA-1 làm hàm băm thông điệp (điều khoản 6.2.1). Đó là hai phép kiểm tra riêng biệt bắt cùng một loại điểm yếu ở hai lớp khác nhau
Tập hợp vấn đề có tổng cộng 26 thành viên, và những vấn đề bạn thường gặp nhất xoay quanh cấu trúc và phạm vi bảo phủ. ppeiByteRangeNotCoveringFile là phép kiểm tra phạm vi bảo phủ đã được mô tả ở trước. ppeiForbiddenCertKey kích hoạt khi từ điển chữ ký chứa một mục nhập /Cert, điều mà PAdES cấm vì chuỗi chứng thư bắt buộc phải nằm bên trong cấu trúc CMS SignedData.certificates thay thế. ppeiMissingSigningCertificate, ppeiMissingContentType và ppeiMissingMessageDigest đánh dấu các thuộc tính được ký bắt buộc nhưng bị vắng mặt, và ppeiDetachedContentViolation bắt một chữ ký nhúng nội dung được ký một cách sai trái thay vì tách rời nó ra. Việc liệt kê tập hợp giúp chuyển đổi một sự từ chối đơn thuần thành một chẩn đoán bạn có thể ghi nhật ký
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
Những gì ValidatePades không kiểm tra
ValidatePades xác thực cấu trúc chứ không phải sự tin cậy, và nhầm lẫn hai điều này là một lỗi nguy hiểm. Một kết quả plB_LTA có nghĩa là tài liệu chứa một chữ ký B-LTA chuẩn với tất cả các thuộc tính, tài liệu xác thực và dấu thời gian bắt buộc ở đúng vị trí — nó không có nghĩa là chữ ký đó hợp lệ về mặt mật mã, chuỗi chứng thư liên kết đến một gốc (root) bạn tin cậy, hoặc không có chứng thư nào trong chuỗi bị thu hồi. Bộ xác thực chủ động không thực hiện xác minh mật mã: nó không tính toán lại chữ ký trên ByteRange, không xây dựng hay đánh giá chuỗi tin cậy, và không kiểm tra trạng thái thu hồi OCSP hoặc CRL. Sự phân chia đó là có chủ ý và hữu ích, bởi vì kiểm tra cấu trúc diễn ra nhanh chóng, hoàn toàn xác định và không cần các khóa, không cần mạng cũng như mật mã nền tảng, nhờ đó ValidatePades chạy giống hệt nhau trên Windows, Linux và macOS dưới dạng mã Pascal thuần túy trên các byte của tệp tin. Ngược lại, xác thực chuỗi tin cậy không thể tách rời khỏi chính sách — gốc nào bạn tin tưởng, cách bạn truy xuất thông tin thu hồi, mức độ chịu sai số dấu thời gian của bạn là bao nhiêu — và nó thuộc về một giai đoạn sau phụ thuộc vào kho chứng thư của nền tảng, do đó hãy xử lý một kết quả đạt ValidatePades như một cổng bắt buộc xác nhận chữ ký được định hình đúng cách, sau đó bàn giao một chữ ký có cấu trúc chuẩn cho quá trình xác minh mật mã thực sự trước khi bạn phụ thuộc vào nó
Lượt duyệt cấu trúc đó là nơi bắt đầu đúng đắn, và nó kết hợp tự nhiên với các phép kiểm tra chỉ đọc rộng hơn trong kiểm toán các rủi ro bảo mật PDF với PDFium Component và với công việc tuân thủ định dạng chẳng hạn như xác thực các tài liệu PDF/X sẵn sàng in. Khi bạn đã có thể đọc và phân loại một chữ ký, bước tiếp theo là tạo một chữ ký: bài viết thứ hai trong loạt bài này bao trùm ký tài liệu PDF bằng PAdES B-B, và bài thứ ba mở rộng tính năng đó lên dấu thời gian đáng tin cậy cùng các chữ ký dài hạn B-LT và B-LTA. Tính năng kiểm tra chữ ký chỉ đọc và bộ phân loại ValidatePades hiển thị ở đây là một phần của PDFium Component dành cho Delphi, C++Builder và Lazarus