跳转到内容

PDF 中签名的存在方式

Spec: ETSI EN 319 142-1 Spec: RFC 5652 Evidence: Standard-backed

PDF 签名并不是包在文件外层的封套。它嵌在文件内部:一个用于描述签名的字典,以及一段针对所声明字节范围计算出的摘要;该范围会有意跳过签名值本身。本页说明这套机制,也同样说明它没有承诺什么。

“这份文件已签署”这句话会让人采取行动。他们可能把它关联到一笔付款、一次批准、一项法律义务。如果你无法精确知道某个签名覆盖哪些字节,就无法说明一个有效结果究竟证明了什么。一份 PDF 可以带有完全有效的签名,却仍向读者显示签署者从未见过的内容,因为这些内容是在签署之后加入到了签名从未声明覆盖的区域。明确签名权威性的起点和终点,正是有依据的决策与只能寄希望于侥幸的决策之间的差别。

  • PDF 签名位于文件内部的签名字典签名字段之中,而不是外部封套。
  • 已签署的字节由 ByteRange 数组声明:两段 (offset, length) 区段共同覆盖整个文件,唯一排除的是存放在 Contents 条目中的十六进制签名值。
  • 这两段拼接数据的摘要,正是密码学签名实际保护的对象。
  • 任何稍后在新修订版中附加的内容,都位于原始字节范围之外。原始签名仍然有效;它从未对那些新字节做出任何声明。
  • 核准签名与认证签名的范围有所不同:认证(DocMDP)会限制后续允许哪些变更;核准则不会。

NextPDF 按照格式的设计方式,以固定顺序构建签名,使字节范围保持精确,而不是近似值。

当引擎写入签名时,它会先为 Contents 值预留一个固定大小的槽位,并写入一个固定宽度的 ByteRange 占位符。它会等到整份文件写入完成——包括交叉引用表和文件结尾标记。只有到这时,它才会计算两个真实偏移量、 在不移动任何字节的情况下将其写回占位符、对这两段数据进行哈希,并将生成的 CMS 对象放入预留的槽位。这个占位符会用零填充到固定长度,目的正是在填入真实数字时不会移动正在被哈希的那些字节。只有这种顺序能产生自洽的签名。引擎会将此序列中的任何失败视为硬错误,而不是静默回退。

对于 PDF 2.0 配置文件,签名对象本身是一个分离式 CMS SignedData 结构。PDF 字典说明位置方式;CMS 对象则承载由谁签署以及密码学证明。

  1. Step 1 of 4: ISO 32000-2 §12.8.1 — ByteRange digest & signature dictionary
  2. Step 2 of 4: ISO 32000-2 §12.8.3.3 — ETSI.CAdES.detached SubFilter
  3. Step 3 of 4: ETSI EN 319 142-1 PAdES baseline profile
  4. Step 4 of 4: RFC 5652 CMS SignedData in Contents
PDF 签名定义之处,从容器格式一路向下到加密对象:ISO 32000-2 规范了字典与字节范围机制,ETSI EN 319 142-1 将其设定为 PAdES 配置文件,而 RFC 5652 则定义了置于 Contents 中的 CMS SignedData 对象。

Evidence: Standard-backed 这套机制定义于 Spec: ISO 32000-2, §12.8.1 。字节范围摘要会针对 ByteRange 项目所指示的字节范围计算。该范围应覆盖整个文件,包含签名字典但排除 签名值——也就是 Contents 条目。ByteRange 是一个由整数配对组成的数组——起始偏移量与长度。采用不连续范围正是为了让摘要跳过签名值本身。

对于 PDF 2.0 配置文件, Spec: ISO 32000-2, §12.8.3.3 规定,当 SubFilterETSI.CAdES.detached 时,Contents 值是一个 DER 编码的 CMS SignedData 对象——与 Spec: RFC 5652 所定义的对象相同——而该对象的 PAdES 配置文件正是 Spec: ETSI EN 319 142-1 所描述的配置文件。

不同签名的范围并不一定相同。 Spec: ISO 32000-2, §12.7.4.5 定义了 MDP 权限:值为 0 时,该签名为核准签名;值为 13 时,则使其成为认证签名,会限制后续哪些修改仍能让文件保持合规。字节范围机制相同;对未来变更的承诺却不同。

