第2章:SM2算法原理——椭圆曲线上的密码学
SM2,说白了就是我国自主设计的椭圆曲线公钥密码算法。它跟RSA不同,不依赖于大整数分解的难度,而是靠椭圆曲线上点群离散对数问题(ECDLP)的难解性。你想想看,同样的安全强度,SM2的密钥长度比RSA短很多,计算量也小,这也是为什么它现在被广泛应用在移动端和物联网设备上。
我刚开始接触SM2时,总觉得椭圆曲线很抽象。没关系的,我们可以把它想象成一个封闭的游戏世界,里面有一些特殊的点。加密和解密,就是在这个世界里做加减法,而破解它,则像是逆向追踪一个极其复杂的迷宫路径。
2.1 椭圆曲线密码体制(ECC)
椭圆曲线密码体制(ECC)的核心,其实不是我们初高中那种椭圆。它用的是这个方程:
y² = x³ + ax + b (mod p)
其中,p是一个大素数,a和b是系数,满足4a³ + 27b² ≠ 0 mod p。为什么有这个条件?为了确保曲线光滑、没有尖点或自交点。嗯,我在验证SM2推荐参数时,确实顺手算过这个判别式,免得后面白跑。
这个曲线上的点集合,再加上无穷远点O,就构成一个阿贝尔群。群里有加法运算,规则挺直观:
- 点加法:P + Q = R,过P和Q做直线,交曲线上另一个点,再关于x轴对称。
- 点倍:2P = P + P,过P做切线。
- 标量乘法:kP = P + P + ... + P(k次)。这就是ECC最难的核心操作。
⚠ 核心概念:已知k和P,求kP很容易;但已知P和kP,反推k就非常困难。这叫椭圆曲线离散对数问题(ECDLP),是SM2安全性的基石。
2.2 密钥对的生成
SM2的密钥对生成过程,我个人觉得是所有流程里最简单的。一共就三步:
- 选择私钥:在区间[1, n-2]内,生成一个随机整数
d,这就是私钥。 - 计算公钥:计算
P = d * G,其中G是SM2基点的坐标。结果是一个椭圆曲线上的点。 - 验证基点:确认P不是无穷远点,确保生成的公钥有效。
# 伪代码示意:SM2密钥对生成
def gen_keypair():
d = random.randint(1, n - 2) # 私钥
P = scalar_mult(d, G) # 公钥 = d * G
if P == O: # 无穷远点检查
return gen_keypair() # 重试
return (d, P)
这里有一个小坑,想跟大家分享。我在一次国产化替代项目中,发现有些硬件SM2引擎输出的公钥,偶尔会返回一个点,在验证坐标时有误差。后来排查发现是标准坐标系没选对——SM2用的是仿射坐标,但有的引擎内部用雅可比坐标,转换时没做模逆处理。从此我就记住了:密钥对生成后,一定要做一次坐标合法性校验。
2.3 加密与解密流程
SM2加密使用混合加密模式:用椭圆曲线做密钥交换,再用对称密码(如SM4)加密数据。它的核心思路,是先把你的明文映射到曲线上吗?不是的。其实它是对明文本身做异或。
加密流程
- 生成临时密钥对:随机数
k,临时公钥C1 = k * G。 - 计算共享秘密点:
S = h * k * P_B(h是协因子,SM2中h=1)。 - 派生密钥:从S的x坐标和y坐标,通过KDF(密钥派生函数)生成对称密钥
t。 - 加密数据:
C2 = M ⊕ t(位异或)。 - 计算校验值:
C3 = Hash(x_S || M || y_S)。 - 输出密文:
C = C1 || C3 || C2。
解密流程
- 从密文取出
C1,验证它是曲线上的点。 - 计算共享秘密点:
S = h * d_B * C1。 - 用同样的KDF派生密钥
t。 - 解密数据:
M = C2 ⊕ t。 - 校验
Hash(x_S || M || y_S)与C3是否一致。如果不一致,直接返回错误。
⚠ 我曾经遇到过:一个系统在加密后,存储到数据库时把C1的坐标压缩了(只存x坐标和奇偶位)。结果解密时解压逻辑有bug,导致C1不完全对应。这个教训让我明白——SM2加密的密文格式必须严格按照GM/T 0003-2012标准来,别自己乱压缩。
2.4 数字签名与验签原理
SM2的数字签名,采用了类似于Schnorr签名的结构。你把消息哈希后的值,与私钥混合起来,生成两个大整数r和s。跟RSA不同,SM2的签名过程依赖于随机数,因此每次签名结果都不一样。这其实是个不错的特性,能抵御重放攻击。
签名流程
- 计算待签名消息的杂凑值:
e = Hash(Z_A || M),其中Z_A是签名者的身份标识和公钥哈希。 - 生成随机数
k,计算椭圆曲线点(x1, y1) = k * G。 - 计算
r = (e + x1) mod n。如果r=0或r+k=n,就重试。 - 计算
s = (1 + d_A)^(-1) * (k - r * d_A) mod n。如果s=0,也重试。 - 输出签名
(r, s)。
验签流程
- 验证r和s都在[1, n-1]范围内。
- 计算
e = Hash(Z_A || M)。 - 计算
t = (r + s) mod n,如果t=0就拒绝。 - 计算椭圆曲线点
(x1', y1') = s * G + t * P_A。 - 验证
r == (e + x1') mod n。如果相等,签名有效;否则无效。
| 步骤 | 签名方(私钥d_A) | 验签方(公钥P_A) |
|---|---|---|
| 1 | e = H(Z_A \|\| M) | e = H(Z_A \|\| M) |
| 2 | (x1, y1) = k*G | t = (r+s) mod n |
| 3 | r = (e + x1) mod n | (x1', y1') = s*G + t*P_A |
| 4 | s = (1+d_A)^(-1)*(k - r*d_A) mod n | 验证 r == (e + x1') mod n |
💡 避坑指南:我在做SM2签名验证时,曾遇到一个诡异的失败问题。签出来的报文,验签方总是报错。后来发现,是签名时把Z_A的计算搞错了。SM2规定Z_A = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)。这个串顺序不能乱,长度也要正确编码。记一次:Z_A错了,一切白搭。
2.5 一小段总结(不做大纲式概括)
SM2绕不开的就是椭圆曲线上的点乘运算。你把这个点乘搞明白了,密钥生成、加密解密、签名验签的核心就都通了。整个算法里,我觉得最需要手算验证的部分,就是共享秘密点的一致性。加密方用k * P_B,解密方用d_B * C1,它们必须相等。这个思想贯穿了SM2的整套流程。