PKE公钥加密实验全解析:从RSA原理到安全测试实战
2026/9/1 13:33:54 网站建设 项目流程

简介:面向华中科技大学操作系统PKE实验的学习者与对操作系统底层机制感兴趣的研究者,这份仅6KB的轻量代码包集中解析了Lab1~Lab4四个挑战任务,内容涵盖用户程序调用栈打印、复杂缺页异常处理、进程等待与数据段复制、相对路径操作等关键知识点,既有实验需求分析,也有关键代码实现。压缩包共3个文件,包含html说明文档、inscode项目文件与gitignore版本控制配置,结构简单但直击核心。作者从问题描述入手,结合流程图与代码片段,深入分析了ELF文件结构解析、函数调用链路的追踪、缺页异常处理逻辑、进程间通信机制以及文件系统路径转换等实现细节,同时分享了内核态与用户态传递信息、动态内存分配、进程状态转换和调试排错的具体经验。目前已有168人学习,适合正在完成PKE实验、希望对照实现思路巩固操作系统原理的开发者参考使用。 每年密码学课程一进入后半程,就会有人抱着"PKE实验"这几个字来问我:PKE到底是什么?代码要从哪一行开始写?其实PKE就是Public Key Encryption(公钥加密)的缩写,华中科大的这个课程实验,核心并不是让你调用某个现成的加密库跑通一个Demo,而是要你从底层数学原理出发,自己实现一套可用的公钥加密方案,并完成安全性分析和性能测试。我当年做这个实验时,天真地以为把RSA算法背下来就万事大吉,结果在密钥生成、填充方案、边界输入测试这几个环节反复翻车,前前后后折腾了好几个通宵。这篇博客就把我从实验目标拆解、框架设计,到核心代码实现、安全测试的完整思路记录下来,尤其是那些代码层面容易忽略、但验收时一定会被问到的细节。

1. PKE实验到底在考什么:目标拆解与常见误区

1.1 隐藏评分点:重点不是"跑通",而是"为什么"

很多同学拿到实验题的第一反应是去GitHub搜一份现成的RSA实现,改改变量名就交上去。这么做最直接的后果是答辩时一问三不知。华中科大这类PKE实验的评分构成通常分四块:方案正确性、实现健壮性、安全性分析、报告与答辩,其中方案正确性只占一部分,安全分析和答辩才是拉开差距的关键。

我后来复盘时发现,这个实验真正想考察的能力是:你是不是真的理解公钥加密的每一个环节,以及每个参数为什么这么选。举个最简单的例子:RSA生成密钥时,两个大素数p和q的差值有没有限制?如果|p-q|很小会怎样?课本上只说了一句"要随机且独立",但工程实现时还需要考虑Fermat分解攻击,因为当两个素数接近时,可以通过n的平方根附近穷举来分解n。这些细节在评分标准里未必白纸黑字写出来,但在验收时随口一问就能看出你是不是真懂。

1.2 最容易跑偏的三个方向

第一个坑是"只调库不实现底层"。有的同学用Java的BigInteger或者Python的pow()函数,几行代码就把加解密跑通了,于是觉得实验太简单。但实验要求往往明确写着"实现大整数运算、素性检测、模幂运算"等核心模块,只用语言自带的库,等于绕开了考核目标。即便允许使用大数库,也要清楚库里每个方法的时间复杂度,比如BigInteger.modPow()用的就是滑动窗口快速幂,而不是朴素循环。

第二个坑是"忽略随机数质量"。RSA密钥生成的随机性直接影响安全性。有人为了调试方便,直接设置固定随机种子,这会导致每次生成的密钥一样,如果被验收老师看到,基本可以断定是临时糊弄的。正确做法是在系统熵源的基础上进行扰动,比如C语言里用/dev/urandom读种子。

第三个坑是"不做异常测试"。我见过很多人的测试用例只有"加密-解密-对比明文"这一条路径,密钥位数、特殊输入、大数边界全都没测。一个只测正常路径的程序,和玩具没什么区别,后面第四章我会详细讲怎么设计异常测试清单。

2. 实验框架设计:模块划分与数据结构先行

2.1 模块边界比代码本身更重要

整个PKE实验看似只是一个加解密算法,但为了后期测试和写报告方便,我强烈建议把工程拆成四个独立模块:参数生成模块、密钥管理模块、加解密核心模块、测试分析模块。模块之间只通过接口通信,不要揉在一起。比如参数生成模块负责调用素性检测生成大素数p、q,并计算出n、φ(n)、e、d;密钥管理模块负责把密钥序列化成文件、从文件加载;加解密模块只接收"明文+公钥"或"密文+私钥"并返回结果。

