
上一篇23-1《国密 SM3 与 KAT 红线——先有答案再写实现》下一篇第 23-3 篇篡改实验——改一个压缩常量看 KAT 怎么崩源码精读篇本文为源码/方法论精读无独立实测文中数字均引述仓库 docs 的板端实测记录。一句话导读SM3 实现导读沿 vllm_crypto.c 逐段精读填充、消息扩展 W 与 W′ 与 64 轮压缩轮把abc变 66c7f0f4 的过程讲成能复述的实现地图本篇属源码精读导览为下一篇篡改实验备好坐标。关键词SM3、实现导读、消息扩展、压缩轮、国密红线证明实现正确。这篇钻进vllm_crypto.c的 SM330–127 行逐段导读填充、消息扩展W 与 W、64 轮压缩。读完你就能回答SM3 为什么把 abc 变成 66c7f0f4…也能在下一篇文章里亲手改坏它。1. 知识点SM3 的三段式骨架SM3GB/T 32905-2016输出 256 位结构是 Merkle–Damgård消息按 512 位64 字节分组每组经一个 64 轮的压缩函数更新 8 个 32 位字的状态IV → 摘要。三段式① 填充消息尾补 0x80 若干 0x00 64 位大端总长使总长 ≡ 0 (mod 512) ② 消息扩展每 64 字节块展开成 W[0..67]再算 W[j]W[j]^W[j4]W[0..63] ③ 压缩64 轮用布尔函数 FF/GG、常量 Tj、置换 P0/P1 更新 a..h最后与旧状态异或设计上它和 SHA-256 同源但不同构布尔函数分两段前 16 轮异或型、后 48 轮多数/选择型常量也只有两个0x79cc4519 / 0x7a879d8a随轮数 Tj 左移 j 位参与。256 位安全性靠的就是这些非线性置换与 64 轮扩散。2. 对应代码vllm_crypto.c 逐段导读2.1 常量与置换33–43 行static const uint32_t SM3_IV[8] { 0x7380166f, 0x4914b2b9, … }; /* 标准初始状态 */ static inline uint32_t sm3_p0(uint32_t x) { return x ^ rotl32(x,9) ^ rotl32(x,17); } static inline uint32_t sm3_p1(uint32_t x) { return x ^ rotl32(x,15) ^ rotl32(x,23); }P1用于消息扩展P0用于每轮输出。23-1 篡改实验改的就是SM3_IV[0]——你现在知道它为什么能让所有摘要失效了。2.2 压缩函数消息扩展 64 轮45–78 行static void sm3_compress(uint32_t s[8], const uint8_t block[64]) { uint32_t w[68], wp[64]; for (int i 0; i 16; i) w[i] GET_U32_BE(block 4*i); /* 块拆 16 个字 */ for (int j 16; j 68; j) /* W 扩展到 68 */ w[j] sm3_p1(w[j-16] ^ w[j-9] ^ rotl32(w[j-3],15)) ^ rotl32(w[j-13],7) ^ w[j-6]; for (int j 0; j 64; j) wp[j] w[j] ^ w[j4]; /* W W^W[4] */ … 装载 a..h s[0..7] … for (int j 0; j 64; j) { uint32_t tj (j16) ? 0x79cc4519u : 0x7a879d8au; /* 两段常量 */ uint32_t ss1 rotl32(rotl32(a,12) e rotl32(tj, j), 7); uint32_t ss2 ss1 ^ rotl32(a, 12); … FF/GG 分段 … tt1/tt2 更新 … dc; crotl32(b,9); ba; att1; /* 循环移位寄存器 */ hg; grotl32(f,19); fe; esm3_p0(tt2); } s[0]^a; …; s[7]^h; /* 与旧状态异或 */ }导读要点W 扩展是向前看 16~1 字的线性反馈w[j-16]^w[j-9]^rotl(w[j-3],15)经 P1 再异或两路——这是把 16 个字搅成 68 个字的扩散引擎每轮只动 7 个寄存器a由 tt1 更新、e由 P0(tt2) 更新其余轮流左移——这就是一字之差64 轮后全盘皆变的结构原因对应 23-1 的雪崩实测FF/GG 的两段设计前 16 轮是线性异或后 48 轮是非线性多数/选择——与 SHA-256 相同思路非线性部分才是抗碰撞的主力。2.3 init/update/final长度计数与填充80–127 行void vc_sm3_update(vc_sm3_ctx *c, const void *p, size_t n) { c-n n; /* 累计字节数final 要算 64 位总长 */ while (n 0) { if (c-buflen 0 n 64) { sm3_compress(c-s, in); … } /* 整块直压 */ else { memcpy 余块进 c-buf … 满 64 再压 … } /* 分块缓冲 */ } } void vc_sm3_final(vc_sm3_ctx *c, uint8_t out[32]) { uint64_t bits c-n * 8; vc_sm3_update(c, \x80, 1); /* 补 0x80 */ while (c-buflen ! 56) vc_sm3_update(c, zero, 1); /* 补 0 直到余 8 字节 */ … 写入 64 位大端长度 … /* 再压最后一组 */ for (i8) PUT_U32_BE(c-s[i], out4*i); /* 状态 → 32 字节摘要 */ }23-1 的第三个向量abcd*16用 361 字节分两次喂专门测的就是这里c-n的 64 位累加在跨块 update 下不丢字节、填充正确落在 56 余数上。填充一个字节错位跨块消息就全错——而单块向量测不出来这就是 KAT 需要不同长度向量的原因。3. 改动后果代码走查式的如果实验本文没有新的板端实验——23-1 已跑过红线23-3 会跑篡改。这里只做纸面推演为 23-3 做铺垫你可以在读代码时验证问题 A把 w[j] 扩展式里的 rotl32(w[j-13],7) 的 7 改成 8 → 单块向量会不会错 → 会。消息扩展任何一位变W[68]/W[64] 全部改变64 轮全受影响。 问题 B把 FF/GG 的分段阈值 16 改成 15 → 只影响第 16 轮开始的非线性函数切换点 → 全向量错。 问题 C把 ss1 的 rotl32(…,7) 改成 6 → 每轮移位错 → 全错。推演的结论先行SM3 里没有任何可以随手改一处还不影响结果的常数。这不是巧合——密码原语就是设计成牵一发动全身。23-3 的实测只改一个 IV 字就让它全红正是这句话的量化版本。4. 学员调试任务A 档配合 23-3先按 2.1–2.3 在源码里标注 10 处红线位IV×8、常量×2、P0/P1、位移参数为 23-3 的篡改清单做准备。B 档纯读源码回答①sm3_compress结尾的s[i]^…76–77 行为什么是异或而不是赋值提示否则多块消息会丢失前块状态②vc_sm3_update里整块直压分支要求buflen0为什么这个前置条件能成立、省掉了 memcpy③c-n为什么必须是 64 位size_t在 32 位板会怎样预期输出一张标好 10 处红线位的 SM3 源码注释图能对任意一处说出改了它哪个 KAT 向量会先红。收尾本篇源码点名vllm_crypto.c常量 33–43、压缩 45–78、填充 107–120开源仓库Kestrel-LLM (Gitee)AGPL-3.0-or-later 或商业许可二选一下篇预告读完实现、标好红线位现在亲手改把压缩常量/IV 改 1重编 KAT看它怎么红——红线价值的可视化最后一课。