Bài viết kỹ thuật

Xác minh chữ ký PDF ECDSA trong Delphi: DER sang P1363

Một signatureValue ECDSA bên trong một container CMS là một DER SEQUENCE { INTEGER r, INTEGER s }. Hàm CNG của Windows BCryptVerifySignature không chấp nhận cả hai dạng đó: nó muốn r || s có độ rộng cố định theo IEEE P1363, không tag, không trường độ dài. HotPDF, thành phần VCL PDF gốc cho Delphi và C++Builder, chuyển đổi giữa hai dạng này theo các quy tắc DER nghiêm ngặt trước khi nhập khóa

Lỗi mà điều này ngăn chặn là một lỗi cụ thể và gây nản lòng. Acrobat mở tài liệu và hiển thị dấu tích xanh. Bộ xác minh của chính bạn, duyệt cùng các byte đó, trả về không hợp lệ, hoặc CNG trả về STATUS_INVALID_SIGNATURE mà không giải thích thêm. Không có gì sai với chữ ký cả. Cái sai là khoảng bảy mươi byte ASN.1 đã được truyền cho một API vốn mong đợi sáu mươi bốn byte số nguyên thô, và sự không khớp này vô hình trừ khi bạn biết cần tìm nó ở đâu

Vì sao BCryptVerifySignature từ chối một chữ ký ECDSA hợp lệ?

Vì hai phía của lệnh gọi nói hai kiểu mã hóa chữ ký khác nhau, và không phía nào thông báo điều đó. ISO 32000-1 §12.8 nói rằng một signature dictionary mang một blob CMS trong /Contents; RFC 5652 §5.3 nói rằng signatureValue trong mỗi SignerInfo là một OCTET STRING mà nội dung là bất cứ thứ gì thuật toán chữ ký định nghĩa. Với ECDSA, nội dung đó là cấu trúc DER theo SEC 1: một SEQUENCE chứa hai INTEGER. Nó có độ dài thay đổi theo thiết kế, vì rs là các số nguyên và DER loại bỏ các octet không dẫn đầu khỏi số nguyên

IEEE P1363 nhìn theo hướng ngược lại. Nó định nghĩa chữ ký là phép nối của hai tọa độ, mỗi tọa độ được đệm số không ở bên trái tới đúng độ rộng byte của trường đường cong. Một chữ ký P-256 luôn luôn là 64 byte. Một mã hóa DER của cùng chữ ký đó thường là 70 hoặc 71 byte và có thể dao động từ khoảng 8 tới 72. Đưa dạng DER cho BCryptVerifySignature và chỉ riêng việc kiểm tra độ dài đã khiến lệnh gọi thất bại chắc chắn, đó là lý do HotPDF chuẩn hóa trước khi xác minh chứ không phải sau

uses
  HPDFECDSA;

// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
  out ARaw: TBytes): Boolean;
begin
  Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;

Các quy tắc DER mà một bộ phân tích chữ ký không được nới lỏng

Mọi lần từ chối được liệt kê ở đây đều là một lần từ chối mà HotPDF thực hiện có chủ đích, và mỗi lần đóng lại một con đường mà một bộ phân tích dễ dãi sẽ để ngỏ. Cám dỗ khi viết một bộ chuyển đổi là tìm hai node INTEGER, sao chép nội dung của chúng, rồi tiếp tục. Điều đó hoạt động tốt trên đầu vào đúng chuẩn và âm thầm chấp nhận cả một họ mã hóa lại có thể biến dạng (malleable) trên đầu vào thù địch. Vì vậy HPDFECDSANormalizeSignature từ chối một số nguyên âm, nghĩa là bất kỳ r hay s nào có octet nội dung đầu tiên đặt bit cao, vì một vô hướng (scalar) ECDSA hợp lệ phải dương. Nó từ chối một giá trị hoàn toàn bằng không, vì r = 0 hay s = 0 không bao giờ là một chữ ký hợp lệ. Nó từ chối một octet không dẫn đầu dư thừa: X.690 §8.3 cho phép đúng một octet như vậy, và chỉ khi octet tiếp theo nếu không có nó sẽ được đọc là âm, nên một 00 theo sau bởi một octet dưới 0x80 là một cách mã hóa lại, không phải một chữ ký. Nó từ chối một header độ dài không tối giản, vì X.690 §10.1 yêu cầu dạng xác định được mã hóa bằng số octet ít nhất, và một độ dài dạng dài mà lẽ ra có thể là dạng ngắn là một chuỗi byte khác mang cùng ý nghĩa. Nó từ chối một số nguyên rộng hơn kích thước tọa độ của đường cong, vì giá trị đó không thể là một phần tử trường. Và nó từ chối bất kỳ node thừa nào sau s, cùng với một SEQUENCE ngoài có tổng độ dài không bằng độ dài của toàn bộ blob

