PDFiumPas tách việc ký PAdES thành hai lệnh gọi để khóa riêng tư không bao giờ phải nằm trong tiến trình của bạn. PreparePadesRemoteSignature ghi một bản cập nhật tăng dần với một chỗ giữ /Contents rỗng có độ rộng cố định, và trả về một bản ghi yêu cầu mang theo digest tài liệu SHA-256, ByteRange chính xác và một fingerprint của tệp đã chuẩn bị. CompletePadesRemoteSignature nhận CMS tách rời mà dịch vụ ký của bạn trả về và đặt nó vào đúng chỗ giữ đã dành sẵn đó
Giữa hai lệnh gọi đó, có thể trôi qua vài phút hoặc vài giờ, tiến trình có thể khởi động lại, và công việc có thể chuyển sang một máy khác. Khoảng trống đó chính là toàn bộ lý do API được thiết kế theo cách này
Vì sao một khóa từ xa không thể dùng lệnh ký thông thường?
Vì SignPadesBytes giả định rằng thao tác ký diễn ra ngay bên trong lệnh gọi đó. Nó xây dựng bản cập nhật tăng dần, tính digest trên ByteRange, ký nó, rồi ghi kết quả ra, tất cả trước khi trả về. Điều đó hoàn toàn đúng khi khóa nằm trong kho chứng thư của Windows hoặc trong một tệp PKCS#12 bạn đã tải
Điều đó là bất khả thi khi khóa nằm trong một HSM mạng, một thiết bị tạo chữ ký đủ điều kiện do một nhà cung cấp dịch vụ tin cậy vận hành, hoặc một API ký cloud yêu cầu người dùng xác nhận trên điện thoại. Trong các trường hợp đó, chuỗi thao tác không phải một lệnh gọi hàm, mà là một cuộc trao đổi: bạn gửi một digest, thứ gì đó xác thực một con người, và một CMS trả về sau đó. Một API đồng bộ không thể diễn đạt "sau đó" mà không chặn một luồng trên một thao tác có thể cần một yếu tố xác thực thứ hai
Giao thức hai giai đoạn
Giai đoạn một chuẩn bị tài liệu. PDFiumPas thêm trường chữ ký và từ điển giá trị, dành sẵn ContentsSize byte không gian mã hex trong /Contents, tính ByteRange xung quanh chỗ dành sẵn đó, và tạo ra một TPadesRemoteSigningRequest chứa FormatVersion, PreparedFingerprint, DocumentDigest, ByteRange bốn phần tử, ContentsHexOffset và ContentsSize
Giá trị duy nhất dịch vụ ký của bạn cần là DocumentDigest: giá trị SHA-256 mà CAdES SignedData trả về phải mang theo dưới dạng message digest của nó. Mọi thứ khác trong bản ghi đó tồn tại để giai đoạn hai có thể chứng minh rằng tệp nó đang hoàn thiện chính là tệp mà digest đó đã được tính toán từ
uses
FPdfPades;
var
Options: TPadesRemoteSignOptions;
Request: TPadesRemoteSigningRequest;
Source, Prepared, Session: TFileStream;
begin
Options := TPadesRemoteSignOptions.Default;
Options.Reason := 'Approved by finance';
Options.Location := 'Lisbon';
Options.Name := 'A. Moreira';
Options.SigningTimeUtc := NowUtc;
Options.ContentsSize := 16384; // byte hex dành sẵn cho CMS
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
try
PreparePadesRemoteSignature(Source, Prepared, Options, Request);
finally
Prepared.Free;
Source.Free;
end;
// Lưu phiên lại để một lượt chạy sau - hoặc một máy khác - có thể hoàn tất nó
Session := TFileStream.Create('contract.signreq', fmCreate);
try
SavePadesRemoteSigningRequest(Session, Request);
finally
Session.Free;
end;
SendDigestToSigningService(Request.DocumentDigest);
end;
Complete từ chối những gì, và vì sao mỗi kiểm tra tồn tại?
Hoàn thiện là nơi một thiết kế ký từ xa thường đi sai, nên việc kiểm tra hợp lệ được thiết kế nghiêm khắc có chủ ý. CompletePadesRemoteSignature từ chối một PDF đã chuẩn bị mà fingerprint không còn khớp với yêu cầu, một ByteRange không khớp với tọa độ chỗ giữ đã ghi lại, các dấu phân cách /Contents bị sửa đổi, một chỗ giữ không còn rỗng, một CMS lớn hơn chỗ dành sẵn, một CMS không phải chính xác một giá trị DER, một dạng SignedData không được hỗ trợ, một thuộc tính signing-certificate-v2 bị thiếu, và một CMS có message digest không bằng digest tài liệu đã chuẩn bị
Mỗi điều đó ứng với một lỗi thực tế. Các kiểm tra fingerprint và ByteRange bắt được trường hợp ai đó đã tạo lại tệp đã chuẩn bị giữa hai giai đoạn, điều này sẽ tạo ra một chữ ký xác minh đúng nhưng lại dựa trên các byte mà không ai còn giữ. Kiểm tra chỗ giữ rỗng bắt được trường hợp hoàn thiện hai lần, khi một CMS thứ hai được ghi đè lên một chữ ký đã tồn tại. Kiểm tra message digest bắt được trường hợp nguy hiểm nhất: một CMS được tạo đúng định dạng nhưng ký trên một tài liệu khác, đây là điều bạn nhận được khi một hàng đợi lẫn lộn hai phiên ký đang diễn ra đồng thời. Thiếu kiểm tra đó, bạn sẽ tạo ra một tệp trông như đã ký nhưng thất bại xác minh ở mọi nơi, hoặc tệ hơn, mang theo sự chấp thuận của người khác
Yêu cầu signing-certificate-v2 là một vấn đề tuân thủ PAdES hơn là một vấn đề toàn vẹn. ETSI EN 319 142 yêu cầu chứng thư ký phải được gắn vào các thuộc tính đã ký, và một CMS thiếu thuộc tính đó không phải là một chữ ký PAdES ngay cả khi nó xác minh đúng về mặt mật mã. Từ chối nó ngay tại lúc hoàn thiện nghĩa là bạn phát hiện ra điều này ở đây, không phải trong một báo cáo từ trình xác thực của khách hàng, một chủ đề được khai thác sâu hơn trong vì sao các trình xác thực từ chối chữ ký PAdES
var
Request: TPadesRemoteSigningRequest;
Session, Prepared, Dest: TFileStream;
CmsDer: TBytes;
begin
Session := TFileStream.Create('contract.signreq', fmOpenRead);
try
Request := LoadPadesRemoteSigningRequest(Session);
finally
Session.Free;
end;
CmsDer := FetchDetachedCmsFromService; // trả về bởi HSM hoặc TSP
Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
try
try
CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
except
on E: EPadesCrypto do
// Mỗi lượt từ chối mang theo một lý do cụ thể; ghi log nguyên văn
FailSession(E.Message);
end;
finally
Dest.Free;
Prepared.Free;
end;
end;
Vượt qua ranh giới tiến trình và máy
SavePadesRemoteSigningRequest và LoadPadesRemoteSigningRequest tuần tự hóa phiên làm việc qua một định dạng nhị phân ổn định có đánh phiên bản, đây là điều khiến thiết kế này thực tế thay vì chỉ đúng đắn về mặt lý thuyết. Một ứng dụng web có thể chuẩn bị một tài liệu trong một yêu cầu, lưu PDF đã chuẩn bị cùng blob phiên, trả về một digest cho trình duyệt để ký bằng smart card, và hoàn tất tệp trong một trình xử lý yêu cầu hoàn toàn khác
Trường FormatVersion chính là điều giữ an toàn cho việc đó qua các lần nâng cấp. Một phiên được ghi bởi một bản build cũ và được tải bởi một bản build mới hơn sẽ được nhận diện hoặc từ chối rõ ràng, thay vì bị đọc sai thành một bản ghi có hình dạng khác. Nếu hàng đợi của bạn có thể giữ các phiên trong nhiều ngày, hãy coi phiên bản định dạng là một dữ kiện vận hành đáng để ghi log, không phải một chi tiết triển khai
Định cỡ chỗ giữ
ContentsSize là tham số duy nhất bạn phải suy nghĩ kỹ, vì nó được cố định trước khi CMS tồn tại. Nó đếm chỗ dành sẵn ở dạng mã hex, nên một CMS DER 6 KB cần ít nhất 12 KB không gian, và cách triển khai giới hạn chỗ dành sẵn ở mức 64 MiB
Dành quá ít và việc hoàn thiện thất bại với lỗi CMS quá khổ sau khi dịch vụ ký của bạn đã hoàn thành công việc, điều này trên một dịch vụ chữ ký đủ điều kiện tính phí theo lượt nghĩa là một thao tác bị lãng phí. Dành quá nhiều và mọi tài liệu đã ký sẽ mang theo phần đệm đó mãi mãi. Cách tiếp cận hợp lý là đo lường: ký một tài liệu với chuỗi chứng thư thực của bạn, xem độ dài DER, nhân đôi cho dạng hex, rồi cộng thêm khoảng dư rộng rãi cho token timestamp nếu bạn định nâng cấp lên chữ ký cấp T. Các chuỗi chứng thư có nhiều trung gian và một phản hồi OCSP dài sẽ phát triển nhanh hơn người ta thường nghĩ
Điều gì đến sau chữ ký
Một chữ ký từ xa đã hoàn thiện là PAdES B-B. Xác thực dài hạn cần một timestamp và tài liệu xác thực, đây là một bản cập nhật tăng dần riêng biệt bổ sung một DSS cùng các từ điển VRI theo từng chữ ký, được mô tả trong chữ ký dài hạn với timestamp RFC 3161 và DSS. Bước đó là cục bộ: nó bổ sung chứng thư, phản hồi OCSP và CRL, không cái nào trong số đó cần khóa riêng tư
Trước khi triển khai, hãy xác minh những gì bạn đã tạo ra bằng đúng đường mã mà một bên phụ thuộc sẽ dùng, được trình bày trong kiểm tra chữ ký số PDF và các cấp PAdES. Ký và xác minh là hai đoạn mã khác nhau, và một pipeline ký từ xa chính là nơi hai đoạn đó có thể lệch nhau mà không ai nhận ra cho đến khi một trình xác thực bên ngoài lên tiếng
PDFiumPas là một thành phần Delphi và Lazarus xây quanh engine PDFium với ngăn xếp PAdES nguyên bản bằng Pascal, nên việc ký, đóng dấu thời gian và xác thực hoạt động mà không cần công cụ dòng lệnh bên ngoài. Tài liệu API đầy đủ và bản dùng thử có tại trang thành phần PDFium cho Delphi