Bài viết kỹ thuật

Xác thực chữ ký số PDF trong Delphi với HotPDF

HotPDF xác thực chữ ký số trong các tài liệu PDF đã tải thông qua ba phương thức của THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature, and VerifyLoadedSignatureEx, được giới thiệu từ phiên bản v2.259.0. Thành phần này sẽ thực hiện băm lại các phân đoạn /ByteRange của tệp gốc, kiểm tra thuộc tính messageDigest của CMS và thực hiện xác thực RSA PKCS#1 v1.5 đối với chứng thư số của người ký được nhúng kèm, trả về giá trị svValid khi các byte của tài liệu còn nguyên vẹn

Tình huống này rất phổ biến nhưng tầm quan trọng của nó thì không hề nhỏ. Đối tác gửi lại một hợp đồng đã ký, quy trình làm việc của bạn cần lưu trữ nó, và ai đó đặt ra câu hỏi quan trọng duy nhất: đây có phải là tài liệu chúng ta đã gửi, chính xác đến từng byte, được ký bằng đúng chứng thư số khai báo hay không? Trả về câu trả lời đó bằng mã nguồn là phần việc xác thực của câu chuyện chữ ký số; phần việc ký số, tạo lập và nhúng chữ ký PAdES ngay từ đầu, được trình bày trong bài viết đi kèm về tạo chữ ký số PAdES với HotPDF. Bài viết này tập trung vào chiều ngược lại: một tệp PDF được gửi đến đã có sẵn chữ ký, và bạn muốn có một kết quả xác thực tự động bằng mã nguồn thay vì chỉ nhìn vào dấu tích xanh của Acrobat

Một tệp PDF đã ký chứng minh nó không bị thay đổi như thế nào?

Chữ ký PDF bảo vệ các phạm vi byte cụ thể của tệp tin, chứ không phải một khái niệm trừu tượng về "tài liệu". Tiêu chuẩn ISO 32000-1 §12.8 định nghĩa cơ chế này: trường biểu mẫu chữ ký mang một từ điển có mục nhập /Contents chứa một thùng chứa dữ liệu CMS SignedData (RFC 5652), và mảng /ByteRange của nó chỉ rõ chính xác các phân đoạn của tệp tin mà chữ ký bao phủ theo mục §12.8.1. Mảng này là danh sách các cặp khoảng lệch (offset) và độ dài, trong thực tế gồm hai phân đoạn: tất cả những gì đứng trước chuỗi hex /Contents, và tất cả những gì đứng sau nó. Chữ ký giá trị không thể bao phủ chính nó, vì vậy tệp tin được băm xung quanh khoảng trống đó

Thiết kế đó dẫn đến một hệ quả quyết định cách xây dựng toàn bộ API: việc xác thực phải thực hiện băm trên các byte đã tuần tự hóa ban đầu, chính xác như cách chúng nằm trên đĩa cứng. Một mô hình đối tượng được phân tích cú pháp là vô dụng cho việc này, bởi vì việc tuần tự hóa lại ngay cả một tài liệu không đổi cũng tạo ra các byte khác biệt. Do đó, HotPDF thực hiện xác thực đối chiếu với tệp nguồn mà tài liệu được tải lên từ đó, hoặc đối chiếu với một luồng TStream chứa các byte thô do bạn cung cấp, chứ không bao giờ đối chiếu với đại diện trong bộ nhớ của tài liệu

Đọc metadata chữ ký trước khi thực hiện xác thực

Hàm GetLoadedSignatureInfo phân tích cú pháp từ điển chữ ký và thùng chứa CMS của nó mà không chạm vào một byte tài liệu nào, điều này giúp nó trở thành lệnh gọi đầu tiên phù hợp khi bạn chỉ cần hiển thị ai là người đã ký và ký khi nào. Các trường chữ ký được chỉ số hóa từ 0 theo thứ tự trường biểu mẫu, và GetLoadedSignatureFieldCount cho biết số lượng trường hiện có. Bản ghi THPDFSignatureInfo trả về mang tên trường, mục /SubFilter, tên chung (common name) trên chứng thư số của người ký, DN (distinguished name) của chủ thể và nhà phát hành, số sê-ri, ngày hiệu lực, thời gian ký (từ thuộc tính được ký nếu có, nếu không thì lấy từ mục /M của từ điển), tên thuật toán băm và các chuỗi /Reason, /Location, và /ContactInfo. Thành viên Status của nó giữ giá trị svNotVerified, một nhãn thực tế cho trạng thái "đã phân tích cú pháp, chưa xác thực"

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Thực thi kiểm tra mã hóa

