别再把签名签错对象了:FOKS 想从协议层堵上密码学里最隐蔽的坑

一类老问题,为什么总能把新系统绊倒
科技行业有一种很典型的错觉:大家以为密码学最难的部分在算法本身,像 RSA、ECDSA、SHA-3 这些名字听上去就足够让人敬畏。可现实里,真正让系统翻车的,往往不是算法,而是“算法之前那一步”——你到底把什么数据、按什么方式喂给了它。
FOKS 最近发布的一篇技术文章,讨论的就是这个听上去不起眼、实际上后果很重的问题:数据结构在进入签名、加密、MAC 或哈希之前,应该怎样被打包。作者的核心观点很直接:行业今天常用的做法,很多时候仍然不够安全,尤其在“域分离”(domain separation)这件事上,做得太随意了。
这个词听起来有点学院派,但意思并不复杂。假设系统里有两种消息,一种是透明日志的树根 TreeRoot,一种是吊销密钥的 KeyRevoke。程序员心里知道这俩完全不是一回事,一个在说“这棵树的根哈希是多少”,另一个在说“这个公钥作废了”。可如果它们在序列化之后恰好长得一模一样,那麻烦就来了:攻击者可能把一份对 TreeRoot 的签名,硬生生“嫁接”到一个 KeyRevoke 上,让验证方误以为签名者同意了后者。
这不是纸上谈兵。比特币历史上吃过这类亏,以太坊上的去中心化交易系统也踩过坑,TLS、JWT、AWS 相关系统里也都出现过相似问题。你会发现,很多安全事故并不是黑客“破解了密码学”,而是他们抓住了系统没有明确表达“这串字节到底代表哪种语义”的空子。密码学在校验内容一致,却没有自动校验“上下文一致”。这就像你在合同上签了名字,结果别人把合同标题换了。
FOKS 的思路:别靠人记得加前缀,让协议自己带上“身份证”
FOKS 的解决方案,来自它们设计的一套 IDL 与序列化系统 Snowpack。思路很漂亮,也很有工程现实感:给需要参与签名、加密、MAC 或哈希的数据结构,分配一个随机生成、长期不变的 64 位域分离标识,并直接写进 IDL。
换句话说,TreeRoot 这个结构体不只是有字段定义,它还带着一个类似“类型身份证号”的东西。运行时在签名时,不是直接对对象序列化后的字节做签名,而是先把这个唯一标识和序列化字节拼起来,再交给签名算法。验证方也做同样的重建。这样一来,即便另一个结构体碰巧序列化结果一模一样,只要它的域分离标识不同,签名验证就会失败。
这个方案最让我觉得聪明的地方,不是“多加了 8 个字节的前缀”,而是它把责任从“应用开发者记得这么做”转成了“协议编译器和类型系统强制这么做”。在 Go、TypeScript 这样的目标语言里,只有带唯一类型 ID 的对象才能被送进 Sign 或 Verify。没有这个标识,编译期就会报错。安全这件事,一旦从文档建议变成类型约束,性质就完全不一样了。
这比很多现有做法更系统。今天行业里也不是没人意识到域分离的重要性。Solana 会哈希方法名,Ethereum 的 EIP-712 通过结构化签名补上下文,TLS 1.3 用 context string 防止协议消息串台。但这些方案大多是分场景、分生态长出来的“土办法”,能用,却不统一,也容易遗漏。FOKS 想做的,是把这件事下沉到协议定义层,像水电煤一样成为基础设施,而不是每个项目自己拉电线。
它为什么比“加个字符串前缀”更重要
很多工程师看到这里,第一反应可能是:这不就是手动在签名内容前面加个固定字符串吗?比如 "TreeRoot:" + payload,听上去也能解决问题。
理论上当然可以,问题出在现实世界不会这么干净。手写前缀最大的问题不是做不到,而是容易忘、容易乱、难审计。团队扩张之后,不同服务、不同语言、不同开发者可能写出不同版本:有人加冒号,有人不加;有人用类型名,有人用模块名;有人重构时顺手把名字改了,结果兼容性悄悄断掉。安全事故最喜欢这种“大家都以为别人已经处理了”的灰色地带。
把域分离放进 IDL,本质上是把“语义绑定”做成协议的一部分。它不是代码注释里的共识,也不是 wiki 页面上的最佳实践,而是参与编译、生成代码、影响运行时行为的正式元数据。这个思路其实击中了现代软件的一块软肋:我们有越来越强的密码学原语,却还在用过于随意的工程流程把它们组装起来。
FOKS 还强调了一点,同样值得关注:域分离不仅对签名有效,对 HMAC、SHA-3 风格的 MAC、普通哈希,甚至现代认证加密也同样关键。因为这些操作都不只是“处理一串字节”,它们处理的是“某种带语义的消息”。只确认字节不被篡改,但不确认消息类型没被偷换,安全性就永远差一口气。
Snowpack 不只想防偷梁换柱,还想顺手修补序列化世界的老毛病
Snowpack 另一个有意思的地方,在于它不是只补一个洞,而是顺带整理了序列化里另外一个老问题:规范编码(canonical encoding)。
这也是个老梗。很多序列化协议能把一份内存对象编码成不止一种合法字节表示。对普通 RPC 场景,这也许只是风格差异;可一旦进入签名和哈希流程,问题就变了。两份不同字节如果表达同一个对象,系统就可能遭遇“可塑性”问题。比特币早年的交易可塑性,本质上就和这类现象脱不开关系。
Snowpack 的做法是把结构体先映射成类似 JSON 的位置数组,再用受限版 Msgpack 压平成字节流。它明确限制整数必须使用最短编码,同时规避多键字典排序问题,以此保证“同一个对象,只有一种编码”。这听上去不性感,但非常重要。安全协议其实最怕“差不多就行”,因为攻击者专挑“差不多”下手。
这里也能看出 Snowpack 对 protobuf 和 JSON 的某种不满。protobuf 在工程世界极其成功,但官方并不保证规范编码;JSON 则在人类可读性上无敌,却天生不擅长表达二进制,常常逼开发者在字符串、十六进制、base64 之间来回绕,混乱与误解随之而来。Snowpack 显然想站在两者中间:保留可调试、可向前兼容的特性,同时尽量让密码学输入更严谨。
这条路线未必会成为主流,但它提出的问题非常准确:如果一个序列化格式经常被拿去做签名和哈希输入,那它就不该假装自己只服务于普通数据交换。过去,很多通用格式在这件事上都有点“兼职上岗”的意思;Snowpack 则试图做一个从出生就知道自己会进入密码学场景的协议描述系统。
它的野心不小,争议也不会少
我很认同 FOKS 对问题的判断,但对它的未来,也保持一点谨慎的乐观。
最大优势在于,这套设计非常适合新系统,尤其适合那些本来就在设计协议、钱包、透明日志、身份系统、去中心化基础设施的人。你越早把域分离固化进 IDL,后面越不容易踩坑。对安全敏感系统来说,这是一种典型的“前期多做一厘米,后期少流一升血”的工程思路。
但它的推广门槛同样真实。今天的工业世界已经被 protobuf、Avro、Thrift、JSON Schema、OpenAPI 这些体系深度占据。Snowpack 若想被更大范围采用,难点未必在技术,而在生态。工具链是否顺手、跨语言支持是否完善、能否与既有协议平滑共存、开发者是否愿意再学一套 IDL——这些问题,往往比“设计是否正确”更决定成败。
还有一个值得思考的点:随机 64 位标识虽然在概率上足够安全,但它把“类型身份”从可读命名转向了不可读数字。对机器来说这是好事,对人类审计来说未必友好。FOKS 的回答是,靠编译器、运行时和工具来兜底项目内唯一性,跨项目则依赖随机碰撞极低的概率。这个逻辑没问题,但在大型组织、自动代码生成、AI 辅助编程越来越普遍的当下,如何防止“复制粘贴旧 ID”或生成器实现不一致,依然是实际工程里不能忽略的细节。
我反而觉得,这篇文章最大的价值,未必是 Snowpack 本身会不会火,而是它把一个长期被低估的问题重新推到台前。过去几年,行业在讨论 AI 安全、供应链安全、云原生安全时,常常把注意力放在更显眼的地方。可很多攻击真正落地,靠的还是协议层那些细小、枯燥、没人愿意看的边角料。域分离就是其中之一——它不够酷,却非常致命。
从这个角度看,FOKS 的工作像是在提醒整个行业:安全设计不能只迷信“强算法”,还得认真处理“强语义”。如果发送方和接收方对“这是什么消息”都不能达成同样严格的共识,那密码学再强,也可能只是给误解盖了个钢印。