Hai điều cuối đó quan trọng hơn vẻ ngoài của chúng. Các byte thừa sau SEQUENCE là mánh khóe biến dạng chữ ký (signature-malleability) kinh điển: thêm rác vào, và một bộ xác minh dễ dãi vẫn nói hợp lệ trong khi chuỗi byte nó xác thực không phải chuỗi byte đã được ký. Cùng bản năng đó thúc đẩy việc siết chặt độ dài ASN.1 được mô tả trong ghi chú về phân tích PKCS#12, và cũng chính bản năng đó ở đây. Trong một đường xác minh, một cấu trúc được chấp nhận mà chưa từng được phát ra bởi một signer tuân thủ chuẩn là một khiếm khuyết, không phải một phép lịch sự

Độ rộng tọa độ thuộc về đường cong, không thuộc về chữ ký

HotPDF suy ra độ rộng đầu ra từ OID đường cong được nêu tên, không bao giờ từ độ dài của DER vừa được phân tích. Đây là nửa thứ hai của phép chuyển đổi và là nửa dễ bị sai một cách tinh vi nhất. RFC 5480 §2.1.1 nhận dạng đường cong trong các tham số SubjectPublicKeyInfo của chứng chỉ, và HPDFECDSACurveFromOID ánh xạ ba OID mà HotPDF hỗ trợ: 1.2.840.10045.3.1.7 cho P-256, 1.3.132.0.34 cho P-384, và 1.3.132.0.35 cho P-521. HPDFECDSACoordinateSize sau đó trả về 32, 48, hoặc 66 byte, và buffer P1363 có kích thước gấp đôi: 64, 96, hoặc 132. Mỗi số nguyên đã giải mã được canh phải vào nửa của nó, nên một r ngắn được đệm số không ở bên trái thay vì bị dịch. P-521 là trường hợp hay bẫy người, vì 521 bit là 65,125 byte và được làm tròn lên 66, cho ra một chữ ký 132 byte mà không trực giác lũy thừa của hai nào dự đoán được. Khóa công khai đi kèm dưới dạng một điểm EC không nén theo RFC 5480 §2.2, tức là 0x04 theo sau bởi X và Y, nên HotPDF kiểm tra nó có đúng 1 + 2 * CoordinateSize byte và bắt đầu bằng 0x04 trước khi chạm vào CNG

var
  Digest, SigDER, PublicPoint: TBytes;
  Curve: THPDFECDSACurve;
  Res: THPDFECDSAVerifyResult;
begin
  // secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
  Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');

  // PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
  Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);

  case Res of
    evrValid:
      Memo1.Lines.Add('signature verifies');
    evrInvalid:
      Memo1.Lines.Add('signature does not match the digest');
    evrMalformed:
      Memo1.Lines.Add('DER encoding or public point rejected');
    evrUnsupported:
      Memo1.Lines.Add('curve or algorithm not supported here');
    evrProviderUnavailable:
      Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
    evrProviderError:
      Memo1.Lines.Add('CNG returned an unexpected status');
  end;
end;

