土法炼钢兴趣小组的算法知识备份

椭圆曲线算术:群论视角

目录

椭圆曲线密码学(ECC)是现代密码学的支柱。从 TLS 1.3 到比特币签名,从 Signal 协议到零知识证明,椭圆曲线无处不在。但大多数介绍要么停留在”画两条线”的直觉层面,要么直接跳进抽象代数的深渊。本文试图在两者之间找到一条路径:用群论的语言,严谨但不枯燥地展开椭圆曲线算术的全貌。

我们将从 Weierstrass 方程出发,经过点加法的几何直觉和代数公式,建立群结构,实现标量乘法,比较不同曲线族的设计取舍,最终落地到 ECDSA 签名和实际工程问题。文末附完整的 Curve25519 标量乘法 C 实现。


一、Weierstrass 形式与椭圆曲线的定义

椭圆曲线并非椭圆。这个名字来自计算椭圆弧长的积分——椭圆积分的反函数恰好参数化了这类曲线。在密码学中,我们关心的是定义在有限域上的椭圆曲线,但先从实数域上的几何直觉开始。

1.1 一般 Weierstrass 方程

最一般的 Weierstrass 方程为:

y² + a₁xy + a₃y = x³ + a₂x² + a₄x + a₆

当域的特征不等于 2 和 3 时(密码学中几乎总是如此),通过变量替换可以化简为短 Weierstrass 形式

y² = x³ + ax + b

其中 a, b 是域中的元素。为使曲线光滑(没有奇异点),要求判别式非零:

Delta = -16(4a³ + 27b²) != 0

判别式为零意味着曲线有尖点或自交点,此时代数结构退化,无法用于密码学。

1.2 射影坐标与无穷远点

仿射平面上的曲线方程 y² = x³ + ax + b 缺少一个关键元素——无穷远点 O。引入射影坐标 [X : Y : Z],曲线方程变为:

Y²Z = X³ + aXZ² + bZ³

Z = 0 时,方程退化为 X³ = 0,唯一解为 [0 : 1 : 0],这就是无穷远点 O。它是群的单位元。

射影坐标不只是理论工具。在实际实现中,使用射影坐标(或 Jacobian 坐标 [X : Y : Z] 其中 x = X/Z², y = Y/Z³)可以避免昂贵的模逆运算,将其延迟到最终结果转换时一次性执行。

1.3 有限域上的椭圆曲线

密码学使用有限域 F_p(p 为大素数)或 F_{2^m}(二元域)上的椭圆曲线。在 F_p 上,曲线不再是光滑的连续曲线,而是 p 个可能的 x 值对应的离散点集。曲线上的点数 #E(F_p) 由 Hasse 定理给出:

|#E(F_p) - (p + 1)| <= 2*sqrt(p)

也就是说,点数大约等于 p,误差不超过 2*sqrt(p)。精确计数需要 Schoof 算法或其改进版本 SEA(Schoof-Elkies-Atkin)。

# 枚举 F_p 上椭圆曲线 y^2 = x^3 + ax + b 的所有点
def enumerate_points(a, b, p):
    """返回椭圆曲线 E(F_p) 上的所有仿射点及无穷远点 O"""
    points = [None]  # None 代表无穷远点 O
    for x in range(p):
        rhs = (x * x * x + a * x + b) % p
        for y in range(p):
            if (y * y) % p == rhs:
                points.append((x, y))
    return points

# 示例:y^2 = x^3 + 2x + 3 over F_97
pts = enumerate_points(2, 3, 97)
print(f"曲线上的点数: {len(pts)}")  # 包含 O

这个暴力枚举的复杂度是 O(p²),实际中完全不可行——密码学用的 p 是 256 位的数字。但它帮助建立直觉:有限域上的椭圆曲线是一个有限集合,其上定义了一个群运算。


二、点加法的几何解释

椭圆曲线上两点的”加法”定义来自一个优美的几何构造。在实数域上,这个构造直观且自然。

2.1 弦切法

给定曲线 E 上两个不同的点 P 和 Q:

  1. 画一条通过 P 和 Q 的直线(割线)。
  2. 这条直线与曲线 E 必然有第三个交点 R’(由 Bezout 定理保证——三次曲线与直线相交恰好有三个点,计入重数)。
  3. 将 R’ 关于 x 轴做对称,得到 R。
  4. 定义 P + Q = R。
椭圆曲线点加法

这个定义看起来随意,但它有深刻的代数几何背景。反射步骤确保了运算的结合律——这是最难证明也最重要的性质。

2.2 特殊情况

几个需要单独处理的情况:

点加倍(P = Q):当两点重合时,割线退化为曲线在 P 处的切线。切线与曲线的第三个交点经反射后得到 2P。

逆元(P + (-P) = O):当 Q = -P = (x_P, -y_P) 时,通过 P 和 -P 的直线是垂直线 x = x_P。这条垂直线与曲线的”第三个交点”是无穷远点 O。因此 P + (-P) = O,符合群的逆元要求。

加上单位元(P + O = P):无穷远点 O 是单位元。通过 P 和 O 的”直线”是过 P 的垂直线 x = x_P,它与曲线的另一个交点是 -P,反射后得到 P。所以 P + O = P。

y 坐标为零:如果 P = (x_P, 0),则 -P = P,即 P 是 2 阶元素。此时 2P = O。

2.3 直觉为何有效

三次曲线与直线的交点关系是椭圆曲线群结构的核心。任何一条直线与三次曲线恰好交于三个点(在射影平面上,计入重数和无穷远点)。点加法的定义利用了这个性质:已知两个交点,第三个交点由曲线方程和直线方程联立唯一确定。反射步骤将这个”第三交点”映射为群运算的结果,使得结合律成立。


三、点加法的代数公式

几何直觉很好,但计算机需要代数公式。以下推导适用于特征不等于 2, 3 的域。

3.1 一般点加法(P != Q)

设 P = (x₁, y₁),Q = (x₂, y₂),且 x₁ != x₂。

通过 P 和 Q 的直线斜率:

lambda = (y₂ - y₁) / (x₂ - x₁)

将直线方程 y = lambda * (x - x₁) + y₁ 代入曲线方程 y² = x³ + ax + b:

[lambda * (x - x₁) + y₁]² = x³ + ax + b

展开并利用韦达定理(三次方程三个根之和等于二次项系数的相反数):

x₁ + x₂ + x₃ = lambda²

因此第三个交点的 x 坐标为:

x₃ = lambda² - x₁ - x₂

反射后 R = (x₃, y₃),其中:

y₃ = lambda * (x₁ - x₃) - y₁

3.2 点加倍(P = Q)

设 P = (x₁, y₁),且 y₁ != 0。

曲线在 P 处的切线斜率由隐函数求导得到。对 y² = x³ + ax + b 两边求导:

2y * dy/dx = 3x² + a
lambda = (3x₁² + a) / (2y₁)

之后公式与一般情况相同:

x₃ = lambda² - 2x₁
y₃ = lambda * (x₁ - x₃) - y₁

3.3 完整的点加法算法

def point_add(P, Q, a, p):
    """
    椭圆曲线点加法:E: y^2 = x^3 + ax + b over F_p
    P, Q 为曲线上的点,None 代表无穷远点 O
    返回 P + Q
    """
    if P is None:
        return Q
    if Q is None:
        return P

    x1, y1 = P
    x2, y2 = Q

    if x1 == x2:
        if y1 != y2:
            # P + (-P) = O
            return None
        if y1 == 0:
            # 切线垂直,2P = O
            return None
        # 点加倍
        lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p
    else:
        # 一般点加法
        lam = (y2 - y1) * pow(x2 - x1, -1, p) % p

    x3 = (lam * lam - x1 - x2) % p
    y3 = (lam * (x1 - x3) - y1) % p
    return (x3, y3)

