跳转到内容

正确验证签名

Spec: RFC 5280, §6 Spec: RFC 6960 Spec: RFC 5652 Evidence: Test-backed

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

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

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

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

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

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

完整性与真实性通过端到端测试覆盖。一张已知的证书会对一个真实的已签名属性结构进行签名,测试套件再使用匹配的公钥,在多个时间向量上验证该签名。因此,任何破坏这种规范结构的变更都会让测试失败。证书路径验证由一组测试固定:这些测试会刻意篡改一个签名字节,并断言结果为有效,且附带一个结构化原因——它不是被丢弃的异常,而是一条明确记录的失败。时间戳令牌的验证被拆解成数个离散步骤——解码、签名者信息、已签名属性、消息摘要、证书绑定、密钥用途、签名、产生时间(produced-at)——而每一个步骤都有独立测试,因此「时间戳通过验证」意味着每一个步骤都通过了验证。吊销的软性失败(无法连接的响应端)在代码和测试中都与明确的「已吊销」区分开。这两者绝不会被混为同一个答案。

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

Evidence: Test-backed 这项行为以测试作为依据,而这些测试实现了各项标准要求的内容。

完整性对应 Spec: ISO 32000-2, §12.8.1 :摘要会在字节范围上重新计算,并与存储的值比对,任何差异都意味着签名无效。针对已签名属性的真实性,由一项集成测试涵盖:它会对一组真实的已签名属性进行签名,并使用匹配的公钥跨多个时间向量加以验证。证书路径问题对应 Spec: RFC 5280, §6.1 :有效的路径始于一个信任锚点,而 Spec: RFC 5280, §6.2 指出,该算法定义了路径被视为有效所需的最低条件——一项路径验证器单元测试会断言:一个被篡改的签名会产生 valid = false,并附带一个明确的原因,绝不会默默接受。

吊销的检查顺序对应 Spec: RFC 6960, §3.2 :在客户端将一个已签名的吊销响应视为有效之前,它「应当」(SHALL)先确认该响应自身的签名为有效,且签名者当前已获授权——而 Spec: RFC 6960, §4.2.2.2 将该授权定义为由相关 CA 直接签发的 id-kp-OCSPSigning 委派授权。因此,一个本身尚未针对已获授权且可验证的签名者完成验证的吊销答案,是毫无意义的。证书绑定检查对应 Spec: RFC 5035, §5.4.2 :如果已签名的 signing-certificate-v2 属性中的证书哈希,与用来验证签名的证书不相符,那么该签名必须被视为无效。这阻断了替换漏洞——也就是签名针对一张由攻击者选择的证书通过验证的情况。时间戳令牌本身会以 Spec: RFC 5652 的方式,作为一个 CMS 对象逐步验证,每一个步骤都有独立测试。

真正有启发性的,不是某一次 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
Edition Availability
Core

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

Pro

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

Enterprise

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

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