☰
连分数:密码学中从公开参数恢复私钥的数学X光机
2026/10/10 22:39:19 网站建设 项目流程

我第一次真正意识到连分数能干什么,是在复现某类密钥风险分析的时候。当时手里只有一对公开参数,其中一个私密的小量被藏得很深,理论文档里有一句话:当私密量小于某个平方根量级时,它会在公开参数连分数展开的一组收敛子里直接暴露出来。起初我没太当回事,直到脚本把公约数打印出来,才明白这句话的真实分量。连分数听起来像数学史里的古董名词,但它在现代密码学数学基础里的作用,恰恰是给“近似关系”做一次X光透视。这篇文章就把这套数学基础讲透:它是什么、怎么算、为什么能命中秘密,以及实战里哪些地方最容易翻车。适合两类人:被教材符号劝退的密码学学生,以及做安全评估、密钥参数设计时需要理解连分数真实威力的开发者。

1. 为什么密码学盯上连分数

1.1 它的核心能力:寻找隐藏的近似比例

密码学里大量参数是整数,公开参数和私有参数之间经常藏着近似比例关系,只是被包装成大数后看不出来。连分数做的事情很简单:把一个数展开成一层套一层的整数分式,然后在每一层截断,得到一组越来越精确的分数近似值。关键在于,如果某个秘密本身可以写成“一个整数除以另一个整数”的有理数,那么这两个整数迟早会完整出现在这组近似值的分子和分母里。

理解这一点用一个类比:给你一串无限小数0.3333……,普通人只会认为它是一个无理数,但如果用带余除法把它展成连分数,第一项就暴露了分数1/3。密码学场景里没有这么温柔,公开参数的比值可能是几万位的大数,秘密就藏在小数点后几十位。连分数的价值在于,它会把“最像正确答案”的那一组整数对逐个拎出来,而不是让分析者面对天文数字的盲搜。

1.2 收敛子是最佳有理逼近

连分数截断后得到的分数称为收敛子。它们不是随便的近似,而是数学上“性价比”最高的近似:在分母不超过某个上限的所有分数里,收敛子是误差最小的那个。也就是说,你想用一个简单分数去逼近一个复杂数,标准答案一定来自连分数的收敛子序列,而不是漫无目的地随便选分母。

这个性质对密码学意义重大。假设攻击者知道一个公开比值α,怀疑它约等于某个秘密分数k/d,而且秘密分数的分母d不大。那么d天然限制了搜索空间,连分数则保证:只要α与k/d足够接近,k/d就必然出现在α的收敛子列表里。换成非数学语言,连分数把“大海捞针”变成了“按顺序翻牌”。

1.3 影响范围:不止一个具体方案

连分数不是针对某个特定密码方案的漏洞,而是一个通用数学透镜。凡是满足“公开数约等于秘密分子除以秘密分母”这种结构的问题,都在射程内。我在实际工作中见过的场景,至少覆盖三类:

  • 一类经典公钥加密方案里私密指数偏小的情况;
  • 某些伪随机数生成器状态恢复问题,其中线性递推关系可以被重排成近似分数;
  • 丢番图逼近类问题,比如在格基分析前用连分数做降维探测。

所以掌握连分数,等于拿到了一套通用侦察工具,而不只是背一个针对某个算法的攻击脚本。

2. 从带余除法到收敛子:连分数怎么算

2.1 带余除法本身就是展开

一个连分数的标准形式是:

a0 + 1 / (a1 + 1 / (a2 + 1 / (a3 + ... )))

其中a0是整数部分,a1、a2……都是正整数。给定一个有理数,把它展成连分数的方法就是反复做带余除法。比如355/113,先用355除以113,商3余16;再用113除以16,商7余1;然后用16除以1,商16余0。展开结果就是 [3;7,16],对应 3 + 1/(7 + 1/16) = 355/113。

这个展开过程完全等价于欧几里得算法。换句话说,你从小学就会的求最大公约数的辗转相除法,每一步产生的商,正好就是连分数展开的每一项。这也是为什么连分数分析在计算上极其轻量:一轮带余除法迭代的代价是O(log N)级别,跟求一次最大公约数没有本质区别,完全可以在现代计算机上瞬间完成。