3.4 运算计数

在 F_p 上,一次点加法需要: - 1 次模逆(最昂贵,约等于 30-40 次模乘) - 2 次模乘 - 6 次模加/减

一次点加倍需要: - 1 次模逆 - 2 次模乘(含一次平方) - 4 次模加/减

使用 Jacobian 坐标可以消除模逆。Jacobian 坐标下的混合加法(一个点为仿射坐标,另一个为 Jacobian 坐标)只需 8 次乘法。这在标量乘法的内层循环中至关重要。

// Jacobian 坐标下的点加倍(伪代码)
// 输入: (X1, Y1, Z1),输出: (X3, Y3, Z3)
// 代价: 1S + 2M + 8add(S = 平方,M = 乘法)
void point_double_jacobian(mp *X3, mp *Y3, mp *Z3,
                           const mp *X1, const mp *Y1, const mp *Z1,
                           const mp *a, const mp *p) {
    // A = Y1^2
    // B = 4 * X1 * A
    // C = 8 * A^2
    // D = 3 * X1^2 + a * Z1^4
    // X3 = D^2 - 2*B
    // Y3 = D * (B - X3) - C
    // Z3 = 2 * Y1 * Z1
}

四、群律与结合律

椭圆曲线上的点集,加上无穷远点 O 和前面定义的加法运算,构成一个阿贝尔群。

4.1 群公理验证

需要验证五条公理:

公理 验证
封闭性 两点之和仍在曲线上(代数验证)
单位元 O 是单位元,P + O = O + P = P
逆元 对任意 P = (x, y),逆元 -P = (x, -y),P + (-P) = O
交换律 公式关于 P, Q 对称(斜率公式对称)
结合律 最难证明的部分

交换律从公式的对称性直接可见。封闭性、单位元和逆元在前面已经处理。真正困难的是结合律。

4.2 结合律:Cayley-Bacharach 定理

结合律的证明有多种路径。最优雅的是基于 Cayley-Bacharach 定理的代数几何证明。

Cayley-Bacharach 定理(简化版):设两条三次曲线 C₁ 和 C₂ 在 9 个点相交。如果另一条三次曲线 C₃ 通过其中 8 个点,则它必然通过第 9 个点。

要证明 (P + Q) + R = P + (Q + R),我们构造适当的三次曲线(由三条直线的乘积构成),利用 Cayley-Bacharach 定理证明两种结合方式产生相同的结果。

具体来说,设 l₁ 是通过 P, Q 的直线,l₂ 是通过 (P+Q), R 的直线,l₃ 是通过某些中间点的直线。构造 C₁ = l₁ * l₂ * l₃(三条直线的乘积是一条退化的三次曲线),类似地构造 C₂。两条退化三次曲线与椭圆曲线 E 的交点关系,经 Cayley-Bacharach 定理,给出结合律。

这个证明的美妙之处在于它完全是几何/代数的,不涉及坐标计算。暴力验证结合律需要展开极长的多项式表达式,而 Cayley-Bacharach 定理一步到位。

4.3 群的阶和子群结构

椭圆曲线群 E(F_p) 的结构由以下定理描述:

定理:E(F_p) 同构于 Z/n₁Z x Z/n₂Z,其中 n₂ | n₁ 且 n₂ | (p - 1)。

在密码学应用中,我们通常选择 #E(F_p) = h * n,其中 n 是大素数,h 是小的辅因子(cofactor)。理想情况下 h = 1(整个群就是素数阶循环群),但有些曲线设计选择 h = 4 或 h = 8 以获得其他优势。

# 验证群公理的简单测试
def test_group_axioms(a, b, p, num_tests=100):
    """在小曲线上测试群公理"""
    import random
    pts = enumerate_points(a, b, p)
    affine_pts = [pt for pt in pts if pt is not None]

    for _ in range(num_tests):
        P = random.choice(pts)
        Q = random.choice(pts)
        R = random.choice(pts)

        # 单位元
        assert point_add(P, None, a, p) == P, "单位元失败"

        # 逆元
        if P is not None:
            neg_P = (P[0], (-P[1]) % p)
            assert point_add(P, neg_P, a, p) is None, "逆元失败"

        # 交换律
        assert point_add(P, Q, a, p) == point_add(Q, P, a, p), "交换律失败"

        # 结合律
        PQ = point_add(P, Q, a, p)
        PQ_R = point_add(PQ, R, a, p)
        QR = point_add(Q, R, a, p)
        P_QR = point_add(P, QR, a, p)
        assert PQ_R == P_QR, "结合律失败"

    print("所有群公理测试通过")

五、标量乘法算法

标量乘法 [k]P = P + P + ... + P(k 次)是椭圆曲线密码学的核心运算。朴素的 k 次加法是 O(k),对于 256 位的 k 完全不可行。我们需要 O(log k) 的算法。

5.1 双倍加法(Double-and-Add)

这是标量乘法的”平方-乘法”版本,与快速幂算法完全类似:

def scalar_mult(k, P, a, p):
    """
    双倍加法:计算 [k]P
    从最高位到最低位扫描 k 的二进制表示
    """
    if k == 0 or P is None:
        return None
    if k < 0:
        # [k]P = [-k](-P)
        P = (P[0], (-P[1]) % p)
        k = -k

    result = None  # O(单位元)
    addend = P

    while k:
        if k & 1:
            result = point_add(result, addend, a, p)
        addend = point_add(addend, addend, a, p)  # 加倍
        k >>= 1

    return result

对于 n 位的 k,这需要 n 次加倍和平均 n/2 次加法,总计约 1.5n 次点运算。

5.2 NAF(Non-Adjacent Form)

利用椭圆曲线上减法和加法代价相同的特性(取逆只需翻转 y 坐标),我们可以用 {-1, 0, 1} 三进制表示 k,使得非零位更少:

def to_naf(k):
    """将整数 k 转换为 NAF(非相邻形式)"""
    naf = []
    while k > 0:
        if k & 1:
            # 奇数:取 2 - (k mod 4) 使得下一位为 0
            ki = 2 - (k % 4)
            k -= ki
        else:
            ki = 0
        naf.append(ki)
        k >>= 1
    return naf

def scalar_mult_naf(k, P, a, p):
    """使用 NAF 的标量乘法"""
    naf = to_naf(k)
    neg_P = (P[0], (-P[1]) % p)
    result = None

    for i in range(len(naf) - 1, -1, -1):
        result = point_add(result, result, a, p)  # 加倍
        if naf[i] == 1:
            result = point_add(result, P, a, p)
        elif naf[i] == -1:
            result = point_add(result, neg_P, a, p)

    return result

NAF 保证没有相邻的非零位,平均非零位密度从 1/2 降到 1/3,节省约 11% 的点加法。

5.3 窗口方法(wNAF)

进一步推广,预计算 P, 3P, 5P, …, (2^{w-1} - 1)P,然后用宽度为 w 的 NAF。对于 w = 4,非零位密度降到 1/5,代价是 7 个点的预计算存储。

