Thành phần PDFium Component thực hiện ký một tệp PDF bằng chữ ký số PAdES B-B thông qua phương thức SignPades của nó: nó tải tài liệu, băm phạm vi byte được ký, xây dựng cấu trúc CAdES CMS, và thêm chữ ký đó dưới dạng một bản cập nhật lũy tiến. Backend mật mã chỉ dành cho Windows, do đó hãy bảo vệ mọi cuộc gọi bằng PadesCryptoAvailable trước khi bạn thực hiện ký
Tình huống này rất quen thuộc. Một tài liệu hợp đồng PDF đặt trên bàn làm việc của bạn, bộ phận pháp lý muốn ký số nó trước khi gửi đi, và bạn tìm đến cùng một bản dựng PDFium bạn đã sử dụng để kết xuất và kiểm tra tài liệu, để rồi phát hiện ra rằng PDFium hoàn toàn không thể ghi chữ ký. API chữ ký của nó nghiêm ngặt chỉ đọc. Thành phần PDFium Component lấp đầy khoảng trống đó bằng cách làm chủ toàn bộ đường ống ký số trong mã Pascal, từ hàm băm cho đến tiêm dữ liệu ở cấp độ byte, và bài viết này sẽ hướng dẫn chi tiết đường ống đó từ đầu đến cuối
Tại sao PDFium không thể ghi một chữ ký số?
PDFium phơi bày các chữ ký dưới dạng các đối tượng chỉ đọc và không cung cấp tính năng nào để tạo ra chúng. Họ hàm FPDFSignatureObj_* cho phép bạn liệt kê một chữ ký hiện có, đọc mục /Contents của nó và kiểm tra /ByteRange của nó, nhưng không có phần đối ứng nào để xây dựng từ điển chữ ký, dự phòng vị trí /Contents hoặc ghi phạm vi byte; tính năng lưu lũy tiến có tồn tại (FPDF_SaveAsCopy với cờ FPDF_INCREMENTAL) tuy nhiên không mang theo hook ký số. Do đó, bất kỳ thành phần nào ký một tệp PDF trên nền PDFium đều bắt buộc phải tự tạo mọi byte chữ ký, đó là lý do tại sao PDFium Component xây dựng cơ chế này từ ba đơn vị mã nguồn Pascal thuần túy. Trình biên dịch FPC 3.2.2 đi kèm md5 và sha1 nhưng hoàn toàn không hỗ trợ SHA-2, và API SHA-256 trong System.Hash của Delphi không tương thích mã nguồn với FPC, do đó FPdfSha256 là một triển khai FIPS 180-4 độc lập giúp giữ mọi đường dẫn mã CMS trên một kiểu dữ liệu TSHA256Digest duy nhất mà không cần rẽ nhánh trình biên dịch. FPdfAsn1 cung cấp bộ mã hóa và bộ đọc DER mà các cấu trúc CMS cần, và FPdfCms lắp ráp dữ liệu CAdES SignedData trên nền tảng của cả hai
Làm thế nào để ký số một tệp PDF trong Delphi?
Tải tài liệu, sau đó gọi SignPades với mã vân tay chứng thư (certificate thumbprint). PDFium Component sẽ phân giải mã vân tay đó đối chiếu với kho chứng thư "MY" của Người dùng Hiện tại, lấy ra chứng thư khớp và khóa riêng tương ứng của nó, và ghi một bản sao đã ký vào đường dẫn bạn chỉ định
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('PAdES signing requires the Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // the document to sign
Pdf.Active := True;
// Second argument: SHA-1 thumbprint of a certificate in the Current
// User "MY" store. First argument: destination for the signed copy.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
PadesCryptoAvailable là phép kiểm tra bạn luôn cần thực hiện trước tiên, trong mọi lần. Trên Windows, nó trả về True và backend crypt32/ncrypt hoạt động; trên bất kỳ nền tảng nào khác, nó trả về False và một cuộc gọi ký sẽ nâng lỗi EPadesCrypto. Việc coi chốt bảo vệ này là bắt buộc giúp ngăn chặn một bản dựng Linux hoặc macOS gặp lỗi tại thời điểm chạy trên một lộ trình không thể hoạt động ở đó. Bản thân mã vân tay là mã băm SHA-1 của chứng thư, cùng giá trị mà trình quản lý chứng thư Windows hiển thị trên tab Details của nó, và nó định danh một người ký cụ thể mà không bao giờ đặt tư liệu khóa (key material) trong mã nguồn của bạn
Những gì đi vào cấu trúc CMS: các thuộc tính được ký và RFC 5652
Một chữ ký cơ sở PAdES không phải là một chữ ký RSA thô trên tệp tin; nó là một cấu trúc CAdES CMS SignedData mang theo một tập hợp các thuộc tính được ký bắt buộc, và FPdfCms.BuildSignedData phát ra chính xác tập hợp đó: content-type, message-digest, và signing-certificate-v2, thuộc tính ESS liên kết chữ ký với chứng thư của người ký bằng mã băm. Có một chi tiết ở đây làm thất bại hầu như mọi triển khai CMS tự viết bằng tay. Mục 5.4 của tiêu chuẩn RFC 5652 yêu cầu mã băm của các thuộc tính được ký phải được tính toán trên cấu trúc mã hóa DER SET OF, thẻ 0x31, trong khi các thuộc tính tương tự di chuyển bên trong SignerInfo dưới thẻ IMPLICIT [0], 0xA0. PDFium Component mã hóa tập hợp thuộc tính một lần, tính toán băm dạng 0x31, sau đó chỉ ghi đè thẻ byte dẫn đầu thành 0xA0 để phát ra, giúp một vùng đệm duy nhất phục vụ cả hai vai trò mà không cần lượt chạy thứ hai trên cây cấu trúc
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // signer in the MY store
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // hex width of /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
Hàm nạp chồng tùy chọn (options overload) thêm siêu dữ liệu từ điển chữ ký được định nghĩa bởi mục 12.8.1 của ISO 32000-1: Reason, Location, ContactInfo, và Name, tất cả đều là tùy chọn và đều được ghi vào từ điển giá trị chữ ký. Có một ràng buộc rất dễ vi phạm. Nếu bạn đặt CommitmentTypeOid để thêm thuộc tính được ký CAdES commitment-type-indication, đừng đặt đồng thời Reason; mục 6.3 của tiêu chuẩn ETSI EN 319 142-1 nghiêm cấm mang theo cả hai, bởi vì hai mục này biểu đạt cùng một ý định bằng các phương tiện khác nhau
Khóa ByteRange và khe cắm /Contents khớp nối với nhau thế nào?
Một chữ ký phải bao phủ toàn bộ tệp ngoại trừ các byte chứa chính chữ ký đó, và PAdES giải quyết tính tuần hoàn đó bằng một trình giữ chỗ có chiều rộng cố định mà SignPadesBytes quản lý một cách chính xác. Nó dự phòng một chuỗi thập lục phân /Contents gồm ContentsSize byte (mặc định là 16384, lớn hơn thoải mái so với một cấu trúc CMS SignedData điển hình), tuần tự hóa bản cập nhật lũy tiến để xác định vị trí độ lệch chính xác của khe cắm, sau đó tính toán /ByteRange dưới dạng hai khoảng đóng khung khe cắm đó, mọi thứ trước dấu phân cách mở của chuỗi thập lục phân và mọi thứ sau dấu phân cách đóng của nó. Phép tính SHA-256 chỉ chạy trên hai khoảng đó. Cấu trúc CMS thành phẩm được mã hóa thập lục phân vào khe cắm được bảo lưu, đệm bằng các số không để đạt chiều rộng cố định, và bản cập nhật tham chiếu chéo được thêm vào. Bởi vì chiều rộng đã được cố định từ trước, việc điền vào khe cắm không làm dịch chuyển bất kỳ byte nào phía hạ nguồn, đó là toàn bộ lý do tại sao phạm vi byte vẫn giữ giá trị; các byte tài liệu gốc được bảo toàn nguyên văn, giúp một chữ ký trước đó trên cùng một tệp tồn tại nguyên vẹn chính xác như yêu cầu ký lũy tiến của mục 12.8.1 của ISO 32000-1
Backend Windows CNG và các giới hạn của nó
Thành phần PDFium Component chỉ thực hiện ký số trên Windows, và ranh giới đó là có chủ ý. FPdfCryptoWin liên kết động tệp crypt32.dll và ncrypt.dll, không thêm bất kỳ phụ thuộc DLL nào tại thời điểm biên dịch, và chuỗi ký là CNG tiêu chuẩn: mở kho MY, tìm chứng thư bằng mã băm, lấy handle khóa riêng của nó thông qua CryptAcquireCertificatePrivateKey, và gọi NCryptSignHash. RSA với PKCS#1 v1.5, RSA-PSS, và ECDSA đều được hỗ trợ. ECDSA cần một sự sửa đổi (fix-up) mà các thuật toán khác không cần, bởi vì NCryptSignHash trả về cặp r-và-s IEEE P1363 thô trong khi CMS mong đợi một cấu trúc SEQUENCE DER ECDSA-Sig-Value, do đó backend mã hóa lại nó tuân theo tiêu chuẩn RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // no signing backend on this platform
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := ReadThumbprintFromConfig;
Pdf := TPdf.Create(nil);
Output := TFileStream.Create('contract-signed.pdf', fmCreate);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.SignPadesToStream(Output, Opts);
finally
Output.Free;
Pdf.Free;
end;
end;
Hậu quả thực tế là khóa riêng phải nằm trong kho chứng thư Windows. Một chứng thư được giữ trong tệp PFX chỉ hoạt động sau khi bạn nhập nó vào kho của Người dùng Hiện tại, tại thời điểm đó mã vân tay của nó chính là giá trị bạn truyền vào SignPades. Bản phát hành này không có đường dẫn PKCS#11 hay HSM và không có backend tệp khóa phần mềm (software-key file backend), do đó khi PadesCryptoAvailable trả về False, đơn giản là không thể thực hiện ký số trên máy tính đó
Nơi PAdES B-B dừng lại
PAdES B-B là mức cơ sở, mức sàn của bốn cấp độ PAdES: nó chứng minh ai đã ký và các byte dữ liệu đã không thay đổi kể từ đó, và không có gì vượt ngoài điều đó. Một chữ ký B-B không mang theo dấu thời gian đáng tin cậy, do đó nó không thể chứng minh thời điểm việc ký diễn ra, và nó không nhúng dữ liệu thu hồi chứng thư, vì vậy một người xác thực nhiều năm sau đó phải tự mình truy xuất chuỗi chứng thư và trạng thái của nó. Các khoảng trống đó chính xác là những gì các cấp độ cao hơn đóng lại. Khi bạn cần một thời gian ký mà kiểm toán viên chấp nhận, việc thêm dấu thời gian RFC 3161 và cấu trúc DSS cho việc xác thực dài hạn sẽ đưa chữ ký lên B-T và cao hơn; khi bạn muốn đọc ngược một chữ ký hoàn thiện và xác nhận cấp độ nó đạt được, bài viết về kiểm tra chữ ký PDF và cấp độ PAdES của nó là công cụ đồng hành; và trước khi thực hiện ký bất kỳ thứ gì, bài viết về kiểm toán các rủi ro bảo mật PDF sẽ cho biết những gì bạn chuẩn bị đặt bút ký tên
Các phương thức SignPades hiển thị ở đây đi kèm với PDFium Component dành cho Delphi và C++Builder, bên cạnh tính năng kiểm tra chữ ký chỉ đọc mà PDFium cung cấp sẵn