TOOL RELEASE

密码学时间账本:给每份证据盖一个可验证的章

2026-09-27 · 文章 GPL-3.0 / 工具 Apache-2.0 · GitHub

FIBEMATE 系列。此前讲过迁移动因、混合 HTTPS、CBOM、FPGA 基准、预硅侧信道、TLA+ 形式化验证。这一篇解决一个更基础的问题:你产出的每一份审计证据——TVLA 报告、TLA+ 记录、KAT 结果——怎么证明它在某个时刻就存在、此后没被改过?答案是哈希链 + RFC 3161 可信时间戳 + 可执行验证。工具已开源,仓里躺着 260 份真时间戳(截至 2026-09-27 实测,分散在 40+ 个目录)。

一、没有时间的证据不算证据

一个典型场景:侧信道评估做完,报告落盘。三个月后有人质疑「这份报告是测试当天生成的,还是后来补的?中间改过没有?」

回答不了。文件系统时间戳可改,git commit 可重写(rebase、重新签名),报告内容对不对得上当时的原始数据,无法证明。

密码工程比一般软件更需要这一层:TVLA 结论、形式化验证记录、KAT 回归结果,都是「某个时刻的状态」的断言。断言没有可信时间锚,审计链条就断在第一步。

解法分三步:把状态快照哈希进链、把链条锚到第三方时间戳、让任何人能在浏览器里重算验证。对应一个工具:crypto-time-ledger(github.com/Lennonhaha/fibemate-tools,Apache-2.0)。

二、账本块:状态进哈希,时间戳进链

每个账本块(block)记录「某算法在某 commit 下被纳入账本」:

LedgerBlock {
  schema_version, index, ts,
  state: { algorithms, git_commit, note },
  tsr_digest,   // sha256:<hex> — 时间戳文件的指纹,进哈希
  tsr_ref,      // 指向 <id>.tsr 的路径 — 不进哈希
  hash_prev,    // "genesis" 或上一块 hash_now
  hash_now      // sha256:<hex> — 对前面全部字段的哈希
}

两个设计决定:

tsr_ref 不进哈希,tsr_digest 进。 路径是定位符,挪个目录哈希就变——不行。时间戳文件的 SHA-256 指纹才是内容绑定,块哈希锁住指纹,指纹锁定那份 .tsr。改动时间戳文件一个字节,链条立刻断。

ts 必须与 TSR 时间一致。 块自己声明的时间如果和时间戳服务器签发的不符,链在逻辑上自洽、在事实上造假——验证规则里两者必须对上。

三、两个细节

细节一:canonicalize。 哈希前要序列化,而原生 JSON.stringify 不排序键——同一个对象,键序不同,哈希不同,跨语言验证直接失败。所以自实现 RFC 8785 风格的规范化:递归键排序、紧凑输出、UTF-8。还有一个容易漏的细节:RFC 8785 要求 U+2028 / U+2029 转义为 \u2028 / \u2029,原生 JSON.stringify 不转——不处理,JS 和其他语言对同一段数据算出的哈希就不一致。跨实现可验证性死在字符转义上,比死在密码学上常见。

细节二:负例。 演示数据里有一份 TSR(d22f33f6…)签发证书不在信任锚列表内,工具拒绝它、不写进链——这是故意留的负例,验证 fail-closed 行为:任何一项检查不满足,宁可拒收也不放行。信任锚机制是 SPKI DER 逐字节对比已知 TSA 证书列表,不是「信任系统证书库」这种软约束。

四、TSR 验签:为什么用 pkijs

RFC 3161 时间戳响应(TSR)是 CMS 结构,验签要解析 ASN.1、比对 messageImprint、验证签名证书链。手写这套解析的风险我们实测过——探索期发现多个具体 bug,ASN.1/CMS 手工解析属于高风险区。最终用 pkijs(成熟库,密码操作委托给平台 webcrypto),自己只写胶水:TSR 加载、eContentType 处理、messageImprint 的 SHA-256 十六进制比对、信任锚 pinning、fail-closed 收口。

当前版本 63 个测试,分布在 7 个套件(ledger add / verify / query / export、全局旗标、stdin、错误格式),文件层面是 cli / core / storage / tsr 四个测试文件,npm test 一条命令跑完(实测 11.5s 全 PASS),CI 挂 ledger 专用 workflow。

五、生产账本:260 份时间戳在仓里

工具之外,主仓已经攒下生产证据(origin/main 实测 260 个 .tsr 文件):

位置数量内容
www/docs/tsa/(按日期序列)135线上文档快照、审计记录、FreeTSA 批次
docs/tsa/96架构文档、协议验证、事件复盘
backend/timestamps/92026-06 后端 PQC 批次
www/docs/tvla/、evidence/tvla/8TVLA 侧信道证据
www/timestamps/digicert/5DigiCert 公共 TSA
www/docs/evidence/4混合 KEM 验证证据
papers/、lib/、fips205/3论文与依赖快照
合计260

时间戳来源刻意分散:自建 TSA、FreeTSA、DigiCert——单一 TSA 失效不拖垮整条证据线。

每份证据有 lg- 编号。lg-069 盖的是 2026-07-14 的 K3 形式化验证审计记录,连同 tsq(请求)和 SHA-256 清单一起进 timestamp-manifest.json——三份 manifest(docs、docs/tsa、www/docs)把全部 260 份 TSR 的文件名、大小、指纹登记在册。TLC 跑出「101,467 状态零违反」(其中 26,115 个去重状态)的那次审计,就是 lg-069 盖的章(两个数字官网 cars-radar.html 可查,26,115 distinct 与 2026-09-25 本机复跑 TLC 及 tla-verifier README 记录一致)。

六、可执行验证:浏览器里重算整条链

工具页(fibemate.net/tools/time-ledger)做成水墨手卷:每个账本块是一个墨点,青绿为链连续,朱红为链断。「验真整链」按钮在浏览器里用 Web Crypto 逐块重算 hash_now(与 core.ts 同一套 RFC 8785 键排序),校验链连续性与 TSR 附着标记——不装任何东西,打开页面就能验。

演示种子 3 个块全部真实:由 cli.ts add 写入,TSR 来自 test fixtures,逐份验签通过后入链;git_commit 记录工具仓当时的 HEAD,整条链可复现。页面声明写死在开头:demo seed,非生产测量。

七、边界

TSR 证明「文件在该时刻存在」,不证明内容正确。 时间戳服务器只对哈希签名,不看内容。报告内容错,盖章也没用——证据正确性靠 TVLA 和 TLA+ 流程,本工具管存证时间线。

信任根是 TSA。 信任锚 pinning 把信任缩到已知 TSA 列表,但列表本身要人维护;TSA 私钥泄露、TSA 关停,都是这条线的现实风险。多 TSA 分散是对冲,不是消除。

演示链 3 块,非生产测量。 生产规模在主仓的 260 份 TSR 和 manifest,工具链与生产账本目前是两条线(工具 v0.1.1,账本由脚本批次生成)——合并是下一步。

八、结语

系列到这凑齐三条证据线:可观测性管运行时(线上真走了混合路径),TLA+ 管协议逻辑(状态空间无反例),时间账本管存证(证据在何时存在、此后未改)。三条线互相不替代:跑着的不代表验证过,验证过的不代表存了证,存了证的不代表内容对——但三样叠在一起,审计的人就不用再信任何一句「我们做过」。

下一篇换视角:把 git 历史变成密码 API 的可查时间轴——算法参数什么时候改的、谁改的,一条命令查清。

参考实践

词汇注释