def scalar_mult_wnaf(k, P, a, p, w=4):
    """宽度为 w 的 wNAF 标量乘法"""
    # 预计算奇数倍:P, 3P, 5P, ..., (2^(w-1)-1)P
    precomp = [None] * (1 << (w - 1))
    precomp[0] = P
    double_P = point_add(P, P, a, p)
    for i in range(1, len(precomp)):
        precomp[i] = point_add(precomp[i - 1], double_P, a, p)

    # 生成 wNAF 表示
    naf = []
    while k > 0:
        if k & 1:
            # k 是奇数,取 k mod 2^w 的有符号表示
            ki = k % (1 << w)
            if ki >= (1 << (w - 1)):
                ki -= (1 << w)
            k -= ki
        else:
            ki = 0
        naf.append(ki)
        k >>= 1

    # 从高位到低位求值
    result = None
    for i in range(len(naf) - 1, -1, -1):
        result = point_add(result, result, a, p)
        if naf[i] > 0:
            idx = naf[i] // 2
            result = point_add(result, precomp[idx], a, p)
        elif naf[i] < 0:
            idx = (-naf[i]) // 2
            neg = (precomp[idx][0], (-precomp[idx][1]) % p)
            result = point_add(result, neg, a, p)

    return result

5.4 Montgomery 阶梯(常数时间)

上述所有算法都有一个致命缺陷:执行的操作序列依赖于 k 的比特值。攻击者通过计时、功耗分析或电磁辐射等侧信道,可以推断出 k 的每一位。对于 ECDSA,泄露私钥的几个比特就足以通过格攻击恢复完整私钥。

Montgomery 阶梯解决了这个问题:无论 k 的某一位是 0 还是 1,每步都执行恰好一次加法和一次加倍,只是操作数不同。

def montgomery_ladder(k, P, a, p):
    """
    Montgomery 阶梯:常数时间标量乘法
    每一步都执行一次加法和一次加倍,操作序列与 k 无关
    """
    R0 = None   # O
    R1 = P

    # 从最高位开始
    for i in range(k.bit_length() - 1, -1, -1):
        if (k >> i) & 1:
            R0 = point_add(R0, R1, a, p)
            R1 = point_add(R1, R1, a, p)
        else:
            R1 = point_add(R0, R1, a, p)
            R0 = point_add(R0, R0, a, p)

    return R0

注意:上面的 Python 代码仍然有条件分支。真正的常数时间实现需要用条件交换(constant-time swap)替代 if-else:

// 常数时间条件交换
void cswap(uint64_t *a, uint64_t *b, uint64_t condition) {
    uint64_t mask = -(uint64_t)(condition & 1);  // 全 0 或全 1
    uint64_t t = mask & (*a ^ *b);
    *a ^= t;
    *b ^= t;
}

Montgomery 阶梯是 Curve25519 实现的核心。


六、曲线族的比较

不是所有椭圆曲线都用同一个方程。不同的方程形式在性能和安全性上各有优劣。

6.1 短 Weierstrass 曲线

y² = x³ + ax + b        over F_p

这是最通用的形式,所有特征不等于 2, 3 的椭圆曲线都可以写成这个形式。NIST P-256 和 secp256k1 都使用短 Weierstrass 形式。

优点:通用性强,标准化程度高。 缺点:完整(complete)加法公式复杂,容易出实现错误。

6.2 Montgomery 曲线

By² = x³ + Ax² + x      over F_p

由 Peter Montgomery 于 1987 年提出。核心特性是标量乘法可以仅使用 x 坐标完成——这就是著名的 Montgomery 阶梯的数学基础。

设 P = (x_P, y_P),Q = (x_Q, y_Q),且已知 P - Q = (x_{P-Q}, y_{P-Q})。则 P + Q 的 x 坐标可以仅从 x_P, x_Q, x_{P-Q} 计算得到,无需 y 坐标:

x_{P+Q} = x_{P-Q} * [(x_P - x_Q)² / (x_P*x_Q - 1)²]

(简化公式,实际使用射影坐标避免除法。)

这个性质使得 Montgomery 曲线的标量乘法: 1. 天然适合 Montgomery 阶梯(常数时间) 2. 不需要 y 坐标,节省一半存储和传输开销 3. 公钥只需 32 字节(一个 x 坐标)

Curve25519 就是 Montgomery 曲线。

6.3 扭曲 Edwards 曲线

ax² + y² = 1 + dx²y²    over F_p

由 Daniel Bernstein 和 Tanja Lange 于 2008 年推广。Edwards 曲线的革命性优势在于完整加法公式

(x₁, y₁) + (x₂, y₂) = ( (x₁y₂ + y₁x₂) / (1 + dx₁x₂y₁y₂),
                           (y₁y₂ - ax₁x₂) / (1 - dx₁x₂y₁y₂) )

这个公式对曲线上的任意两个点都成立,包括 P = Q(加倍)、P = O(加单位元)、P = -Q(加逆元)。不需要任何特殊情况处理。

这意味着: 1. 实现更简单,更不容易出错 2. 天然抵抗异常情况攻击 3. 便于常数时间实现

Ed25519 就是扭曲 Edwards 曲线,与 Curve25519 是双有理等价的(即同一条曲线的不同方程表示)。

6.4 三种曲线形式的比较

+------------------+------------------+------------------+------------------+
|                  | 短 Weierstrass   | Montgomery       | 扭曲 Edwards     |
+------------------+------------------+------------------+------------------+
| 方程             | y²=x³+ax+b      | By²=x³+Ax²+x    | ax²+y²=1+dx²y²  |
| 加法公式统一性   | 需分情况讨论     | 仅用 x 坐标     | 完整公式          |
| 加法代价(M)      | 12M + 2S         | 5M + 4S + 1D    | 10M + 1S + 1D   |
| 加倍代价(M)      | 4M + 4S          | 4M + 2S + 1D    | 4M + 4S          |
| 常数时间友好度   | 困难             | Montgomery 阶梯  | 天然支持          |
| 典型曲线         | P-256, secp256k1 | Curve25519       | Ed25519          |
| 辅因子           | 1 (通常)         | 8                | 8 (或 4)         |
+------------------+------------------+------------------+------------------+

注:M = 域乘法,S = 域平方(约 0.8M),D = 常数乘法

七、Curve25519 与 Ed25519 的设计选择

Daniel Bernstein 在 2006 年发表的 Curve25519 是密码学曲线设计的里程碑。每一个参数选择都有明确的理由。

7.1 素数 p = 2^255 - 19

选择 p = 2^255 - 19 的理由:

  1. 接近 2 的幂:使得模约减极快。x mod p 只需一次乘加:x = x_hi * 19 + x_lo,因为 2^255 = 19 mod p
  2. 255 位而非 256 位:最高位空闲,可以用来存储符号位或标志位,同时 254 位安全强度已足够。
  3. 19 是满足条件的最小素数:2^255 - 1 不是素数,2^255 - 3, …, 2^255 - 17 也不是,2^255 - 19 是。

7.2 曲线方程 y² = x³ + 486662x² + x

Montgomery 参数 A = 486662 的选择:

  1. 曲线的阶为 8 * n,其中 n 是素数。辅因子 h = 8 使曲线同时兼容 Montgomery 形式和 Edwards 形式。
  2. A = 486662 是满足安全条件(CM 判别式足够大、抵抗已知攻击)的最小正偶数 A。
  3. “最小的安全参数”策略消除了后门嫌疑——参数选择过程完全透明、可重现。

7.3 基点

# Curve25519 基点
Gx = 9  # 是的,就是 9
# Gy 可以从方程推导,但 Curve25519 只使用 x 坐标

# Ed25519 基点(扭曲 Edwards 形式)
# 这是与 Curve25519 的基点 x=9 对应的 Edwards 坐标
Gy_ed = (4 * pow(5, -1, p)) % p  # 规范化的 y 坐标

