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
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
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
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