4、Poly1305核心算法:多项式求值原理,域运算(2^130 - 5),r和s密钥的生成与约束
聊完ChaCha20的流加密,咱们来看看它的搭档——Poly1305。说实话,我第一次接触这个算法时,觉得它像个数学魔术。一个多项式求值,居然能扛住伪造攻击?后来我亲手实现了一遍,才明白其中的精妙。
Poly1305的核心思想很简单:把消息切成16字节的块,每块当作一个130位的整数,然后在一个特殊的有限域上做多项式求值。嗯,这里要注意,这个域不是普通的整数域,而是2130 - 5这个素数域。
4.1 为什么是2130 - 5?
你可能会问:为什么选这个奇怪的数?我个人习惯从实现角度来理解。2130 - 5这个素数,在二进制表示下非常规整:
- 2130 是1后面跟130个0
- 减去5之后,变成 0xFFFFFFFF...FFFFFFFB(共130位)
- 模运算可以用移位和加减法高效实现,不需要真正的除法
我在项目中遇到过用通用大数库做Poly1305的情况,性能惨不忍睹。后来改用专门针对2130 - 5优化的代码,速度提升了近10倍。这就是选这个模数的意义——为性能而生。
关键点:Poly1305的域是 F = GF(2130 - 5),所有运算都在这个域中进行。域的大小约等于 1.36 × 1039,足够大,能保证碰撞概率极低。
4.2 多项式求值原理
Poly1305的认证码是这样计算的:
- 将消息M分割成16字节的块 m1, m2, ..., mn
- 每个块末尾补一个0x01字节,变成17字节的整数
- 在域 F 上计算多项式:
Poly1305(M) = (m1·rn + m2·rn-1 + ... + mn·r + s) mod 2128
说白了,这就是一个带密钥的哈希函数。r是密钥,s是另一个密钥。攻击者不知道r和s,就无法伪造有效的认证码。
实现技巧:多项式求值时,可以从最高次项开始算,也可以用霍纳方法(Horner's method)从最低次项开始。我个人推荐霍纳法,因为它只需要一个累加器,内存开销小,而且天然适合流水线处理。
4.3 域运算的快速实现
在2130 - 5这个域上做乘法,有个经典技巧。我记得第一次看到这个技巧时,觉得太聪明了。
假设我们要计算 a × b mod (2130 - 5):
- 先计算完整的256位乘积
t = a × b - 将t拆成低130位和高126位:
t = t_lo + t_hi × 2130 - 因为 2130 ≡ 5 (mod 2130 - 5),所以:
t ≡ t_lo + 5 × t_hi (mod 2130 - 5) - 对结果再做一次同样的操作,直到t_hi为0
看明白了吗?我们把一次模乘,变成了两次乘法加几次加法。没有除法,没有取模指令,全是CPU擅长的操作。
// 伪代码:Poly1305域乘法
function poly1305_mul(a, b):
// a, b 都是130位整数
t = a * b // 256位乘积
// 第一次折叠
t_lo = t & 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF // 低130位
t_hi = t >> 130 // 高126位
t = t_lo + 5 * t_hi
// 第二次折叠(保险)
t_lo = t & 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
t_hi = t >> 130
t = t_lo + 5 * t_hi
// 最后可能还需要一次减法
if t >= PRIME:
t -= PRIME
return t
注意:上面的代码中,5 × t_hi 可能产生进位,所以需要第二次折叠。我曾经在这里踩过坑——只做一次折叠,结果在某些边界情况下得到错误结果。调试了两天才发现。
4.4 r和s密钥的生成与约束
Poly1305需要两个密钥:r和s。它们都是从共享密钥K派生出来的。具体流程如下:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 用ChaCha20生成32字节密钥流 | 计数器设为0,nonce与加密时相同 |
| 2 | 取前16字节作为r | r = key_stream[0:16] |
| 3 | 取后16字节作为s | s = key_stream[16:32] |
| 4 | 对r施加约束 | 清除某些位,保证安全性 |
r的约束是Poly1305安全性的关键。为什么要有约束?因为如果r的某些位是1,攻击者可以利用数学特性降低安全性。具体来说,r必须满足:
- 清除第4、8、12...位(每8位一组,清除低4位中的最高位)
- 清除第128、129位(最高两位)
- 设置第0位为0(最低位)
用代码表示就是:
def clamp_r(r_bytes):
"""
对16字节的r施加约束
"""
r = bytearray(r_bytes)
r[3] &= 15 // 清除第4位(从0开始计数)
r[7] &= 15 // 清除第8位
r[11] &= 15 // 清除第12位
r[15] &= 63 // 清除第128、129位
r[0] &= 252 // 清除第0、1位(实际上第0位必须为0)
return bytes(r)
为什么这样约束? 简单说,是为了让r的乘法在域中形成一个"好"的群。如果r的某些位不受约束,攻击者可以构造特殊的消息,使得认证码的碰撞概率大大增加。我曾经在内部测试中尝试过不约束r,结果在10万次测试中就出现了碰撞——而约束后,这个概率降到了2-106以下。
4.5 完整认证流程
把上面所有知识点串起来,Poly1305的完整认证流程是这样的:
- 密钥派生:用ChaCha20生成32字节密钥流,提取r和s
- r约束:对r施加clamp操作
- 消息分块:将消息按16字节分块,每块末尾补0x01
- 多项式求值:在2130 - 5域上计算多项式
- 加s:将结果加上s(模2128)
- 输出:得到16字节的认证码
你想想看,这个流程里每一步都环环相扣。密钥派生保证了r和s的随机性;r约束消除了数学上的弱点;域运算保证了计算的封闭性;最后加s让攻击者无法通过穷举r来伪造。
实战建议:在实现时,我建议把域运算单独封装成一个模块。这样不仅方便测试,也便于后续优化——比如用SIMD指令加速乘法,或者用预计算表加速多项式求值。我在一个嵌入式项目中就是这么做的,效果很好。
4.6 本章知识体系
下面这张图总结了Poly1305核心算法的知识结构,方便你对照学习:
这张图把Poly1305的四个核心模块串起来了。你可以看到,多项式求值是骨架,域运算是血肉,密钥生成和约束是安全屏障。缺了任何一个,整个认证体系都会出问题。