Caloogy 文章
English

LLL 格基约化:背包密码是怎样倒下的

日期
2025 年 9 月 29 日
系列
密码学
阅读
5 分钟
主题
LatticeLLLKnapsackMerkle–Hellman

一句话版本

同一个格可以有很多组基:有的基向量又长又斜,有的又短又正。LLL 算法(1982)能在多项式时间内把一组"坏基"变成"比较好的基",从而找到相当短的向量。 很多密码的秘密恰好就是某个格里的短向量。1978 年的 Merkle–Hellman 背包密码就这样被攻破了。今天 LLL 依然是攻击 ECDSA 随机数泄露、评估后量子格密码参数的核心工具。

全文分三层:

  1. 通俗理解:格、好基与坏基,背包问题
  2. 实际用处:背包密码的兴衰、从侧信道泄露中恢复私钥、后量子参数选择
  3. 数学原理:二维的高斯约化、LLL 的思路、背包密码与它的格攻击,附手算例子

第一层:通俗理解

1.1 格、好基与坏基

在平面上取两个向量 b1,b2b_1, b_2,它们所有的整数组合 a1b1+a2b2a_1 b_1 + a_2 b_2 构成一片规则排列的点,叫格。

同一片点阵可以用不同的基来生成。比如整数格 Z2\mathbb{Z}^2(所有整数坐标点):

拿到坏基的人,很难看出"这片格里最短的向量只有长度 1"。这正是格密码的基本思路:公钥给坏基,私钥是好基(见《后量子密码》)。

1.2 背包问题

给你一堆重量各异的物品 {82,123,287,83,248,373,10,471}\{82, 123, 287, 83, 248, 373, 10, 471\},问:哪几件加起来恰好重 881881?

这是子集和问题,一般情形是 NP 完全的,物品多了只能接近穷举。1978 年 Merkle 和 Hellman 据此设计了最早的公钥密码之一:挑哪几件就是明文的比特,总重量就是密文。

1.3 为什么倒下

Merkle–Hellman 为了让合法接收者能解密,在物品重量里藏了一个"暗门"结构。1982 年 Shamir 利用这种结构把它破解了;不久之后人们发现,更通用的办法是把背包问题写成一个格,秘密的比特串对应格中一个异常短的向量,LLL 一跑就出来了。


第二层:实际用处

背包密码的兴衰

年份 事件
1978 Merkle–Hellman 背包密码提出,和 RSA 几乎同时
1982 Shamir 给出多项式时间攻击
1982 Lenstra、Lenstra、Lovász 发表 LLL 算法(原本是为了分解有理系数多项式)
1985 起 Lagarias–Odlyzko 等人用格约化攻击"低密度"背包;此后各种改进版背包几乎全军覆没

LLL 在今天的用途


第三层:数学原理

3.1 格的定义

给定线性无关的向量 b1,…,bn∈Rmb_1, \dots, b_n \in \mathbb{R}^m,格为

L={∑iaibi:ai∈Z}.L = \Big\{\sum_i a_i b_i : a_i \in \mathbb{Z}\Big\}.

两组基 BB、B′B' 生成同一个格,当且仅当 B′=UBB' = UB,其中 UU 是幺模矩阵(整数矩阵,行列式 ±1\pm 1),即 U∈GLn(Z)U \in GL_n(\mathbb{Z})。所以"换基"就是在群 GLn(Z)GL_n(\mathbb{Z}) 里乘一个元素。

格的行列式(基本平行体的体积)∣det⁡B∣|\det B| 不随换基改变,是格的不变量。

3.2 二维:高斯约化,就是向量版的欧几里得算法

二维情形下有一个精确算法(Lagrange–Gauss):反复用短向量去"削"长向量,直到削不动为止。这和求最大公因数时"大数减小数"完全是同一个思路。

手算例子:把坏基 (5,8),(8,13)(5, 8), (8, 13) 约化

每一步都用一个向量减去另一个,这是幺模变换,生成的格不变:

步 操作 基
0 (5,8), (8,13)(5, 8),\ (8, 13)
1 (8,13)−(5,8)(8, 13) - (5, 8) (5,8), (3,5)(5, 8),\ (3, 5)
2 (5,8)−(3,5)(5, 8) - (3, 5) (2,3), (3,5)(2, 3),\ (3, 5)
3 (3,5)−(2,3)(3, 5) - (2, 3) (2,3), (1,2)(2, 3),\ (1, 2)
4 (2,3)−(1,2)(2, 3) - (1, 2) (1,1), (1,2)(1, 1),\ (1, 2)
5 (1,2)−(1,1)(1, 2) - (1, 1) (1,1), (0,1)(1, 1),\ (0, 1)
6 (1,1)−(0,1)(1, 1) - (0, 1) (1,0), (0,1)(1, 0),\ (0, 1)

