本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容:
- 我的帖子已经打上 开源推广 标签: 是
- 我的开源项目完整开源,无未开源部分: 是
- 我的开源项目已链接认可 LINUX DO 社区: 是
- 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是
- 以上选择我承诺是永久有效的,接受社区和佬友监督: 是
以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出
在全都是ai项目的开源社区中,我做了一个这样的开源软件。 我不擅长写推广贴,但是有推广之心,大家也可以把这个看成技术分享。
老板问:
昨晚谁改了生产权限?
运维查了一遍:
审计日志里没有这条操作。
老板又问:
审计日志存在哪?
我们数据库里。
谁能改?
还是我们。
这就有点尴尬了。
审计日志如果是内部审计肯定没问题,但真出了事故,仅仅拿出一张自己保管的日志表,很难说服另一方:
- 你怎么证明这条记录不是后来补的?
- 又怎么证明原来的记录没有被删掉或换掉?
- 掌握日志的人,能不能同时改日志?
TrustDB 是我对这个问题的一次尝试。
它做的是把已经产生的关键审计记录,变成一条可以独立验证的证据链。
项目地址:GitHub - ryan-wong-coder/trustdb: 高可信的存证系统 · GitHub
官网:https://www.trustdb.ryan-wong.cn/
一个数据有没有变动,不能只告诉第三方一句"请相信我们"
一条比较完整的审计记录,通常会写明:
- 谁执行了操作
- 操作了什么对象
- 执行了什么动作
- 操作前后的状态摘要
- 请求编号或任务编号
- 记录产生时间
- 业务附加信息
这些内容可以来自业务系统、网关、发布平台或独立审计代理。
TrustDB 首先计算记录正文的 SHA-256,再把内容摘要、长度、客户端身份、密钥编号、事件类型、时间、随机数和元数据组成一份客户端声明。
接下来由审计记录的产生方签名。
input =
"trustdb.client-claim.v1"
|| 0x00
|| canonical_cbor(claim)
signature = Ed25519.Sign(client_private_key, input)
这里使用确定性 CBOR,而不是随手生成的 JSON。
因为数字签名面对的是字节,不是"看起来差不多的意思"。JSON 字段顺序、空格和数字写法不同,都会产生不同字节。确定性 CBOR 把编码固定下来,让签名方和验证方得到完全相同的输入。
解码时还会拒绝重复字段、未知字段、CBOR tag、不定长编码、NaN、无穷大和尾随数据。
做这么多主要是怕把两个解析器把同一段数据理解成不同意思。
为什么签名前要写一串固定文字
TrustDB 的客户端声明、服务端回执和全局日志树头,都使用 Ed25519 签名,但它们不能共用同一种签名语义。
所以每类数据都有自己的签名域:
trustdb.client-claim.v1
trustdb.accepted-receipt.v1
trustdb.committed-receipt.v1
trustdb.signed-tree-head.v1
客户端声明只能当客户端声明验证,接受回执只能当接受回执验证。
不能拿着一份合法的客户端签名,换个外壳说它是服务端回执;也不能把旧格式里的签名塞进新格式继续使用。
记录编号同样绑定了声明和客户端签名:
record_id =
SHA-256(
"trustdb.record-id.v1"
|| 0x00
|| claim_cbor
|| client_signature
)
因此,两条正文相同的审计记录,只要产生方、时间、随机数或签名不同,就会得到不同编号。
内容相同,不代表是同一次操作。
客户端签名,只能证明"记录是谁生成的"
客户端签名能够证明:
持有对应私钥的一方,签过这条审计声明。
但它不能证明 TrustDB 服务端确实收到。
所以服务端还会签发两种回执。
接受回执表示:
我验证了客户端签名,并且接受了这条记录。
提交回执表示:
这条记录已经进入某个 Merkle 批次,位置和批次根如下。
"收到"和"已经进入可验证批次"被明确分开。
如果客户端声称提交过,服务端却说没收到,可以检查接受回执;如果服务端声称记录已经入账,可以继续检查提交回执和 Merkle 证明。
这比查一列 status=success 麻烦,但数据库里的状态可以被管理员直接修改,签名回执却不能在没有私钥的情况下重新生成。
Merkle 树让一条记录牵动整批记录
每条服务端记录先计算叶子哈希:
leaf = SHA-256(0x00 || canonical_cbor(server_record))
两个节点合并时:
node = SHA-256(0x01 || left || right)
0x00 和 0x01 用来区分叶子与内部节点,避免两种对象互相冒充。
一条记录进入批次后,会得到一条通往批次根的证明路径。
验证器重新计算这条路径,就能检查:
这条审计记录是否真的属于这个批次根。
有人修改记录正文,内容哈希会变;客户端声明和记录编号随之变化;Merkle 叶子、路径和批次根也会跟着变化。
想只改一条审计记录,却保持原来的批次根不变,需要找到 SHA-256 碰撞。
这已经不是改一行数据库的问题了。
如果管理员把整批记录一起重做呢
只做一层 Merkle 树还不够。
服务器管理员可以丢掉旧批次,重新生成一批记录,再拿出一个新的批次根。
因此 TrustDB 会把每个批次根继续写入全局透明日志。
全局日志本身也是一棵持续增长的 Merkle 树。每次追加后,服务端都会生成带签名的树头 STH,里面有:
- 当前树的大小;
- 当前根哈希;
- 日志编号;
- 节点编号;
- 生成时间;
- 服务端签名。
这里有两种证明。
包含证明回答:
这批审计记录是否真的进入了全局日志?
一致性证明回答:
今天的全局日志,是否由昨天的日志继续追加得到?
假设昨天有一万个批次,今天有一万零三个。一致性证明检查的不是"今天这棵树能不能算通",而是昨天那一万个批次有没有被原样保留下来。
这样就不能趁夜里换掉一段历史,再重新生成一棵结构正确的新树。
如果服务端给不同的人看不同历史呢
透明日志仍然可能出现分裂视图。
管理员给甲看一份 STH,给乙看另一份 STH,两边单独验证都能通过,但看到的历史不同。
所以,TrustDB 可以把 STH 的全局根继续提交给 OpenTimestamps。
提交出去的只是一个 32 字节哈希,不包含审计记录正文,也无法从中还原业务数据。
证明关系变成:
审计记录
↓ SHA-256
客户端声明
↓ 客户端 Ed25519 签名
服务端回执
↓ 批次 Merkle 证明
批次根
↓ 全局透明日志
签名 STH
↓ OpenTimestamps
外部时间见证
OpenTimestamps 日历服务器会先返回待确认的时间戳证明,后续还可以升级到比特币区块见证。
它能帮助证明的是:
与这条审计记录相连的全局根,最迟在某个时间以前已经存在。
它不能证明审计内容本身符合事实,也不表示审计记录正文被上传到了外部网站。
验证器不会相信证明文件自报家门
TrustDB 可以把客户端声明、服务端回执、Merkle 路径、全局日志证明、STH 和外部锚定放进一个 .sproof 文件。
文件里虽然有 proof_level,但验证器不会因为它写着 L5,就直接认定达到 L5。
验证时会重新计算:
- 审计记录正文的长度和 SHA-256;
- 客户端声明的确定性 CBOR;
- 客户端 Ed25519 签名;
- 记录编号;
- 服务端接受回执;
- 服务端提交回执;
- Merkle 叶子和批次根;
- 批次根到全局 STH 的包含证明;
- STH 的服务端签名;
- 外部锚定与 STH 根是否一致。
能算到哪一层,才算达到哪一级。
证明文件说自己是 L5,和嫌疑人说自己无罪一样,只能作为提示,不能作为结论。
它能防事后改账,不能防一开始就不记账
这是审计场景里必须说明的边界。
如果某次操作从一开始就没有进入审计采集端,TrustDB 不可能凭空知道它发生过。
如果审计客户端私钥已经被控制,攻击者也可以签署新的虚假记录。
如果业务系统把错误的操作者身份交给审计端,密码学只会忠实地保护这个错误身份。
TrustDB 主要解决的是:
一条审计记录一旦被签名、接受并进入证明链,后来还能不能悄悄修改或替换。
至于"是否每次操作都生成了审计记录",仍要依靠独立采集、连续序号、权限隔离和业务制度。
一个防改,一个防漏,不是一回事。
最后
TrustDB 目前使用:
- SHA-256;
- Ed25519;
- Core Deterministic CBOR;
- RFC 6962 风格 Merkle 树;
- 签名树头;
- 包含证明和一致性证明;
- OpenTimestamps 外部锚定;
- 可离线验证的
.sproof v1。
代码、证明格式和测试向量全部公开。
如果佬友们做过操作审计、发布审计、权限变更审计、数据流通记录或者密码学、区块链研究,欢迎从实际情况里挑问题,提issue
注:使用大模型从邮件中复制后重新md格式,当前版本无润色内容(确信)