Hàm VerifyLoadedSignatureEx thực hiện toàn bộ việc xác thực đối với tài liệu được tải từ tệp và trả về bản ghi thông tin đầy đủ trong một lệnh gọi duy nhất: nó mở lại tệp nguồn, băm các phân đoạn /ByteRange bằng thuật toán băm của SignerInfo, so sánh kết quả với thuộc tính được ký messageDigest (RFC 5652 §5.4), và sau đó xác thực RSA cho chữ ký trên mã hóa ngược DER SET của các thuộc tính được ký. Khi một chữ ký không mang thuộc tính được ký, việc kiểm tra RSA sẽ chạy trực tiếp trên mã băm của tài liệu. Các chữ ký được hỗ trợ là RSA PKCS#1 v1.5 với các mã băm SHA-1, SHA-256, SHA-384, hoặc SHA-512, đáp ứng các bộ lọc phụ (subfilters) adbe.pkcs7.detachedETSI.CAdES.detached được tạo ra bởi các công cụ ký số phổ biến

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Có hai chi tiết triển khai cần lưu ý vì chúng giải thích cho các lỗi xác thực trông có vẻ bí ẩn đối với bên ngoài. Thứ nhất, việc kiểm tra thuộc tính được ký rất khắt khe về mặt mã hóa: bên trong tệp tin các thuộc tính được gắn thẻ là [0] IMPLICIT, nhưng chữ ký lại được tính toán trên cấu trúc DER SET OF của chúng, do đó bộ xác thực phải thực hiện gắn thẻ lại trước khi băm, chính xác như yêu cầu của RFC 5652 §5.4. Một bộ xác thực tự viết thực hiện băm trực tiếp các byte hiển thị trong tệp sẽ từ chối mọi tài liệu được ký đúng chuẩn. Thứ hai, mục /Contents theo quy ước được chèn thêm các ký tự không (zero-padded) để lấp đầy khoảng trống byte dự phòng, vì vậy bộ xác thực sẽ cắt ngắn khối DER về đúng độ dài thực tế của cấu trúc SEQUENCE bên ngoài của nó trước khi phân tích cú pháp; các số 0 thừa trông giống như lỗi thực tế là hoàn toàn bình thường, không phải là tệp bị hỏng. Nhóm các nguy cơ phân tích cú pháp ASN.1 tương tự ở phía nhập chứng thư số được trình bày chi tiết trong bài viết về củng cố bảo mật PKCS#12 và ASN.1 trong HotPDF

Một chữ ký số hợp lệ thực sự đảm bảo điều gì?

Kết quả svValid có nghĩa chính xác là: các byte được chỉ định bởi /ByteRange tạo ra giá trị băm trùng khớp với giá trị người ký đã ký, và chữ ký được xác thực thành công bằng khóa công khai của chứng thư số được nhúng trong thùng chứa CMS. Đó chỉ là tính toàn vẹn của byte cộng với liên kết khóa, và không có gì hơn. Việc xác thực chuỗi chứng thư số và độ tin cậy nằm ngoài phạm vi xử lý của bộ xác thực HotPDF: nó không duyệt chuỗi để tìm chứng thư số gốc (root certificate), không kiểm tra trạng thái thu hồi chứng thư số, hoặc truy vấn bất kỳ kho lưu trữ tin cậy nào. Một chứng thư số tự ký từ một kẻ tấn công thực hiện ký lại một tài liệu đã sửa đổi vẫn sẽ trả về kết quả svValid, vì các phép toán toán học bên trong là nhất quán. Việc người ký có thực sự là người họ khai báo hay không, và liệu có nên tin tưởng họ hay không, là quyết định chính sách thuộc về một tầng xử lý khác, chẳng hạn như danh sách trắng chứng thư số của tổ chức bạn, kho chứng thư của Windows, hoặc một cơ quan xác thực

