数学基础:椭圆曲线、有限域与核心运算

好,我们正式开始接触 SM2 的核心数学。很多同学一听到「数学基础」就头疼,觉得离工程太远。其实我刚开始做密码学开发时也这么想 —— 直到有一次在生产环境调试签名失败,花了整整两天才发现是在点加运算的一个边界条件上翻了车。从那以后,我就老老实实把数学基础补了一遍。

说白了,SM2 的安全性建立在椭圆曲线离散对数问题的难解性上。你不需要成为数学家,但必须搞清楚三个东西:曲线长什么样、运算怎么算、乘法为什么能防破解。来看看这一章的知识地图。

SM2 数学基础 —— 知识结构 椭圆曲线基本概念 有限域运算 点加 / 点倍运算 标量乘法 Weierstrass 方程 模素数 p 的整数域 弦切法则 无穷远点 O 的单位元 Double-and-Add 算法

嗯,这张图基本就是本章的全部内容。我们从上往下走:先搞懂椭圆曲线方程,然后放进有限域里运算,接着定义点加和点倍,最后用标量乘法把私钥变成公钥。环环相扣,一个环节掉链子后面全崩。

椭圆曲线基本概念

SM2 用的曲线定义在 Weierstrass 方程上:

y² = x³ + ax + b  (mod p)

其中 a 和 b 是系数,p 是一个大素数。这里的 (mod p) 非常重要 —— 它把连续的实数曲线切成有限个离散点,这些点就构成一个加法群。

你想想看,如果 a=0、b=7,这就是比特币用的 secp256k1 曲线。SM2 的曲线参数由国家密码管理局指定,a 和 b 有固定值。我个人习惯在实现时先检查参数是否正确,免得踩到「曲线无效」的坑。

关键性质:椭圆曲线上的点集合 + 无穷远点 O(单位元),在弦切法则下构成阿贝尔群。这条性质是 ECC 一切安全性的基石。

有限域运算

SM2 的有限域是 GF(p),也就是模一个大素数 p 的剩余类。说白了,所有整数运算的结果都要 mod p。

有限域里的基本运算有四种:

  • 加法:(a + b) mod p
  • 减法:(a - b + p) mod p —— 注意要加 p 防止负数
  • 乘法:(a * b) mod p
  • 求逆:a⁻¹ mod p —— 用扩展欧几里得算法或费马小定理

我在项目中遇到过一个问题:求逆运算特别慢,导致整个签名流程延迟。后来改成用费马小定理配合预计算,速度提升了好几倍。嗯,这里要注意,SM2 的 p 是 256 位的素数,乘法必须用大数运算库来处理。

避坑指南:我曾经过分相信 Python 的 pow 函数,在批量签名时直接调用了 pow(a, -1, p)。在 CPython 3.8 下这个用法没问题,但迁移到 MicroPython 嵌入式环境时发现不支持负指数求逆,导致整个设备无法签名。千万别假设所有运行环境都支持一样的内置函数。

点加运算

椭圆曲线上的点加法不是普通向量加法。给定两个不同的点 P(x₁, y₁) 和 Q(x₂, y₂),它们的和 R(x₃, y₃) 这样计算:

λ = (y₂ - y₁) * (x₂ - x₁)⁻¹ mod p
x₃ = λ² - x₁ - x₂ mod p
y₃ = λ * (x₁ - x₃) - y₁ mod p

几何意义是什么?连接 P 和 Q 作直线,这条直线与曲线相交于第三点,然后取该点关于 x 轴的对称点就是 R。这就是所谓的「弦切法则」。

运算 条件 公式特点
点加(P + Q) P ≠ Q,且均非无穷远点 需要模逆运算,较慢
点倍(2P) P = Q 切线法,只需要求一次逆
加无穷远点 P + O = P 恒等,直接返回 P

点倍运算

点倍其实是点加的特殊情况。当 P = Q 时,连接线变成 P 点的切线:

λ = (3 * x₁² + a) * (2 * y₁)⁻¹ mod p
x₃ = λ² - 2 * x₁ mod p
y₃ = λ * (x₁ - x₃) - y₁ mod p

你注意看,公式里出现了参数 a —— 就是我们之前说的曲线系数。所以不同曲线的点倍运算结果不同,这在工程实现时一定要核对曲线参数。

我习惯在点倍函数里先判断输入的 y 坐标是否为零,因为切线在 y=0 时会变成垂直状态,交点就是无穷远点 O。这个边界情况很多人会忽略,但 SM2 签名验签时必须正确处理。

标量乘法

标量乘法就是把一个点 P 重复加上自己 k 次,记作 k·P。这是 ECC 里最核心的操作 —— 私钥 d 乘以基点 G 就等于公钥 Q。

直接循环加 k 次?别开玩笑了,k 是个 256 位的数,循环 2²⁵⁶ 次要等到宇宙毁灭。实际用 Double-and-Add 算法:

def scalar_mult(k: int, P: Point) -> Point:
    """对标量 k 做二进制展开,逐位点倍和点加"""
    result = INFINITY      # 无穷远点 O
    addend = P
    while k > 0:
        if k & 1:          # 当前位为 1,加上 addend
            result = point_add(result, addend)
        addend = point_double(addend)  # 始终点倍
        k >>= 1
    return result

这个算法的时间复杂度是 O(log k)。嗯,具体来说是 256 次点倍,平均 128 次点加。我在项目里测过,用 Python 内置整数实现大概需要 5-8 毫秒,足够满足大多数应用场景。

工程优化技巧:如果你的设备 CPU 资源紧张(比如智能卡或物联网模组),可以考虑 Montgomery Ladder 算法。它比 Double-and-Add 多一点点计算量,但天然抵抗侧信道攻击 —— 因为无论密钥位是 0 还是 1,执行的运算序列都一模一样。

讲到这里,你应该能理解 SM2 的数学本质了:私钥是一个随机的大整数 d,公钥是 d·G(G 是标准基点)。要由公钥反推私钥,就得解离散对数问题 —— 目前还没人找到通用有效的方法。

下次写代码时,如果你发现签名结果不对,别急着翻调用栈。先回头检查数学运算:有限域求逆有没有溢出?点加里 P = Q 的情况有没有单独处理?标量乘法的循环条件是不是写成了 while k >= 0?这些问题我都踩过,希望你少走弯路。