PDFium Component for Delphi 会按 ISO/TS 32002 算法 profile 检查每一条 ECDSA 和 EdDSA PDF 签名,并把结论报在 TPadesSignatureValidation.AlgorithmPolicyStatus 里。只有 P-256、P-384、P-521、三条 Brainpool r1 曲线、Ed25519 和 Ed448 能通过,且各自要求匹配的摘要,不匹配则抛出 ppeiSignatureAlgorithmMismatch。这条政策比听起来更重要:brainpoolP160r1 上的签名,或者 P-256 密钥签 SHA-512 摘要,在数学层面都能验证通过,于是 Windows CryptoAPI 报告签名值良好,严格的 PDF 2.0 校验器却拒收文件。政策检查补上了这个缺口,而且它刻意与「签名字节在密码学上是否正确」这个问题分开
ISO/TS 32002 到底允许椭圆曲线签名用什么?
ISO/TS 32002 在 PDF 签名里恰好允许六条 ECDSA 曲线和两种 EdDSA 方案,并把每条曲线和它可携带的摘要尺寸绑定。PDFium Component 把这张表编码在 PadesCurveDigestAllowed 里,以签名者证书中的曲线 OID 为键。NIST 曲线很严:摘要必须与曲线同位宽,SHA-2 或 SHA-3 皆可。Brainpool 曲线宽松些,接受自身位宽或任何更宽的:
- P-256(
1.2.840.10045.3.1.7):仅 SHA-256 或 SHA3-256 - P-384(
1.3.132.0.34):仅 SHA-384 或 SHA3-384 - P-521(
1.3.132.0.35):仅 SHA-512 或 SHA3-512 - brainpoolP256r1(
1.3.36.3.3.2.8.1.1.7):256 到 512 位的任意 SHA-2 或 SHA-3 摘要 - brainpoolP384r1(
1.3.36.3.3.2.8.1.1.11):384 或 512 位的 SHA-2 / SHA-3 - brainpoolP512r1(
1.3.36.3.3.2.8.1.1.13):仅 SHA-512 或 SHA3-512
EdDSA 没有曲线可选、也没有摘要可选,这恰恰说明它的规则关乎编码而非强度。按 RFC 8419,Ed25519 的 SignerInfo 必须声明 SHA-512 为其 digestAlgorithm 且不带参数;Ed448 的 SignerInfo——在 PAdES 永远走的 signed-attributes 路径上——必须声明 id-shake256-len(2.16.840.1.101.3.4.2.18)并带恰好 512 的 INTEGER 参数。两种方案的签名 AlgorithmIdentifier 和证书公钥 AlgorithmIdentifier 都必须完全不带参数。在此处写 NULL 的生成方——RSA 编码器把这个习惯训练进了许多 ASN.1 库——产出的签名不合规,哪怕密钥和签名值都没问题
PDFium Component 如何从 CMS 中提取算法三元组
PDFium 本身回答不了这个问题,因为它的公开签名 API 只读签名字典,既不验证 CMS,也不暴露签名者证书的曲线。因此是构建在 PDFium 之上的 PAdES 检查层自己解析 CMS SignedData(RFC 5652)。InspectPadesSignatureAlgorithm 读取第一条 SignerInfo 的 digestAlgorithm 和 signatureAlgorithm,然后找到签名者证书、读它的 SubjectPublicKeyInfo 拿到密钥算法和曲线。证书查找刻意设了界:CMS certificates 集合最多检查 64 张证书,匹配是对 issuerAndSerialNumber 里颁发者与序列号的精确字节比较,只有集合里恰好只有一张可解析证书时才回退到「就用那唯一一张」。从无序集合里挑第一张 EC 证书很容易,但那等于让一张 CA 证书决定签名者「应该」用的曲线
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 在合规检查 pass 里执行这套政策,从它在 CMS 里定位到的证书出发;TPdf.ValidatePadesTrust 则针对 Windows CryptoAPI 验证时实际使用的签名者证书重跑一遍,所以 CryptoAPI 报告的证书拥有最终决定权。所有原始输入都落在 TPadesSignatureAlgorithmInfo 里,包括 DigestParametersPresent、DigestParameterBits、SignatureParametersPresent 和 PublicKeyParametersAreNamedCurve,拒收永远可以从记录本身解释,而不必翻日志
P-256 签名配 SHA3-256 为什么过不了政策?
只要 CMS 的 digestAlgorithm 与 ECDSA signatureAlgorithm 暗示的摘要不一致,P-256 签名就过不了 PDFium Component 的政策——哪怕两者单独看对这条曲线都可接受。EvaluatePadesSignatureAlgorithm 先把 ecdsa-with-SHA256、ecdsa-with-SHA3-256 和同族标识映射成摘要,与声明的 digestAlgorithm 比对,查曲线表之前任何差异都返回 pcsInvalid。这个案例是真实存在的:签名工具把哈希切到 SHA3-256,却留着硬编码的 ecdsa-with-SHA256 标识,产出的文件任何合规验证器都无法一致地解读。这个函数是公开的,所以矩阵可以不建 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); // 摘要不匹配
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,此时一致
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // 曲线不在 profile 内
end;
曲线编码执行同样的严格度。RFC 5480 §2.1.1 允许 ECParameters 是命名曲线 OID、隐式曲线(NULL)或完整的显式参数集,而 PKIX profile 要求命名形式。id-ecPublicKey 证书若无参数、带隐式参数或显式参数,PDFium Component 返回 pcsInvalid,因为显式参数让攻击者可以描述一条只是长得像标准曲线的曲线。命名正确、只是不在 ISO/TS 32002 清单里的曲线——比如上面的 brainpoolP160r1 或 secp256k1——得到的则是 pcsUnsupported
invalid、unsupported 还是 indeterminate:诚实地读状态
AlgorithmPolicyStatus 的三个非 valid 状态含义各不相同,把它们折叠进同一个「失败」桶,等于丢掉审计者需要的信息。pcsInvalid 意味着可识别的算法组合畸形或不匹配;它会往 TPadesValidationResult.Issues 加 ppeiSignatureAlgorithmMismatch,并把汇总的 IntegrityStatus 压到 pcsInvalid,于是即使 CMS 签名值本身验证无误,IsCryptographicallyValid 也返回 False。pcsUnsupported 意味着曲线或摘要不在 profile 点名的范围内,这是能力判定,不是篡改证明。pcsIndeterminate 意味着没能钉住签名者证书,通常是 CertificateSet 里有多个候选、又没有精确的 issuerAndSerialNumber 匹配,代码因此拒绝猜测曲线;从 v3.124.0 起,它还标记对 SHA-1 或 SHA-224 这类 112 位摘要的 RSA 签名——这类摘要已不再被现行验证认可。同一套划分也适用于 CryptoAPI 验证不了 Ed25519 或 Ed448 的机器上的 EdDSA:CmsSignatureStatus 停在 pcsUnsupported,而 AlgorithmPolicyStatus 仍可以是 pcsValid,因为编码是对的、缺的只是验证器。如果你在追 Adobe 或基于 DSS 的校验器给出的拒收,校验器为何拒收 PAdES 签名指南覆盖了其余常见原因
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; // 离线,不做吊销检查
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 不保证什么?
AlgorithmPolicyStatus = pcsValid 只证明 ECDSA 或 EdDSA 签名用了获批曲线加一个一致且编码正确的摘要,RSA 签名用了现行套件中的摘要;它对签名值本身是否正确只字未提。v3.124.0 之前,EvaluatePadesSignatureAlgorithm 的 RSA 分支刻意放得很宽:PKCS #1 arc 1.2.840.113549.1.1.* 之下的任何 signatureAlgorithm 都返回 pcsValid,包括老掉牙的 sha1WithRSAEncryption。从 PDFiumPas v3.124.0 起,RSA 分支改用 ETSI TS 119 312 签名套件:MD2、MD4 和 MD5 摘要判 pcsInvalid;SHA-1 和 SHA-224 这类 112 位摘要判 pcsIndeterminate,所以 SHA-1 签名保留整体完整性结论、被标记待人工复核而不是直接拒收。与签名算法固定的摘要不一致的 digestAlgorithm——比如 sha256WithRSAEncryption 配 SHA-1 摘要——或非 RSA 签名者密钥上的 RSA 签名算法,判 pcsInvalid、以 ppeiSignatureAlgorithmMismatch 上报并判完整性失败。找不到签名者证书给 pcsIndeterminate(ECDSA 一直如此),无法识别的摘要或非签名的 RSA OID 给 pcsUnsupported。模长仍然不检查;PSS 参数不在这里验证(RFC 4055 RSASSA-PSS-params 一文讲了签名侧如何编码它们);SHA-1 或 MD5 摘要还会额外触发独立的 ppeiBadDigestAlgorithm 问题。同样,签名数学、证书链和吊销仍是 CmsSignatureStatus、CertificateTrustStatus 和 RevocationStatus 的职责,它们来自 Windows CryptoAPI。把 pcsValid 当作「算法 profile 成立」,永远别当作「这把密钥够强」
对接收签名 PDF 发票、合同或归档包的 Delphi 应用,实用配置很短:跑 ValidatePadesTrust,遇 ppeiSignatureAlgorithmMismatch 拒收,pcsUnsupported 和 pcsIndeterminate 转人工,并自己执行 RSA 密钥长度下限——政策不会替你把这道关。PDFium Component for Delphi and Lazarus 附带 PAdES 校验器、证据报告生成器和签名管线,同一个库既能产出这些签名,也能端到端地检查它们