Hãy chú ý tham số cuối cùng. HPDFECDSAVerifyDigest cũng chấp nhận eseP1363 cho các bên gọi đã sẵn có một chữ ký độ rộng cố định, từ một token phần cứng hay một dịch vụ ký từ xa trả về r || s thô. Đường đó vẫn thực thi việc kiểm tra độ dài và kiểm tra khác-không trên cả hai nửa, nên một buffer đúng kích thước toàn số không sẽ bị từ chối thay vì được truyền thẳng cho nhà cung cấp

Vì sao tên thuật toán ECDSA chung thất bại trên Windows cũ hơn?

Vì tên chung này mới hơn nền tảng triển khai mà bạn đang phát hành vào. CNG bộc lộ một định danh thuật toán ECDSA suy ra đường cong từ khóa đã nhập, và đó là cách viết mã sạch sẽ, nhưng BCryptOpenAlgorithmProvider chỉ được bảo đảm giải quyết được nó trên các phiên bản Windows gần đây hơn. Trên một máy cũ hơn, lệnh gọi mở thất bại, handle nhà cung cấp vẫn là nil, và mọi lần xác minh ECDSA trong ứng dụng của bạn báo cáo không được hỗ trợ trên một chữ ký hoàn toàn tốt. HotPDF tránh vực thẳm đó bằng cách mở các định danh theo từng đường cong thay vào đó. Nó giải quyết ECDSA_P256, ECDSA_P384, và ECDSA_P521 một lần, lưu cache một handle nhà cung cấp cho mỗi đường cong, và đóng chúng trong phần finalization của unit. Mỗi lần xác minh khi đó chỉ làm công việc rẻ tiền: nhập một khóa công khai tạm thời từ một ECCPUBLICBLOB, gọi BCryptVerifySignature, hủy khóa. Không có LoadLibrary lặp lại, không có GetProcAddress lặp lại, không có mở và đóng nhà cung cấp cho mỗi chữ ký. Xác minh hàng loạt vài trăm tài liệu cảm nhận rõ sự khác biệt, và một tiến trình dịch vụ đáng lẽ sẽ liên tục tạo và hủy handle nhà cung cấp dưới tải cũng vậy

Các mã kết quả vẫn trung thực về sự khác biệt đó. evrProviderUnavailable nghĩa là máy không thể cung cấp cho HotPDF một nhà cung cấp; evrInvalid nghĩa là CNG trả lời STATUS_INVALID_SIGNATURE. Gộp hai điều đó thành một thất bại duy nhất là cách một vấn đề triển khai bị báo cáo sai thành một tài liệu bị giả mạo. Cùng sự phân tách giữa lỗi môi trường và lỗi mật mã học đó xuyên suốt cách xử lý CNG và CAPI ở phía ký, được trình bày trong bài viết về ký bằng certificate store và thứ tự byte

Chứng chỉ nào đã ký cái này? SignerIdentifier là hai thứ khác nhau

RFC 5652 §5.3 khiến SignerIdentifier trở thành một CHOICE, và một bộ xác minh chỉ xử lý một nhánh sẽ âm thầm xác minh với sai khóa. Nhánh đầu tiên là issuerAndSerialNumber, một SEQUENCE chứa Name của issuer ở dạng DER thô và số serial INTEGER, và việc khớp nó là một phép so sánh byte với từng chứng chỉ trong tập certificates của CMS. Nhánh thứ hai là [0] subjectKeyIdentifier, một OCTET STRING được gắn tag ngầm định, và việc khớp nó đòi hỏi đào sâu vào bên trong chứng chỉ chứ không chỉ so sánh các trường header của nó

