Bài viết Kỹ thuật

Tại sao các Bộ Xác thực Từ chối Chữ ký PAdES: PDFium trong Delphi

Một bộ xác thực từ chối một chữ ký PAdES vì một trong ba lý do trong hầu hết mọi trường hợp chúng tôi đã debug với PDFium Component trong Delphi: mảng /ByteRange vẫn giữ các giữ chỗ bằng không, thời gian ký /M không phải là một chuỗi ngày PDF đúng định dạng, hoặc một bản cập nhật gia tăng (incremental update) đã làm mất mục /Encrypt khỏi một tài liệu được mã hóa. Cả ba đều tạo ra các file mở bình thường, dựng hình bình thường, nhưng thất bại ngay khi một bộ xác thực tuân thủ đọc từ điển chữ ký

Kịch bản thúc đẩy bài viết này rất cụ thể. Bạn ký một hợp đồng với quy trình từ bài viết về ký PAdES B-B, code kiểm tra của riêng bạn báo cáo chữ ký hiện diện và cấu trúc ổn định, mọi bài kiểm thử cục bộ đều xanh — và sau đó đối tác tải file lên nền tảng xác thực của họ và nhận một dấu thập đỏ. Không có gì về thất bại hiển thị trong trình xem, vì không có lỗi nào trong số ba lỗi này chạm đến nội dung trang. Chúng tồn tại hoàn toàn trong từ điển chữ ký và trailer của file, đó chính xác là nơi các bộ xác thực tìm kiếm và các trình xem hầu như không làm vậy

Tại sao ByteRange chữ ký PDF của tôi không hợp lệ?

Một sự từ chối ByteRange hầu như luôn có nghĩa là bốn trường chưa bao giờ được điền đầy đủ, chứ không phải các phạm vi bị sai lệch một cách tinh vi. Mảng /ByteRange [A B C D] khai báo hai khoảng, từ byte A đến A+B và từ byte C đến C+D, và EN 319 142-1 §6.3 (yêu cầu k) đòi hỏi rằng chúng phải cùng nhau bao phủ toàn bộ file ngoại trừ chuỗi /Contents được mã hóa hex. Bằng số: A là 0, C >= A+B, C+D bằng độ dài file, và khoảng trống giữa BC giữ chính xác chuỗi hex <...> — hai byte dấu ngoặc cộng với hai ký tự hex cho mỗi byte dữ liệu CMS. Một bộ xác thực đọc thấy [0 0 0 0] kết luận chữ ký không bao phủ gì cả và từ chối nó, bất kể cấu trúc CMS bên trong /Contents có chính xác đến đâu

Cơ chế xảy ra điều này rất đáng hiểu vì cùng một mô hình tồn tại trong mọi stack ký. Một người ký không thể biết các offset cuối cùng cho đến khi file được bố cục, do đó trình viết phát ra mảng với các giữ chỗ bằng không có độ rộng cố định và điền ngược lại chúng sau khi bố cục. Trong các phiên bản của PDFium Component trước 2.14.1, việc điền ngược đó tìm kiếm mô hình giữ chỗ bắt đầu từ vị trí /Contents — nhưng /ByteRange lại nằm trước /Contents trong từ điển, do đó việc tìm kiếm không tìm thấy gì và cả ba sự thay thế đều thất bại trong im lặng. Bản tóm tắt CMS được tính toán trên các phạm vi chính xác, vì vậy mật mã học là chuẩn xác; việc khai báo các phạm vi đó vẫn ở mức bằng không, vì vậy mọi bộ xác thực tuân thủ đều từ chối file. PAdES B-LTA Document Time-stamps, sử dụng cùng một bố cục từ điển, cũng thất bại theo cách tương tự — điều này liên quan nếu bạn xây dựng trên các chữ ký B-LT và B-LTA dài hạn. Phiên bản 2.14.1 điền ngược từ điểm bắt đầu đối tượng chữ ký, và bản sửa lỗi được bao quát bởi một bài kiểm thử hồi quy phân tích cú pháp file được tạo ra và khẳng định các phép toán bên dưới

function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
  Raw: TBytes;
  Text: AnsiString;
  P, N: Integer;
  F: array[0..3] of Int64;
begin
  Raw := TFile.ReadAllBytes(FileName);
  SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
  P := Pos('/ByteRange', Text);          // first signature only
  Result := P > 0;
  if not Result then Exit;
  Inc(P, Length('/ByteRange'));
  for N := 0 to 3 do
  begin
    while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
    F[N] := 0;
    while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
    begin
      F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
      Inc(P);
    end;
  end;
  // EN 319 142-1 §6.3 req k: spans cover everything except /Contents
  Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
            (F[2] + F[3] = Int64(Length(Raw)));
