Bài viết kỹ thuật

Chính sách ECDSA/EdDSA ISO/TS 32002 trong PDFium Delphi

PDFium Component cho Delphi kiểm tra mọi chữ ký PDF ECDSA và EdDSA đối chiếu profile thuật toán ISO/TS 32002 và báo phán quyết trong TPadesSignatureValidation.AlgorithmPolicyStatus. Chỉ P-256, P-384, P-521, ba curve Brainpool r1, Ed25519 và Ed448 có thể pass, mỗi cái với một digest khớp, và một sự lệch nhau sẽ raise ppeiSignatureAlgorithmMismatch. Chính sách này quan trọng hơn vẻ ngoài của nó. Một chữ ký trên brainpoolP160r1, hay một key P-256 ký một digest SHA-512, hoàn toàn verify ngon ở cấp toán học, nên Windows CryptoAPI báo giá trị chữ ký tốt trong khi một validator PDF 2.0 nghiêm ngặt từ chối file. Phép kiểm chính sách lấp khoảng hổng đó, và nó được cố ý tách khỏi câu hỏi các byte chữ ký có đúng về mặt mật mã hay không

ISO/TS 32002 thực sự cho phép gì cho chữ ký đường cong elip?

ISO/TS 32002 cho phép đúng sáu curve ECDSA và hai scheme EdDSA trong chữ ký PDF, và nó buộc mỗi curve với các kích thước digest mà nó được mang. PDFium Component mã hóa bảng đó trong PadesCurveDigestAllowed, khóa theo curve OID từ certificate của người ký. Các curve NIST nghiêm ngặt: digest phải có cùng độ rộng bit với curve, hoặc SHA-2 hoặc SHA-3. Các curve Brainpool khoan dung hơn và nhận độ rộng của chính nó hoặc bất cứ cái gì rộng hơn:

  • P-256 (1.2.840.10045.3.1.7): chỉ SHA-256 hay SHA3-256
  • P-384 (1.3.132.0.34): chỉ SHA-384 hay SHA3-384
  • P-521 (1.3.132.0.35): chỉ SHA-512 hay SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): mọi digest SHA-2 hay SHA-3 từ 256 tới 512 bit
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 384 hay 512 bit
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): chỉ SHA-512 hay SHA3-512
Ma trận profile thuật toán ISO TS 32002 trong PDFium Component: P-256, P-384 và P-521 chỉ nhận các độ rộng digest khớp trong PadesCurveDigestAllowed, brainpoolP256r1 nhận 256 tới 512 bit, brainpoolP384r1 nhận 384 và 512, Ed25519 và Ed448 khai báo SHA-512 và SHAKE256 độ dài 512, và sự lệch nhau raise ppeiSignatureAlgorithmMismatch trong khi các curve ngoài profile trả pcsUnsupported
Sáu curve ECDSA và hai scheme EdDSA có thể pass, và mỗi curve bị buộc với các độ rộng digest mà nó được mang; mọi thứ khác là invalid hay unsupported, không bao giờ được chấp nhận âm thầm

EdDSA chẳng có lựa chọn curve nào cũng như lựa chọn digest nào, và chính vì thế mà các luật của nó xoay quanh encoding chứ không phải độ mạnh. Theo RFC 8419, một SignerInfo Ed25519 phải khai báo SHA-512 làm digestAlgorithm không kèm tham số, và một SignerInfo Ed448, trên đường signed-attributes mà PAdES luôn dùng, phải khai báo id-shake256-len (2.16.840.1.101.3.4.2.18) với tham số INTEGER đúng bằng 512. Với cả hai scheme, chữ ký AlgorithmIdentifier và public key AlgorithmIdentifier của certificate không được mang bất kỳ tham số nào. Một producer viết một NULL ở đó — thói quen mà các RSA encoder đã huấn luyện vào nhiều thư viện ASN.1 — sẽ sinh ra một chữ ký không tuân thủ dù key và giá trị chữ ký đều ổn

PDFium Component trích bộ ba thuật toán từ CMS thế nào

