简介:面向西安电子科技大学计算机科学与技术专业网络方向学生的应用密码学与网络安全期末复习资料,依据课程PPT梳理考点,适合考前突击、查漏补缺与课堂笔记对照使用。压缩包共1个文件,为单个PPT,大小约1.94MB,按考试题型组织内容:选择题10道共20分,大题8道共80分,涵盖计算题、问答题与应用题三类。知识点覆盖OSI安全框架与安全攻击类型、安全服务与安全机制、混淆与扩散及Feistel与DES结构、S-盒作用与雪崩效应、AES基本结构与有限域设计动机、双重DES的中间相遇攻击、分组密码工作模式、公钥密码与陷门函数、DH密钥交换与中间人攻击,以及Hash函数抗碰撞性、消息认证码、数字签名、密钥分发、防重放与Kerberos远程认证、SSL握手交互和IPSec传输与隧道模式等章节要点。已有1052人学习,适合按章节逐条对照复习、配合PPT巩固重点。
1. 应用密码学与网络安全期末复习:先分清哪些分是算出来的、哪些是背出来的
应用密码学与网络安全这门课,期末最容易翻车的不是不会算,而是算对了却写不出过程,或者概念题把“防泄露”和“防篡改”混成一锅。某高校计算机专业网络方向的复习材料年年传,真正有用的只有两类:一类是把 AES 一轮、RSA 求逆元、DH 推演这些必须动手的步骤固化成肌肉记忆;另一类是搞清楚防火墙、IDS、TLS 握手这些词的分界到底在哪。
下面按考点顺序走一遍:分组密码与工作模式、公钥密码的计算题模板、哈希与签名、协议与设备,最后是几条用血泪经验换来的避坑提醒。新手照着代码能自己验证结论,熟手可以直接看参数表和边界说明。所有结论都能用十几行 Python 或者一条命令复现,不靠玄学记忆。
2. 分组密码与工作模式:AES 一轮怎么手算,五种模式怎么区分
对称密码这部分的分值通常不算最高,但极其稳定,属于“套公式就该拿到”的区间。丢分基本只有两种:手算轮函数时步骤顺序错了,以及工作模式的性质张冠李戴。这一章把这两件事拆开讲透。
2.1 Feistel 结构与 SPN 结构:DES 和 AES 的差别决定了考法
Feistel 结构的做法是把分组劈成左右两半,每一轮只对右半边做变换,然后与左半边异或:左半新值直接取旧的右半,右半新值等于旧左半异或轮函数输出。轮函数本身不要求可逆,这是它能用同一套电路完成加解密的根本原因——解密时把子密钥顺序倒过来,其余流程一模一样。DES 就是典型:64 位分组、64 位密钥里 8 位是奇偶校验位,有效密钥 56 位,16 轮,前后各一次置换。
SPN 结构不一样。以 AES 为例,每一轮对整块 128 位做代换、行移位、列混淆、轮密钥异或,四个步骤全部作用在完整状态上。代价是解密必须实现逆 S 盒、逆向行移位、逆列混淆,所以解密路径比加密路径复杂,这也是考卷上常问“为什么 AES 解密电路比加密电路大”的标准答案。轮数按密钥长度走:128 位密钥 10 轮,192 位 12 轮,256 位 14 轮;最后一轮省略列混淆,因为那一层在密码学上不提供额外扩散,去掉反而省一次运算。
容易填错的两个数值:AES-128 的密钥扩展一共产生 44 个 32 位字(11 组轮密钥,每组 4 个字),初始轮加一次密钥,所以是 1+10 次。记住“轮密钥数 = 轮数 + 1”,比死记 44 更稳。
2.2 AES 一轮四步与列混淆的 GF(2^8) 乘法
手算一轮的顺序是固定的,不能调换:
第一步字节代换。把状态看成 4×4 字节矩阵,每个字节拆成高 4 位和低 4 位,高 4 位当行号、低 4 位当列号,去 S 盒里查。比如 0x53 查第 5 行第 3 列。S 盒的本质是 GF(2^8) 上的乘法逆元再叠一个仿射变换,考场上不必推,直接查表,但要知道它非线性、可逆。
第二步行移位。第 0 行不动,第 1 行循环左移 1 字节,第 2 行左移 2,第 3 行左移 3。作用是让每一列在下一轮混合时来自不同的列。
第三步列混淆。每一列左乘固定矩阵,系数是 02、03、01。这里的乘法不是整数乘法,是 GF(2^8) 上模多项式 0x11B 的乘法。手算技巧:乘 02 就是左移一位,如果溢出(最高位是 1)就异或 0x1B;乘 03 等于乘 02 再异或自己。
第四步轮密钥异或。把扩展出来的轮密钥按字节异或进去。
列混淆是最容易算错的一步,写几行代码把测试向量跑通,比看十遍书管用:
# GF(2^8) 上的乘法,模多项式 0x11B(AES 规定) def xtime(a: int) -> int: a <<= 1 if a & 0x100: # 溢出则模掉 0x11B a ^= 0x11B return a & 0xFF def gmul(a: int, b: int) -> int: p = 0 for _ in range(8): # 逐位乘,b 的每一位决定是否累加 a if b & 1: p ^= a a = xtime(a) b >>= 1 return p def mix_column(col): # AES 列混淆矩阵第一行 [02 03 01 01],后面三行循环右移 return [ gmul(col[0], 2) ^ gmul(col[1], 3) ^ col[2] ^ col[3], col[0] ^ gmul(col[1], 2) ^ gmul(col[2], 3) ^ col[3], col[0] ^ col[1] ^ gmul(col[2], 2) ^ gmul(col[3], 3), gmul(col[0], 3) ^ col[1] ^ col[2] ^ gmul(col[3], 2), ] # FIPS-197 里的标准测试列,算出来必须是 [0x8e, 0x4d, 0xa1, 0xbc] assert mix_column([0xdb, 0x13, 0x53, 0x45]) == [0x8e, 0x4d, 0xa1, 0xbc]xtime里那个条件判断是整个 AES 有限域运算的核心:0x11B 的二进制是 1 0001 1011,左移溢出时异或它相当于做了一次模运算。gmul用最朴素的逐位算法,8 次循环,虽然慢但对拍手算结果最直观。验证用的那组数据来自标准文档,只要结果对得上,说明你的乘法和矩阵方向都没错。
状态矩阵的填充顺序也要留意:明文按字节顺序先填第一列再填第二列,也就是列优先,不是行优先。这一点在手算题里错了,后面全盘皆错。
2.3 五种工作模式:一张表记住错误传播和并行性
分组密码只能处理定长块,长消息要靠工作模式拼起来。五种模式的性质几乎每年都考,背的时候抓三个维度:要不要 IV、加密能不能并行、一位出错会影响多少。
| 模式 | 需要 IV/计数器 | 加密并行 | 解密并行 | 一位密文错误的影响 |
|---|---|---|---|---|
| ECB | 不需要 | 可并行 | 可并行 | 只影响本块对应位 |
| CBC | 需要随机 IV | 串行 | 可并行 | 本块全乱,下一块对应位出错 |
| CFB | 需要 IV | 串行 | 可并行 | 本块对应位错,后续块对应位错 |
| OFB | 需要 IV | 串行(可预计算密钥流) | 可并行 | 只影响本块对应位 |
| CTR | 需要唯一计数器 | 可并行 | 可并行 | 只影响本块对应位 |
ECB 的死穴是相同明文块产生相同密文块,结构化数据里重复的头信息会原样暴露出来,所以只能用于单块数据或者随机密钥场景。CBC 的 IV 必须随机且不可预测,因为 IV 会直接影响第一块的密文,可预测的 IV 会带来选择明文攻击的窗口。CFB 和 OFB 把分组密码变成流密码,区别在于反馈的是密文还是密钥流输出。CTR 最像流密码、最好并行、还能随机访问,代价是“计数器 + 密钥”这一对值绝对不能重复使用,一旦复用,两段密文异或就直接等于两段明文异或。
填充规则也要写清楚:PKCS#7 规定补 n 个值为 n 的字节,n 等于缺少的字节数,取值范围 1 到 16。特别注意,明文长度正好是块长度整数倍时必须补一整块 0x10,这不是冗余,是为了让去填充无歧义。
2.4 用 Python 跑一遍 CBC:填充、IV 复用与 openssl 对照
把上面的结论落到代码上,最省事的做法是用成熟库,把注意力放在流程和参数上:
import os from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def pkcs7_pad(data: bytes, block: int = 16) -> bytes: n = block - len(data) % block # 正好整除时 n = 16,补一整块 return data + bytes([n]) * n def pkcs7_unpad(data: bytes) -> bytes: n = data[-1] if not 1 <= n <= block_len or data[-n:] != bytes([n]) * n: raise ValueError("padding error") # 实验里的 padding oracle 就从这条分支漏信息 return data[:-n] block_len = 16 def cbc_encrypt(key: bytes, iv: bytes, plain: bytes) -> bytes: enc = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor() return enc.update(pkcs7_pad(plain)) + enc.finalize() def cbc_decrypt(key: bytes, iv: bytes, ct: bytes) -> bytes: dec = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor() return pkcs7_unpad(dec.update(ct) + dec.finalize()) key, iv = os.urandom(16), os.urandom(16) # 密钥 16/24/32 字节对应 AES-128/192/256 msg = b"applied-cryptography-final" * 3 ct = cbc_encrypt(key, iv, msg) assert cbc_decrypt(key, iv, ct) == msg # 翻转 IV 的第一个字节,解密后只有第一块受影响,第二块之后完好 bad_iv = bytes([iv[0] ^ 0x01]) + iv[1:] print(cbc_decrypt(key, bad_iv, ct)[:16] != msg[:16]) # True print(cbc_decrypt(key, bad_iv, ct)[16:] == msg[16:]) # Truekey的长度直接决定用哪个 AES 版本,16 字节就是 AES-128,写错长度库会直接抛异常,这是最常见的报错来源。iv必须 16 字节、每次加密都重新随机生成,并且要和密文一起存下来传给解密方——IV 不是秘密,但它必须不可预测。最后那两行验证的正是 CBC 的错误传播特性:改 IV 只污染第一块,后面的块不受影响,这跟改密文块的结果完全不同,考卷上问“错误传播范围”时按这个逻辑推。
如果环境里不方便装库,用命令行对拍:
# -K 和 -iv 都要求十六进制,AES-128 就是 32 个 hex 字符 openssl enc -aes-128-cbc -K 000102030405060708090a0b0c0d0e0f \ -iv 101112131415161718191a1b1c1d1e1f -in plain.bin -out cipher.bin命令行工具默认会自己做填充,跟上面的 PKCS#7 一致,两边结果应当逐字节相同。对不上的时候先查 IV 是不是被当成了密钥的一部分,这是新手最容易搞错的地方。
3. 公钥密码计算题模板:RSA 求逆元、模幂与 DH 推演的固定套路
公钥这部分是整张卷子里最“可训练”的部分。题型高度固定:给两个素数算密钥、给底数和指数算模幂、给一组参数推共享密钥,偶尔加一道“为什么这里不安全”的分析题。把模板背成条件反射,比临场推导省十分钟。
3.1 RSA 密钥生成:扩展欧几里得求 d 的手算表格
流程是死的:选两个大素数 p、q,算 n = p·q 和 φ(n) = (p−1)(q−1),选一个与 φ 互质的 e,再用扩展欧几里得求 d 满足 e·d ≡ 1 mod φ(n)。公钥是 (n, e),私钥是 (n, d),加密 c = m^e mod n,解密 m = c^d mod n。
手算示例用教科书上那组经典小数字:p = 61,q = 53,n = 3233,φ = 3120,取 e = 17。求 d 就是解 17d ≡ 1 (mod 3120)。用扩展欧几里得往回代:3120 = 183×17 + 9;17 = 1×9 + 8;9 = 1×8 + 1。于是 1 = 9 − 8 = 9 − (17 − 9) = 2×9 − 17 = 2×(3120 − 183×17) − 17 = 2×3120 − 367×17。所以 −367×17 ≡ 1,d = 3120 − 367 = 2753。验证一下:17×2753 = 46801 = 15×3120 + 1,成立。
代码实现比手算更不容易错:
def egcd(a: int, b: int): # 返回 (g, x, y),满足 a*x + b*y = g = gcd(a, b) if b == 0: return a, 1, 0 g, x1, y1 = egcd(b, a % b) return g, y1, x1 - (a // b) * y1 def modinv(a: int, m: int) -> int: g, x, _ = egcd(a % m, m) if g != 1: raise ValueError("gcd != 1,e 与 φ(n) 不互质,密钥作废") return x % m # Python 取模保证落回 [0, m) p, q, e = 61, 53, 17 n, phi = p * q, (p - 1) * (q - 1) d = modinv(e, phi) print(n, phi, d) # 3233 3120 2753 assert pow(65, e, n) == 2790 and pow(2790, d, n) == 65egcd的返回值里第三个分量容易被忽略,但它是回代求系数的关键;如果按下标取错,modinv会算出一个看着像模逆、代回去却不等于 1 的数。x % m这一步不能省,扩展欧几里得给出的解可能是负数,负数在模运算里合法但手写答卷时容易漏掉。
关于 e 的选择:实际系统普遍用 65537,因为它的二进制是 1 后面跟十五个 0 再跟一个 1,只有两个 1,模幂时乘法次数极少,速度快而且不会小到不安全。考试里如果题目给 e = 3 并且明文很短,那基本是在暗示“无填充的 RSA 可以直接开立方”,这是分析题的入口。
3.2 平方-乘:模幂拆成平方与乘法
模幂是 RSA、DH、ElGamal 共用的底层运算,手算时用平方-乘,把指数写成二进制,从高位到低位扫,遇到 1 就多乘一次底数。以 7^23 mod 17 为例,23 = 10111₂:
| 步骤 | 当前指数位 | 操作 | 结果 |
|---|---|---|---|
| 起始 | — | result = 1 | 1 |
| 1 | 1 | 平方后乘 7 | 7 |
| 2 | 0 | 只平方 | 49 mod 17 = 15 |
| 3 | 1 | 平方后乘 7 | 15²=4,4×7=28 mod 17 = 11 |
| 4 | 1 | 平方后乘 7 | 11²=2,2×7=14 |
| 5 | 1 | 平方后乘 7 | 14²=9,9×7=63 mod 17 = 12 |
结果是 12,用快速幂或者暴力连乘都能对上。代码写起来就是:
def modpow(base: int, exp: int, mod: int) -> int: # 平方-乘,每一步都取模,防止中间结果爆掉 result = 1 base %= mod while exp: if exp & 1: # 当前最低位是 1,累乘 result = result * base % mod base = base * base % mod # 底数平方 exp >>= 1 return result assert modpow(7, 23, 17) == 12注意这段代码是从低位往高位扫的,跟手算表的顺序相反,结果一样但过程解释不同。答卷上写清“按指数二进制从高位到低位”就行,别把两种写法混着描述,改卷时最忌讳过程自相矛盾。另一个高频错误是中间结果忘了取模,数字滚到几百位之后连计算器都不给你算。
3.3 Diffie-Hellman 与中间人:同一组数字两种结局
DH 的骨架是:公开大素数 p 和生成元 g,双方各选私密随机数 a、b,各自算出 A = g^a mod p、B = g^b mod p 并交换,共享密钥 K = B^a = A^b mod p。
手算一组:p = 23,g = 5,a = 6,b = 15。A = 5^6 mod 23 = 8,B = 5^15 mod 23 = 19。K = 19^6 mod 23 = 2,另一侧 8^15 mod 23 也是 2,对上了。用 3.2 的modpow两行就能验证。
中间人攻击的推演是同一组数字的另一种结局:攻击者在中间,跟 A 侧协商出 K1,跟 B 侧协商出 K2,然后把两侧的流量解密、重加密。双方看到的“共享密钥”并不相同,但他们各自都以为对方是本人。根因不是 DH 算错了,而是 DH 本身没有任何身份认证——它只保证“密钥协商的过程没被被动窃听”,不保证“对面是谁”。这道题的标准答法要落到“必须叠加认证机制”,比如用证书对临时公钥签名,或者走带认证的密钥交换流程,而不是笼统写“加个密就好了”。
还有个细节常考:p 必须是安全素数或者至少用足够大的素数群,且要校验对端送来的公钥落在正确子群里。小参数下直接枚举离散对数是可行的,这也是为什么题干给 23 这种小数字只能用来演示流程,不能当真实强度讨论。
3.4 ElGamal 与 ECC:随机数 k 不能复用
ElGamal 的加密是两段:随机选 k,密文是 (C1, C2) = (g^k mod p, m·y^k mod p),其中 y = g^x 是公钥。解密用 m = C2 · (C1^x)^(−1) mod p。它的安全性建立在“密文看起来随机”之上——正因为每次加密的 k 不同,同一段明文每次加密结果都不一样,这是它比教科书 RSA 好的地方。
但 k 一旦复用就会导致灾难性后果。两次加密用同一个 k,加密 m1 和 m2,得到 C1 相同、C2' / C2'' = m1 / m2,明文比值直接暴露,配合一点点已知明文就能还原全部。DSA 和 ECDSA 的签名里同一个 k 复用会直接泄露私钥,因为两条签名方程一减,k 就解出来了,再代回去私钥也到手。这条结论在历年卷子上出现的频率相当高,答“随机数必须由密码学安全的随机源生成、且每次独立”比答“要随机”得分高。
ECC 的考点偏向概念加一点计算。曲线是 y² = x³ + ax + b mod p,点加法的几何意义是“两点连线与曲线交于第三点,再关于 x 轴取对称”,代数上先算斜率 λ,过两点时 λ = (y2 − y1)/(x2 − x1) mod p,倍点时 λ = (3x² + a)/(2y) mod p,然后 x3 = λ² − x1 − x2,y3 = λ(x1 − x3) − y1。分母要求逆元,所以模数必须是素数。
对比优势的答法要克制:ECC 在同等安全强度下密钥和签名都短得多,所以嵌入式、移动端、证书链这类带宽和存储敏感的场景喜欢用它。别写成“ECC 绝对更安全”,它面对的是不同的数学难题(椭圆曲线离散对数),参数选错(比如曲线本身不安全)照样完蛋。
4. 哈希、MAC 与数字签名:证书链和 TLS 握手里每一步在防什么
这一块是概念题和简答题的主战场,也是最容易“背了但答不到点上”的地方。核心是分清三件工具各自能干什么:哈希只管完整性,MAC 管完整性和来源认证(双方共享密钥),签名管完整性、来源认证和不可否认(私钥独占)。
4.1 哈希的三个性质与生日攻击的 2^(n/2)
密码学哈希要满足三条:抗原像性(给摘要推不出原文)、抗第二原像性(给一个原文,找不到另一个同摘要的)、抗碰撞性(找不到任意两个同摘要的不同消息)。前两条弱一些,第三条最强,也最难满足。
生日攻击的结论要会推:n 位输出的哈希,随机尝试大约 2^(n/2) 次就能找到碰撞。SHA-256 是 256 位输出,所以碰撞界在 2^128,目前算安全;MD5 只有 128 位,碰撞界 2^64,早已被实际构造出来,所以任何“用 MD5 做完整性校验”的方案在答题时都算错。这个数字关系要记牢,题目经常直接问“128 位哈希的抗碰撞强度相当于多少位对称密钥”。
另外要区分“哈希”和“加盐哈希存口令”:加盐解决的是彩虹表和相同口令同哈希的问题,迭代拉长的是单次验证的计算成本,两者都不改变哈希本身的抗碰撞性质。答简答题时把这两层分开写,比堆一堆名词清楚。
4.2 HMAC 的两层结构与拼接式 MAC 的翻车
很多人的第一反应是“MAC 不就是 H(K ‖ M) 吗”。这个写法在 Merkle-Damgård 结构(MD5、SHA-1、SHA-256 都是)下是危险的:攻击者在知道 H(K ‖ M) 和 M 的长度之后,可以在不知道 K 的情况下算出 H(K ‖ M ‖ padding ‖ X),也就是长度扩展攻击。根因是这类结构把消息分块迭代压缩,最后一轮的状态直接当输出,攻击者可以把它当成中间状态接着算。
HMAC 的构造正是为了堵这个洞:H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ M))。内层先用密钥和一个填充常量处理消息,外层再套一次哈希,这样长度扩展接不上去,因为外层密钥你不知道。
import hmac, hashlib key = b"course-key-2024" msg = b"final-exam-note" mac_naive = hashlib.sha256(key + msg).hexdigest() # 拼接式,别在生产里用 mac_hmac = hmac.new(key, msg, hashlib.sha256).hexdigest() # 校验必须用常量时间比较,普通 == 会在第一个不同字节处提前返回,泄露时序 print(hmac.compare_digest(mac_hmac, hmac.new(key, msg, hashlib.sha256).hexdigest()))hmac.compare_digest这一行是实操细节,考卷上不考,但实验报告里写进去会显得你真的动过手。mac_naive那行保留是为了对照,不是让你去用它——真要在报告里做长度扩展演示,需要自己实现分块压缩函数,用库反而不方便。
4.3 数字签名与证书链:先哈希再签名,验证按什么顺序
签名的标准流程是“对消息摘要做私钥运算”,不是直接对整段消息做。原因有两条:公钥运算比哈希慢几个数量级,消息一长就受不了;而且哈希先把任意长度压成定长,签名算法的输入规模才可控。
RSA 签名有个必须强调的坑:教科书式的“s = m^d mod n”在没有填充时是可伪造的,因为 RSA 具有乘法同态性——s1 是 m1 的签名、s2 是 m2 的签名,那 s1·s2 就是 m1·m2 的签名,攻击者不需要私钥也能造出新消息的合法签名。所以实际方案必须加填充,签名用 PKCS#1 v1.5 或者 PSS,加密用 OAEP。答题时写“RSA 签名需要填充,否则存在乘法同态伪造”基本就是满分点。
三种工具的边界用一张表最清楚:
| 工具 | 密钥形态 | 能提供 | 不能提供 |
|---|---|---|---|
| 哈希 | 无密钥 | 完整性(配合可信信道) | 来源认证、抗抵赖 |
| MAC/HMAC | 双方共享 | 完整性 + 来源认证 | 抗抵赖(双方都能算) |
| 数字签名 | 私钥签、公钥验 | 完整性 + 来源认证 + 抗抵赖 | 保密性 |
证书链验证的顺序值得单独记:先看证书里的域名或用途是否匹配当前场景,再查有效期,然后逐级验证签名直到信任锚(根证书),最后查吊销状态(CRL 或 OCSP)。把这个顺序倒过来写,比如“先查吊销再验签名”,在某些评分标准里是要扣分的,因为吊销查询本身依赖你已经确认过这条链是可信的。
4.4 TLS 握手里三个随机数各防什么
以经典的两段式握手为线索:客户端发 ClientHello(带 client_random 和支持的套件列表),服务端回 ServerHello(带 server_random 和选定的套件)、Certificate、ServerKeyExchange;客户端验证证书链,生成 pre_master_secret,用服务器公钥加密后放进 ClientKeyExchange 发出去;双方各自用 PRF 把 pre_master 和两个随机数展开成主密钥,再发 ChangeCipherSpec 和 Finished,Finished 里是对前面所有握手消息的摘要校验。
| 握手环节 | 作用 | 常见考法 |
|---|---|---|
| client_random / server_random | 保证每次会话密钥不同,防重放和预计算 | 问“随机数能不能省掉” |
| Certificate + 验证 | 确认服务器身份,防中间人 | 问“证书验证发生在握手哪一步” |
| pre_master_secret | 提供新鲜密钥材料,只有服务器能解 | 问“RSA 密钥传输模式的缺陷” |
| Finished | 校验握手过程未被篡改 | 问“它校验的是什么” |
三个随机数各管一段:两个 random 保证新鲜性和双向绑定,pre_master 提供不可预测的密钥材料。少了任何一个,重放或预计算攻击就有了入口。
RSA 密钥传输模式的问题在于它没有前向安全:私钥一旦泄露,历史流量全部可解。后来的版本改成只用临时 DH/ECDH,双方的临时私钥用完即弃,这就是常说的前向安全。新版本
本文还有配套的精品资源,点击获取