之所以强调模块边界,是因为实验后期你必然要做安全攻击实验(比如共模攻击、低指数攻击),这些攻击需要你灵活地操作密文和密钥。如果所有代码耦合在一起,写攻击脚本时就得翻遍整个项目找变量,而模块化之后,你只需要import对应函数就行。我自己在重构前就吃过这个亏,第一版代码把所有逻辑塞在一个main函数里,后来做共模攻击时不得不删掉一大半代码重写。

2.2 密钥格式与文件读写:最容易翻车的地方

密钥管理模块看起来不起眼,但设计得好不好,直接影响后面所有环节。公钥通常用(n, e)表示,私钥用(n, d)或(p, q, d)表示。实验报告里为了演示方便,可以选择自定义的文本格式,而不是直接生成标准PEM文件(PEM涉及ASN.1编码,会分散你对核心算法的注意力)。

自定义格式需要注意两个细节:一是大数如何与字节流互转。RSA的n通常是1024位,转成字节数组是128字节,C语言里可以用无符号大数组逐字节处理,Python里可以用int.to_bytes(len, 'big'),这里必须统一字节序,建议全部采用大端序,与网络字节序保持一致,避免移植时出错。二是文件读写时不要直接用printf("%d")输出大数,因为大数远超int范围,正确做法是先转成十进制字符串或十六进制字符串再落盘,读回来时再逆解析。

def save_key_to_file(key_path, n, e): with open(key_path, 'w') as f: f.write(f"{n:x}\n") # 十六进制输出大整数 f.write(f"{e:x}\n")

这里用十六进制而不是十进制,是因为十六进制转bytes更方便,且每一位数字固定对应4个bit,调试时肉眼检查也更容易看出规律。

2.3 核心接口定义

参数生成、加解密、签名验证这三个操作,建议设计成统一风格的接口。比如Python版可以这样定义:

def keygen(bits: int = 1024) -> tuple[dict, dict]: """生成公钥和私钥""" pass def encrypt(public_key: dict, plaintext: bytes) -> bytes: """公钥加密,输入输出均为字节串""" pass def decrypt(private_key: dict, ciphertext: bytes) -> bytes: """私钥解密""" pass

接口统一之后,测试函数就可以用同一套代码测不同密钥长度、不同输入,写报告时的性能对比表格也能自动生成。我当时用的就是这种设计,后面做1024位、2048位、3072位的耗时对比,只改一个参数就全部跑完。

3. 核心算法实现详解:从素性检测到加解密

3.1 Miller-Rabin素性检测:概率性背后的工程考量

RSA的安全性根基是"大整数分解困难",所以第一步必须生成足够大的素数。实际工程中几乎不使用确定性素性检测,因为AKS算法虽然理论上多项式时间,但常数因子太大,根本跑不动。通行做法是Miller-Rabin概率素性检测:对候选奇数n,随机选基a,计算a^(n-1) mod n是否满足特定条件,如果满足则n可能是素数;反复测试多轮,错误率会降到2^(-k)以下。

实现时有一个容易被忽略的点:Miller-Rabin里需要把n-1分解成2^s * d的形式,其中d为奇数。这里要用大数右移来除2,而不是直接除2。另外,对随机基a的选择,课本上说的是"随机抽取",但工程实现时,不同基底的组合可以覆盖更多的小素数情况,实践中可以先跑一遍预先计算好的固定基表(比如前12个素数),再补充几个随机基,这样既保证了速度,又能显著降低误判概率。

import random def miller_rabin_test(n: int, rounds: int = 40) -> bool: if n < 2: return False for p in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]: if n % p == 0: return n == p d = n - 1 s = 0 while d % 2 == 0: d //= 2 s += 1 for _ in range(rounds): a = random.randint(2, n - 2) x = pow(a, d, n) if x == 1 or x == n - 1: continue for _ in range(s - 1): x = pow(x, 2, n) if x == n - 1: break else: return False return True

这里rounds取40是安全性和速度的折中。误判率2^(-40)对于本科实验来说绰绰有余,同时整轮测试在1024位大数上耗时可接受。

3.2 快速幂取模:公钥运算的效率基础

