Technical Article

ISO/TS 32002 ECDSA and EdDSA Policy Checks in PDFium Delphi

The PDFium Component for Delphi checks every ECDSA and EdDSA PDF signature against the ISO/TS 32002 algorithm profile and reports the verdict in TPadesSignatureValidation.AlgorithmPolicyStatus. Only P-256, P-384, P-521, the three Brainpool r1 curves, Ed25519 and Ed448 can pass, each with a matching digest, and a mismatch raises ppeiSignatureAlgorithmMismatch. That policy matters more than it sounds. A signature on brainpoolP160r1, or a P-256 key signing a SHA-512 digest, can verify perfectly well at the maths level, so Windows CryptoAPI reports the signature value as good while a strict PDF 2.0 validator rejects the file. The policy check closes that gap, and it is deliberately separate from the question of whether the signature bytes are cryptographically correct

What does ISO/TS 32002 actually allow for elliptic curve signatures?

ISO/TS 32002 allows exactly six ECDSA curves and two EdDSA schemes in PDF signatures, and it ties each curve to the digest sizes it may carry. The PDFium Component encodes that table in PadesCurveDigestAllowed, keyed by the curve OID from the signer certificate. The NIST curves are strict: the digest must have the same bit width as the curve, either SHA-2 or SHA-3. The Brainpool curves are looser and accept their own width or anything wider:

  • P-256 (1.2.840.10045.3.1.7): SHA-256 or SHA3-256 only
  • P-384 (1.3.132.0.34): SHA-384 or SHA3-384 only
  • P-521 (1.3.132.0.35): SHA-512 or SHA3-512 only
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): any SHA-2 or SHA-3 digest from 256 to 512 bits
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): 384- or 512-bit SHA-2 / SHA-3
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): SHA-512 or SHA3-512 only
Matrix of the ISO TS 32002 algorithm profile in PDFium Component: P-256, P-384 and P-521 accept only matching digest widths in PadesCurveDigestAllowed, brainpoolP256r1 accepts 256 to 512 bits, brainpoolP384r1 accepts 384 and 512, Ed25519 and Ed448 declare SHA-512 and SHAKE256 with length 512, and mismatches raise ppeiSignatureAlgorithmMismatch while outside-profile curves return pcsUnsupported
Six ECDSA curves and two EdDSA schemes can pass, and each curve is tied to the digest widths it may carry; everything else is invalid or unsupported, never silently accepted

EdDSA has no curve choice and no digest choice, which is exactly why its rules are about encoding rather than strength. Per RFC 8419, an Ed25519 SignerInfo must declare SHA-512 as its digestAlgorithm with no parameters, and an Ed448 SignerInfo, on the signed-attributes path that PAdES always uses, must declare id-shake256-len (2.16.840.1.101.3.4.2.18) with an INTEGER parameter of exactly 512. For both schemes the signature AlgorithmIdentifier and the certificate public key AlgorithmIdentifier must carry no parameters at all. A producer that writes a NULL there, the habit RSA encoders have trained into many ASN.1 libraries, produces a non-conforming signature even though the key and the signature value are fine

How the PDFium Component extracts the algorithm triple from CMS

PDFium itself cannot answer this question, because its public signature API reads the signature dictionary but neither verifies the CMS nor exposes the signer certificate's curve. The PAdES inspection layer built on PDFium therefore parses the CMS SignedData (RFC 5652) itself. InspectPadesSignatureAlgorithm reads the digestAlgorithm and signatureAlgorithm of the first SignerInfo, then finds the signer certificate and reads its SubjectPublicKeyInfo to get the key algorithm and the curve. Certificate lookup is deliberately bounded: at most 64 certificates in the CMS certificates set are examined, the match is an exact byte comparison of issuer and serial number from issuerAndSerialNumber, and the code falls back to "the only certificate there is" only when the set holds exactly one parseable certificate. Picking the first EC certificate from an unordered set would be easy, and it would let a CA certificate decide which curve the signer supposedly used

How the PDFium Component inspects a PDF signature algorithm triple: InspectPadesSignatureAlgorithm reads the digestAlgorithm and signatureAlgorithm of the first SignerInfo in the CMS, pins the signer certificate through an exact issuerAndSerialNumber match among at most 64 candidates, reads the SubjectPublicKeyInfo for the curve, and EvaluatePadesSignatureAlgorithm returns the AlgorithmPolicyStatus
PDFium itself neither verifies the CMS nor exposes the signer curve, so the PAdES layer parses the SignedData and keeps every raw OID in the record for an explainable rejection
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 applies the policy as part of its compliance pass, starting from the certificate it locates inside the CMS, and TPdf.ValidatePadesTrust re-runs it against the signer certificate that Windows CryptoAPI actually used for verification, so the CryptoAPI-reported certificate has the final word. Every raw input lands in TPadesSignatureAlgorithmInfo, including DigestParametersPresent, DigestParameterBits, SignatureParametersPresent and PublicKeyParametersAreNamedCurve, so a rejection is always explainable from the record rather than from a log line

Why does a P-256 signature with SHA3-256 fail the policy?

