首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >时间账本 × 时间机器:六次 patch,一条信任链

时间账本 × 时间机器:六次 patch,一条信任链

原创
作者头像
用户12439200
修改于 2026-09-28 09:50:05
修改于 2026-09-28 09:50:05
280
举报
文章被收录于专栏:FIBEMATEFIBEMATE

2026-09-27 · 文章 GPL-3.0 / 工具 Apache-2.0 · GitHub 仓库 Lennonhaha/fibemate-tools

工具文章容易写成决定清单:我们决定用 pkijs,我们决定加 --follow。决定是结果。结果谁都能抄,过程抄不走。这篇只写过程——六次失败,每次都写「试了什么、为什么错、怎么发现」。密码工程里,失败的原因比成功的方法活得久。——题记

一、负例腐烂(时间账本)

时间账本要证明自己会失败。scripts/smoke-freetsa-verify.mjs 里有一条负例断言:拿一个错误的证书 pin 集去验 TSR,必须验签失败、必须 fail-closed。「错误的 pin 集」用的是 pinned-certs.pem。

2026-09-23 给账本加外部 FreeTSA 锚定块(PR #30),顺手动了夹具:为第 3 个块把 FreeTSA 的证书也放进了 pinned-certs.pem。没人改负例。

负例从此变成这样:拿 FreeTSA 的 pin 集去验 FreeTSA 签发的 TSR——验签当然通过。断言期待的失败没来,测试全绿。失败路径被静默移除,CI 上一点动静都没有。

修正(PR #31)花了三层:

  1. 按解析后的 X509 subject 过滤 pinned-certs.pem,只留项目自己的 Test TSA 证书。pin 集这才真正「错」。
  2. 不能用文本子串匹配 PEM——PEM 正文是 base64,换行一变匹配就失效。改用 node:crypto 的 X509Certificate.subject。
  3. 同步删掉 www/README.md 里已不成立的「not yet committed」说明。

可迁移的结论只有一句:负例的失败路径会随夹具演化静默消失。夹具不是测试,夹具是会腐烂的测试。

二、门禁拆了,债从缺口进来(时间账本)

CI 卡住的时候,PR #15 做了一个交换:把 ledger-ci.yml 里的 Typecheck 步骤删掉,先让流水线绿。

同一天,TypeScript 就把账送来了。makeBlock() 返回 Record<string, unknown>,所以 genesis.hash_now 的类型是 unknown。把它传给 makeBlock 的 hash_prev: string 参数,TS2322。同一个文件里 L181 对同一模式已经写了 as string,L235 漏了——一个字的债。

修复(PR #16)是给 L235 补 as string,然后把 Typecheck 步骤装回 CI。两个 PR 的合并时间相隔 11 分钟(01:16Z → 01:27Z)。删门禁的代价不是「几小时后」,是立即。

这次失败的价值不在那一行。在于它证明了:删门禁换来的 CI 绿是借来的。编译器抓住的错误,测试抓不住——npm test 一直是绿的,TS2322 不影响运行时行为,它只在类型层面存在。类型层面的债没有运行时症状,唯一的报警器就是那个被拆掉的步骤。

三、一个工具里两套「验」的定义(时间账本)

更隐蔽的一处:同一个账本工具里,VerifyTSR 存在过两个不兼容的定义。

core.ts 的是三参数同步签名 (tsrRef, ts, tsrDigest) => boolean;tsr.ts 的是单参数异步 (digest) => Promise<boolean>。CLI 层用一个临时桥接函数把两边缝起来,TODO 里挂着「长期统一」的标记,编号 CLI-DESIGN-1。

桥接期的问题是结构性的:每个调用点都要自己记住「现在在跟哪套合同说话」。缝的时候没错,但缝的数量会涨。

PR #22 把合同统一为 (digest: string) => Promise<boolean>,verifyChain 改为 async,cli.ts 直接调 makeVerifyTSR,genTime 交叉核对单独走。改动只有十几行,前置的讨论比改动长。

结论:两套合同并存时,桥接函数不是解决方案,是缓刑。统一要趁调用点还少的时候做。

四、git 语义交手三次(时间机器)

时间机器的输入是 git。git 的语义三次和预期不一致。

第一次:单 commit 仓里 -S 返回空。 CI 环境没有真实仓库,最初没法跑测试。改成自建临时仓(提交 bafa255):建一个 git 仓,commit 1 引入 foo,commit 2 引入 bar。第一版临时仓只建了单个 commit——git log -S bar 返回空,因为符号没有「被引入」这个事件,它一直在。这是 git 的语义:pickaxe 追踪的是出现次数的变化。测试仓必须至少两个 commit。

第二次:重命名断史。 git log <file> 只走文件名。文件改过名,历史就断成两截,改名前的一切消失。PR d3bee69(2026-09-24)给 collect_file_history 加 --follow,并把解析拆成独立的 parse_git_log。文档字符串里写清了边界:--follow 追踪重命名,但它是路径历史,不是符号追踪——要回答「这行代码哪来的」,仍然要用 -S。

第三次:环境变量名传错,静默跑错仓库。 时间机器有两个环境变量命名空间:ctime/core.py:23 读 CTM_REPO,test/local_test.py:20 读 CTM_TEST_REPO,默认值都是当前目录。跑测试的时候传了 REPO=...——三个名字一个都不匹配,Python 不报错,默认值接管,测试对着 fibemate-tools 自己的目录跑,FAIL,而且是解释不了为什么 FAIL 的 FAIL。

根子在文档:当时仓库根 README 写的是 REPO=/path/to/repo python test/local_test.py,代码读的是 CTM_TEST_REPO——文档给的名字就是错的。更早一版 README 的默认值写的是开发机绝对路径。修正把那一行改成 CTM_TEST_REPO=/path/to/repo ...(README.md:53)。这次修正没有独立 PR,随恢复提交 dc25c20 一起进来。

三个都是 git 或 shell 的默认行为在替你做决定。失败的时候先查默认值接管了什么。

五、两条线怎么咬合

两个工具各答一半问题。

时间账本回答「以谁为准」:一个声明在某时刻存在,由谁的密钥签名、锚在哪个账本上、验签走哪条信任链。时间机器回答「什么时候变的」:一个符号、一个文件,在 git 历史里的引入和消失点。

咬合处的合同是 PR #22 定的:两处对 TSR 验证的判定统一成一个签名。盘点(CBOM 工具)给出资产清单,账本给清单盖章,时间机器追溯清单里每一项的来历。三条线各自的发布文章都写过;合起来看,这条线是完整的时间语义:是什么(盘点)、何时为真(存证)、如何演变(考古)。

六、边界

三条,照旧不软:

  1. 负例防腐没有系统性方案。 负例腐烂不报警:引入(PR #30,09-23 11:09Z 合并)和修正(PR #31,同日 14:29Z 合并)相隔 3 小时 20 分,中间没有报警器响过。这次发现不构成方法论。能做的下一步是把「负例必须断言特定错误码」从约定变成测试。
  2. smoke 脚本不是 CI 步骤。 smoke-freetsa-verify.mjs 定位是手动检查,不在流水线里——它依赖真实网络锚定服务,CI 里不可复现。
  3. --follow 的边界是 git 的边界。 它追踪重命名的能力依赖 diff 相似度启发,重写历史、大改内容后再改名,照样断。

词汇注释

  • TSR:Time Stamp Response,RFC 3161 时间戳响应,含签名与 genTime。
  • 信任锚 pinning:只信任预先固定的证书集,而非系统信任库。
  • fail-closed:验证失败时拒绝,而不是放行。
  • git pickaxe(-S):追踪某符号在历史中出现次数变化的检索方式。
  • 负例测试:断言「错误输入必须失败」的测试。

参考实践

  • 账本:Lennonhaha/fibemate-tools → crypto-time-ledger/
  • 时间机器:Lennonhaha/fibemate-tools → crypto-time-machine/
  • 提交与 PR 号:均在上述仓库的 git 历史中,git log <hash> 与 gh pr view <n> 可查
  • 负例修正:fibemate-tools PR #31(c4f668f);合同统一:PR #22(68d59d9);--follow:PR #33(d3bee69);外部锚定:PR #30;TS2322 与 typecheck 恢复:PR #16(724949d);CI 临时仓:提交 bafa255(无 PR);README 变量名修正:随 dc25c20(无独立 PR)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、负例腐烂(时间账本)
  • 二、门禁拆了,债从缺口进来(时间账本)
  • 三、一个工具里两套「验」的定义(时间账本)
  • 四、git 语义交手三次(时间机器)
  • 五、两条线怎么咬合
  • 六、边界
  • 词汇注释
  • 参考实践
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档