RSA加解密本质上都是模幂运算,即计算a^b mod n。如果直接先算a^b再取模,b可能是个2048位的大数,中间结果会膨胀到根本存不下。快速幂取模的思路是把指数b写成二进制,从低位到高位逐位处理,底数每轮自乘取模,遇到指数位为1就乘入结果。这样时间复杂度从O(b)降到O(log b),同时每一步都做模运算,中间数值一直控制在n的范围内。

def mod_pow(base: int, exp: int, mod: int) -> int: result = 1 base %= mod while exp > 0: if exp & 1: result = (result * base) % mod base = (base * base) % mod exp >>= 1 return result

Python内置的pow(base, exp, mod) 其实内部已经做了优化,我在实验里为了展示原理,自己实现了上面这个版本,然后在性能分析里对比了内置函数和自己实现的差距,这反而是报告里一个不错的亮点:可以看到Python内置C实现比我手写Python版本快一个数量级,从而引出"工程优化与教学实现之间差异"的讨论。

3.3 RSA加解密与密钥生成流程

密钥生成的完整流程是:先随机生成两个长度接近但不相近的大素数p和q,计算n = p * q,φ(n) = (p-1)(q-1),选一个与φ(n)互质的公钥指数e(常用65537,因为它是费马素数且二进制只有一个1,模幂计算更快),再用扩展欧几里得算法求e模φ(n)的逆元d。

这里必须提醒一个新手容易踩的坑:生成p和q后,一定要检查|p-q|是否过小。如果两个素数太接近,Fermat分解法可以从√n附近的整数开始逐个尝试,快速分解n。我在代码里加了一个校验,如果差值小于某个阈值就重新生成。