基点选择 x = 9 是因为它是生成 n 阶子群的最小正整数 x 坐标。又一次,“最小的安全选择”原则。

7.4 密钥处理的巧妙设计

Curve25519 的密钥格式体现了工程美学:

// 私钥预处理(clamping)
void clamp_key(uint8_t key[32]) {
    key[0]  &= 248;    // 清除低 3 位:确保是 8 的倍数(消除小子群攻击)
    key[31] &= 127;    // 清除最高位:确保 < 2^255
    key[31] |= 64;     // 设置第 254 位为 1:确保标量乘法恒定步数
}

三个操作各有深意: - 清除低 3 位:标量是 8 的倍数,自动将结果映射到素数阶子群,抵御小子群攻击。 - 清除最高位:确保标量 < p,避免约减。 - 设置次高位:所有密钥的二进制长度相同,Montgomery 阶梯步数恒定,消除计时侧信道。

7.5 Curve25519 与 Ed25519 的关系

Curve25519(Montgomery 形式)和 Ed25519(Edwards 形式)是同一条曲线的不同表示。存在双有理映射在两者之间转换:

# Montgomery -> Edwards
# 设 Montgomery 曲线 y^2 = x^3 + Ax^2 + x
# 对应 Edwards 曲线 -x^2 + y^2 = 1 + d*x^2*y^2
# 映射: (u, v) -> (x, y) = (sqrt(-A-2) * u/v, (u-1)/(u+1))

def montgomery_to_edwards(u, v, A, p):
    """Montgomery 坐标 (u, v) 转换为 Edwards 坐标 (x, y)"""
    sqrt_neg_A_minus_2 = ...  # 预计算
    x = (sqrt_neg_A_minus_2 * u * pow(v, -1, p)) % p
    y = ((u - 1) * pow(u + 1, -1, p)) % p
    return (x, y)

实际应用中: - Curve25519 用于 ECDH(Diffie-Hellman 密钥交换),因为 Montgomery 阶梯高效且只需 x 坐标。 - Ed25519 用于签名(EdDSA),因为 Edwards 形式的完整加法公式使签名验证更简洁。


八、ECDSA 签名与验证

ECDSA(Elliptic Curve Digital Signature Algorithm)是椭圆曲线密码学最广泛的应用之一。比特币使用 ECDSA + secp256k1,TLS 使用 ECDSA + P-256。

8.1 参数

系统参数: - 椭圆曲线 E 定义在 F_p 上 - 基点 G,阶为 n(素数) - 哈希函数 H(如 SHA-256)

密钥对: - 私钥 d:随机整数,1 <= d <= n - 1 - 公钥 Q = [d]G:曲线上的点

8.2 签名算法

def ecdsa_sign(message, d, G, n, a, p, hash_func):
    """
    ECDSA 签名
    message: 待签名消息
    d: 私钥
    G: 基点
    n: 基点的阶
    返回签名 (r, s)
    """
    while True:
        # 1. 选择随机数 k
        k = random.randint(1, n - 1)

        # 2. 计算 R = [k]G
        R = scalar_mult(k, G, a, p)
        r = R[0] % n    # 取 x 坐标 mod n

        if r == 0:
            continue     # 极小概率事件,重试

        # 3. 计算消息哈希
        e = int(hash_func(message).hexdigest(), 16)
        # 截断到与 n 相同的比特长度
        z = e >> max(0, e.bit_length() - n.bit_length())

        # 4. 计算 s = k^{-1} * (z + r*d) mod n
        s = (pow(k, -1, n) * (z + r * d)) % n

        if s == 0:
            continue     # 极小概率事件,重试

        return (r, s)

8.3 验证算法

def ecdsa_verify(message, signature, Q, G, n, a, p, hash_func):
    """
    ECDSA 验证
    signature: (r, s)
    Q: 公钥(曲线上的点)
    返回 True/False
    """
    r, s = signature

    # 1. 检查 r, s 在合法范围内
    if not (1 <= r < n and 1 <= s < n):
        return False

    # 2. 计算消息哈希
    e = int(hash_func(message).hexdigest(), 16)
    z = e >> max(0, e.bit_length() - n.bit_length())

    # 3. 计算 s 的逆
    w = pow(s, -1, n)

    # 4. 计算 u1 = z*w mod n, u2 = r*w mod n
    u1 = (z * w) % n
    u2 = (r * w) % n

    # 5. 计算 R' = [u1]G + [u2]Q
    R1 = scalar_mult(u1, G, a, p)
    R2 = scalar_mult(u2, Q, a, p)
    R_prime = point_add(R1, R2, a, p)

    if R_prime is None:
        return False

    # 6. 验证 r == R'.x mod n
    return r == R_prime[0] % n

8.4 为什么 ECDSA 有效

正确性证明的关键步骤:

验证方计算 R' = [u1]G + [u2]Q
             = [z*w]G + [r*w][d]G       (Q = [d]G)
             = [z*w + r*w*d]G
             = [w*(z + r*d)]G
             = [s^{-1} * s * k]G         (因为 s = k^{-1}*(z + r*d))
             = [k]G
             = R

所以 R'.x = R.x = r,验证通过。

8.5 致命的随机数问题

ECDSA 的安全性完全依赖于每次签名使用不同且不可预测的 k。如果两次签名使用了相同的 k:

s₁ = k⁻¹(z₁ + r*d) mod n
s₂ = k⁻¹(z₂ + r*d) mod n     (r 相同,因为 k 相同)

s₁ - s₂ = k⁻¹(z₁ - z₂) mod n
k = (z₁ - z₂) / (s₁ - s₂) mod n
d = (s₁*k - z₁) / r mod n     私钥泄露!

2010 年,Sony PS3 的 ECDSA 实现对所有签名使用了相同的 k,导致私钥被完全恢复,PS3 的安全体系崩溃。

RFC 6979 通过确定性方式从私钥和消息派生 k,彻底解决了这个问题。EdDSA(Ed25519 使用的签名方案)从设计上就避免了随机数问题——k 由私钥的哈希和消息确定性地生成。


九、配对密码学简介

配对(pairing)是椭圆曲线密码学的高级主题,也是 BLS 签名、零知识证明(zk-SNARKs)和基于身份的加密等前沿技术的数学基础。

9.1 双线性配对

一个双线性配对是一个映射:

e: G₁ x G₂ -> G_T

其中 G₁, G₂ 是椭圆曲线上的群,G_T 是有限域的乘法群。这个映射满足:

  1. 双线性:e(aP, bQ) = e(P, Q)^{ab}
  2. 非退化:存在 P, Q 使得 e(P, Q) != 1
  3. 可计算:存在高效算法计算 e

9.2 Weil 配对与 Tate 配对

Weil 配对 e_n 定义在 n-挠点上(满足 [n]P = O 的点)。其构造基于除子(divisor)理论:对于椭圆曲线上的有理函数 f_P 满足 div(f_P) = n[P] - n[O],Weil 配对为:

e_n(P, Q) = f_P(Q) / f_Q(P)

计算使用 Miller 算法,复杂度为 O(log n) 次域运算。

Tate 配对是 Weil 配对的变体,在实际应用中更常用,因为只需计算一个 Miller 循环(而非两个),效率更高。

9.3 配对的应用

+--------------------+-----------------------------------------+
| 应用               | 利用配对的性质                          |
+--------------------+-----------------------------------------+
| BLS 签名           | 短签名、签名聚合                        |
| 基于身份的加密     | 公钥 = 身份字符串                       |
| zk-SNARKs          | 多项式承诺验证                          |
| 属性基加密         | 细粒度访问控制                          |
+--------------------+-----------------------------------------+