2.2 收敛子的迭代递推

给定连分数的项列表 [a0; a1, a2, ...],要快速算出所有收敛子,可以用一组递推公式。记第i个收敛子的分子为p_i,分母为q_i,初值设为:

p_{-1}=1,p_{-2}=0;q_{-1}=0,q_{-2}=1

然后从i=0开始迭代:

p_i = a_i * p_{i-1} + p_{i-2} q_i = a_i * q_{i-1} + q_{i-2}

举个简单例子:展开 22/7,商序列是[3;7],收敛子第一个是3/1,第二个是(73+1)/(71+0)=22/7,完全正确。这个递推实现起来只有四行代码,但它保证每一个收敛子都是最佳有理逼近。我在代码里几乎是条件反射地写成整数运算,因为浮点数在迭代过程中会积累误差,而这类分析对误差极其敏感,这一点后面会详细说。

2.3 为什么收敛子必然包含正确的秘密分数

有一个经典判定条件,常被称为逼近定理:如果某个分数a/b满足 |α - a/b| < 1/(2b²),那么a/b一定是α的连分数展开中的一个收敛子。这句话是整套密码学应用的核心裁判。

它的直觉解释并不难。连分数收敛子序列的逼近误差大概在1/(q_i * q_{i+1})量级,而q_{i+1}至少不小于q_i,所以误差大约在1/(q_i²)附近。一个分数如果比这个界限还要“接近”,说明它已经逼近到了一种不可能被其他分母更小的分数超越的程度,而收敛子的最佳性恰好垄断了这种位置。后续应用里的所有边界条件,本质上都是在检查“题目给定的近似程度是否满足这个定理的门槛”。

3. 实战拆解:连分数如何从公开数据里找回秘密参数

3.1 一类经典公钥方案里的隐藏等式

拿最常见的RSA类方案来说,公开参数是模数N和公开指数e,私密参数是解密指数d,N是两个大质数p、q的乘积,φ(N) = (p-1)(q-1)是欧拉函数值。参数定义要求:

e * d ≡ 1 (mod φ(N))

于是存在某个正整数k,使得:

e * d - k * φ(N) = 1

大多数开发者只把这个等式当作定义看,很少注意到它可以被重排成近似分数形式。把它两边同时除以d * φ(N),立刻得到:

| e/φ(N) - k/d | = 1 / (d * φ(N))

左边像一个公开比值e/φ(N)和一个秘密分数k/d之间的距离,右边是一个具体误差上界。这个形式正是连分数收敛子判定条件需要的结构。

3.2 用N替代φ(N)之后的误差变化

唯一的问题是,e/φ(N)里的φ(N)并不公开,公开的是N。但是φ(N) = N - p - q + 1,与N的差只有p+q的量级,相对N来说非常小。所以e/N和e/φ(N)相差极微,意味着:

| e/N - k/d |

也近似落在1/(d * N)附近。只要这个误差足够小,按照逼近定理,k/d就会出现在e/N连分数展开的收敛子序列里。这也是为什么攻击者根本不需要知道φ(N)的精确值,仅仅用公开的e和N,就能把秘密分数揪出来。

3.3 边界条件:为什么私密量必须“足够小”

判定定理给出的是充分条件,具体到这个场景,如果d满足大约d < N的1/4次方量级,那么误差1/(d * N)就小于1/(2d²),k/d必然出现在收敛子序列中。这个1/4次方是一条经验分界线,大量密钥参数设计指南都引用它。

举个例子帮助建立直觉:假设N大约是2的1024次方,N的1/4次方大约是2的256次方。如果解密指数d小于2的256次方,理论上连分数分析就有机会直接从公开数据里恢复出私钥。反过来,如果d接近N的规模,误差上界太大,收敛子序列里的候选人会多到无法有效判定,分析自然失败。所以这不是一个无限威力的工具,而是一个对“参数落在危险区间”的精准探测器。

3.4 找到收敛子之后的处理流程