NextPDF 引擎的实现正是如此:使用一个固定宽度的 ByteRange 占位符、两段拼接数据的摘要,以及一个放在预留 Contents 槽位中的分离式 CMS 对象;所有这些都只会在文件完成后最终确定。

你很少需要手动构建 ByteRange。这个示例的重点是展示结果的形状,方便你在查看已签署文件时识别它。

<?php
declare(strict_types=1);
use NextPDF\Security\Signature\ByteRangeCalculator;
// Offsets the engine knows only after the whole PDF is written:
// $contentsStart — byte just before the '<' of the hex signature
// $contentsEnd — byte just after the '>' that closes it
// $fileLength — total file size in bytes
$range = ByteRangeCalculator::calculate(
contentsStart: $contentsStart,
contentsEnd: $contentsEnd,
fileLength: $fileLength,
);
// $range === [0, $contentsStart, $contentsEnd, $fileLength - $contentsEnd]
// Segment 1: file start → just before the signature value
// Segment 2: just after the signature value → end of file
// The signature value itself is the gap. It is never hashed.
$signedMessage = ByteRangeCalculator::extractSignedData($pdfBytes, $range);
// $signedMessage is segment 1 concatenated with segment 2 — exactly the
// bytes the cryptographic digest is computed over.

两段之间的空隙就是签名值。它不能成为自身摘要的一部分,这正是范围必须分成两段而不是一段的原因。

常见陷阱,是误以为有效签名意味着你正在查看的整份文件就是当初被签署的内容。并非如此。它只意味着所声明范围内的字节完好无损。后续修订版可以按规则将内容附加到该范围之外——第二个签名、表单数据、验证材料。第一个签名仍然有效,但它没有对这些新增内容作出任何声明。正确的查看器会告诉你,某个签名覆盖的是“签署当时存在的那份文件”,而不是“屏幕上的每一个字节”。把这两者视为一回事,正是让已签署文件承载看似已签署、实则未签署内容的途径。

本页说明的是结构,而不是信任。格式正确的 ByteRange 与 CMS 对象会告诉你字节完好无损,以及是哪把密钥签署了它们。但它们本身并不会告诉你那把密钥是否属于你所认为的那个人、其证书在签署时是否有效,或它是否在事后被撤销。那属于证书路径与撤销检查的工作,涵盖于 正确验证签名。 本页也不讨论签署在何时发生是否有任何独立权威佐证。自我声明的签署时间不是受信任时间—— 请见 时间戳与受信任时间。 NextPDF 构建此处所述的结构;证书、信任锚点、 以及时间戳权威机构则由你的部署环境提供,而不是由引擎提供。

引擎按方案层级交付的是结构构建能力:

PAdES signature structure (byte range, signature dictionary, detached CMS) — edition availability
Edition Availability
Core

PAdES B-B:签名字典、固定宽度的 ByteRange,以及本页所述的分离式 CMS SignedData 对象。

Pro

新增 PAdES B-T——对签名值附加受信任时间戳——叠加于相同的结构之上。

Enterprise

新增长期配置文件(B-LTB-LTA):内嵌的验证材料与文件时间戳,叠加于相同的字节范围基础之上。

  • 增量更新及其重要性 ——为什么采用附加而不是重写,是让第一个签名的字节范围保持完好的关键。
  • PAdES 基线配置文件 ——哪些内容会叠加在这个结构之上,以及某项义务需要哪种配置文件。
  • 长期验证 ——验证证据如何被嵌入,使签名在多年后仍可验证。
  • 签名字典 —— 包含签名处理程序、SubFilterByteRangeContents 值的 PDF 字典。
  • ByteRange —— 一个由 (offset, length) 整数配对组成的数组,声明签名摘要所涵盖的确切字节。
  • Contents —— 存放签名值的十六进制条目(就 PDF 2.0 而言,是一个分离式 CMS SignedData 对象);它被排除在自身摘要之外。
  • CMS SignedData —— 加密消息语法(RFC 5652)结构,承载签署者的证书与签名字节。
  • PAdES —— PDF 高级电子签名:ETSI 为 PDF 制定的 CMS 签名配置文件,定义于 ETSI EN 319 142 系列标准中。
  • 核准签名 —— 带有 MDP 权限 0 的签名;它声明内容,但不限制后续变更。
  • 认证签名 —— 带有 DocMDP 权限(MDP 13)的签名,会限制后续哪些修改仍能让文件保持合规。