最终得到标准基,证实这个格就是 Z2\mathbb{Z}^2。注意坐标全是斐波那契数,和欧几里得算法求 gcd⁡(8,13)\gcd(8, 13) 的过程一模一样,斐波那契数恰好是欧几里得算法的"最坏情况"。(已用 Python 验算,实际的高斯约化每步会一次减去最优的整数倍,步数更少,结果相同。)

3.3 LLL 的思路

高维时没有精确又高效的算法(求最短向量 SVP 是 NP 难的),LLL 做的是近似:

  1. 尺寸约化:对每个 bib_i,减去前面向量的整数倍,让它在 Gram–Schmidt 方向上的分量尽量小(就是二维高斯约化的推广);
  2. Lovász 条件:如果相邻两个向量的顺序"不好"(后一个的 Gram–Schmidt 长度比前一个短太多),就交换它们,再回到第 1 步。

可以证明它在多项式时间内结束,输出的第一个向量满足

∥b1∥≤2(n−1)/2⋅λ1(L),\|b_1\| \le 2^{(n-1)/2} \cdot \lambda_1(L),

λ1\lambda_1 是真正的最短向量长度。最坏情况下这个近似因子是指数级的,但实际表现比理论好得多,在几十维时经常直接找到最短向量。更强的 BKZ 算法在 LLL 基础上分块求精确解,用时间换质量。

3.4 Merkle–Hellman 与它的格攻击

私钥:一个超递增序列 w=(2,3,7,14,30,57,120,251)w = (2, 3, 7, 14, 30, 57, 120, 251)(每项大于前面所有项之和),模数 q=491>∑wiq = 491 > \sum w_i,乘数 r=41r = 41,gcd⁡(r,q)=1\gcd(r, q) = 1。

公钥:bi=rwi mod qb_i = r w_i \bmod q,即 b=(82,123,287,83,248,373,10,471)b = (82, 123, 287, 83, 248, 373, 10, 471)。它看上去是一般的背包,超递增结构被模乘"藏起来"了。

加密与解密

加密明文比特 0110000101100001(字母 a 的 ASCII 码):取第 2、3、8 件, C=123+287+471=881.C = 123 + 287 + 471 = 881.

解密:r−1 mod 491=12r^{-1} \bmod 491 = 12,C′=881⋅12 mod 491=261C' = 881 \cdot 12 \bmod 491 = 261。对超递增序列做贪心:

  • 261≥251261 \ge 251,取第 8 件,剩 1010
  • 120,57,30,14120, 57, 30, 14 都大于 1010,跳过
  • 10≥710 \ge 7,取第 3 件,剩 33
  • 3≥33 \ge 3,取第 2 件,剩 00

得到比特 0110000101100001,还原出 a。(已用 Python 验算。)

格攻击:构造以下面各行为基的格(最后一列是重量):

(10⋯0b101⋯0b2⋮⋱⋮00⋯1bn00⋯0−C)\begin{pmatrix} 1 & 0 & \cdots & 0 & b_1 \\ 0 & 1 & \cdots & 0 & b_2 \\ \vdots & & \ddots & & \vdots \\ 0 & 0 & \cdots & 1 & b_n \\ 0 & 0 & \cdots & 0 & -C \end{pmatrix}

用明文比特 xix_i 作系数,把前 nn 行加起来再加上最后一行,得到

(x1,…,xn, ∑xibi−C)=(x1,…,xn, 0).(x_1, \dots, x_n,\ \textstyle\sum x_i b_i - C) = (x_1, \dots, x_n,\ 0).

这是一个各分量只有 0 和 1 的极短向量,长度至多 n\sqrt{n};而格中其他向量通常长得多。当背包"密度"较低时,LLL 能直接找到它,比特串就是明文。


总结

层次 要点
直觉 同一个格有好基和坏基;短向量藏在坏基后面
代数 换基 == 乘幺模矩阵(GLn(Z)GL_n(\mathbb{Z}));二维约化就是欧几里得算法
LLL 多项式时间找到"足够短"的向量,近似因子 2(n−1)/22^{(n-1)/2}
影响 击溃背包密码;攻击 ECDSA 随机数泄露;决定后量子参数