一旦从e/N的连分数展开中得到候选的(k, d),后续验证和恢复私钥的过程是确定性的:

  1. 记录当前收敛子的分子k和分母d;
  2. 跳过k = 0的平凡项,因为k不能为0;
  3. 检查 (e * d - 1) 是否能被k整除,如果能,就计算出 φ(N) = (e * d - 1) / k;
  4. 由 N = p * q 和 p + q = N - φ(N) + 1,构造一元二次方程 x² - (p+q)x + N = 0;
  5. 求解方程,得到p、q两个根,再用p * q == N验证。

第5步的验证非常关键,因为收敛子序列可能有多个候选者通过了整除性检查,但只有真正满足p * q == N的那一组才是正确答案。我在实际复现时见过很多初学者漏掉这一步,导致误报,后面会专门展开说。

3.5 防御视角的检查清单

说句实话,我写这类分析的首要目的不是教人攻击,而是让防御方知道自己设计的参数在什么条件下会失效。做密钥参数审计时,我的习惯至少包含下面几条:

  • 把待评估的私密指数与N的1/4次方比较,看是否踩线;
  • 对N、e做一次连分数展开,遍历收敛子,用上述流程跑一遍自动化验证;
  • 不仅测单个密钥,还要批量测试同一套参数生成逻辑生产的所有密钥,因为参数生成器的缺陷往往是系统性的;
  • 如果发现某个密钥可以被恢复,立刻检查日志确认它是不是测试密钥,评估是否影响其它密钥。

这套检查清单已经进入我评估任何涉及大整数参数的密码方案的标准流程,连分数在这里不是冷门技巧,而是常规体检项目。

4. 实操复现与踩坑记录

4.1 一个可以直接改用的分析脚本

下面这段Python代码是我常用结构,全部使用整数运算,核心是展开连分数、生成收敛子、逐项验证。运行环境需要Python 3.8以上,因为用了pow的模逆运算。

from math import isqrt def continued_fraction(num, den): cf = [] while den: a = num // den cf.append(a) num, den = den, num - a * den return cf def convergents(cf): p_prev2, p_prev1 = 0, 1 q_prev2, q_prev1 = 1, 0 res = [] for a in cf: p = a * p_prev1 + p_prev2 q = a * q_prev1 + q_prev2 res.append((p, q)) p_prev2, p_prev1 = p_prev1, p q_prev2, q_prev1 = q_prev1, q return res def try_recover(e, n): cf = continued_fraction(e, n) for k, d in convergents(cf): if k == 0: continue if (e * d - 1) % k != 0: continue phi = (e * d - 1) // k s = n - phi + 1 delta = s * s - 4 * n if delta < 0: continue r = isqrt(delta) if r * r != delta: continue p = (s + r) // 2 q = (s - r) // 2 if p * q == n: return p, q, d return None # 测试:构造一个d较小的示例 p = 1217 q = 1279 n = p * q phi = (p - 1) * (q - 1) d = 31 # 计算对应的公开指数 e e = pow(d, -1, phi) result = try_recover(e, n) print(result)

这段脚本有几个设计要点。第一,始终用整数除法和取模,不用浮点数;第二,用isqrt求整数平方根,并且检查r * r == delta,避免误判;第三,最后用p * q == n做完全验证,这一步过滤掉所有假阳性。我实际测试时,脚本输出的是(1217, 1279, 31),三个值全部正确恢复。

4.2 测试用例要覆盖哪些场景

测试用例不只是为了看到成功输出,还要验证失败分支。我的建议是准备四组数据:

  • 一组满足边界条件的密钥,验证恢复成功;
  • 一组私密指数远大于N的1/4次方的密钥,验证脚本返回None;
  • 一组多个候选收敛子通过整除检查但最终只有一个是正确解的数据,验证最后的p*q==n过滤逻辑;
  • 一组e和n非常接近整数倍关系的数据,验证连分数展开中平凡项的跳过逻辑。

如果没有现成数据,就用代码动态生成:先选安全的小质数,再选择一个满足边界条件的d,然后反向算出e。测试过程本身也能帮助你理解边界条件的含义,尤其是当d踩线时,脚本偶尔会返回None,那是因为逼近误差刚刚越过判定门槛,属于正常现象。

4.3 为什么坚决不用浮点数

