签名验证报告

OfdDoc.verifySignatures(trustedKeyHex?, anchors?, crls?) 返回 SigReport[](v0.18.0 起支持 CRL 离线吊销检查),每个元素对应文档中的一份签名。

三层结论

一份签名的可信度分三层呈现——不要只看一个布尔值:

层 字段 回答的问题
签名值 signatureValid 签名是否由声明的公钥生成(SM2 对 TBS 校验)
形态 unsupportedFormat 签名值形态本内核无法解析——「签名值无效」结论不适用。v0.18.0 起 CMS/PKCS#7(SM2 国密变体 + SHA 族 RSA)已支持真验签,政务签章不再落入此态
文档完整性 digestValid 签名保护的内容是否被改动(dataHash / References 逐文件比对)
身份可信 chainTrusted / chainStatus 签名者证书是否可信(链校验 + 信任锚命中)

常见组合:

  • signatureValid ✓ digestValid ✓ → 徽章显示「有效」(文档未被修改且签名真实)
  • signatureValid ✓ digestValid ✗ → 「文档已被修改」
  • signatureValid ✗ → 「签名值无效」
  • 链为 no-anchor → 前两层可信,但未配置信任锚,无法确认身份——这是诚实的中间态
  • signatureValid ✓ digestValid ✓ 但未配置信任锚/信任公钥 → 只能证明"未被修改且与内嵌证书 一致";内嵌证书是签名方自带的(可自造自签),signatureValid 不能单独作为可信结论, 集成方应以 chainTrusted 为准

字段全表

字段 类型 说明
index number 签名序号(从 0 开始)
signer string? 签名人(SES 内声明的显示名)
signTime string? 签名时间(SES 记录,如 20200723130907.763Z)
signatureValid boolean 签名值是否有效
digestValid boolean 文档摘要是否一致
unprotectedFiles string[] 签名后新增的未保护文件(注释/附件等合法增补)。只作提示,不影响 digestValid——签名后的增补工作流与篡改分级呈现
usedEmbeddedCert boolean 是否用内嵌证书验签(v0.18.0:显式传入 trustedKeyHex 时恒为 false——签名值只按传入公钥判定)
warnings string[] 附加告警(v0.18.0):如「内嵌证书自验通过,但其公钥与传入的信任公钥不一致」——显式信任优先语义下的判定依据说明
timestamp object | null RFC 3161 时间戳验证(v0.18.0):{ present, verified, genTime, message },CMS unsignedAttrs 携带 TimeStampToken 时给出;verified=true 时 genTime 为 TSA 可信时间(不可被签名者自报操纵),并作为报告的 signTime
sesVersion 1 | 4 | null SES 版本;null 为纯数字签名
chainStatus string 机器可读结论(见下表)
chainLabel string 中文短标签(可直接上界面)
chainTrusted boolean 链是否受信任(命中锚且逐跳通过)
chainPath string? 链上主体路径(A ← B ← C)
chainErrors string[] 链校验失败原因/警告
anchorSubject string? 命中的信任锚主体
certSubject string? 签名证书主体(CN=…, O=…, C=CN)
certIssuer string? 签发者
certNotBefore / certNotAfter string? 证书有效期
message string? 失败说明(成功为 null)
references {file, ok}[] 逐文件摘要比对结果

chainStatus 取值

值 含义
trusted 链顶命中信任锚,逐跳校验通过
untrusted-root 链顶不是给定锚
incomplete 链不完整(缺签发者证书),无法到达锚
bad-signature 上级签名无效
expired 按签名时间证书已过期/未生效
not-ca 上级证书不是 CA
unsupported 签名算法不支持
no-cert 签名没有内嵌证书(需 trustedKeyHex)
revoked 签名者证书被 CRL 列入吊销(离线 CRL 输入,v0.18.0)
no-anchor 未配置信任锚(只验了签名值与摘要)

信任锚与证书链配置(以税务监制章电票为例)