PDFium bản thân nó không thể trả lời câu hỏi này, vì API chữ ký công khai của nó đọc dictionary chữ ký nhưng chẳng verify CMS cũng như chẳng phơi curve của certificate người ký. Vì thế lớp inspection PAdES dựng trên PDFium tự parse CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm đọc digestAlgorithm và signatureAlgorithm của SignerInfo đầu tiên, rồi tìm certificate người ký và đọc SubjectPublicKeyInfo của nó để lấy thuật toán key và curve. Việc tra certificate được cố ý giới hạn: tối đa 64 certificate trong tập certificates của CMS được xem xét, phép khớp là so byte chính xác issuer và số serial từ issuerAndSerialNumber, và code chỉ rơi về “certificate duy nhất có ở đó” khi tập chứa đúng một certificate parse được. Chọn certificate EC đầu tiên từ một tập không thứ tự thì dễ, và nó sẽ để một certificate CA quyết định curve mà người ký đáng lẽ đã dùng

PDFium Component kiểm tra bộ ba thuật toán chữ ký PDF thế nào: InspectPadesSignatureAlgorithm đọc digestAlgorithm và signatureAlgorithm của SignerInfo đầu tiên trong CMS, ghim certificate người ký qua phép khớp chính xác issuerAndSerialNumber trong tối đa 64 ứng viên, đọc SubjectPublicKeyInfo để lấy curve, và EvaluatePadesSignatureAlgorithm trả về AlgorithmPolicyStatus
PDFium bản thân nó chẳng verify CMS cũng như chẳng phơi curve người ký, nên lớp PAdES parse SignedData và giữ mọi OID thô trong record để một lần từ chối luôn có lý giải
uses
  PDFium, FPdfPades;

const
  StatusNames: array[TPadesCryptoStatus] of string =
    ('not checked', 'valid', 'invalid', 'unsupported', 'indeterminate');

var
  Pdf: TPdf;
  R: TPadesValidationResult;
  A: TPadesSignatureAlgorithmInfo;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice-ecdsa-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    for I := 0 to Length(R.Signatures) - 1 do
    begin
      A := R.Signatures[I].AlgorithmInfo;
      Writeln('Signature ', I);
      Writeln('  digestAlgorithm    : ', string(A.DigestAlgorithmOid));
      Writeln('  signatureAlgorithm : ', string(A.SignatureAlgorithmOid));
      Writeln('  public key / curve : ', string(A.PublicKeyAlgorithmOid),
        ' / ', string(A.CurveOid));
      Writeln('  policy             : ',
        StatusNames[R.Signatures[I].AlgorithmPolicyStatus]);
    end;
    if ppeiSignatureAlgorithmMismatch in R.Issues then
      Writeln('At least one signature violates the ISO/TS 32002 profile');
  finally
    Pdf.Free;
  end;
end;

TPdf.ValidatePades áp chính sách như một phần của lượt kiểm tuân thủ, bắt đầu từ certificate nó định vị bên trong CMS, và TPdf.ValidatePadesTrust chạy lại nó đối với certificate người ký mà Windows CryptoAPI thực sự dùng để verify, nên certificate mà CryptoAPI báo cáo mới là người có tiếng nói cuối. Mọi input thô đều nằm trong TPadesSignatureAlgorithmInfo, bao gồm DigestParametersPresent, DigestParameterBits, SignatureParametersPresent và PublicKeyParametersAreNamedCurve, nên một lần từ chối luôn lý giải được từ record thay vì từ một dòng log

Vì sao một chữ ký P-256 với SHA3-256 gãy chính sách?

Một chữ ký P-256 gãy chính sách của PDFium Component mỗi khi digestAlgorithm của CMS và digest mà signatureAlgorithm ECDSA hàm ý bất đồng, kể cả khi cả hai riêng lẻ đều chấp nhận được cho curve. EvaluatePadesSignatureAlgorithm trước tiên map ecdsa-with-SHA256, ecdsa-with-SHA3-256 và các anh em của chúng về một digest, so sánh với digestAlgorithm đã khai báo, và trả pcsInvalid ngay mọi khác biệt trước khi bảng curve được tra. Ca này có thật: một công cụ ký đổi hash sang SHA3-256 nhưng giữ nguyên identifier ecdsa-with-SHA256 hard-code, và kết quả là một file mà chẳng verifier nào tuân thủ có thể diễn giải một cách nhất quán. Hàm là public, nên ma trận có thể được ghim trong một unit test mà chẳng cần dựng PDF:

