4、SM3 的消息填充与初始值
说实话,SM3 的填充规则跟 SHA-256 长得挺像。我第一次看标准文档的时候,觉得这块没啥难度——不就是补位嘛。可后来在项目中真正动手写代码,才发现越是简单的地方,越容易翻车。
你想想看,哈希算法处理消息的单位是分组,SM3 的分组大小是 512 比特。但实际的消息哪有那么巧,刚好是 512 的整数倍?所以必须填充。我今天就把填充规则和初始哈希值彻底讲透,争取让你看完就能上手写代码,不在踩坑上浪费时间。
消息填充规则
填充的目标只有一个:让消息长度变成 512 比特的整数倍。SM3 的填充分三步,顺序固定,一步都不能错。
- 补一个“1”:在消息末尾追加 1 比特的“1”。
- 补 k 个“0”:接着追加 k 个比特的“0”,k 是满足 (l + 1 + k) ≡ 448 mod 512 的最小非负整数。说白了,就是让“原始长度 + 1 + k”除以 512 余 448。
- 补 64 位长度:最后追加 64 比特,用大端序表示原始消息的比特长度 l。
核心公式:
(l + 1 + k) ≡ 448 (mod 512),其中 l 为原始消息比特长度,k 为补 0 的个数。
填充后总长度 = l + 1 + k + 64,一定是 512 的整数倍。
我举一个具体的例子。假设原始消息是 "abc",长度为 24 比特(3 字节)。
- 先补 1 个“1”:长度变为 25 比特。
- 需要补 k 个“0”,让 25 + k ≡ 448 mod 512 → k = 423。
- 再补 64 比特的长度值:24 的 64 位大端表示为
0x0000000000000018。
填充后的消息总长度 = 24 + 1 + 423 + 64 = 512 比特,刚好一个分组。我在项目里第一次验证填充逻辑时,就是拿 "abc" 做的测试,跟标准向量对上了才放心。
小技巧:我在代码里实现填充时,习惯先把原始长度 l 算出来并保存好,再做位操作。因为最后一步需要用到 l,一旦开始填充,原始长度就丢了。提前存一份,省得到时候抓瞎。
初始哈希值(IV)
SM3 的初始值 IV 是 8 个 32 比特的字,直接写死在标准里。迭代压缩的第一轮就用它们作为初始状态。说白了,这 8 个数就是哈希运算的“种子”。
| 变量 | 值(十六进制) | 说明 |
|---|---|---|
| V0 | 0x7380166F |
第1个初始字 |
| V1 | 0x4914B2B9 |
第2个初始字 |
| V2 | 0x172442D7 |
第3个初始字 |
| V3 | 0xDA8A0600 |
第4个初始字 |
| V4 | 0xA96F30BC |
第5个初始字 |
| V5 | 0x163138AA |
第6个初始字 |
| V6 | 0xE38DEE4D |
第7个初始字 |
| V7 | 0xB0FB0E4E |
第8个初始字 |
我个人习惯把这 8 个值记成两组:前四个是一组,后四个是一组。V0~V3 看起来比较“整”,V4~V7 更像随机数。不过这只是帮助记忆,实际用的时候直接复制粘贴就好。
填充与初始值的协同
填充完成后,消息被分割成若干个 512 比特的分组。每个分组依次进入迭代压缩函数,而初始 IV 就是压缩函数第一轮的输入状态。
你想想看,如果没有正确的填充,分组长度不对,压缩函数根本跑不起来。如果没有正确的 IV,算出来的哈希值全都不对。这两个东西就像是算法的地基,地基歪了,上面建什么都白搭。
常见陷阱
避坑指南:
- 字节序搞反:64 位长度字段必须用大端序。我曾经因为手误写了小端序,结果算出来的哈希值跟标准向量全对不上,排查了整整一个下午。
- k 算错:k 是满足条件的最小非负整数,不是随便选一个。我见过有人直接 k = 448 - l - 1,结果算出来负数,一脸懵。
- 漏掉长度字段:填充的最后 64 位是原始消息长度,不是填充后的长度。这个千万别搞混。
嗯,还有一个细节:SM3 的初始 IV 是固定的,但如果你做的是 HMAC-SM3 或者定制化实现,初始值可能会被修改。不过标准 SM3 场景下,就用上面表里的 8 个数,一个字都不要改。
验证方法:我每次实现完填充逻辑,都会拿空字符串和单字符做测试。空字符串填充后应该是 512 比特,单字符填充后也是 512 比特。跟标准输出向量对一下,没问题再往下走。这样能提前过滤掉大部分低级错误。