A P-256 signature fails the PDFium Component policy whenever the CMS digestAlgorithm and the digest implied by the ECDSA signatureAlgorithm disagree, even if both are individually acceptable for the curve. EvaluatePadesSignatureAlgorithm first maps ecdsa-with-SHA256, ecdsa-with-SHA3-256 and their siblings to a digest, compares that with the declared digestAlgorithm, and returns pcsInvalid on any difference before the curve table is consulted. The case is real: a signing tool switches its hash to SHA3-256 but keeps a hard-coded ecdsa-with-SHA256 identifier, and the result is a file that no conforming verifier can interpret consistently. The function is public, so the matrix can be pinned in a unit test without building a 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 mismatch

  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, now congruent
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // curve not in the profile
end;

The curve encoding gets the same strictness. RFC 5480 §2.1.1 lets ECParameters be a named curve OID, an implicit curve (NULL) or a full explicit parameter set, and PKIX profiles require the named form. The PDFium Component returns pcsInvalid when an id-ecPublicKey certificate carries no parameters, implicit parameters or explicit parameters, because explicit parameters let an attacker describe a curve that merely resembles a standard one. A correctly named curve that is simply missing from the ISO/TS 32002 list, such as brainpoolP160r1 above or secp256k1, gets pcsUnsupported instead

Invalid, unsupported or indeterminate: reading the status honestly

The three non-valid statuses of AlgorithmPolicyStatus mean different things, and collapsing them into one "failed" bucket throws away the information auditors need. pcsInvalid means a recognised algorithm combination is malformed or mismatched; it adds ppeiSignatureAlgorithmMismatch to TPadesValidationResult.Issues and drives the aggregate IntegrityStatus to pcsInvalid, so IsCryptographicallyValid returns False even when the CMS signature value checks out. pcsUnsupported means the curve or digest is outside what the profile names, which is a capability result, not proof of tampering. pcsIndeterminate means the signer certificate could not be pinned down, usually a CertificateSet with several candidates and no exact issuerAndSerialNumber match, so the code refuses to guess the curve; since v3.124.0 it also marks an RSA signature over SHA-1 or a 112-bit digest such as SHA-224, which is no longer agreed for current validation. The same split applies to EdDSA on a machine whose CryptoAPI cannot verify Ed25519 or Ed448: CmsSignatureStatus stays pcsUnsupported while AlgorithmPolicyStatus can still be pcsValid, because the encoding was correct and only the verifier was missing. If you are chasing a rejection from Adobe or a DSS-based validator, the guide to why validators reject PAdES signatures covers the other common causes

Decision path of EvaluatePadesSignatureAlgorithm in PDFium Component: a digest mismatch sets pcsInvalid and ppeiSignatureAlgorithmMismatch, a named curve outside the ISO TS 32002 profile such as brainpoolP160r1 sets pcsUnsupported, a signer certificate that cannot be pinned sets pcsIndeterminate, and since v3.124.0 the RSA branch applies the ETSI TS 119 312 digest suites, with the key size still left to the application
The three non-valid statuses mean different things: invalid is proof of a broken combination, unsupported is a capability result, and indeterminate means the code refused to guess
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, no revocation
    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;

What does pcsValid not guarantee?

AlgorithmPolicyStatus = pcsValid only certifies that an ECDSA or EdDSA signature uses an approved curve with a congruent, correctly encoded digest, and that an RSA signature uses a digest from a current suite; it says nothing about whether the signature value is correct. Before v3.124.0 the RSA branch of EvaluatePadesSignatureAlgorithm was intentionally wide: any signatureAlgorithm under the PKCS #1 arc 1.2.840.113549.1.1.* returned pcsValid, legacy sha1WithRSAEncryption included. Since PDFiumPas v3.124.0 the RSA branch applies the ETSI TS 119 312 signature suites. MD2, MD4 and MD5 digests are pcsInvalid. SHA-1 and 112-bit digests such as SHA-224 are pcsIndeterminate, so a SHA-1 signature keeps its overall integrity result and is flagged for review rather than rejected. A digestAlgorithm that differs from the digest fixed by the signature algorithm, such as sha256WithRSAEncryption over a SHA-1 digest, or an RSA signature algorithm on a non-RSA signer key, is pcsInvalid, reported as ppeiSignatureAlgorithmMismatch and fails integrity. A signer certificate that cannot be found gives pcsIndeterminate, as it already did for ECDSA, and unrecognised digests or non-signature RSA OIDs give pcsUnsupported. Modulus length is still not checked, PSS parameters are not validated here (the RFC 4055 RSASSA-PSS-params article covers how they are encoded on the signing side), and SHA-1 or MD5 digests additionally raise the separate ppeiBadDigestAlgorithm issue. Likewise, the signature maths, certificate chain and revocation remain the job of CmsSignatureStatus, CertificateTrustStatus and RevocationStatus, which come from Windows CryptoAPI. Treat pcsValid as "the algorithm profile holds", never as "this key is strong enough"

For a Delphi application that accepts signed PDF invoices, contracts or archive packages, the practical setup is short: run ValidatePadesTrust, reject on ppeiSignatureAlgorithmMismatch, route pcsUnsupported and pcsIndeterminate to a human, and enforce your own RSA key-size floor because the policy will not. The PDFium Component for Delphi and Lazarus ships the PAdES validator, the evidence report builder and the signing pipeline, so the same library can produce these signatures and check them end to end