技术文章

HotPDF 中的 PAdES LTV 证据与种子值

你刚签完的 PDF 只是一个 B-B 签名,仅此而已。它能证明谁签的名、字节没有被动过,却不携带任何证明签署者证书在签名时刻有效的材料,于是多年后的验证者只能去寻找可能已不复存在的吊销数据。补上这个缺口意味着把 OCSP 响应与 CRL 写入文档级的 Document Security Store,而在 HotPDF 里这只是一次调用:PopulatePAdESLTVEvidence 遍历每个已加载的签名,从证书集合推导吊销请求,经由你提供的传输层执行它们,再把取回的材料连同 CMS 链一起写入 DSS。它返回证据成功落地的签名数量,文档完全没有签名字段时返回负一

使用它之前值得理解的设计决策是:库绝不自己打开套接字。每一个从网络到达的字节都经过你写的回调。这不是为谨慎而谨慎;这是该功能能真正在那些要求长期验证的环境里工作的唯一方式

为什么库拒绝自己做 HTTP?

因为要求 B-LT 签名的场所,恰恰是网络不能托付给库的地方。签名服务运行在带企业根证书的认证代理之后。物理隔离的签名层没有到响应服务器的路由,必须喂缓存证据。审计制度要求每个出站请求都由应用记录在案,而不是埋进依赖里。测试套件需要确定性响应,如果库自行拨号就无从谈起

传输层是一个形状固定的普通函数引用,策略仍然归你。HotPDF 递给你一个请求记录,准确描述要取什么,包括内容类型与响应大小上限;你返回字节外加一个状态

HotPDF PopulatePAdESLTVEvidence 流程:调用方提供的 FetchEvidence 传输层、请求记录字段与逐签名状态结果
每个网络字节都经过你的 FetchEvidence 回调,每个签名有自己的状态,一次超时绝不会中断整轮处理
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind 说明这是 OCSP POST 还是 CRL GET;
    // Request.ContentType 与 Request.Body 已准备好,
    // Request.MaxResponseBytes 是你必须遵守的上限
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry 让重试策略退避;404 或坏 URL 用
      // setsPermanentFailure
      Result := setsRetry;
    end;
  end;
end;

// 对已加载文件中的每个签名做一次调用完成 B-B 到 B-LT 升级
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // 只追加保存:既有签名覆盖的字节
      // 原样保留
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

失败按签名计,不按文档计。为某签署者超时的响应服务器只跳过该签署者的材料,其余处理原样保留——这正是批处理想要的行为:部分证据好过中途作废,返回值则告诉你有多少签名真正得到了改善

CMS 忘了装进去的证书链

吊销检查需要颁发者证书,而相当多的签名技术栈在 CMS 容器里省略了中间证书。恢复路径是 Authority Information Access 扩展,访问方法 1.3.6.1.5.5.7.48.2,它公告一个可下载颁发者证书的 URL。HPDFFetchAIAIntermediates 经同一传输层遍历这些 URL,从每个响应解析 DER,只返回 CMS 尚未携带的证书,以 DER 哈希为键,重复与环路都转不起来

两个细节决定它在真实证书颁发机构面前是否管用。第一个是编码:CA 端点以裸 DER 提供证书的频率与以 PEM 盔甲提供相当,而没有任何可靠的内容类型可区分两者。稳健的探测是先文本后结构:寻找 -----BEGIN CERTIFICATE----- 标记,若存在则剥掉盔甲并解码 base64;两条路径都要确认结果首字节是 $30,即 SEQUENCE 的 DER 标签。第二个是深度:取回的中间证书自身可能为其颁发者公告 AIA URL,因此遍历会把新候选追加进队列,补全差两三跳的链。必须设上限,这正是 MaxFetch 参数的用途

HotPDF 的 AIA 证书链补全图:caIssuers URL 抓取、PEM 与 DER 探测、DER 哈希去重以及 MaxFetch 深度上限
HPDFFetchAIAIntermediates 经同一传输层遍历 caIssuers URL,探测 PEM 盔甲并用 MaxFetch 限制队列

什么是签名种子值,它为什么静默失效?

种子值(seed value)是文档作者附加在签名字段上的约束,用来告诉签署者什么样的签名可接受:哪个 SubFilter、哪个摘要算法、哪些理由、最低 PDF 版本是多少、是否必须内嵌吊销信息。它位于字段的 /SV 字典中,定义于 ISO 32000-1 §12.7.5.5。HotPDF 用 AttachPAdESSeedValue 写入它,用 CheckLoadedSignatureSeedValue 检查它:字段无约束或所有存在的约束都通过时返回 True,返回 False 时通过输出参数给出第一个未通过的约束,可直接放进错误消息

让种子值容易出错的是 §12.7.5.5.3 描述的 /Ff 标志条目。置位的比特把它的约束标记为必需:不匹配即错误,签署者必须拒绝。清零的比特把同一约束标记为偏好:该值只是过滤 UI 应提供什么,仅此而已。由此引出两个陷阱。第一,/Ff 位于 /SV 字典内部,不在部件注释(widget annotation)上,所以读取字段级 /Ff 的代码永远得到空答案,并断定没有任何东西是强制的。第二,比特分配不是 1、2、4、8 的简单连跑;HotPDF 的写入器为 SubFilter 发 2,MinVersion 发 4,AddRevInfo 发 32,DigestMethod 发 64。假定比特连续的读取器会把每个约束都解码成可选,通过除真正要紧那一项之外的每一个测试

HotPDF PAdES 签名的种子值标志位表:Ff 位 2、4、32、64,以及必需与偏好约束的处理
/Ff 条目位于 /SV 内部,每个比特位决定不匹配是硬性拒绝还是 UI 偏好
var
  Violation: AnsiString;
begin
  // 询问字段:我们即将使用的签名 profile 是否被允许
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // 约束满足:继续签名流程
end;

暴露最初解码 bug 的测试不是一个正向测试,而是一条断言:被强制的失配必须被拒绝。这也是唯一能抓住这类缺陷的测试:读错字典或读错比特位的解码器对任何输入都产出"没有违反约束",看起来与正确行为一模一样,直到你故意违反一条

它在 LTV 阶梯上的位置

四级阶梯,每级都需要它下面那级。B-B 是裸签名。B-T 加可信时间戳,固定签名时刻,让验证者知道该以哪个时间点评估吊销。B-LT 把吊销证据加进 DSS,这正是 PopulatePAdESLTVEvidence 自动化的部分。B-LTA 加文档时间戳并在前一个减弱之前续期,把有效性无限延长;HotPDF 以 RenewPAdESLTATimestamp 暴露它,以增量修订的形式追加新时间戳,且不碰任何更早的签名、时间戳与 DSS 条目

增量更新模型是向已签文档添加证据的唯一正确方式,因为重写文件会破坏既有签名覆盖的字节范围。如果你需要推理修订之间变了什么、以及那些变化是否属于签名允许的类别,该分析在DocMDP 与 FieldMDP 修订分析中单独展开。签名管线本身,包括证书来源与字节序陷阱,见PAdES 签名演练;验证一侧见验证已加载文档上的签名

关于时序有一条务实的警告。签名之后尽快收集证据,理想情况下在同一个作业里。能为证书作答的响应服务器在证书有效期内在线,多年后便消失无踪,所以一份以 B-B 状态离开你管线的文档可能永远失去升级机会。HotPDF 以原生 VCL 组件运行于 Delphi 与 C++Builder,除你自己的传输层外整个证据流程都在进程内;支持的 profile 列在 HotPDF Delphi PDF component 产品页