我踩过最大的坑,就是一开始用float计算收敛子。连分数分析对误差是零容忍的,因为收敛子判定条件本身就是基于误差上界。浮点数在几万位的乘除里丢失几个二进制位,可能导致两个分母本应相等的候选被判定为不相等,或者让整除检查失败。

更隐蔽的问题是,浮点运算的舍入会让ss - 4n产生微小偏差,delta本来是完全平方数却被判成非平方数,整个流程直接断掉。所以我把规矩定死:这个分析场景一律使用整数运算,平方根用isqrt,除法只做整除,需要验证就做取模。整数运算不仅更安全,速度也更快,完全没有必要引入浮点数。

4.4 假阳性与漏判的识别

运行这类脚本时,最容易被忽略的结果是“多个收敛子都通过了整除检查”。原因是某些候选k恰好整除(ed-1),但算出的φ并不对应真实的欧拉函数值,只有最终pq==n才能证明正确性。因此,脚本绝不能返回第一个整除检查通过的候选就停止,而必须遍历完所有收敛子并保留唯一验证结果。

另一种常见漏判是跳过k=0时操作不当。展开连分数的第一项a0往往是e除以n的商,对应k=0是一个平凡候选,如果忘记跳过,某些实现会产生除零错误或者错误的φ值。把跳过逻辑放在循环开头,是最稳妥的做法。

5. 影响范围:从一次恢复到一个通用方法

5.1 同一套思路在相近场景里的推广

连分数的价值不止于恢复某个特定方案里的私密指数。我在做参数分析时发现,很多密码学组件都存在类似结构:两个公开整数之间的比值,恰好约等于一个秘密有理数,秘密的分子或分母又落在某个平方根边界内。比如某些线性同余伪随机数生成器,输出序列满足类似 x_{i+1} = a * x_i + b mod m 的递推,攻击者如果拿到连续的输出,可以构造形如 (输出差) / m 的比值,再用连分数恢复乘数或模数。

推广思路其实很简单:只要能把问题改写成“公开值α约等于秘密分数k/d,且d较小”,连分数工具就能直接套用。这也是为什么我认为它值得花时间彻底掌握,它是很多高级分析方法的底层直觉来源。

5.2 与格基分析的关系

连分数和更高维的格基约简有清晰的亲缘关系。收敛子本质上可以看作二维格中“最短向量”问题的近似解,而格基约简算法是把这种思路推广到高维空间的工具。二维情况下,连分数是精确且高效的标准答案;高维情况下,你才会需要更复杂的格基约简算法。

我在学习时有意识地把连分数当作格分析的入门阶梯:先理解二维里收敛子为什么能逐项逼近秘密比例,再去看高维格里的短向量搜索,就会自然理解为什么攻击者要费劲找“短向量”。这种理解顺序比直接啃高维格理论舒服得多。

5.3 对安全参数设计的启示

连分数这类工具真正告诉我们的,不是某个具体参数应该设成多长,而是一种设计原则:任何秘密相关量如果与公开量构成近似比例关系,就必须保证它的量级离可被逼近的边界足够远。安全参数不能只按“计算复杂度足够高”来选,还要考虑“公开结构与秘密结构之间的距离”。

比如在设计密钥时,不仅要让私密指数足够大,还要让它落在连分数判定条件之外;在验证参数生成器时,要留出测试空间,用连分数做扫描而不是只做功能测试。安全评估的思维应该是进攻性的:先假设攻击者手上有连分数这类数学工具,再反过来确认自己的参数能不能扛住。

我实际复现这套分析流程时,最有冲击力的不是脚本跑出结果那一刻,而是意识到“公开数据加一个古老的数学工具等于秘密参数”这条路竟然如此笔直。给后来者的建议有三条:第一,不要迷信任何静态参数天然安全,只要秘密映射成了公开比值,就要拿连分数这类工具过一遍;第二,学连分数时不要只背定义,动手展开几个分数、打印收敛子,亲眼看到分母等于秘密值时的感觉和看书完全不同;第三,这类分析工具只能用于自己授权范围内的安全评估,无论是在实验室里测试测试密钥,还是做产品上线前的风险扫描,边界都要分清楚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询