椭圆曲线密码学基础:有限域、基点与阶

为什么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
实际编程时,模逆运算最容易出错。我建议直接用扩展欧几里得算法,别偷懒用费马小定理(除非p是公认素数且你算过指数)。

SM2使用的是国密推荐曲线sm2p256v1,p是一个256位素数。你可以记下来:p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF。没错,就这么写死。

椭圆曲线上的加法:从几何到代数

实数域上,椭圆曲线加法有漂亮的「弦切」几何解释。但转到有限域,你得扔掉几何直觉,直接上代数公式。

给定两个点 P(x₁,y₁) 和 Q(x₂,y₂),且 P ≠ -Q:

  1. 计算斜率 λ = (y₂ - y₁) / (x₂ - x₁) mod p(若P≠Q)
  2. 或者 λ = (3x₁² + a) / (2y₁) mod p(若P=Q,倍点)
  3. 结果点 R(x₃,y₃):x₃ = λ² - x₁ - x₂ mod p,y₃ = λ(x₁ - x₃) - y₁ mod p
0点(无穷远点)是加法单位元。P + 0 = P。所有椭圆曲线加法形成一个阿贝尔群。

基点G与阶n:签名的「计时器」

基点G是曲线上的一个固定点。所有SM2签名操作都围绕它展开。G的阶n,是一个素数,表示 n * G = 0 的最小正整数n。换句话说,对G重复加法n次,回到原点。

为什么要用素数阶?因为素阶子群没有非平凡子群,能抵抗各种攻击。SM2的n是:n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123。注意这个n略小于p,但不是所有曲线都这样。

我曾经遇到过有人把p和n混淆,导致私钥生成时随机数范围取错。签名时r=0直接死循环。记住:私钥d 在 [1, n-2] 里随机选,不是 [1, p-2]。

群结构:子群与循环群

椭圆曲线全体点加上加法构成群。但实际密码学只用G生成的循环子群。这个子群的大小正好是n。所有非0点都可以写成 k * G,其中k是整数。

SM2签名的核心安全假设——椭圆曲线离散对数问题(ECDLP)——就是在给定G和Q= dG的情况下,很难求出d。所以私钥d不能泄露。

一张图厘清关系

椭圆曲线密码学核心概念关系 有限域 Fp 椭圆曲线 E(Fp) 基点 G 阶 n SM2 签名 私钥 d / 公钥 Q 有限域Fp给椭圆曲线提供离散数学环境;基点G的阶n决定子群大小;私钥d与G生成公钥Q = dG;签名过程需要n保证循环防碰撞。 任何一个概念出错(比如选错n的长度)都会直接导致签名失败。

阶的两种含义:别搞混

椭圆曲线里「阶」这个词有两层意思,我踩过坑:

  • 点的阶:某个点重复加法多少次回到0点。基点G的阶n就是群的阶。
  • 曲线的阶:曲线上的所有点(包括0点)的总数,记为#E。SM2曲线的#E = h×n,其中余因子h很小(通常为1)。

SM2的h=1,所以曲线阶 = n。这简化了实现,但你别在别的曲线上也这么假设。比如secp256k1的h=1,但其他商用曲线不一定是。

重要公式: #E = p + 1 - t,其中t是Frobenius迹。Hasse定理说 |t| ≤ 2√p。SM2的#E ≈ p,所以t很小。

用代码感受一下

下面用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。