end;

Hai mươi dòng code RTL thuần túy, không gọi thư viện, và nó bắt được toàn bộ nhóm lỗi tại thời điểm sản xuất. Nếu bạn giữ lại một xác nhận (assertion) từ bài viết này, hãy giữ lại xác nhận này: phân tích cú pháp đầu ra đã ký của riêng bạn và kiểm tra ba đẳng thức trước khi file rời khỏi process của bạn

Tại sao các bộ xác thực gắn cờ thời gian ký /M là sai định dạng?

Các bộ xác thực nghiêm ngặt từ chối một thời gian ký không phải là một chuỗi ngày PDF đầy đủ, và hai phần thường bị thiếu nhất là tiền tố D: và ký tự offset UTC. ISO 32000-1 §7.9.4 định nghĩa định dạng ngày là D:YYYYMMDDHHmmSS theo sau bởi một offset — Z cho UTC, hoặc một mối quan hệ +HH'mm' có dấu với nó. Một chuỗi trần 20260709143000 được phân tích cú pháp thành một ngày đối với một trình đọc khoan dung, nhưng một bộ xác thực áp dụng ngữ pháp theo nghĩa đen sẽ nhìn thấy một chuỗi sai định dạng trong một trường có định dạng bắt buộc và gắn cờ chữ ký. Các phiên bản của PDFium Component trước 2.14.4 đã ghi mục /M của TPdf.SignPadesSignPadesBytes ở chính dạng trần đó; kể từ phiên bản 2.14.4, mục này mang tiền tố D: và ký tự Z, do đó thời gian ký được khai báo sẽ đọc là D:20260709143000Z

Có hai lưu ý thực tế gắn liền với trường này. Thứ nhất, hãy viết UTC và chỉ rõ điều đó: một timestamp không có ký tự offset bắt buộc bộ xác thực phải đoán mối quan hệ múi giờ, và §7.9.4 coi offset là một phần của định dạng chứ không phải là một sự lựa chọn tùy ý. Thứ hai, hãy nhớ /M là gì — thời gian do người ký khai báo, một tuyên bố chứ không phải một minh chứng. Một bộ xác thực kiểm tra định dạng của nó ở đây, không phải tính xác thực của nó; thời gian có thể chứng minh được đến từ một timestamp RFC 3161 tại PAdES B-T trở lên. Định dạng một trường chính xác và tin tưởng nó là các quyết định riêng biệt, và các bộ xác thực chỉ thực thi điều đầu tiên

Tại sao SignPades đưa ra ngoại lệ EPadesCrypto trên một file PDF được mã hóa?

PDFium Component từ chối ký một tài liệu được mã hóa vì giải pháp thay thế là tạo ra một file mà các trình đọc tuân thủ sẽ phá hủy khi mở. Một chữ ký PAdES được thêm vào dưới dạng một bản cập nhật gia tăng, và ISO 32000-1 §7.5.6 yêu cầu trailer mới của một phần cập nhật phải mang mọi mục của trailer trước đó ngoại trừ /Prev — trong một tài liệu được mã hóa, điều đó bao gồm /Encrypt. Bỏ nó đi và trailer mới nhất sẽ khai báo file không được mã hóa, do đó một trình đọc tuân thủ sẽ phân tích cú pháp phần body được mã hóa dưới dạng plaintext và nhận được dữ liệu rác. Tệ hơn nữa, các stream và chuỗi mà người ký thêm vào sẽ tự chúng phải được mã hóa bằng key tài liệu để được hợp lệ, điều mà một bộ tiêm chữ ký plaintext không thể thực hiện được. Không có cách nào để thêm một chữ ký plaintext hợp lệ vào một file được mã hóa, vì vậy kể từ phiên bản 2.14.2 TPdf.SignPades, SignPadesBytesInjectPadesDssMarkers đưa ra ngoại lệ EPadesCrypto thay vì phát ra một kết quả bị hỏng hoặc không thể xác minh

try
  if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
    Writeln('Signing reported failure');
except
  on E: EPadesCrypto do
  begin
    // e.g. 'SignPadesBytes: the source document is encrypted;
    //       remove encryption before signing'
    Writeln('Cannot sign: ', E.Message);
  end;
end;

