跳转到内容
getnextpdf.com

正确验证签名

Spec: RFC 5280, §6Spec: RFC 6960Spec: RFC 5652

“签名有效”通常只表示检查了一件事:密码学计算是否匹配。一次正确的验证至少要检查五件彼此独立的事;其中任何一件出错,都可能让那个绿色对勾失去意义。本页列出完整的检查项,并说明为什么不完整的答案会带来风险。

在这个主题中,单一布尔值是最危险的输出。它会让读者误把“有效”当成“值得信任”;然而“有效”可能只代表那些字节未经篡改,而签名所用的密钥,来自一张三年前就过期、上个月遭到吊销、且链接不到任何你认可的机构的证书。上述每一项都是独立检查。返回单一布尔值的软件,已经默默替你决定了哪些检查才重要,而且这个决定是替你做的。在受监管或具有合同效力的场合,如果工具只验证了成本最低的那项属性,“工具说它有效”并不能作为辩护。

一次完整的验证会回答五个各自独立的问题。它们彼此独立——通过其中一项,并不能说明其他项的任何情况:

  1. 完整性——对已签名的字节计算哈希后,是否仍与签名所涵盖的值相符?(重新计算字节范围摘要,再进行比对。)
  2. 真实性——密码学签名是否能使用签名证书中的公钥,针对已签名属性完成验证?
  3. 证书路径——该证书是否能链接到所选择的信任锚点,且每个环节都有效?
  4. 时间——在相关时间点,证书是否处于其有效期内,而那个时间是否属于受信任的时间、而非自行声称的时间?
  5. 吊销——在当时,证书是否未遭吊销,且相关证据(OCSP/CRL)是否是你实际能够获取的,或已经内嵌在文档中?

一个未执行全部五项检查的“有效”,是一个看起来像完整答案的不完整答案。

NextPDF 的立场是:每一个问题都各自独立,且每一个都必须得到明确回答。绝不把一个问题压缩成单一的乐观标记,也绝不因为某项检查不方便而默默跳过。这一点由测试强制保证。这正是本页被标示为由测试支撑(test-backed)、而非由标准支撑(standard-backed)的原因:这项行为由测试套件固定,而不仅仅是从某条条款推导而来。

完整性与真实性通过端到端测试覆盖。一张已知的证书会对一个真实的已签名属性结构进行签名,测试套件再使用匹配的公钥,在多个时间向量上验证该签名。因此,任何破坏这种规范结构的变更都会让测试失败。证书路径验证由一组测试固定:这些测试会刻意篡改一个签名字节,并断言结果为有效,且附带一个结构化原因——它不是被丢弃的异常,而是一条明确记录的失败。时间戳令牌的验证被拆解成数个离散步骤——解码、签名者信息、已签名属性、消息摘要、证书绑定、密钥用途、签名、产生时间(produced-at)——而每一个步骤都有独立测试,因此“时间戳通过验证”意味着每一个步骤都通过了验证。对于签名时间戳而言,其中有一个步骤既非可选、也非通用:令牌的消息压印(message imprint)必须绑定它所声称涵盖的那个签名——令牌内部的哈希必须等于签名者自身签名值的哈希,否则该令牌所证明的便是另一批不同的字节。NextPDF 将此实现为一项专门的 verifySignatureTimestamp 检查,它以签名值作为必需输入,因而无法被跳过;并在遇到空输入、无法识别或弱(SHA-1)的压印算法、格式错误的压印,或哈希不匹配时,一律判定为失败(fail closed)。吊销的软性失败(无法连接的响应端)在代码和测试中都与明确的“已吊销”区分开。这两者绝不会被混为同一个答案。

  1. IntegrityRecompute the byte-range digest and compare it to the value the signature covers.
  2. AuthenticityVerify the cryptographic signature against the certificate’s public key, over the signed attributes — not the raw content.
  3. Certificate pathBuild and validate the chain to a trust anchor you chose; every link’s signature, validity, and constraints must hold.
  4. TimeConfirm the certificate was valid at the relevant instant, and that the instant is trusted time, not the signer’s clock.
  5. RevocationConfirm the certificate was not revoked at that time, using obtainable or embedded OCSP/CRL evidence.
