为了使已签名的 PDF 在数年后(在签名证书过期且其 CA 轮换后)仍能进行验证,PDFium 组件可以在 Windows 上生成长期 PAdES 签名:用于可信时间的 RFC 3161 时间戳、嵌入验证材料的文档安全存储(Document Security Store)以及覆盖整个文件的存档 DocTimeStamp。这三项添加是 PAdES B-T、B-LT 和 B-LTA 基线级别,PDFium 组件通过单次签名调用即可达到所有这些级别。这是一项仅限 Windows 的功能,受 PadesCryptoAvailable 限制
本文是系列文章中的第三篇。第一篇 在 PDFium VCL 中使用 PAdES B-B 签署 PDF 涵盖了基础签名;第二篇 检查 PDF 数字签名和 PAdES 级别 涵盖了读回签名并判断其达到的级别。这里关注的是存档:签名必须在证书过期后存续,并且在以年(而不是周)为单位衡量的保留期内保持可验证性
为什么有效的 PDF 签名会随着时间推移而停止验证?
基础签名只回答了一个问题:这些字节在签名后是否被更改过。它本身并不能证明签名发生在何时,而这一差距正是后来破坏它的原因。CMS 结构携带了签名时间属性,但签名者是根据自己的时钟写入该值的,因此验证器没有理由信任它。当签名证书稍后过期或 CA 将其吊销时,严格的验证器就无法再将证书有效时进行的签名与证书失效后伪造的签名区分开来。签名在制作当天是没有问题的,却会因为非自身的原因而变得无法验证
长期存档通过两个步骤解决这个问题。首先,可信第三方(时间戳权威机构)通过密码学方式确认时间,因此它不再依赖于签名者的诚实。其次,构建和检查信任链所需的每个证书、吊销响应和 CRL 都嵌入在 PDF 本身内部,因此验证不依赖于在任何人查看时可能已经消失的服务器。标准化为 ETSI EN 319 142-1 的 PAdES 将这些分层为基线级别,而 PDFium 组件则写入每个级别所需的结构
什么是 LTV 或存档时间戳 PDF 签名?
PAdES 定义了四个相互构建的基线级别,PDFium 组件通过 TPadesLevel 枚举 (plB_B, plB_T, plB_LT, plB_LTA) 暴露它们。B-B 是普通签名。B-T 在签名值上添加了可信时间戳。B-LT(长期)添加了嵌入式验证材料,即大多数人所说的“LTV”属性。B-LTA(带有存档时间戳的长期)将所有内容包装在覆盖整个文件的另一个时间戳中,因此嵌入材料本身自存档时刻起是可证明完好无损的
实际的解读是一个阶梯。B-T 证明了“何时”。B-LT 证明了“使用什么”仍可以检查签名。B-LTA 证明了整个包自密封以来未被篡改,它是您要更新的级别(在之前的时间戳过期前添加一个全新的 DocTimeStamp,以便无限期地延长存档)。对于具有长期保留义务的合同或合规记录, B-LTA 是目标,B-LT 是务实的底线
添加可信时间:带有 RFC 3161 的 PAdES B-T
PDFium 组件通过在 TPadesSignOptions 上设置两个字段,将 B-B 签名转为 B-T:将 Level 提升为 plB_T 并提供 TsaUrl。当两者都存在时,签名管道使用 SHA-256 对原始签名字典字节进行哈希处理,将该摘要打包为 RFC 3161 的 messageImprint,通过 WinHttp 向 TSA 发送 POST 请求,并将返回的令牌嵌入为 SignerInfo unsignedAttrs [1] 中的 signature-time-stamp 无签名属性(OID 1.2.840.113549.1.9.16.2.14)(根据 EN 319 142-1 §6.3)。时间戳专门覆盖签名值,这正是将可信时间绑定到该确切签名上的原因
uses
PDFium, FPdfPades;
procedure SignWithTimestamp;
var
Pdf: TPdf;
Options: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
if not Pdf.PadesCryptoAvailable then
raise Exception.Create('PAdES 签名后端仅限 Windows');
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F6071829304152637485960A1B2C3D';
Options.Level := plB_T;
Options.TsaUrl := 'http://timestamp.example-tsa.com/tsr';
Options.Reason := 'Contract execution';
// Nonce 保持为 0:该组件会派生一个唯一的防重放值
Pdf.SignPades('contract-bt.pdf', Options);
finally
Pdf.Free;
end;
end;
CertificateThumbprint 是当前用户“MY”存储中您可以使用的其私钥的证书的十六进制 SHA-1 哈希值。Nonce 字段是 RFC 3161 防重放值;将其保持为零,组件将根据计数器、时钟、滴答计数和进程 ID 的 SHA-256 折叠,为每个请求派生一个唯一的防重放值,以便在同一毫秒内发出的两个请求不会发生碰撞。仅在您拥有更愿意信任的密码学 RNG 时才传递您自己的值。一个客观的警告:B-T 需要一个可达的 TSA,因此现在签名需要进行 network 调用,并继承 TSA 的可用性和延迟
嵌入验证材料:一次调用实现 B-LT 和 B-LTA
向长期验证迈出的一步是单一更改:将 Level 设置为 plB_LTA 并保留 TsaUrl。然后,PDFium 组件在单次 SignPades 调用中运行完整的阶梯。它签署 B-B、为 B-T 添加时间戳、为 B-LT 注入文档安全存储并为 B-LTA 追加存档时间戳,每一项都作为其自己的增量更新,因此前面的字节按字节保持完好
procedure SignForArchive;
var
Pdf: TPdf;
Options: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F6071829304152637485960A1B2C3D';
Options.Level := plB_LTA; // 驱动 B-B -> B-T -> B-LT -> B-LTA
Options.TsaUrl := 'http://timestamp.example-tsa.com/tsr';
Options.Location := 'Head Office';
// 一次调用即可写入时间戳、DSS 和存档 DocTimeStamp
Pdf.SignPades('contract-lta.pdf', Options);
finally
Pdf.Free;
end;
end;
对于 B-LT 阶段,PDFium 组件使用 CertGetCertificateChain 构建证书链,并写入一个 /DSS 字典,其中在 /Certs 数组中携带签名者证书以及每个中间 CA(配有平行的 /OCSPs 和 /CRLs 数组),同时在 1 级带有 /Extensions /ESIC 标记,这完全符合 EN 319 142-1 §5.4.2 的规定。自签名根被刻意省略,因为 RFC 5652 §10.2.3 允许这样做,且根已经是验证器持有的信任锚点。结果是读者只需手头的文件即可验证签名
对于 B-LTA,组件会追加文档时间戳(Document Time-stamp):一个带有 /SubFilter /ETSI.RFC3161 的签名字典(其 /ByteRange 跨越整个文件,其 /Contents 在该范围内持有 RFC 3161 令牌),并将 ESIC 标记提升为 2 级。这就是 §5.4.3 和 TS 119 142-3 存档步骤。因为 DocTimeStamp 覆盖了 DSS 以及签名,所以它证明了嵌入的验证材料在存档时就已存在且未被更改,这正是长期保留策略必须能够展示的内容
增强已经存在的签名
有时签名已经存在于文档中,您只需要添加或刷新验证存储即可(例如为了将第三方的 B-B 签名提升为 B-LT 进行存档)。PDFium 组件通过 SaveAsPadesDss 直接公开了 DSS 注入器,它会追加 /DSS 和 /Extensions 结构作为增量更新,而不会触及现有的签名字节
procedure AddValidationStore;
var
Pdf: TPdf;
DssOpts: TPadesDssOptions;
Cert: TPadesDssMaterial;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'already-signed.pdf';
Pdf.Active := True;
DssOpts := TPadesDssOptions.Default;
DssOpts.EsicExtensionLevel := 1; // 1 代表 B-LT,2 代表 B-LTA
Cert.DerBytes := LoadSignerCertDer; // 您的 DER 编码证书
SetLength(DssOpts.Certs, 1);
DssOpts.Certs[0] := Cert;
Pdf.SaveAsPadesDss('already-signed-lt.pdf', DssOpts);
finally
Pdf.Free;
end;
end;
您提供验证器所需的 DER 编码的证书,以及可选的 OCSP 响应和 CRL。将 EsicExtensionLevel 设置为 1(对于 B-LT 存储)或 2(当后续会有 DocTimeStamp 时)。将 IncludeVri 保持在其默认值 False:EN 319 142-1 §5.4.2.3 将每个签名的 /VRI 字典视为可选的,并建议不要使用它,除非您针对的特定阅读器要求这样做
值得规划的成本和边界
三个客观的限制塑造了这些级别的适用场景。首先,密码学后端仅限 Windows:签名和添加时间戳通过平台 CNG 和 WinHttp 存储进行,因此 PadesCryptoAvailable 在其他地方返回 False,且 SignPades 会引发异常。配套文章中涵盖的读取端检查保持跨平台;只有签名的写入绑定到 Windows。其次,B-T 和 B-LTA 依赖于时间戳权威机构(TSA),这意味着签名时有网络往返,并且外部服务的运行时间和速率限制会成为您签名路径的一部分
第三,每个级别都需要存储成本。每个阶段都会追加增量更新,而不是重写文件,DSS 嵌入了完整的证书链,且存档 DocTimeStamp 为其令牌预留了空间,因此 B-LTA 文件明显大于 B-B 原文件。这是特意的权衡:您现在花费字节以购买未来的可验证性。对于大多数合同和合规存档来说,这是正确的权衡,但值得在您自己的文档上进行测量,而不是假设。当您需要确认完成的文件实际达到了您预期的级别时,使用 检查 PDF 数字签名和 PAdES 级别 中的技术对其进行验证,并且如果您正在更广泛地强化文档管道,关于 审计 PDF 安全风险 的说明涵盖了周围的检查
这里展示的 PAdES 签名、DSS 和存档时间戳功能都是适用于 Delphi 和 C++Builder 的 PDFium Component 的一部分,其产品页面携带了完整的签名参考和平台要求