电票(数科、百望等厂商的 OFD 发票)通常只嵌签名者证书,监制章证书的上级 (如「税务电子证书管理中心 (SM2)」中级、税务 SM2 根)不在文件内——不配锚时 链结论为 no-anchor 或 incomplete,这正是「组件验签报信任锚缺失」的原因。 配锚要点:

  1. 锚列表同时充当链上证书来源:anchors 里可以(也应该)把中级与根 证书一并喂入——链校验会用其中主体匹配的证书补全中间环节,只有链顶命中 锚集合才算受信任。只喂中级不喂根时,中级本身即被当作锚(见下)。
  2. anchors 接受 PKCS#7 证书包(.p7b / .p7c):各 CA 官网发放的根 证书包常见此格式——PEM 块(-----BEGIN PKCS7-----)原样文本传入,DER 文件转 base64 后传入,包内全部证书自动展开,无须先在系统证书管理器里 手工导出 PEM。
  3. 拿不到根证书时的逃生门:把可信的中级 CA 证书直接作为锚——从一张 已知真实的同类发票的 SES 证书集中提取中级(v0.14.0 起 SES 内嵌的全部证书 ——含嵌在非标准位置的——都会进链候选),或在受控环境导出后固化到应用里。 这等价于「信任该 CA 签发的所有监制章证书」,适合无法获取根的场景。
  4. 算法边界:链校验要求各级证书签名算法为 SM3withSM2(国密链)。全国 查验平台 taxca.p7b 实测 只含 RSA 体系两张(税务电子证书一级根 + 税务电子证书管理中心,2006/2008 年签发)——喂入后 SM2 链仍走不通(unsupported),SM2 根需从增值税发票 阅读器安装包、开票软件或税务数字证书渠道获取。
  5. 离线吊销检查(v0.18.0):verifySignatures(..., crls) 第三参或 CLI verify --crl <文件> 接受 X.509 CRL(PEM 可多块或 base64 DER);CRL 须由签发者证书(信任锚/签名包证书集)验签后才被采信,命中吊销 → chainStatus="revoked"。OCSP 与联网吊销仍不在范围(本库不发起网络请求)。签名者证书的 keyUsage 若明确不含 digitalSignature/nonRepudiation,会在 chainErrors 中给出「不具备签名用途」告警(不影响链结论,真实生态证书扩展质量参差)。
  6. 时间戳边界:v0.18.0 起可验证 CMS SignedData unsignedAttrs 中携带的 RFC 3161 TimeStampToken(校验 messageImprint 与 TSA 签名,timestamp.verified=true 时 genTime 作为可信时间);不提供 TSA 签发服务,也不联网取时——签名者自报时间仍只作参考,不能替代 TSA。
  7. 费用边界:本库验签为纯本地免费计算——SES/CMS 解析、SM2/RSA 验签、摘要比对、 证书链校验、离线 CRL 与内嵌时间戳校验全部离线完成,不调用任何在线验签 API、CA 实名认证或 TSA 服务。 业界的付费能力(发票通/翔云等在线验签 API 按次计费、e签宝/法大大等签署 平台约 1 元/次起、签章服务器等密码设备、在线 TSA)均不在范围内,也不需要 它们参与——开源参考实现 ofdrw 官方同样将证书验证交给接入方自行处理。

本地签名:signOfd

验签之外,WASM 端也提供本地签名生成(同步返回新字节,详见 API 参考):

import { signOfd } from "rust-ofd";

const signed = signOfd(ofdBytes, {
  signer: "张三",        // 签署人显示名(自签证书 CN)
  seal: sealPngBytes,    // 印章图片(缺省内置占位章)
  page: 0, x: 140, y: 240, // 盖章页(0 起)与 Boundary(mm,缺省 0,0)
  basis: { reason: "同意", location: "北京" },
});

即时生成 SM2 密钥对与自签证书(SES v4 + 印章)。签名针对当前内容:签名后再做旋转/合并等修改,摘要即失效(验签显示「文档已被修改」)——生产场景的最终签名建议放在文档定稿之后。

调用示例

const doc = new OfdDoc(ofdBytes);
const reports = doc.verifySignatures(undefined, pemText);
for (const r of reports) {
  console.log(r.signer, r.signatureValid && r.digestValid, r.chainLabel);
}

信任锚文件支持 .cer/.crt/.pem/.der/.p7b/.p7c:PEM 原样文本传入,DER 与 p7b 转 base64 后传入(demo 站与 WPF 端均有现成实现可参考)。