一次正确的 PDF 签名验证所依序回答的五个独立问题。每一项都各自独立:通过其中一项,并不能说明其他项的任何情况,而跳过任何一项都会让整体结果变得不完整。

真正有启发性的,不是某一次 API 调用,而是在你依据某个结果采取行动之前,你必须能够回答的那些问题。请把它当作审查会用来核查你的清单。

<?php
declare(strict_types=1);
// A correct validation produces a structured outcome, not one boolean.
// Before you trust a signature, you must be able to answer ALL of these:
//
// integrity : Does the byte-range digest still match? (tamper check)
// authenticity: Does the signature verify over the SIGNED ATTRIBUTES,
// not just the content?
// path : Does the certificate chain to a trust anchor YOU chose,
// with every link valid at the relevant time?
// time : Is the relevant time TRUSTED (a timestamp), or merely the
// signer's self-asserted clock?
// revocation : Was the certificate not revoked at that time, by evidence
// you obtained or that the document embedded?
//
// "valid: true" without an answer to every line above is an incomplete
// result. A path-validation outcome carries a `valid` flag AND a structured
// `reasons` list precisely so a failure says WHY — never a bare false.

如果其中任何一行的答案是“我不知道”,那么诚实的状态就不是“有效”,而是“尚未确定”——把这两者当成同一回事,正是本页要防止的错误。

陷阱在于把“密码学上有效”等同于“值得信任”。完整性与真实性合在一起,只能证明这些字节确实由持有这把密钥的人签名。至于这把密钥的证书是否受信任、是否仍在有效期内、或是否未遭吊销,它们都只字未提。使用自行生成的证书签名的文档,可以是“密码学上有效”却一文不值。相反的陷阱,则是把一个无法确定的吊销检查(响应端离线)当成通过——或当成失败。两者都不是。它是未知的,而一个正确的验证器会如实返回未知,而不是向任一方向猜测。一个隐藏了五项检查中实际执行了哪些检查的绿色对勾,并不是验证结果。它是别人替你做的一个决定。

NextPDF 执行并测试结构性和密码学检查。它不会替你选择信任锚点,也不保证建立在其上的策略。你信任哪些证书,是引擎无法替你做出的部署决策。一条验证到某个你本不该信任的锚点的链,仍然是一个你无法依赖的验证结果。吊销证据只有在可获取或已内嵌时才能被检查。一个离线的响应端会产生“无法确定”,而把它转换成一个判定结果是一项策略选择,而非引擎的选择。本页描述的是检查项,而非法律上的充分性。一个通过验证的签名是否具有某种特定的法律效力,取决于证书、签名者、司法管辖区,以及相关义务。内嵌证据如何让这些检查在多年后仍能被回答,在 长期验证 中有所说明;完整性检查背后的字节范围机制,则收录于 签名如何置于 PDF 中

验证接口的方案版本可用性:

Signature validation checks — edition availability
EditionAvailability
Core

覆盖针对已签名属性的完整性与真实性,外加 RFC 5280 §6 针对所提供信任锚点的证书路径验证。

Pro

新增 RFC 3161 时间戳令牌验证,也就是把受信任时间这个问题拆解为可独立检查的多个步骤。

Enterprise

新增吊销评估(OCSP/CRL),以及针对内嵌长期素材的验证,并将无法确定的结果与明确的结果区分开。

  • 完整性检查——重新计算字节范围摘要,并将它与签名所涵盖的值比对。
  • 真实性检查——使用签名证书的公钥,针对已签名属性验证该密码学签名。
  • 已签名属性——签名实际用于计算的、经过鉴别的 CMS 属性(content-typemessage-digestsigning-timesigning-certificate-v2)。
  • 证书路径验证——构建并检查从签名证书到所选信任锚点的链(RFC 5280 §6)。
  • 信任锚点——你决定要信任的一个证书颁发机构;一条可接受路径的根。
  • 吊销检查——通过 OCSP 或 CRL,判定一张证书在相关时间点是否曾遭吊销。
  • 无法确定——一个既非“良好”也非“已吊销”的吊销结果,因为证据无法获取;既不算通过,也不算失败。
  • 压印绑定(签名时间戳)——一项检查,用于确认签名时间戳的消息压印等于它所涵盖的签名者 SignerInfo 签名值的哈希,从而使该时间戳可被证明适用于那个签名,而非另一批不同的字节。