var
  Info: TPadesSignatureAlgorithmInfo;
begin
  Info := Default(TPadesSignatureAlgorithmInfo);
  Info.Family := psafEcdsa;
  Info.PublicKeyAlgorithmOid := '1.2.840.10045.2.1';      // id-ecPublicKey
  Info.PublicKeyParametersPresent := True;
  Info.PublicKeyParametersAreNamedCurve := True;
  Info.CurveOid := '1.2.840.10045.3.1.7';                 // P-256
  Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.8';    // SHA3-256
  Info.SignatureAlgorithmOid := '2.16.840.1.101.3.4.3.10'; // ecdsa-with-SHA3-256
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsValid);

  Info.SignatureAlgorithmOid := '1.2.840.10045.4.3.2';    // ecdsa-with-SHA256
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsInvalid); // digest lệch nhau

  Info.CurveOid := '1.3.36.3.3.2.8.1.1.1';                // brainpoolP160r1
  Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.1';    // SHA-256, giờ đã tương hợp
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // curve không nằm trong profile
end;

Encoding của curve nhận cùng độ nghiêm ngặt. RFC 5480 §2.1.1 cho ECParameters là một named curve OID, một implicit curve (NULL) hay một bộ tham số tường minh đầy đủ, và các profile PKIX đòi hình thức named. PDFium Component trả pcsInvalid khi một certificate id-ecPublicKey mang không tham số, tham số implicit hay tham số explicit, vì tham số explicit cho phép kẻ tấn công mô tả một curve chỉ giống một curve chuẩn. Một curve có tên đúng đắn nhưng đơn giản vắng mặt trong danh sách ISO/TS 32002, như brainpoolP160r1 ở trên hay secp256k1, nhận pcsUnsupported thay thế

Invalid, unsupported hay indeterminate: đọc trạng thái một cách trung thực

Ba trạng thái không-hợp-lệ của AlgorithmPolicyStatus mang những nghĩa khác nhau, và dồn chúng vào chung một xô “thất bại” là vứt bỏ đúng thứ thông tin các auditor cần. pcsInvalid nghĩa là một tổ hợp thuật toán được nhận diện bị dị dạng hay lệch nhau; nó thêm ppeiSignatureAlgorithmMismatch vào TPadesValidationResult.Issues và kéo IntegrityStatus tổng thể về pcsInvalid, nên IsCryptographicallyValid trả False kể cả khi giá trị chữ ký CMS kiểm ra đúng. pcsUnsupported nghĩa là curve hay digest nằm ngoài những gì profile nêu tên, một kết quả về khả năng chứ không phải bằng chứng can thiệp. pcsIndeterminate nghĩa là certificate người ký không thể được ghim xuống, thường là một CertificateSet với vài ứng viên và chẳng khớp chính xác issuerAndSerialNumber nào, nên code từ chối đoán curve; từ v3.124.0 nó còn đánh dấu một chữ ký RSA trên SHA-1 hay một digest 112 bit như SHA-224, thứ không còn được công nhận cho validation hiện hành. Phân tách tương tự áp dụng cho EdDSA trên một máy mà CryptoAPI của nó không verify được Ed25519 hay Ed448: CmsSignatureStatus giữ pcsUnsupported trong khi AlgorithmPolicyStatus vẫn có thể là pcsValid, vì encoding đúng và chỉ thiếu verifier. Nếu bạn đang truy một lần từ chối từ Adobe hay một validator dựa trên DSS, hướng dẫn vì sao validator từ chối chữ ký PAdES trình bày các nguyên nhân phổ biến khác