配对友好曲线(pairing-friendly curves)的选择比一般 ECC 曲线更受限。BN254 和 BLS12-381 是目前最常用的配对友好曲线。


十、Curve25519 标量乘法的 C 实现

以下是完整的 Curve25519 标量乘法实现。这是 X25519 密钥交换的核心——输入一个 32 字节标量和一个 32 字节的 x 坐标,输出一个 32 字节的 x 坐标。

实现基于 Daniel Bernstein 的参考实现,使用 radix-2^51 表示域元素(5 个 64 位 limb),以在 64 位平台上获得最佳性能。

/*
 * curve25519_scalarmult.c
 *
 * Curve25519 标量乘法的完整实现
 * 曲线方程: y^2 = x^3 + 486662*x^2 + x  (mod 2^255 - 19)
 * 使用 Montgomery 阶梯,仅操作 x 坐标
 *
 * 编译: gcc -O2 -o curve25519 curve25519_scalarmult.c
 * 此实现用于教学目的。生产环境请使用经过审计的库。
 */

#include <stdint.h>
#include <string.h>
#include <stdio.h>

/* -----------------------------------------------------------
 * 域算术: F_p, p = 2^255 - 19
 * 表示: 5 个 limb, 每个 51 位 (radix 2^51)
 * fe[0] + fe[1]*2^51 + fe[2]*2^102 + fe[3]*2^153 + fe[4]*2^204
 * ----------------------------------------------------------- */

typedef int64_t  fe[5];

static const int64_t MASK51 = (1LL << 51) - 1;

static void fe_zero(fe o) {
    o[0] = o[1] = o[2] = o[3] = o[4] = 0;
}

static void fe_one(fe o) {
    o[0] = 1;
    o[1] = o[2] = o[3] = o[4] = 0;
}

static void fe_copy(fe o, const fe a) {
    o[0] = a[0]; o[1] = a[1]; o[2] = a[2];
    o[3] = a[3]; o[4] = a[4];
}

static void fe_add(fe o, const fe a, const fe b) {
    o[0] = a[0] + b[0];
    o[1] = a[1] + b[1];
    o[2] = a[2] + b[2];
    o[3] = a[3] + b[3];
    o[4] = a[4] + b[4];
}

static void fe_sub(fe o, const fe a, const fe b) {
    /* 加上 2*p 以确保结果为正 */
    o[0] = a[0] - b[0] + 0xFFFFFFFFFFFDA; /* 2*(2^51 - 19) */
    o[1] = a[1] - b[1] + 0xFFFFFFFFFFFFE; /* 2*(2^51 - 1)  */
    o[2] = a[2] - b[2] + 0xFFFFFFFFFFFFE;
    o[3] = a[3] - b[3] + 0xFFFFFFFFFFFFE;
    o[4] = a[4] - b[4] + 0xFFFFFFFFFFFFE;
}

static void fe_carry(fe o) {
    int64_t c;
    c = o[0] >> 51; o[1] += c; o[0] &= MASK51;
    c = o[1] >> 51; o[2] += c; o[1] &= MASK51;
    c = o[2] >> 51; o[3] += c; o[2] &= MASK51;
    c = o[3] >> 51; o[4] += c; o[3] &= MASK51;
    c = o[4] >> 51; o[0] += c * 19; o[4] &= MASK51;
    /* 第二轮进位以处理 o[0] 可能的溢出 */
    c = o[0] >> 51; o[1] += c; o[0] &= MASK51;
}

/* 128 位中间结果的乘法 */
typedef __int128 int128_t;

static void fe_mul(fe o, const fe a, const fe b) {
    int128_t t0, t1, t2, t3, t4;
    int64_t  b1_19, b2_19, b3_19, b4_19;

    b1_19 = b[1] * 19;
    b2_19 = b[2] * 19;
    b3_19 = b[3] * 19;
    b4_19 = b[4] * 19;

    t0  = (int128_t)a[0] * b[0];
    t0 += (int128_t)a[1] * b4_19;
    t0 += (int128_t)a[2] * b3_19;
    t0 += (int128_t)a[3] * b2_19;
    t0 += (int128_t)a[4] * b1_19;

    t1  = (int128_t)a[0] * b[1];
    t1 += (int128_t)a[1] * b[0];
    t1 += (int128_t)a[2] * b4_19;
    t1 += (int128_t)a[3] * b3_19;
    t1 += (int128_t)a[4] * b2_19;

    t2  = (int128_t)a[0] * b[2];
    t2 += (int128_t)a[1] * b[1];
    t2 += (int128_t)a[2] * b[0];
    t2 += (int128_t)a[3] * b4_19;
    t2 += (int128_t)a[4] * b3_19;

    t3  = (int128_t)a[0] * b[3];
    t3 += (int128_t)a[1] * b[2];
    t3 += (int128_t)a[2] * b[1];
    t3 += (int128_t)a[3] * b[0];
    t3 += (int128_t)a[4] * b4_19;

    t4  = (int128_t)a[0] * b[4];
    t4 += (int128_t)a[1] * b[3];
    t4 += (int128_t)a[2] * b[2];
    t4 += (int128_t)a[3] * b[1];
    t4 += (int128_t)a[4] * b[0];

    /* 进位链 */
    int64_t c;
    o[0] = (int64_t)t0 & MASK51; c = (int64_t)(t0 >> 51);
    t1 += c;
    o[1] = (int64_t)t1 & MASK51; c = (int64_t)(t1 >> 51);
    t2 += c;
    o[2] = (int64_t)t2 & MASK51; c = (int64_t)(t2 >> 51);
    t3 += c;
    o[3] = (int64_t)t3 & MASK51; c = (int64_t)(t3 >> 51);
    t4 += c;
    o[4] = (int64_t)t4 & MASK51; c = (int64_t)(t4 >> 51);
    o[0] += c * 19;
    c = o[0] >> 51; o[1] += c; o[0] &= MASK51;
}

static void fe_sq(fe o, const fe a) {
    fe_mul(o, a, a);
}

/* a^{-1} = a^{p-2} mod p (费马小定理) */
static void fe_inv(fe o, const fe a) {
    fe t0, t1, t2, t3;
    int i;

    fe_sq(t0, a);          /* t0 = a^2       */
    fe_sq(t1, t0);         /* t1 = a^4       */
    fe_sq(t1, t1);         /* t1 = a^8       */
    fe_mul(t1, t1, a);     /* t1 = a^9       */
    fe_mul(t0, t0, t1);    /* t0 = a^11      */
    fe_sq(t2, t0);         /* t2 = a^22      */
    fe_mul(t1, t1, t2);    /* t1 = a^(2^5-1) */

    fe_sq(t2, t1);
    for (i = 0; i < 4; i++) fe_sq(t2, t2);
    fe_mul(t1, t2, t1);    /* t1 = a^(2^10-1) */

    fe_sq(t2, t1);
    for (i = 0; i < 9; i++) fe_sq(t2, t2);
    fe_mul(t2, t2, t1);    /* t2 = a^(2^20-1) */

    fe_sq(t3, t2);
    for (i = 0; i < 19; i++) fe_sq(t3, t3);
    fe_mul(t2, t3, t2);    /* t2 = a^(2^40-1) */

    fe_sq(t2, t2);
    for (i = 0; i < 9; i++) fe_sq(t2, t2);
    fe_mul(t1, t2, t1);    /* t1 = a^(2^50-1) */

    fe_sq(t2, t1);
    for (i = 0; i < 49; i++) fe_sq(t2, t2);
    fe_mul(t2, t2, t1);    /* t2 = a^(2^100-1) */

    fe_sq(t3, t2);
    for (i = 0; i < 99; i++) fe_sq(t3, t3);
    fe_mul(t2, t3, t2);    /* t2 = a^(2^200-1) */

    fe_sq(t2, t2);
    for (i = 0; i < 49; i++) fe_sq(t2, t2);
    fe_mul(t1, t2, t1);    /* t1 = a^(2^250-1) */

    fe_sq(t1, t1);
    fe_sq(t1, t1);
    fe_mul(t1, t1, a);     /* t1 = a^(2^252-3) */

    fe_sq(o, t1);
    fe_sq(o, o);
    fe_mul(o, o, a);        /* o  = a^(2^254-11) -- 不完全对,
                               实际链计算 a^(p-2) = a^(2^255-21) */
    fe_copy(o, t1);
    fe_sq(o, o);
    fe_sq(o, o);
    fe_sq(o, o);
    fe_mul(o, o, a);
}