Cập nhật lũy tiến của PDF cho phép chèn thêm nội dung sau khi ký mà không làm mất hiệu lực của chữ ký, điều này được thiết kế có chủ ý và là cách vận hành của quy trình đa chữ ký. Cờ CoversWholeDocument được tính toán trong quá trình xác thực và chỉ nhận giá trị true khi hai phân đoạn cộng với khoảng trống /Contents bao phủ toàn bộ tệp tin. Khi kết quả svValid trả về kèm theo CoversWholeDocument bằng false, phiên bản được ký vẫn nguyên vẹn nhưng tệp tin đã có các phần bổ sung sau đó, và việc những phần bổ sung đó đã thay đổi những gì là điều mà quy trình xử lý của bạn cần quyết định xem có chấp nhận hay không

Các tài liệu được tải bằng luồng và đã mã hóa cần các byte nguồn riêng của chúng

Các hàm không đối số VerifyLoadedSignatureVerifyLoadedSignatureEx phụ thuộc vào việc thành phần ghi nhớ tệp tài liệu được tải lên từ đâu. Nếu bạn tải tài liệu từ một luồng, sẽ không có tên tệp để mở lại; điều tương tự cũng xảy ra sau khi tải lại bằng mật khẩu đối với các tài liệu đã mã hóa, quy trình được mô tả trong bài viết về mã hóa PDF AES-256 trong Delphi. Trong cả hai trường hợp, các phương thức nạp chồng dựa trên tệp sẽ trả về svSourceUnavailable thay vì đoán. Giải pháp khắc phục là sử dụng phương thức nạp chồng nhận TStream, cho phép bạn truyền vào các byte thô ban đầu từ bất kỳ nơi nào bạn lưu trữ chúng, ví dụ một tệp tin bạn vẫn giữ, một bộ đệm bộ nhớ, hoặc một blob cơ sở dữ liệu

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Stream-loaded document: the component holds no source
  // file name, so supply the original bytes yourself.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Báo cáo các trường hợp không thể xác thực

Một bộ xác thực chỉ biết hiển thị "hợp lệ" và "không hợp lệ" sẽ báo cáo sai các tài liệu mà nó đơn giản là không hiểu được, vì vậy kiểu liệt kê trạng thái đã tách biệt các trường hợp mà giao diện của bạn cần phân biệt rõ. Trạng thái svDigestMismatch có nghĩa là các byte tài liệu đã thay đổi sau khi ký, dấu hiệu rõ ràng của việc chỉnh sửa trái phép. Trạng thái svSignatureInvalid có nghĩa là các byte băm chính xác nhưng phép kiểm tra RSA thất bại, chỉ ra giá trị chữ ký bị hỏng hoặc bị giả mạo. Trạng thái svUnsupportedAlgorithm là câu trả lời thực tế cho các khóa ECDSA và các mã băm chưa được hỗ trợ: chữ ký có thể hoàn toàn bình thường nhưng HotPDF đơn giản là không thể kiểm tra được, và việc báo cáo nó là "không hợp lệ" sẽ đánh giá sai một tài liệu hoàn toàn tốt. Trạng thái svMalformed đánh dấu một thùng chứa CMS hoàn toàn không thể phân tích cú pháp được. Đối với các kiểm tra dạng cổng kiểm soát, hàm VerifyAllLoadedSignatures chỉ trả về true khi có ít nhất một trường chữ ký tồn tại và tất cả chúng đều xác thực là svValid, một giá trị boolean duy nhất tiện lợi cho luồng tiếp nhận lưu trữ vốn từ chối mọi tài liệu không đạt chuẩn

Tính năng xác thực chữ ký số, ký số PAdES, mã hóa AES-256 và API chỉnh sửa tài liệu đã tải đều đi kèm trong cùng một thư viện VCL gốc dành cho Delphi và C++Builder, không phụ thuộc vào DLL bên ngoài; danh sách tính năng đầy đủ và các phiên bản IDE hỗ trợ có sẵn trên trang sản phẩm HotPDF Component