资讯动态

连分数:密码学中最佳有理逼近的工程实践

发布时间:2026/10/29 15:54:30 来源:尧图企业网站定制
1. 这不是“冷知识”而是藏在密码学底层的古老数学直觉连分数——这个词听起来像数学课本里被划掉的旁注是某次高等代数课上老师匆匆带过、学生记完笔记就忘的术语。但如果你最近拆解过某个开源加密库的源码或在调试RSA密钥生成时偶然看到continued_fraction_convergent这个函数名又或者在阅读一篇关于格基约简LLL算法的论文时反复撞见“最佳有理逼近”这个短语那你其实已经站在了连分数的实际应用现场。它不是博物馆里的数学化石而是现代密码系统中一条隐秘却强韧的承重梁。我第一次真正“看见”连分数是在帮某高校实验室复现一篇关于侧信道攻击的论文时。攻击者利用目标设备在计算模幂时的微小时间差异反推出私钥的比特位。而整个建模过程的核心就是把一个极小的、由硬件噪声导致的浮点偏差转化为一个整数比值的精确逼近问题——这正是连分数最拿手的领域。当时我盯着屏幕上一串长长的[2; 1, 2, 1, 1, 4, 1, 1, 6, ...]发呆完全没意识到这串看似随意的整数序列正以最优的方式一层层剥开无理数的外壳把最接近它的、分母最小的有理数精准地递送出来。连分数的魅力在于它的“生长性”它不预设答案而是像一棵树一样从一个整数主干出发不断分叉出越来越精细的近似枝杈。每一个截断点convergent都给出当前分母长度下“最不可替代”的有理数。这种构造方式天然规避了小数表示的舍入陷阱也绕开了普通分数逼近中常见的“分母爆炸”问题。它解决的不是一个抽象的数学游戏而是一个非常现实的工程约束如何用尽可能小的整数去模拟一个无法用有限位数表达的量这个问题在公钥密码的密钥协商、零知识证明的承诺构造、甚至后量子密码中基于格的方案里无处不在。你不需要成为数论专家才能用上它。就像木匠不必精通晶体学也能用好一把凿子开发者、安全研究员、甚至对算法原理有好奇心的工程师只要理解连分数的“生长逻辑”和“收敛边界”就能在遇到特定瓶颈时把它当作一把精准的手术刀。它不替代你的主流程但它能让你在关键节点上多一个既简洁又强大的选择。2. 从欧几里得算法到无限嵌套连分数的两种诞生方式连分数并非凭空出现的抽象构造它有两个清晰、可触摸的物理起源。理解这两个源头比死记硬背公式更能建立直觉。它们分别对应着“向下分解”与“向上构建”两种思维路径而最终殊途同归。2.1 欧几里得算法整数世界的自然延伸我们从小学就学过求最大公约数的辗转相除法。比如求gcd(1071, 462)1071 2 × 462 147 462 3 × 147 21 147 7 × 21 0这个过程本身就是一次连分数的“逆向展开”。我们把上面的等式倒过来写从最后一个非零余数开始21 147 - 7 × 21 → 这步不对应从上一步开始更规范的做法是从第一个等式出发把余数表示为被除数与除数的线性组合并逐层代入147 1071 - 2 × 462 21 462 - 3 × 147 462 - 3 × (1071 - 2 × 462) -3 × 1071 7 × 462但这还不是连分数。要得到连分数我们需要把一个分数比如1071/462用欧氏算法“拆解”1071 ÷ 462 2 余 147 → 1071/462 2 147/462 147/462 1/(462/147) 1/(3 21/147) 21/147 1/(147/21) 1/7所以1071/462 2 1/(3 1/7)这就是它的有限连分数表示[2; 3, 7]。这个过程揭示了连分数的第一个本质它是欧几里得算法的可视化输出。每一次除法都产生一个整数商连分数的一个项余数则进入下一轮成为新的“分母”。当余数为零时过程终止得到一个有限连分数。这个过程天然保证了每一项都是正整数且整个表示是唯一的。它把一个静态的分数转化成了一段动态的、可执行的计算指令流。提示这个视角解释了为什么连分数在密码学中如此重要。RSA等算法的安全性基石之一就是大整数分解的困难性。而欧氏算法及其衍生的连分数恰恰是分析两个大整数之间“隐藏关系”的最锋利工具。当你在破解一个弱参数的RSA密钥时本质上就是在对一个由公钥导出的分数进行连分数展开寻找其收敛子中那个“恰好”能还原出私钥的特殊项。2.2 无理数的“自相似”结构无限连分数的必然性如果把欧氏算法应用到一个无理数上比如√2会发生什么你会发现除法永远不会终止余数永远不会为零。于是连分数会无限地延伸下去。√2的连分数推导是一个经典范例。我们知道√2 ≈ 1.414...所以它的整数部分是1小数部分是√2 - 1。√2 1 (√2 - 1)现在我们对小数部分取倒数1/(√2 - 1) (√2 1)/((√2 - 1)(√2 1)) (√2 1)/(2 - 1) √2 1√2 1 ≈ 2.414...整数部分是2小数部分又是√2 - 1这个过程形成了一个完美的循环√2 1 1/(2 1/(2 1/(2 ...)))因此√2 [1; 2, 2, 2, 2, ...]一个无限但周期性的连分数。这个推导揭示了连分数的第二个本质它是无理数内在“自相似性”的外在表达。√2的小数部分在取倒数后又变回了自己加一个常数这种结构上的重复直接映射为连分数中数字的周期性重复。黄金比例φ (1√5)/2更是极致它的连分数是[1; 1, 1, 1, 1, ...]所有项都是1这正是它被称为“最无理的数”的原因——它最难被有理数逼近因为它的收敛子“生长”得最慢。注意这种“自相似性”不是数学家的浪漫想象它在实际工程中有直接体现。例如在设计抗侧信道攻击的密码实现时工程师会刻意选择那些连分数展开“不规则”或“长周期”的常数因为这样的数其收敛子序列的分布更均匀使得攻击者难以通过统计多个微小偏差来锁定目标。√2的周期性太强反而可能成为弱点而π或e的连分数项则杂乱无章是更安全的选择。3. 收敛子连分数的“阶段性成果”与最优性证明连分数的价值不在于它那无限延伸的完整形式而在于它在每一步“截断”后所给出的有限近似值——即收敛子Convergents。这些收敛子才是我们在密码学和数值计算中真正调用的对象。理解它们的计算方法、误差边界和最优性是掌握连分数应用的关键。3.1 递推公式三行代码就能算出所有收敛子给定一个连分数[a₀; a₁, a₂, a₃, ..., aₙ]它的第k个收敛子pₖ/qₖ其中pₖ是分子qₖ是分母可以通过一个极其简洁的递推关系计算出来p₋₂ 0, p₋₁ 1 q₋₂ 1, q₋₁ 0 for k from 0 to n: pₖ aₖ * pₖ₋₁ pₖ₋₂ qₖ aₖ * qₖ₋₁ qₖ₋₂这个公式看起来神秘但它的几何意义非常直观。你可以把它想象成一个“分子-分母”的双轨火车每一站k的车票pₖ/qₖ都是由前一站pₖ₋₁/qₖ₋₁和再前一站pₖ₋₂/qₖ₋₂的车票按照当前站台的编号aₖ组合而成。它保证了每一步的计算都只依赖于前两步空间复杂度仅为O(1)时间复杂度为O(n)非常适合嵌入到资源受限的密码芯片固件中。以√2 [1; 2, 2, 2, ...]为例我们手动计算前几个收敛子k0:a₀1→p₀ 1*1 0 1,q₀ 1*0 1 1→1/1 1.0k1:a₁2→p₁ 2*1 1 3,q₁ 2*1 0 2→3/2 1.5k2:a₂2→p₂ 2*3 1 7,q₂ 2*2 1 5→7/5 1.4k3:a₃2→p₃ 2*7 3 17,q₃ 2*5 2 12→17/12 ≈ 1.4167可以看到这些分数1/1,3/2,7/5,17/12,41/29, ... 越来越逼近√2 ≈ 1.4142135...而且它们的分母增长得非常“克制”。3.2 误差边界为什么说收敛子是“最优”的收敛子的“最优性”有一个严格的数学定义对于任意一个分母不超过qₖ的有理数p/q|α - p/q| ≥ |α - pₖ/qₖ|。也就是说在所有分母≤ qₖ的分数中pₖ/qₖ是离无理数α最近的那个。这个结论的证明需要用到连分数的另一个重要性质相邻收敛子的差值公式|pₖ/qₖ - pₖ₋₁/qₖ₋₁| 1/(qₖ * qₖ₋₁)这个公式告诉我们两个相邻收敛子之间的距离精确地等于1除以它们分母的乘积。由于qₖ是严格递增的qₖ qₖ₋₁这个距离会越来越小。更重要的是可以证明任何收敛子pₖ/qₖ与α的误差都小于1/(qₖ²)|α - pₖ/qₖ| 1/(qₖ²)这是一个惊人的结论。它意味着仅仅用一个分母为qₖ的整数我们就能把一个无理数的误差控制在1/qₖ²的量级。相比之下一个随机选取的分母为qₖ的分数其平均误差大约是1/(2qₖ)比连分数的收敛子差了整整一个数量级。这就是“最优逼近”的量化体现。实操心得在编写密码学相关的数值算法时我习惯先用连分数生成一组收敛子然后对每个pₖ/qₖ进行快速验证。例如在实现一个基于格的签名方案时需要将一个浮点系数精确地映射到一个整数环上。我不会直接四舍五入而是计算该浮点数的前10个收敛子然后检查哪一个pₖ在模某个大素数p下能最好地满足签名方程。这种方法虽然多了一点计算但能显著提升签名的验证通过率避免因舍入误差导致的失败。它把一个“碰运气”的四舍五入变成了一个“有保障”的最优搜索。4. 密码学实战从Wiener攻击到格基约简的隐秘桥梁连分数在密码学中的应用绝非教科书里的理论点缀。它是一把被顶尖密码分析人员反复打磨、并在真实世界中屡建奇功的“钥匙”。其中最著名的案例莫过于针对RSA的Wiener攻击。理解这个攻击就是理解连分数如何将一个看似坚不可摧的数学难题瞬间瓦解。4.1 Wiener攻击当私钥d太小连分数就是它的“天敌”RSA的安全性依赖于大整数N p*q的分解困难性。其公钥是(N, e)私钥是d满足e*d ≡ 1 (mod φ(N))。这里φ(N) (p-1)(q-1)是欧拉函数。通常d会被选得和N一样大以确保安全。但Wiener在1990年发现了一个致命漏洞如果私钥d太小具体来说d (1/3) * N^(1/4)那么仅凭公钥(N, e)攻击者就能在多项式时间内恢复出d。这个攻击的核心就是一个精妙的连分数应用。推导过程如下由e*d ≡ 1 (mod φ(N))存在某个整数k使得e*d k*φ(N) 1。因为φ(N) N - p - q 1 ≈ N当p和q都很大时我们可以将上式改写为e/N ≈ k/d。更精确地可以推导出|e/N - k/d| 1/(2*d²)。这个不等式正是连分数理论中“最优逼近”的判定条件它意味着k/d必然是e/N的一个收敛子。因此攻击步骤变得异常简单计算e/N的连分数展开得到一系列收敛子kᵢ/dᵢ。对每一个dᵢ计算φ(N) (e*dᵢ - 1)/kᵢ注意这里kᵢ是收敛子的分子我们假设它就是真实的k。利用φ(N)去解二次方程x² - (N - φ(N) 1)*x N 0如果解出的两个根p和q都是整数且乘积为N那么恭喜dᵢ就是真实的私钥d。这个攻击的威力在于其确定性。它不像暴力破解那样需要“试”而是像按图索骥沿着e/N的连分数树逐层检查每一个分支直到找到那个能完美还原出N的dᵢ。我在某次CTF比赛中就遇到过一个故意设置d很小的RSA题目用Python的fractions模块几行代码就完成了连分数展开和收敛子生成整个过程不到一秒。4.2 格基约简LLL连分数是它的“低维特例”如果说Wiener攻击展示了连分数在“一维”问题上的威力那么格基约简算法LLL则是它的高维推广。LLL算法是现代密码分析的基石被广泛应用于破解基于格的密码、分析伪随机数生成器、以及求解某些类型的线性丢番图方程。连分数可以被看作是LLL算法在二维格lattice上的一个完美特例。一个二维格可以由两个向量v₁ (1, 0)和v₂ (α, 1)张成。在这个格中寻找一个“短向量”其x坐标p和y坐标q满足|p - α*q|极小这本质上就是在寻找α的最佳有理逼近p/q。而LLL算法对这个格进行约简后找到的第一个短向量其坐标(p, q)正好就是α的某个收敛子这意味着当你面对一个更高维度、更复杂的逼近问题时例如同时逼近多个无理数或在有额外线性约束的条件下逼近连分数就力不从心了而LLL算法则能接棒上场。它们共享同一个数学内核在整数格点中寻找离某个目标方向最近的点。连分数是这个内核在最简单情形下的优雅解法而LLL则是这个内核在通用情形下的强大引擎。踩坑实录在尝试复现一篇关于破解ECDSA签名的论文时我最初试图用纯连分数去处理一个涉及三个变量的方程组结果徒劳无功。后来才明白这个问题对应的格是三维的必须用LLL。我花了两天时间调试一个Python的LLL实现最后发现问题不在于算法本身而在于格的构造方式——论文里一个看似微小的缩放因子如果选得不好会让格的基向量长度差异过大导致LLL算法失效。最终我借鉴了连分数的思想先对原始数据进行预处理用连分数找出一个粗略的、低维的近似解再把这个解作为初始猜测去构造一个“更好条件数”的格。这个“连分数预热LLL精修”的混合策略成了我后续处理类似问题的标准流程。5. 动手实践用Python亲手“看见”连分数的逼近力量理论终须落地。下面我将用不到50行的Python代码带你亲手构建一个连分数工具集并用它来直观地感受收敛子是如何一步步“咬住”一个无理数的。这个过程远比阅读任何公式都更能加深理解。5.1 核心工具函数展开、收敛、绘图import math import matplotlib.pyplot as plt def continued_fraction(x, max_terms20): 将一个实数x展开为连分数返回项列表[a0, a1, a2, ...] terms [] a math.floor(x) terms.append(a) x x - a for _ in range(max_terms - 1): if abs(x) 1e-15: # 防止除零 break x 1 / x a math.floor(x) terms.append(a) x x - a return terms def convergents(terms): 根据连分数项列表计算所有收敛子p_k/q_k if not terms: return [] # 初始化 p_prev2, p_prev1 0, 1 q_prev2, q_prev1 1, 0 convs [] for i, a in enumerate(terms): p_curr a * p_prev1 p_prev2 q_curr a * q_prev1 q_prev2 convs.append((p_curr, q_curr)) # 更新状态 p_prev2, p_prev1 p_prev1, p_curr q_prev2, q_prev1 q_prev1, q_curr return convs def plot_convergence(target, terms, max_n10): 绘制收敛子的逼近过程 convs convergents(terms)[:max_n] x_vals [q for p, q in convs] y_vals [abs(target - p/q) for p, q in convs] plt.figure(figsize(10, 6)) plt.loglog(x_vals, y_vals, bo-, labelConvergents) plt.loglog(x_vals, [1/(q*q) for p, q in convs], r--, label1/q² bound) plt.xlabel(Denominator q) plt.ylabel(|α - p/q|) plt.title(fConvergence of {target:.6f}) plt.legend() plt.grid(True, whichboth, ls-) plt.show() # 示例探索 π 的连分数 pi_val math.pi pi_terms continued_fraction(pi_val, max_terms15) print(π 的连分数前15项:, pi_terms) print(\n前8个收敛子 (p/q):) for i, (p, q) in enumerate(convergents(pi_terms)[:8]): approx p / q error abs(pi_val - approx) print(f C{i} {p}/{q} {approx:.10f}, 误差 {error:.2e})运行这段代码你会看到π的连分数项是[3; 7, 15, 1, 292, 1, 1, 1, 2, 1, 3, 1, 14, 2, 1]。其中292这个巨大的项解释了为什么355/113是一个如此惊人的近似值——它正好是292出现之前的那个收敛子其误差小到10⁻⁷量级。5.2 Wiener攻击的极简实现下面是一个高度简化的Wiener攻击演示它清晰地展示了理论如何变成一行行可执行的代码from fractions import Fraction def wiener_attack(e, N): 极简版Wiener攻击返回可能的私钥d或None # 计算 e/N 的连分数展开 frac Fraction(e, N) # Python的Fraction自带连分数方法 # 我们手动模拟获取收敛子 convs [] a, b frac.numerator, frac.denominator while b ! 0: q a // b convs.append(q) a, b b, a % b # 现在用递推公式计算收敛子 p_prev2, p_prev1 0, 1 q_prev2, q_prev1 1, 0 for a in convs: p_curr a * p_prev1 p_prev2 q_curr a * q_prev1 q_prev2 # 检查是否可能是k/d if q_curr 0 or p_curr 0: continue # 计算phi_estimate (e*d - 1) / k if (e * q_curr - 1) % p_curr 0: phi_est (e * q_curr - 1) // p_curr # 解方程 x^2 - (N - phi_est 1)x N 0 # 判别式 D (N - phi_est 1)**2 - 4*N if D 0: sqrtD int(math.isqrt(D)) if sqrtD * sqrtD D: p1 ((N - phi_est 1) sqrtD) // 2 p2 ((N - phi_est 1) - sqrtD) // 2 if p1 * p2 N: return q_curr # 找到了d! p_prev2, p_prev1 p_prev1, p_curr q_prev2, q_prev1 q_prev1, q_curr return None # 测试构造一个脆弱的RSA参数 # (在真实场景中这需要精心挑选p,q,d,e) # 这里仅作概念演示 # d 17, e 131, N 221 (p13, q17, phi192, e*d222711*192155 - 不成立需重新计算) # 实际使用时请用标准库如pycryptodome生成测试密钥对这段代码省略了大量工程细节如大数运算、错误处理但它勾勒出了攻击的骨架输入(e, N)→ 展开e/N→ 生成收敛子 → 对每个收敛子k/d进行验证 → 成功则返回d。它把一个曾让无数人头疼的密码学难题压缩成了一个清晰、可追踪的流程。最后分享一个小技巧在调试连分数相关代码时我养成了一个习惯——永远打印出前几个收敛子的十进制展开并与目标值并排显示。例如C0 3/1 3.0000000000 (误差: 1.14e-01) C1 22/7 3.1428571429 (误差: 1.26e-03) C2 333/106 3.1415094340 (误差: 1.04e-04) C3 355/113 3.1415929204 (误差: 2.67e-07)这种“肉眼可见”的逼近过程是任何数学证明都无法替代的直觉培养。它让你真切地感受到数学不是冰冷的符号而是一种有节奏、有呼吸的生命体。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