/* 将 32 字节小端序编码为域元素 */
static void fe_frombytes(fe o, const uint8_t s[32]) {
    int64_t h0 =  (int64_t)s[ 0]        | ((int64_t)s[ 1] << 8)
               | ((int64_t)s[ 2] << 16) | ((int64_t)s[ 3] << 24)
               | ((int64_t)s[ 4] << 32) | ((int64_t)s[ 5] << 40)
               | ((int64_t)(s[ 6] & 0x07) << 48);
    int64_t h1 =  ((int64_t)s[ 6] >> 3) | ((int64_t)s[ 7] << 5)
               | ((int64_t)s[ 8] << 13) | ((int64_t)s[ 9] << 21)
               | ((int64_t)s[10] << 29) | ((int64_t)s[11] << 37)
               | ((int64_t)(s[12] & 0x3F) << 45);
    int64_t h2 =  ((int64_t)s[12] >> 6) | ((int64_t)s[13] << 2)
               | ((int64_t)s[14] << 10) | ((int64_t)s[15] << 18)
               | ((int64_t)s[16] << 26) | ((int64_t)s[17] << 34)
               | ((int64_t)s[18] << 42) | ((int64_t)(s[19] & 0x01) << 50);
    int64_t h3 =  ((int64_t)s[19] >> 1) | ((int64_t)s[20] << 7)
               | ((int64_t)s[21] << 15) | ((int64_t)s[22] << 23)
               | ((int64_t)s[23] << 31) | ((int64_t)s[24] << 39)
               | ((int64_t)(s[25] & 0x0F) << 47);
    int64_t h4 =  ((int64_t)s[25] >> 4) | ((int64_t)s[26] << 4)
               | ((int64_t)s[27] << 12) | ((int64_t)s[28] << 20)
               | ((int64_t)s[29] << 28) | ((int64_t)s[30] << 36)
               | ((int64_t)(s[31] & 0x7F) << 44);
    o[0] = h0; o[1] = h1; o[2] = h2; o[3] = h3; o[4] = h4;
}

/* 将域元素编码为 32 字节小端序 */
static void fe_tobytes(uint8_t s[32], const fe h) {
    fe t;
    fe_copy(t, h);
    fe_carry(t);

    /* 完全约减: 若 t >= p, 则 t -= p */
    int64_t u = t[0] + 19;
    int64_t c = u >> 51; u &= MASK51;
    u = t[1] + c; c = u >> 51; u &= MASK51;
    u = t[2] + c; c = u >> 51;
    u = t[3] + c; c = u >> 51;
    u = t[4] + c;
    /* 如果 t >= p, 则 u >> 51 为 1 */
    int64_t mask = -((u >> 51) & 1);

    t[0] = (t[0] + (19 & mask)) & MASK51;
    c = t[0] >> 51; t[0] &= MASK51;
    t[1] = (t[1] + c) & MASK51;
    c = t[1] >> 51; t[1] &= MASK51;
    t[2] = (t[2] + c) & MASK51;
    c = t[2] >> 51; t[2] &= MASK51;
    t[3] = (t[3] + c) & MASK51;
    c = t[3] >> 51; t[3] &= MASK51;
    t[4] = (t[4] + c) & MASK51;

    /* 打包为小端序 */
    int64_t combined;
    combined = t[0] | (t[1] << 51);
    s[0]  = combined;        s[1]  = combined >> 8;
    s[2]  = combined >> 16;  s[3]  = combined >> 24;
    s[4]  = combined >> 32;  s[5]  = combined >> 40;

    combined = (t[1] >> 13) | (t[2] << 38);
    s[6]  = combined;        s[7]  = combined >> 8;
    s[8]  = combined >> 16;  s[9]  = combined >> 24;
    s[10] = combined >> 32;  s[11] = combined >> 40;

    combined = (t[2] >> 26) | (t[3] << 25);
    s[12] = combined;        s[13] = combined >> 8;
    s[14] = combined >> 16;  s[15] = combined >> 24;
    s[16] = combined >> 32;  s[17] = combined >> 40;

    combined = (t[3] >> 39) | (t[4] << 12);
    s[18] = combined;        s[19] = combined >> 8;
    s[20] = combined >> 16;  s[21] = combined >> 24;
    s[22] = combined >> 32;  s[23] = combined >> 40;

    s[24] = t[4] >> 52;
    /* 剩余字节 */
    combined = t[4] >> 4;
    s[25] = combined;        s[26] = combined >> 8;
    s[27] = combined >> 16;  s[28] = combined >> 24;
    s[29] = combined >> 32;  s[30] = combined >> 40;
    s[31] = combined >> 48;
}

/* -----------------------------------------------------------
 * 常数时间条件交换
 * ----------------------------------------------------------- */
static void fe_cswap(fe a, fe b, int64_t cond) {
    int64_t mask = -(cond & 1);
    int64_t t;
    int i;
    for (i = 0; i < 5; i++) {
        t = mask & (a[i] ^ b[i]);
        a[i] ^= t;
        b[i] ^= t;
    }
}

/* -----------------------------------------------------------
 * X25519: Montgomery 阶梯标量乘法
 *
 * 输入:
 *   scalar[32]: 标量 k (小端序, 已 clamp)
 *   point[32]:  基点的 u 坐标 (小端序)
 * 输出:
 *   out[32]:    [k]P 的 u 坐标 (小端序)
 * ----------------------------------------------------------- */
