椭圆曲线密码学基础:有限域、基点与阶
为什么SM2签名会失败?很多人第一反应是算法实现有bug。但我在排查线上问题时发现,八成以上的失败都源于对椭圆曲线数学基础的理解偏差。说白了,你连它加法的几何意义都没搞明白,怎么写代码都悬。
这一章我们只聊三个核心概念:有限域、基点、阶。搞懂它们,SM2的签名流程就不再是黑盒子。
有限域:椭圆曲线的「画布」
椭圆曲线方程 y² = x³ + ax + b 大家都知道。但在密码学里,x、y不能取全体实数,只能在一个有限域 Fp 里选值。p是一个大素数,域里只有 {0,1,2,...,p-1} 这些整数,加减乘除全部模p。
我当年写第一个椭圆曲线项目时,直接用了浮点运算,结果坐标计算全错。后来才意识到:有限域里没有「小数」,只有模p的余数。加法是模加,乘法是模乘,除法算模逆。
| 运算 | 实数域 | 有限域 Fp |
|---|---|---|
| 加法 | a + b | (a + b) % p |
| 乘法 | a × b | (a * b) % p |
| 除法 | a / b | a * b⁻¹ mod p |
| 负数 | -a | p - a |
SM2使用的是国密推荐曲线sm2p256v1,p是一个256位素数。你可以记下来:p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF。没错,就这么写死。
椭圆曲线上的加法:从几何到代数
实数域上,椭圆曲线加法有漂亮的「弦切」几何解释。但转到有限域,你得扔掉几何直觉,直接上代数公式。
给定两个点 P(x₁,y₁) 和 Q(x₂,y₂),且 P ≠ -Q:
- 计算斜率
λ = (y₂ - y₁) / (x₂ - x₁) mod p(若P≠Q) - 或者
λ = (3x₁² + a) / (2y₁) mod p(若P=Q,倍点) - 结果点
R(x₃,y₃):x₃ = λ² - x₁ - x₂ mod p,y₃ = λ(x₁ - x₃) - y₁ mod p
基点G与阶n:签名的「计时器」
基点G是曲线上的一个固定点。所有SM2签名操作都围绕它展开。G的阶n,是一个素数,表示 n * G = 0 的最小正整数n。换句话说,对G重复加法n次,回到原点。
为什么要用素数阶?因为素阶子群没有非平凡子群,能抵抗各种攻击。SM2的n是:n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123。注意这个n略小于p,但不是所有曲线都这样。
群结构:子群与循环群
椭圆曲线全体点加上加法构成群。但实际密码学只用G生成的循环子群。这个子群的大小正好是n。所有非0点都可以写成 k * G,其中k是整数。
SM2签名的核心安全假设——椭圆曲线离散对数问题(ECDLP)——就是在给定G和Q= dG的情况下,很难求出d。所以私钥d不能泄露。
一张图厘清关系
阶的两种含义:别搞混
椭圆曲线里「阶」这个词有两层意思,我踩过坑:
- 点的阶:某个点重复加法多少次回到0点。基点G的阶n就是群的阶。
- 曲线的阶:曲线上的所有点(包括0点)的总数,记为#E。SM2曲线的#E = h×n,其中余因子h很小(通常为1)。
SM2的h=1,所以曲线阶 = n。这简化了实现,但你别在别的曲线上也这么假设。比如secp256k1的h=1,但其他商用曲线不一定是。
用代码感受一下
下面用Python模拟有限域上的点加(不考虑性能,只说明原理):
def add_points(p, a, P, Q):
"""有限域 Fp 上的椭圆曲线点加法,曲线 y² = x³ + ax + b"""
if P is None:
return Q
if Q is None:
return P
x1, y1 = P
x2, y2 = Q
if x1 == x2 and (y1 + y2) % p == 0: # P = -Q
return None
if x1 == x2 and y1 == y2: # P = Q
lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p
else: # P ≠ Q
lam = (y2 - y1) * pow(x2 - x1, -1, p) % p
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
这段代码里 pow(..., -1, p) 是Python 3.8后内置的模逆运算,比自己写扩展欧几里得方便很多。我习惯用内建函数,但如果你的Python版本太老,就得自己实现了。
小练习:验证SM2基点阶
你可以用现成的密码库验证SM2的n是否正确:
import hashlib
from gmssl import sm2
sm2_crv = sm2.CryptSM2(public_key=None, private_key=None)
G = sm2_crv._sm2_public_key._point_G
n = sm2_crv._ecc_table['n']
# 计算 n * G 是否等于0点
R = sm2_crv._point_mul(G, n)
print(R == None) # 应该输出 True
我在调签名库时用这个检查过一遍,确保曲线参数没被篡改。毕竟安全第一。
避坑指南
我曾经在项目里用了一个非正规的椭圆曲线参数,群阶n不是素数。结果用户私钥生成后,签名时而成功时而失败。检查了一天,发现n被攻击者故意选成有因子2的值。换回国密标准参数后一切正常。
记住:永远使用标准参数,不要自己造曲线。
小结
有限域、基点、阶,这三个概念构成了SM2签名运算的数学基石。下一层就是签名算法本身。但如果你今天回去写代码,建议先跑一次 n*G == 0 的验证——这个测试能帮你提前狙击大批潜在的签名失败bug。