第2章:SM2算法原理——椭圆曲线上的密码学

SM2,说白了就是我国自主设计的椭圆曲线公钥密码算法。它跟RSA不同,不依赖于大整数分解的难度,而是靠椭圆曲线上点群离散对数问题(ECDLP)的难解性。你想想看,同样的安全强度,SM2的密钥长度比RSA短很多,计算量也小,这也是为什么它现在被广泛应用在移动端和物联网设备上。

我刚开始接触SM2时,总觉得椭圆曲线很抽象。没关系的,我们可以把它想象成一个封闭的游戏世界,里面有一些特殊的点。加密和解密,就是在这个世界里做加减法,而破解它,则像是逆向追踪一个极其复杂的迷宫路径。

SM2 算法核心知识体系 椭圆曲线基础 密钥对生成 加密与解密 数字签名与验签 有限域运算 · 点乘 · 哈希 💡 学习路径建议 一切基于一个共同的椭圆曲线方程与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. 选择私钥:在区间[1, n-2]内,生成一个随机整数d,这就是私钥。
  2. 计算公钥:计算P = d * G,其中G是SM2基点的坐标。结果是一个椭圆曲线上的点。
  3. 验证基点:确认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)加密数据。它的核心思路,是先把你的明文映射到曲线上吗?不是的。其实它是对明文本身做异或。

加密流程

  1. 生成临时密钥对:随机数k,临时公钥C1 = k * G。
  2. 计算共享秘密点:S = h * k * P_B(h是协因子,SM2中h=1)。
  3. 派生密钥:从S的x坐标和y坐标,通过KDF(密钥派生函数)生成对称密钥t。
  4. 加密数据:C2 = M ⊕ t(位异或)。
  5. 计算校验值:C3 = Hash(x_S || M || y_S)。
  6. 输出密文:C = C1 || C3 || C2。

解密流程

  1. 从密文取出C1,验证它是曲线上的点。
  2. 计算共享秘密点:S = h * d_B * C1。
  3. 用同样的KDF派生密钥t。
  4. 解密数据:M = C2 ⊕ t。
  5. 校验Hash(x_S || M || y_S)与C3是否一致。如果不一致,直接返回错误。

⚠ 我曾经遇到过:一个系统在加密后,存储到数据库时把C1的坐标压缩了(只存x坐标和奇偶位)。结果解密时解压逻辑有bug,导致C1不完全对应。这个教训让我明白——SM2加密的密文格式必须严格按照GM/T 0003-2012标准来,别自己乱压缩。

2.4 数字签名与验签原理

SM2的数字签名,采用了类似于Schnorr签名的结构。你把消息哈希后的值,与私钥混合起来,生成两个大整数r和s。跟RSA不同,SM2的签名过程依赖于随机数,因此每次签名结果都不一样。这其实是个不错的特性,能抵御重放攻击。

签名流程

  1. 计算待签名消息的杂凑值:e = Hash(Z_A || M),其中Z_A是签名者的身份标识和公钥哈希。
  2. 生成随机数k,计算椭圆曲线点(x1, y1) = k * G。
  3. 计算r = (e + x1) mod n。如果r=0或r+k=n,就重试。
  4. 计算s = (1 + d_A)^(-1) * (k - r * d_A) mod n。如果s=0,也重试。
  5. 输出签名(r, s)。

验签流程

  1. 验证r和s都在[1, n-1]范围内。
  2. 计算e = Hash(Z_A || M)。
  3. 计算t = (r + s) mod n,如果t=0就拒绝。
  4. 计算椭圆曲线点(x1', y1') = s * G + t * P_A。
  5. 验证r == (e + x1') mod n。如果相等,签名有效;否则无效。
SM2签名与验签核心步骤对比
步骤签名方(私钥d_A)验签方(公钥P_A)
1e = H(Z_A \|\| M)e = H(Z_A \|\| M)
2(x1, y1) = k*Gt = (r+s) mod n
3r = (e + x1) mod n(x1', y1') = s*G + t*P_A
4s = (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的整套流程。