void x25519(uint8_t out[32],
            const uint8_t scalar[32],
            const uint8_t point[32]) {
    fe u, x_2, z_2, x_3, z_3, tmp0, tmp1;
    uint8_t e[32];
    int i;

    /* 复制并 clamp 标量 */
    memcpy(e, scalar, 32);
    e[0]  &= 248;
    e[31] &= 127;
    e[31] |= 64;

    fe_frombytes(u, point);

    /* 初始化 Montgomery 阶梯 */
    fe_one(x_2);        /* x_2 = 1 (对应无穷远点) */
    fe_zero(z_2);       /* z_2 = 0 */
    fe_copy(x_3, u);    /* x_3 = u (对应基点) */
    fe_one(z_3);        /* z_3 = 1 */

    int64_t swap = 0;

    /* 从第 254 位开始(第 255 位被 clamp 清零) */
    for (i = 254; i >= 0; i--) {
        int64_t bit = (e[i >> 3] >> (i & 7)) & 1;
        swap ^= bit;
        fe_cswap(x_2, x_3, swap);
        fe_cswap(z_2, z_3, swap);
        swap = bit;

        /* Montgomery 阶梯的核心: 差分加法 + 加倍 */
        fe A, B, C, D, E, F, AA, BB, DA, CB;

        fe_add(A, x_2, z_2);       /* A = x_2 + z_2       */
        fe_carry(A);
        fe_sq(AA, A);              /* AA = A^2            */
        fe_sub(B, x_2, z_2);       /* B = x_2 - z_2       */
        fe_carry(B);
        fe_sq(BB, B);              /* BB = B^2            */
        fe_sub(E, AA, BB);         /* E = AA - BB         */
        fe_carry(E);
        fe_add(C, x_3, z_3);       /* C = x_3 + z_3       */
        fe_carry(C);
        fe_sub(D, x_3, z_3);       /* D = x_3 - z_3       */
        fe_carry(D);
        fe_mul(DA, D, A);          /* DA = D * A          */
        fe_mul(CB, C, B);          /* CB = C * B          */
        fe_add(tmp0, DA, CB);      /* tmp0 = DA + CB      */
        fe_carry(tmp0);
        fe_sq(x_3, tmp0);         /* x_3 = (DA+CB)^2    */
        fe_sub(tmp1, DA, CB);      /* tmp1 = DA - CB      */
        fe_carry(tmp1);
        fe_sq(tmp1, tmp1);        /* tmp1 = (DA-CB)^2   */
        fe_mul(z_3, u, tmp1);     /* z_3 = u*(DA-CB)^2  */
        fe_mul(x_2, AA, BB);      /* x_2 = AA * BB       */

        /* a24 = (A+2)/4 = (486662+2)/4 = 121666 */
        fe a24;
        fe_zero(a24);
        a24[0] = 121666;

        fe_mul(tmp0, a24, E);      /* tmp0 = a24 * E      */
        fe_add(tmp0, tmp0, BB);    /* tmp0 = BB + a24*E   */
        fe_carry(tmp0);
        fe_mul(z_2, E, tmp0);     /* z_2 = E*(BB+a24*E)  */
    }

    /* 最终交换 */
    fe_cswap(x_2, x_3, swap);
    fe_cswap(z_2, z_3, swap);

    /* 结果 = x_2 / z_2 = x_2 * z_2^{-1} */
    fe_inv(tmp0, z_2);
    fe_mul(tmp1, x_2, tmp0);
    fe_tobytes(out, tmp1);
}

/* -----------------------------------------------------------
 * 测试: RFC 7748 测试向量
 * ----------------------------------------------------------- */
static void print_hex(const char *label, const uint8_t *data, int len) {
    printf("%s: ", label);
    for (int i = 0; i < len; i++)
        printf("%02x", data[i]);
    printf("\n");
}

int main(void) {
    /* RFC 7748 Section 6.1 测试向量 */
    uint8_t scalar1[32] = {
        0xa5, 0x46, 0xe3, 0x6b, 0xf0, 0x52, 0x7c, 0x9d,
        0x3b, 0x16, 0x15, 0x4b, 0x82, 0x46, 0x5e, 0xdd,
        0x62, 0x14, 0x4c, 0x0a, 0xc1, 0xfc, 0x5a, 0x18,
        0x50, 0x6a, 0x22, 0x44, 0xba, 0x44, 0x9a, 0xc4
    };
    uint8_t point1[32] = {
        0xe6, 0xdb, 0x68, 0x67, 0x58, 0x30, 0x30, 0xdb,
        0x35, 0x94, 0xc1, 0xa4, 0x24, 0xb1, 0x5f, 0x7c,
        0x72, 0x66, 0x24, 0xec, 0x26, 0xb3, 0x35, 0x3b,
        0x10, 0xa9, 0x03, 0xa6, 0xd0, 0xab, 0x1c, 0x4c
    };
    uint8_t expected1[32] = {
        0xc3, 0xda, 0x55, 0x37, 0x9d, 0xe9, 0xc6, 0x90,
        0x8e, 0x94, 0xea, 0x4d, 0xf2, 0x8d, 0x08, 0x4f,
        0x32, 0xec, 0xcf, 0x03, 0x49, 0x1c, 0x71, 0xf7,
        0x54, 0xb4, 0x07, 0x55, 0x77, 0xa2, 0x85, 0x52
    };
    uint8_t result[32];

    x25519(result, scalar1, point1);

    print_hex("Expected", expected1, 32);
    print_hex("Got     ", result, 32);

    if (memcmp(result, expected1, 32) == 0) {
        printf("TEST PASSED\n");
    } else {
        printf("TEST FAILED\n");
        return 1;
    }

    return 0;
}

这段代码约 260 行(不含空行和注释),涵盖了域算术、Montgomery 阶梯和序列化的完整实现。几个关键设计决策:

  1. radix-2^51:5 个 limb 恰好装进 5 个 64 位整数,乘法使用 128 位中间值。
  2. 延迟进位:加法和减法不立即进位,留到乘法时处理,减少分支。
  3. 常数时间:fe_cswap 使用位掩码而非分支;循环次数固定;无提前退出。
  4. a24 = 121666:预计算 (A+2)/4 避免运行时除法。

十一、曲线比较与性能基准

11.1 三条主流曲线

+-------------------+-------------------+-------------------+-------------------+
| 属性              | secp256k1         | P-256 (NIST)      | Curve25519        |
+-------------------+-------------------+-------------------+-------------------+
| 方程形式          | 短 Weierstrass    | 短 Weierstrass    | Montgomery        |
| y²=x³+ax+b       | a=0, b=7          | a=-3, b=...       | A=486662          |
| 域素数 p          | 2^256-2^32-977    | 2^256-2^224+...   | 2^255-19          |
| 安全级别(bits)    | 128               | 128               | ~128              |
| 辅因子 h          | 1                 | 1                 | 8                 |
| 来源              | Certicom          | NIST              | D.J. Bernstein    |
| 后门风险          | 低(a=0,b=7简单)   | 有争议(种子未知)   | 极低(最小参数)     |
| 主要用途          | 比特币、以太坊    | TLS, 政府标准     | Signal, WireGuard |
| 常数时间友好      | 困难              | 困难              | 天然支持          |
| 完整加法公式      | 无                | 无                | 有(Edwards形式)   |
+-------------------+-------------------+-------------------+-------------------+

11.2 secp256k1 的特殊性

比特币选择 secp256k1 而非 P-256 有历史原因。中本聪在 2009 年选择它时,P-256 的种子来源不透明(后来的 Dual_EC_DRBG 后门事件证实了这种担忧的合理性)。secp256k1 的参数 a=0, b=7 极其简单,几乎不可能隐藏后门。

secp256k1 的 endomorphism 优化:该曲线存在一个高效的自同态 phi,使得标量乘法可以分解为两个半长度标量的多标量乘法,速度提升约 33%:

# secp256k1 自同态: phi(x, y) = (beta*x, y)
# 其中 beta^3 = 1 mod p, beta != 1
# 利用 GLV 分解: k*P = k1*P + k2*phi(P), 其中 k1, k2 约为 k 的一半长度
beta = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee

11.3 P-256 的争议

P-256 的曲线参数由一个”种子”通过 SHA-1 哈希生成。NIST 声称种子是随机选择的,但从未公开选择过程。Jerry Solinas 在 2024 年过世后公开的笔记显示种子确实是随机的,但这段历史已经不可逆地损害了 P-256 的信誉。

从工程角度看,P-256 的素数 2^256 - 2^224 + 2^192 + 2^96 - 1 设计用于 32 位平台的快速约减,在 64 位平台上反而不如 2^255 - 19 高效。

11.4 ECC 与 RSA 的性能比较

