密码学 02:密码学处理哪些字节
订单页面上看见 quantity=1,不等于签名程序看到相同的消息。浏览器可以发送 JSON、表单字段或二进制协议;同一个 JSON 对象还可以有不同的空格和键顺序。如果一个端点对它收到的原始字节做摘要,另一个端点先解析再重新输出 JSON,即使两边都在谈“同一笔订单”,校验也可能失败。更糟的是,两个不同字段组合能拼出完全相同的字节:此时再强的哈希或签名算法也无法代替消息边界的约定。
第 00、01 篇讲“要保护什么”。这篇回答一个更靠前的问题:确切保护了哪些字节?输入没有说清,关于身份、用途和完整性的结论都容易越界。
从可读字符走到被验证的输入
假设订单文本里有中文“订单”。程序要先约定字符编码,例如 UTF-8,再确定字段顺序、数字表示、空白处理与字段长度,最后才可把结果交给摘要或签名操作。
1 | |
"订单".encode("utf-8").hex() 的输出是 e8aea2e58d95:一个可供读者检查的具体字节结果。十六进制和 Base64 改变的是表示方法,并没有隐藏原文;拿到 Base64 的人可以直接解码。这与 07 篇要使用的认证加密不同,后者有秘密密钥和校验失败条件。
实验里的订单 JSON {"quantity":1,"sku":"book"} 和 {"sku": "book", "quantity": 1} 解析为相等的 Python 对象,原始字节却不同。它们的 SHA-256 分别是 2d25aa19273f0207fb907d8f93bcf8da97eb8a6a4dfdc0e79b50ca10697a990f 和 de39bcace818b7ff09a4210ac435c8a576884d7fa505848c1d26a56455861aa9。一端签第一串,另一端把它重排成第二串再验,失败首先应查验签输入,不要先宣布密钥已被攻破。
序列化是把结构化字段变成约定的字节;分帧是让接收者确定字段从哪里开始、到哪里结束;规范表示是使允许的同一对象有统一的可验证字节表示。这里只引入这三个操作词;具体链的交易编码、TLS 结构和 HTTP 签名覆盖范围在对应章节各自核对。Python 的 json.dumps(sort_keys=True) 固定了本系列教学订单的部分表示,但并不是 RFC 8785 的 JSON Canonicalization Scheme 实现;尤其别把浮点数、Unicode、重复键等所有可能输入都交给这几行代码,随后声称得到通用签名格式。
两个字段之间究竟有多宽
考虑 merchant 和 order 两个字节字段,省事做法是直接拼起来再计算摘要或签名输入。
1 | |
不分帧时,两个“业务对象”的输入本来就完全相同,算出相同摘要不是发现了哈希碰撞;即使换成数字签名,它也没办法从 b"abc" 判断签署者想签哪一组字段。这是输入约定错误,不是算法失效。定义长度、字段类型和顺序后,两串字节不同,验证者也必须按同一个约定解析;否则仍可能对同一签名产生两种解释。教学脚本用两字节长度,因此字段超过 65535 字节时主动报错,避免无声截断。生产协议通常有更完整的字段类型、上下文、版本和长度约束,不能直接复制这个玩具格式。
更隐蔽的情形是两边处理顺序不同:浏览器可能先把请求体里的 UTF-8 字节发给代理,代理先解析再重排 JSON,后端随后根据新字节验证老签名。若签名覆盖的就是原始正文,这个“等义重排”也会失败;若某一方签的只是部分字段,则未覆盖字段甚至可被中间人改掉而签名仍有效。协议必须说清签名输入是否包含请求方法、主机名、路径、主体、时间和域信息,而不是一句“对请求签名”就算完成设计。HTTP 消息签名另在 23 篇细讲。
一次可失败实验
examples/cryptography/02_encoding.py 只依赖 Python 3 标准库:输出直接拼接与显式长度格式的十六进制、JSON 对象和字节是否相同、两种表示的摘要以及可反解的 Base64。仓库根目录运行:
1 | |
2026-10-06 UTC,在 Python 3.12.3 上两条命令退出码均为 0,5 个断言通过。直接拼接的输入都是 616263;加长度后分别为 00026162000163 与 00016100026263。脚本输出的 Base64 为 eyJxdWFudGl0eSI6MSwic2t1IjoiYm9vayJ9,解开就是公开 JSON,没有秘密参与。
失败对照在脚本测试里已经写明:删除长度,原本应有区别的两组输入重新相等,test_two_byte_lengths_separate_two_fields 会失败;让字段超长却不报错,test_length_boundary_is_checked 会失败。这个实验是手算/教学模型加标准库摘要,不是 Bitcoin 或 Ethereum 交易签名测试,也没验任何真实网络端点。
把它放回两张图:HTTPS 的握手会把各阶段约定的消息表示纳入特定的握手摘要,双方不能各自“看起来一样”就算一致;钱包签的通常是特定类型交易的规定预像或其摘要,交易展示页显示的字段不等于直接送进签名算法的字节。后者需要链、交易类型和用途绑定;如果还要求保证提交一次有效授权只能被消费一次,另查 nonce、未花费输出或应用状态。编码可确定输入,不能提供保密性;确定输入也不能单独提供来源认证。
两道带答案的练习
推导题。 将 merchant=b"shop"、order=b"7" 放进实验定义的两字节大端长度格式,手写十六进制。为什么 merchant=b"sho"、order=b"p7" 在直接拼接时无法区分,而在分帧后可以?
可核对答案: 长度分别为 4 和 1,第一组得到 0004 73686f70 0001 37;第二组长度为 3 和 2,得到 0003 73686f 0002 7037。直接拼接均为 73686f7037。两组分帧输入不同才允许验证者恢复正确字段;不能从未分帧的相同摘要倒推原字段。
变更题。 把 02_encoding.py 里的 framed 改成只给第一个字段写长度,第二个字段保持原样,再跑五个测试。现有两字段实验可能仍会区分,这足以证明这个新格式在所有协议中安全无歧义吗?
可核对答案: 不足以。固定只有两个字段、并且消息长度完全可信时,可以由第一段长度和整体长度推出余下字段;但如果还允许尾随字段、嵌套结构或多种消息类型,就要额外明确整体长度和字段范围。现有测试只覆盖指定的输入与两字段模型;请增加“第三字段/可选字段”反例后再声称支持新的结构。不应把“测试未失败”当作通用解析器的证明。
资料与导航
- RFC 3629,UTF-8、RFC 4648,Base16/Base64:编码和可逆表示。
- RFC 8785,JSON Canonicalization Scheme:真正的 JCS 规则应按规范或成熟库实现,本篇代码不声称兼容。
- 00 从 HTTPS 与交易两张图开始 · 01 窃听、篡改、冒充与重放 · 03 哈希能证明什么。






