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