Đường quyết định của EvaluatePadesSignatureAlgorithm trong PDFium Component: một digest lệch nhau đặt pcsInvalid và ppeiSignatureAlgorithmMismatch, một named curve ngoài profile ISO TS 32002 như brainpoolP160r1 đặt pcsUnsupported, một certificate người ký không ghim được đặt pcsIndeterminate, và từ v3.124.0 nhánh RSA áp các bộ digest ETSI TS 119 312, trong khi kích thước key vẫn để dành cho ứng dụng
Ba trạng thái không-hợp-lệ mang ba nghĩa khác nhau: invalid là bằng chứng của một tổ hợp gãy, unsupported là một kết quả về khả năng, còn indeterminate nghĩa là code từ chối đoán
var
  Pdf: TPdf;
  Options: TPadesTrustValidationOptions;
  R: TPadesValidationResult;
  S: TPadesSignatureValidation;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice-ecdsa-signed.pdf';
    Pdf.Active := True;
    Options := TPadesTrustValidationOptions.Default; // offline, không kiểm thu hồi
    R := Pdf.ValidatePadesTrust(Options);
    for I := 0 to Length(R.Signatures) - 1 do
    begin
      S := R.Signatures[I];
      if S.IsDocumentTimeStamp then
        Continue;
      case S.AlgorithmPolicyStatus of
        pcsInvalid:       Writeln(I, ': reject, algorithm combination is invalid');
        pcsUnsupported:   Writeln(I, ': manual review, curve or digest outside the profile');
        pcsIndeterminate: Writeln(I, ': manual review, signer not identified or digest no longer agreed');
        pcsValid:
          if S.AlgorithmInfo.Family = psafRsa then
            Writeln(I, ': RSA digest suite accepted, check key size yourself')
          else
            Writeln(I, ': EC/EdDSA profile satisfied');
      end;
    end;
    Writeln('Cryptographically valid: ', R.IsCryptographicallyValid);
  finally
    Pdf.Free;
  end;
end;

pcsValid không đảm bảo điều gì?

AlgorithmPolicyStatus = pcsValid chỉ chứng nhận một chữ ký ECDSA hay EdDSA dùng một curve được duyệt với một digest tương hợp và được mã hóa đúng, và một chữ ký RSA dùng digest từ một bộ hiện hành; nó chẳng nói gì về việc giá trị chữ ký có đúng hay không. Trước v3.124.0, nhánh RSA của EvaluatePadesSignatureAlgorithm được cố ý để rộng: bất kỳ signatureAlgorithm nào dưới arc PKCS #1 1.2.840.113549.1.1.* đều trả pcsValid, kể cả sha1WithRSAEncryption cũ kỹ. Từ PDFiumPas v3.124.0, nhánh RSA áp các bộ chữ ký ETSI TS 119 312. Các digest MD2, MD4 và MD5 là pcsInvalid. SHA-1 và các digest 112 bit như SHA-224 là pcsIndeterminate, nên một chữ ký SHA-1 giữ nguyên kết quả integrity tổng thể và được cờ để rà soát thay vì bị từ chối. Một digestAlgorithm khác với digest mà thuật toán chữ ký cố định, như sha256WithRSAEncryption trên một digest SHA-1, hay một thuật toán chữ ký RSA trên một key người ký không phải RSA, là pcsInvalid, báo cáo như ppeiSignatureAlgorithmMismatch và làm gãy integrity. Một certificate người ký không tìm được cho pcsIndeterminate, như nó vốn đã làm với ECDSA, còn các digest không nhận diện được hay các OID RSA không phải chữ ký cho pcsUnsupported. Độ dài modulus vẫn không được kiểm, các tham số PSS không được validate ở đây (bài về RSASSA-PSS-params RFC 4055 trình bày cách chúng được mã hóa phía ký), và các digest SHA-1 hay MD5 còn raise thêm issue riêng ppeiBadDigestAlgorithm. Tương tự, phép toán chữ ký, chuỗi certificate và thu hồi vẫn là việc của CmsSignatureStatus, CertificateTrustStatus và RevocationStatus, các thứ đến từ Windows CryptoAPI. Hãy coi pcsValid là “profile thuật toán đứng vững”, đừng bao giờ coi là “key này đủ mạnh”

Với một ứng dụng Delphi nhận các hóa đơn PDF, hợp đồng hay gói lưu trữ có chữ ký, thiết lập thực dụng khá ngắn: chạy ValidatePadesTrust, từ chối khi thấy ppeiSignatureAlgorithmMismatch, chuyển pcsUnsupported và pcsIndeterminate cho người xử lý, và tự đặt sàn kích thước key RSA của riêng bạn vì chính sách sẽ không làm điều đó. PDFium Component for Delphi and Lazarus giao kèm PAdES validator, bộ dựng báo cáo bằng chứng và pipeline ký, nên cùng một thư viện có thể sinh các chữ ký này và kiểm chúng trọn vẹn từ đầu tới cuối