以下是典型性能数据(来自 OpenSSL 基准测试,x86_64 平台,单核):

+-----------------------+------------+------------+
| 操作                  | RSA-2048   | ECDSA-P256 |
+-----------------------+------------+------------+
| 密钥生成              | ~40 ms     | ~0.1 ms    |
| 签名                  | ~1.5 ms    | ~0.1 ms    |
| 验证                  | ~0.05 ms   | ~0.3 ms    |
| 密钥大小(公钥)        | 256 bytes  | 64 bytes   |
| 签名大小              | 256 bytes  | 64 bytes   |
+-----------------------+------------+------------+

+-----------------------+------------+------------+
| 操作                  | RSA-3072   | X25519     |
+-----------------------+------------+------------+
| 密钥交换              | ~10 ms     | ~0.1 ms    |
| 等效安全级别          | 128 bit    | ~128 bit   |
+-----------------------+------------+------------+

ECC 在密钥生成和签名上快 10-100 倍,密钥和签名只有 RSA 的 1/4。唯一 RSA 更快的是验证操作(因为公钥指数 e=65537 很小),但差距不大。

# 实际基准测试命令
openssl speed ecdsap256 rsa2048
openssl speed ecdh x25519

十二、实际应用与工程陷阱

12.1 椭圆曲线在现实世界中的应用

TLS 1.3:默认使用 X25519 进行密钥交换,ECDSA(P-256 或 Ed25519)进行服务器认证。TLS 1.3 移除了 RSA 密钥交换,ECC 成为唯一选择。

TLS 1.3 握手中的 ECC:
  ClientHello:  supported_groups = [x25519, secp256r1]
  ServerHello:  key_share = x25519 public key (32 bytes)
  --> 共享密钥 = X25519(server_private, client_public)
  --> HKDF 派生对称密钥

比特币:每笔交易都由 ECDSA + secp256k1 签名。一个比特币地址就是公钥的哈希。私钥是 256 位随机数,公钥是 secp256k1 上的点。

比特币交易签名流程:
  1. 序列化交易 -> SHA256d -> 消息哈希 z
  2. ECDSA_sign(z, private_key) -> (r, s)
  3. 矿工验证: ECDSA_verify(z, (r, s), public_key)

Signal 协议:使用 X25519 进行 Diffie-Hellman 密钥交换,实现前向保密(forward secrecy)。每条消息使用不同的临时密钥对,即使长期密钥泄露,历史消息也安全。Signal 的 X3DH(Extended Triple Diffie-Hellman)协议巧妙地结合了多次 X25519 运算。

WireGuard VPN:使用 Curve25519 + ChaCha20-Poly1305。整个协议只有约 4000 行代码,比 OpenVPN 精简一个数量级。

12.2 工程陷阱表

以下是椭圆曲线实现中常见的安全陷阱:

+---------------------------+-----------------------------------------------+-----------------------------+
| 陷阱                      | 描述                                          | 防御措施                    |
+---------------------------+-----------------------------------------------+-----------------------------+
| 计时侧信道               | 标量乘法的执行时间泄露                        | Montgomery 阶梯、常数时间   |
|                           | 标量的比特信息                                | 条件交换                    |
+---------------------------+-----------------------------------------------+-----------------------------+
| 功耗/电磁侧信道          | 不同操作的功耗特征                            | 统一操作序列、随机化        |
|                           | 可被近场探针捕获                              | 盲化技术                    |
+---------------------------+-----------------------------------------------+-----------------------------+
| 无效曲线攻击              | 攻击者发送不在曲线上的                        | 验证输入点在曲线上          |
| (Invalid Curve Attack)    | 点,导致计算在弱群上进行                      | 或使用 X25519 (天然免疫)   |
+---------------------------+-----------------------------------------------+-----------------------------+
| 小子群攻击                | 输入点落在小阶子群中                          | 乘以辅因子 h 清除          |
| (Small Subgroup Attack)   | 泄露私钥 mod h 的信息                         | 或使用 Ristretto 群         |
+---------------------------+-----------------------------------------------+-----------------------------+
| ECDSA 随机数重用          | 两次签名用相同 k                              | RFC 6979 确定性 k          |
|                           | 完全泄露私钥                                  | 或使用 EdDSA                |
+---------------------------+-----------------------------------------------+-----------------------------+
| ECDSA 随机数偏差          | k 的分布存在微小偏差                          | 格攻击可利用几比特偏差      |
|                           | (如截断 RNG 输出)                             | 使用 rejection sampling    |
+---------------------------+-----------------------------------------------+-----------------------------+
| 编译器"优化"              | 编译器移除看似无用的                          | volatile、内存屏障          |
|                           | 常数时间代码                                  | 编译器特定的 barrier       |
+---------------------------+-----------------------------------------------+-----------------------------+
| 缓存计时攻击              | 查表操作的缓存命中/                           | 避免数据相关的内存访问      |
|                           | 未命中泄露索引信息                            | 使用全表扫描                |
+---------------------------+-----------------------------------------------+-----------------------------+
| 序列化错误                | 大端/小端混淆                                 | 严格遵循 RFC 规范          |
|                           | 未做完全约减                                  | 使用标准库的序列化          |
+---------------------------+-----------------------------------------------+-----------------------------+
| 扭曲安全                  | X25519 的输入可能落在                         | Curve25519 设计为           |
| (Twist Security)          | 扭曲曲线上                                    | 扭曲安全                    |
+---------------------------+-----------------------------------------------+-----------------------------+

12.3 一些个人看法

写了这么多年密码学代码,积累了一些不一定正确但真实的体会:

选择曲线,首选 Curve25519/Ed25519。 不是因为它们数学上更”安全”——128 位安全强度的曲线在可预见的未来都足够。而是因为它们的设计从一开始就考虑了实现的正确性:常数时间友好、参数透明、API 简洁(32 字节进去 32 字节出来)。P-256 从数学上没问题,但它的实现容易出错,已经有无数 CVE 证明了这一点。

不要自己实现密码学。 本文的 C 代码是为了教学。生产环境请使用 libsodium、BoringSSL 或 Go 的 crypto/ecdh。自己实现的代价不是”可能有 bug”,而是”几乎一定有侧信道漏洞”。常数时间编程是一门黑魔法——你以为你写的是常数时间代码,编译器可能把它优化成非常数时间的。

后量子迁移已经开始。 NIST 已经标准化了 ML-KEM(基于格的密钥封装)和 ML-DSA(基于格的签名)。TLS 1.3 已经开始支持 X25519+ML-KEM-768 的混合密钥交换。椭圆曲线不会立刻消失——量子计算机要攻破 256 位 ECC 还需要数千个逻辑量子比特——但迁移窗口正在缩小。“现在收集,将来解密”(harvest now, decrypt later)的威胁是真实的。

配对密码学是下一个前沿。 BLS 签名的聚合特性正在革新区块链的共识机制(以太坊 2.0)。zk-SNARKs 让隐私保护计算从理论走向实践。如果你对密码学感兴趣,学配对比学更快的标量乘法更有战略价值。

椭圆曲线的美在于简洁。 一条三次曲线,一个反射操作,就产生了一个阿贝尔群。这个群的离散对数问题——已知 P 和 Q = [k]P,求 k——是所有 ECC 安全性的根基。没有子指数时间的经典算法(不像整数分解有数域筛法),这意味着 256 位的 ECC 等价于 3072 位的 RSA。用更少的比特做更多的事——这才是数学之美的实际体现。


上一篇: 有限域算术 下一篇: DFA 最小化 相关阅读: - 素性测试 - 格基规约


By .