def generate_keypair(bits: int = 1024): e = 65537 while True: p = generate_large_prime(bits // 2) q = generate_large_prime(bits // 2) if abs(p - q) > (1 << (bits // 2 - 100)): # 防止Fermat分解 break n = p * q phi = (p - 1) * (q - 1) d = mod_inverse(e, phi) return (n, e), (n, d)

那个差值阈值怎么定的?简单说,就是让p和q的高位不要完全相同。更严格的做法是要求p/q既不是太接近1,也不是差距过大,因为差距过大意味着其中一个因数很小,同样容易被试除抓到。

3.4 如果实验换成了ElGamal或ECC,框架怎么迁移

有些年份的实验题可能不指定RSA,而是让你实现ElGamal或基于椭圆曲线的加解密。遇到这种情况不要慌,上面这套"模块划分+接口设计+异常处理"的框架照样适用。ElGamal的底层是模素数p的有限域上的离散对数问题,需要实现大素数生成、原根搜索(Primitive Root,即群的生成元)、以及一次性的随机数k;ECC则需要实现椭圆曲线点运算(点加、倍点、标量乘)。只要把"密钥生成、加密、解密"这三个接口定义好,内部算法换掉就行。我当时的做法是先跑通RSA版本,再对照着实现ElGamal,两个版本的接口完全一致,连测试代码都没改。

4. 实验报告里真正加分的部分:安全测试与性能分析

4.1 共模攻击复现:不只是懂原理

RSA有一个经典弱点:如果同一份明文m分别用同一个n、两个不同的指数e1和e2加密,只要e1和e2互质,攻击者不需要私钥就能通过扩展欧几里得算法恢复出明文。这是课本上的经典结论,但真正去代码里复现时,你会发现一个坑:由于m^s1 * m^s2 = m,但扩展欧几里得算出来的s1和s2可能一正一负,负指数的运算需要先求模逆,很多人的攻击脚本就卡在这里。

def common_modulus_attack(c1, c2, e1, e2, n): g, s1, s2 = extended_gcd(e1, e2) if s1 < 0: c1 = mod_inverse(c1, n) s1 = -s1 if s2 < 0: c2 = mod_inverse(c2, n) s2 = -s2 m = (mod_pow(c1, s1, n) * mod_pow(c2, s2, n)) % n return m

把这段代码跑通,然后在报告里展示攻击前后的明文对比截图,比任何文字描述都有说服力。我当时就是靠这个实验,把一个看似简单的作业变成了有攻击演示的完整安全分析案例。

4.2 填充方案缺失的演示:教科书式漏洞

另一个适合写进报告的实验是"无填充RSA"的语义安全隐患。如果加密时直接对数字化的明文m做模幂运算,一来同样明文总会产生同样密文,二来对于很小的m(比如m = 2),密文c = 2^e mod n可能直接等于2^e自身,攻击者通过开e次方就能恢复明文。

正确做法是引入OAEP(Optimal Asymmetric Encryption Padding)填充,在加密前把明文与随机数混合,再作为底数进行模幂运算。实验里至少要展示两类结果:无填充RSA的确定性输出对比,以及加入OAEP后同一明文每次加密结果都不同。这一步能把你的报告从"实现了一个算法"拉升到"从工程角度考虑了实际安全性"的层次。

4.3 性能测量与密钥长度对比

实验报告里通常要求分析不同参数的性能。我建议至少做三组测试:固定密钥长度下加密/解密耗时对比,不同密钥长度的密钥生成耗时对比,以及公钥指数e取3、17、65537时的加密耗时差异。下面是我当年测试时用的表格形式,供参考:

密钥长度(位)密钥生成耗时(ms)加密耗时(ms)解密耗时(ms)
51238.70.45.2
1024212.50.923.4
20481534.22.8156.7

从这几组数据里能读出的结论是:解密耗时远大于加密耗时,而且随着密钥长度增长呈超线性上升,这体现了私钥指数d的规模直接影响模幂运算复杂度。写报告时不要只放数据,还要解释数据背后的算法原理。

4.4 异常输入测试清单

这部分是很多同学完全没做、但验收老师最喜欢翻的部分。我总结了一份测试清单:密钥长度为0、为负数时程序要报错而不是崩溃;非法密文长度(密文位数超过n、或者正好等于n)需被拒绝;解密时私钥不匹配要返回明确错误;加密空字符串时,无填充RSA会出现m = 0得到密文 = 0的特殊情况,需要识别并拒绝;大数进制转换时字节序不一致导致的解密乱码,也要用测试用例覆盖。每条测试用例都应当记录输入、预期行为和实际行为。

5. 我踩过的坑和最终调试清单

5.1 三个让我纠结一整晚的bug

第一个bug是模逆元算法返回负数。扩展欧几里得算法求出的d可能是负数,但RSA里d必须落在[1, φ(n)-1]区间,否则解密必然失败。解决方法是最后做d = d % phi。

第二个bug是明文长度超过n的字节数限制时没有任何报错。RSA能加密的明文长度是有限制的,比如1024位n最多加密128字节,其中还要减去填充长度。如果明文超长,直接做模幂运算会得到无法还原的密文。正确做法是在加密前检测明文长度,超长就抛出异常或自动分组。

第三个bug在C语言版本中体现得淋漓尽致:用int类型暂存大数中间结果导致溢出。RSA中间值动辄几十上百字节,必须用大数数组或专用BigInteger类型处理,任何"先用int算完再转大数"的偷懒思路都会在边界测试时炸掉。

5.2 验收时被追问最多的三个问题

老师最常问的问题我整理了三类,大家可以提前想好答案:为什么公私钥的e通常取65537而不是一个随机数?答案是取一个固定的小费马素数可以减少模幂运算的乘法和平方次数,同时满足与φ(n)互质的概率很高。Miller-Rabin的误判概率为什么可以接受?答案是经过指定轮次测试后误判率不超过2^(-k),且实际测试中选取固定基表加随机基的组合已经远低于硬件出错的概率。如果公钥(e, n)中n被分解,会泄露什么?答案是p和q泄露后,攻击者可算出φ(n)进而通过求逆元得到d,所以私钥保护的关键在于大整数分解困难性。

5.3 最终自查清单

交实验前,我会把下面这份清单从头到尾过一遍,任何一项不通过都不提交:

  • 密钥生成时p和q不是固定值,每次运行结果不同;
  • p和q的差值校验已实现,不会生成能被Fermat分解的弱密钥;
  • 加解密模块支持不同位数(至少512、1024、2048);
  • 相同明文每次加密得到不同密文(如果实现了填充);
  • 超长明文、零长度明文、非法密文均有明确报错;
  • 密钥文件读写完整,换一台机器加载密钥仍能正确解密。

最后再分享一个小技巧:代码里所有核心算法旁,我习惯用注释写明时间复杂度推导,比如"快速幂取模的循环次数为指数二进制位数,即O(log b)"。这些注释不写也罢,但写了之后,写报告时会非常省力,直接照着注释展开就是一篇高质量的安全性分析。PKE实验这个题目上限很高、下限也低,认真走完"框架设计-编码实现-攻击复现-性能分析"这套流程之后,你对公钥密码学的理解会完全不一样,这也是这个实验真正想让你收获的东西。

本文还有配套的精品资源,点击获取

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

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

立即咨询