Việc đào sâu đó có một lớp khiến người ta bất ngờ. Định danh khóa nằm trong một extension X.509v3, nên HotPDF duyệt trường extensions [3] của tbsCertificate, tìm extension có OID là 2.5.29.14, bỏ qua BOOLEAN critical tùy chọn, và lấy OCTET STRING extnValue. Chuỗi octet đó không phải là định danh. Theo RFC 5280 §4.2.1.2, nội dung của nó tự nó là DER, và kiểu KeyIdentifier là một OCTET STRING khác, nên bạn phải phân tích lần thứ hai để tới được các byte thực sự. Dừng sớm một lớp và bạn so sánh một lớp bọc 22 byte với một định danh 20 byte, không chứng chỉ nào khớp cả, và bộ xác minh rơi về bất cứ phương pháp phỏng đoán nào bạn viết tiếp theo, đó mới là mối nguy thực sự. Lấy chứng chỉ đầu tiên trong tập là một lối tắt hấp dẫn và nó sai bất cứ khi nào CMS mang một chuỗi (chain), tức là hầu hết thời gian, vì leaf không bắt buộc phải đứng đầu tiên. HotPDF chỉ chấp nhận một chứng chỉ không khớp khi container chỉ giữ đúng một chứng chỉ; khi có nhiều chứng chỉ hiện diện, việc khớp SignerIdentifier chính xác là bắt buộc. Xác minh một digest với khóa công khai của một CA trung gian không tạo ra một lỗi thân thiện, nó tạo ra một kết quả không hợp lệ đầy chắc chắn trên một tài liệu hoàn toàn ổn

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('signed.pdf') > 0 then
      for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
        if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
          Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
            [String(Info.FieldName), String(Info.PublicKeyAlgorithm),
             String(Info.CurveName), String(Info.HashAlgorithm),
             String(Info.SignerName)]))
        else
          Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
  finally
    Pdf.Free;
  end;
end;

THPDFSignatureInfo.CurveName báo cáo P-256, P-384, hoặc P-521 để một nhật ký kiểm toán ghi lại đường cong nào thực sự đã được dùng thay vì chỉ mỗi từ ECDSA. Phần hạ tầng cấp tài liệu xung quanh lệnh gọi này, đặc biệt là cách các đoạn /ByteRange được băm (hash) và vì sao digest phải được tính trên tệp thay vì trên cây đối tượng đã phân tích, là chủ đề của bài viết đồng hành về xác minh chữ ký số PDF

Điều này không mang lại cho bạn những gì

Một kết quả xanh từ HPDFECDSAVerifyDigest chỉ trả lời đúng một câu hỏi: các byte này đã được ký bởi khóa riêng khớp với khóa công khai này. Nó không nói gì về việc khóa đó có thuộc về ai đó bạn nên tin tưởng hay không. Việc xây dựng chuỗi tới một trust anchor, thu hồi qua CRL hay OCSP, và các kiểm tra chính sách là công việc riêng biệt, và bất kỳ sản phẩm nào báo cáo một chữ ký hợp lệ mà không có những thứ đó đang báo cáo ít hơn những gì người dùng giả định. Ngày hiệu lực của chứng chỉ được hiển thị riêng trong THPDFSignatureInfo chính vì lý do đó: một chữ ký có thể xác minh đúng về mặt mật mã học trong khi chứng chỉ tạo ra nó đã hết hạn hai năm trước. Việc hỗ trợ đường cong cũng cố tình hẹp. Ba đường cong NIST prime được xử lý, và một chữ ký trên bất kỳ đường cong nào khác trả về không được hỗ trợ thay vì một phỏng đoán. Đường CNG chỉ dành cho Windows, đây là sự đánh đổi đúng đắn cho một thành phần VCL nhưng đáng nói rõ trước khi bạn lên kế hoạch một dịch vụ đa nền tảng xoay quanh nó. Và tính nghiêm ngặt không thể cấu hình được: không có chế độ dễ dãi nào chấp nhận một độ dài DER không tối giản chỉ vì một signer cũ nào đó đã phát ra như vậy. Nếu bạn gặp một tệp như vậy trong sản xuất, phản ứng trung thực là ghi lại nó và truy tìm bên tạo ra nó, không phải nới rộng bộ phân tích cho tới khi tệp đó vượt qua được

Đường xác minh ECDSA được mô tả ở đây được cung cấp như một phần của HotPDF Component tiêu chuẩn cho Delphi và C++Builder, cùng với các đường RSA PKCS#1 v1.5 và RSA-PSS cùng bản ghi thông tin chữ ký đầy đủ; trang sản phẩm có đầy đủ tài liệu tham khảo về chữ ký số