Ngoại lệ là kết quả chính xác, vì vậy hãy thiết kế quy trình công việc xung quanh nó thay vì bắt lỗi và thử lại. Hãy giải mã trước với quyền chủ sở hữu, ký bản sao plaintext, và nếu kênh phân phối yêu cầu mã hóa, hãy chấp nhận rằng mã hóa-rồi-ký và ký-rồi-mã hóa tạo ra các artifact khác nhau với các câu chuyện xác thực khác nhau. Một chữ ký được tính toán trên các byte plaintext không thể tồn tại sau khi mã hóa lại các byte đó, vì vậy các lựa chọn thực tế là một file plaintext được ký hoặc một quyết định chính sách được tài liệu hóa bên cạnh code

Làm thế nào hai lỗi có thể chứng thực lẫn nhau

Lỗi ByteRange đã ẩn giấu lâu như vậy vì chính bộ xác thực của chúng tôi đã bị sai theo một cách bổ sung, và đó là bài học dễ chuyển giao nhất trong bài viết này. Kiểm tra độ bao phủ trong ValidatePadesCompliance yêu cầu khoảng thứ hai bắt đầu chính xác tại A+B — một khoảng trống bằng không — điều này phân loại sai bố cục tiêu chuẩn nơi khoảng trống giữ chuỗi hex /Contents. Vì vậy, bộ xác thực nội bộ đã từ chối chính xác bố cục tuân thủ và trình tạo nội bộ không bao giờ tạo ra bố cục đó, và các pipeline ký-rồi-xác-thực đầu-cuối tình cờ vẫn xanh. Mỗi lỗi đã từ chối bộ kiểm thử một ví dụ phản chứng vốn đã có thể phơi bày lỗi kia. Phiên bản 2.14.1 đã sửa cả hai bên trong cùng một bản phát hành: trình tạo điền ngược tất cả bốn trường, và bộ xác thực chấp nhận C >= A+B với C+D bằng độ gia tăng của file

Bản sửa lỗi về mặt phương pháp là xác thực chéo (cross-validation) với một triển khai mà bạn không viết. Một trình tạo và một trình xác thực chia sẻ chung một codebase, một tác giả, hoặc thậm chí chỉ một mô hình tinh thần về định dạng có thể đồng ý về một sự hiểu lầm chung vô thời hạn; một trình xác thực độc lập sẽ phá vỡ sự bất đối xứng đó. Hãy chạy đầu ra đã ký của bạn qua ít nhất một bộ kiểm tra tuân thủ bên ngoài trước khi phát hành, và giữ một kiểm tra cấu trúc tự động trong bản dựng làm tuyến phòng thủ đầu tiên nhanh chóng — TPdf.ValidatePades, được mô tả trong bài viết kiểm tra chữ ký, báo cáo lỗi bao phủ dưới dạng một vấn đề có tên cụ thể

var
  R: TPadesValidationResult;
begin
  Pdf.FileName := 'contract-signed.pdf';
  Pdf.Active := True;
  R := Pdf.ValidatePades;
  if ppeiByteRangeNotCoveringFile in R.Issues then
    Writeln('ByteRange does not cover the file');
  if not R.IsCompliant then
    Writeln('Structural issues present: do not ship this file');
end;

Danh sách kiểm tra từ chối cho đầu ra đã ký

Khi một nền tảng từ chối chữ ký PAdES của bạn, hãy kiểm tra các nguyên nhân cấu trúc đơn giản trước khi nghi ngờ các chứng chỉ hoặc chuỗi tin cậy. Đọc mảng /ByteRange và xác minh ba đẳng thức: trường thứ nhất bằng không, khoảng thứ hai bắt đầu tại hoặc sau khi kết thúc khoảng thứ nhất, khoảng thứ hai kết thúc chính xác tại điểm cuối của file. Đọc mục /M và xác minh nó là một chuỗi ngày §7.9.4 đầy đủ có tiền tố D: và một ký tự offset. Xác nhận tài liệu nguồn không bị mã hóa khi chữ ký được thêm vào — và nếu stack của bạn vẫn ký nó mà không báo lỗi, hãy coi sự im lặng đó là một lỗi trong stack. Cả ba kiểm tra đều chạy trên các byte thô trong vài phần nghìn giây, và theo kinh nghiệm của chúng tôi, chúng giải thích sự từ chối thường xuyên hơn nhiều so với bất kỳ nguyên nhân mật mã nào

Các cuộc gọi ký, kiểm tra và xác thực được sử dụng ở đây — SignPades, ValidatePades và các kiểm tra tuân thủ PAdES với phép toán ByteRange đã sửa, định dạng ngày tháng và kiểm soát mã hóa — đi kèm với PDFium Component dành cho Delphi, C++Builder và Lazarus