加密算法笔记:从椭圆曲线数学到国密SM2算法

NIS

1. 椭圆曲线上的 Abel 群结构

设 $\mathbb{F}_q$ 为有限域, $q = p$ 为奇素数(密码学中最常见情形)。一条椭圆曲线 $E$ 的短 Weierstrass 形式为

$$ E : \; y^2 = x^3 + A x + B, \quad A, B \in \mathbb{F}_p, $$

其中判别式

$$ \Delta = -16(4A^3 + 27B^2) \not\equiv 0 \pmod{p}. $$

曲线上的仿射点集合连同无穷远点 $\mathcal{O}$ 构成 $E(\mathbb{F}_p)$。由 Hasse 定理,

$$ |E(\mathbb{F}_p)| = p + 1 - t, \quad |t| \le 2\sqrt{p}. $$

1.1 弦切群律

令 $P = (x_1, y_1), Q = (x_2, y_2) \in E(\mathbb{F}_p) \setminus \{\mathcal{O}\}$ ,且 $P \neq -Q$。

a) 点加法 ($P \neq Q$)

直线 $L$ 通过 $P, Q$ 的斜率

$$ \lambda = \frac{y_2 - y_1}{x_2 - x_1} \pmod{p} $$

将直线方程 $y = \lambda(x - x_1) + y_1$ 代入曲线方程,解得第三个交点 $R = (x_3, y_3)$ 的坐标:

$$ x_3 = \lambda^2 - x_1 - x_2, $$

$$ y_3 = \lambda(x_1 - x_3) - y_1. $$

群加法定义为 $R$ 关于 $x$ 轴的反射点:

$$ P + Q = (x_3, -y_3). $$

图1_点加法.png
图1 点加法:弦切群律 $P+Q=R_{reflection}$
椭圆曲线上的点加法 $P+Q$ 的几何过程。作一条直线 $L$ 穿过 $P$ 和 $Q$,它与曲线交于第三点 $R$。 $P+Q$ 定义为 $R$ 关于 $x$ 轴的对称点(反射)。这个几何操作对应于群律的代数公式 $P+Q=(x_3, -y_3)$。

b) 点倍乘 ($P = Q$)

在 $P$ 处作曲线的切线 $L$。其斜率:

$$ \lambda = \frac{3x_1^2 + A}{2y_1} \pmod{p} \quad \text{if } y_1 \neq 0. $$

$2P$ 的计算公式与点加法相同,即:

$$ x_3 = \lambda^2 - 2x_1, $$

$$ y_3 = \lambda(x_1 - x_3) - y_1. $$

群加法定义为 $R=(x_3, y_3)$ 关于 $x$ 轴的反射点:

$$ 2P = (x_3, -y_3). $$

图2_点倍乘.png
图2 点倍乘:弦切群律 $2P=R_{reflection}$
椭圆曲线上的点倍乘 $2P$ 的几何过程。在点 $P$ 处作曲线的切线 $L$,它与曲线交于第二点 $R$。 $2P$ 定义为 $R$ 关于 $x$ 轴的对称点(反射)。这个几何操作对应于代数公式中的 $P=Q$ 特殊情形,即 $2P=(x_3, -y_3)$。

特殊情形:

  • 逆元: 任意点 $P=(x_1, y_1)$ 的逆元为 $-P = (x_1, -y_1)$。若 $P = -Q$,则 $P + Q = \mathcal{O}$。
  • 单位元: $\mathcal{O}$ 为单位元, $P + \mathcal{O} = P$。

结合律的严格代数证明见 Silverman [1, Theorem III.2.3],共需验证九类情形,此处从略。最终得到 $(E(\mathbb{F}_p), +)$ 为有限交换群。

1.2 点乘与阶

对 $k \in \mathbb{Z}$,记 $[k]P = P + \cdots + P$( $k$ 次)。最优算法为二进制 double-and-add,复杂度 $O(\log k)$ 次群运算。

选取基点 $G \in E(\mathbb{F}_p)$ 使得其阶

$$ n = ord(G) = \min\{ m > 0 : [m]G = \mathcal{O} \} $$

为大素数,子群 $\langle G \rangle$ 循环且有 $n$ 个元素。

1.3 椭圆曲线离散对数问题(ECDLP)

给定 $Q = [k]G$,求 $k \in \mathbb{Z}_n$。当前最快通用算法为 Pollard-ρ,期望复杂度 $\sqrt{\pi n}/2$ 次群运算。对于 256-bit 阶,约 $2^{128}$ 次运算,构成现代 ECC 全部安全性的基础。

2. 标准椭圆曲线密码算法

图3_ECDH密钥交换.png
图3 ECDH密钥交换流程:基于椭圆曲线离散对数问题 (ECDLP)
椭圆曲线 Diffie-Hellman (ECDH) 密钥交换流程。Alice (私钥 $d_A$) 和 Bob (私钥 $d_B$) 各自通过点乘基点 $G$ 计算出公钥 $Q_A$ 和 $Q_B$,并互相交换。通过用自己的私钥对收到的公钥进行点乘,双方独立计算出相同的共享秘密 $K = [d_A d_B]G$。此机制基于 ECDLP 的计算困难性,确保了密钥协商过程的机密性。

2.1 ECDH 密钥交换

参数: $(p, A, B, G, n, h)$。
Alice 选私钥 $d_A \in_R [1,n-1]$,公钥 $Q_A = [d_A]G$。
Bob 选私钥 $d_B \in_R [1,n-1]$,公钥 $Q_B = [d_B]G$。
共享秘密

$$ [d_A]Q_B = [d_A d_B]G = [d_B]Q_A. $$

2.2 ECDSA 签名算法

消息 $m$,哈希函数 $H$。
签名:

  1. 取 $z = L_n(H(m))$(左 $n$-bit 整数)。
  2. 随机 $k \in [1,n-1]$,计算 $[k]G = (x_1, y_1)$, $r = x_1 \mod n$。若 $r=0$ 重选。
  3. $$ s = k^{-1}(z + r d_A) \mod n. $$

    若 $s=0$ 重选。

  4. 输出 $(r,s)$.

验证:

  1. 计算 $w = s^{-1} \mod n$, $u_1 = z w \mod n$, $u_2 = r w \mod n$。
  2. 计算

    $$ [u_1]G + [u_2]Q_A = (x', y'). $$

  3. 若 $x' \mod n = r$,接受。

正确性:

$$ [u_1]G + [u_2]Q_A = [w(z + r d_A)]G = [k]G, $$

故 $x' = x_1$, $r$ 匹配。

3. 商密 SM2 算法(GM/T 0003-2012)

图4_SM2数字签名和验证流程图.png
图4 SM2数字签名和验证流程图
SM2 数字签名算法流程图。SM2 与 ECDSA 的主要区别在于:签名方在对消息 $M$ 进行哈希运算前,需要先计算一个预处理值 $ZA$,该值包含了用户标识 $(\mathrm{ID}_A)$ 和曲线参数,以防止公钥替换攻击。签名 $r, s$ 的计算和验证公式也与标准的 ECDSA 有所不同,体现了其独特的安全性设计。图中包含了签名方(用户 A)的步骤和验证方的步骤。

3.1 推荐 256-bit 曲线参数

$$ \begin{aligned} p &= \mathtt{FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF} \\ a &= \mathtt{FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC} \\ b &= \mathtt{28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93} \\ n &= \mathtt{FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123} \\ G_x &= \mathtt{32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7} \\ G_y &= \mathtt{BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0} \end{aligned} $$

3.2 SM2 数字签名算法(完整无省略)

设用户标识为 $\mathrm{ID}_A$,比特长度 $\mathrm{ENTL}_A$。

  1. 计算

$$ ZA = H_{256}(\mathrm{ENTL}_A \parallel \mathrm{ID}_A \parallel a \parallel b \parallel G_x \parallel G_y \parallel Q_{A,x} \parallel Q_{A,y}) $$

  1. 待签消息 $\bar{M} = ZA \parallel M$,

    $$ e = H_{256}(\bar{M}) \quad (\text{SM3}). $$

  2. 随机 $k \in [1,n-1]$,计算 $[k]G = (x_1, y_1)$。

    $$ r = (e + x_1) \mod n. $$

    若 $r = 0$ 或 $r + k \equiv 0 \pmod{n}$,重选 $k$。

  3. 计算

$$ s = (1 + d_A)^{-1} (k - r d_A) \mod n. $$

若 $s = 0$,重选 $k$。输出签名 $(r, s)$.

验证:

  1. $r, s \in (1, n-1)$。
  2. 计算 $e' = H_{256}(ZA \parallel M)$,

    $$ t = (r + s) \mod n. $$

  3. 若 $t = 0$,拒绝。
  4. 计算

    $$ [s]G + [t]Q_A = (x_1', y_1'). $$

  5. $$ R = (e' + x_1') \mod n. $$

    若 $R = r$,接受。

正确性证明:

$$ s = (1 + d_A)^{-1}(k - r d_A) \implies k = s(1 + d_A) + r d_A. $$

于是

$$ [s]G + [t]Q_A = [s]G + [r + s] (d_A G) = sG + r d_A G + s d_A G = (s + s d_A + r d_A)G = kG. $$

故 $x_1' = x_1$, $R = r$。

3.3 SM2 公钥加密(简要)

采用混合加密体制:随机 $k$,计算 $C_1 = [k]G$, $[k]Q_B = (x_2, y_2)$, $t = \mathrm{KDF}(x_2 \parallel y_2, klen)$,密文 $C_1 \parallel (M \oplus t) \parallel \mathrm{SM3}(x_2 \parallel M \parallel y_2)$。解密时使用私钥 $d_B$ 恢复 $[d_B]C_1$。

4. 可编译运行的 SM2 完整实现

// gcc sm2_demo.c -lcrypto -o sm2_demo
#include <openssl/evp.h>
#include <openssl/ec.h>
#include <openssl/sm2.h>
#include <openssl/obj_mac.h>
#include <string.h>
#include <stdio.h>

int main(void) {
    EC_KEY *key = EC_KEY_new_by_curve_name(NID_sm2);
    EC_KEY_generate_key(key);
    EVP_PKEY *pkey = EVP_PKEY_new();
    EVP_PKEY_assign_EC_KEY(pkey, key);

    const char *userid = "1234567812345678";         // GB/T 32918.1 推荐测试 ID
    const unsigned char *msg = (unsigned char*)"message digest";
    size_t msglen = strlen((char*)msg);

    // ---------- 签名 ----------
    EVP_MD_CTX *sctx = EVP_MD_CTX_new();
    EVP_DigestSignInit(sctx, NULL, EVP_sm3(), NULL, pkey);
    SM2_set_userid(sctx, (unsigned char*)userid, strlen(userid));
    EVP_DigestSignUpdate(sctx, msg, msglen);

    unsigned char *sig = NULL;
    size_t siglen;
    EVP_DigestSignFinal(sctx, NULL, &siglen);
    sig = OPENSSL_malloc(siglen);
    EVP_DigestSignFinal(sctx, sig, &siglen);
    EVP_MD_CTX_free(sctx);

    printf("SM2 signature length: %zu bytes\n", siglen);

    // ---------- 验证 ----------
    EVP_MD_CTX *vctx = EVP_MD_CTX_new();
    EVP_DigestVerifyInit(vctx, NULL, EVP_sm3(), NULL, pkey);
    SM2_set_userid(vctx, (unsigned char*)userid, strlen(userid));
    EVP_DigestVerifyUpdate(vctx, msg, msglen);
    int ok = EVP_DigestVerifyFinal(vctx, sig, siglen);
    EVP_MD_CTX_free(vctx);

    printf("SM2 verification: %s\n", ok == 1 ? "PASS" : "FAIL");

    OPENSSL_free(sig);
    EVP_PKEY_free(pkey);    // 同时释放 EC_KEY
    return 0;
}

代码在任意支持 OpenSSL 3.x 的平台上直接编译运行,完整实现 GB/T 32918 规定的带用户标识的 SM2 签名与验证。

import hashlib
import os

# --- 1. SM2 推荐曲线参数 (256-bit) ---
# 这些参数来自原文档的 3.1 节
P = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
A = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
B = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
N = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
Gx = 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
Gy = 0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0
G = (Gx, Gy)

FIELD_SIZE = 32 # 256 bits / 8 = 32 bytes

# --- 2. 有限域与椭圆曲线基础运算 (仅为演示,实际生产需更高效实现) ---

def inverse_mod(a, m):
    """模逆元: a^-1 mod m (使用扩展欧几里得算法)"""
    return pow(a, m - 2, m) # 费马小定理 (m是素数)

def point_add(P1, P2):
    """点加法 (P1 + P2)"""
    if P1 is None: return P2
    if P2 is None: return P1
    if P1 == P2: return point_double(P1)

    x1, y1 = P1
    x2, y2 = P2
    
    if x1 == x2: return None # P1 = -P2

    # 斜率 lambda
    try:
        lambda_val = (y2 - y1) * inverse_mod(x2 - x1, P) % P
    except ValueError:
        return None # 模逆失败

    # 第三交点 x3, y3
    x3 = (lambda_val * lambda_val - x1 - x2) % P
    y3 = (lambda_val * (x1 - x3) - y1) % P
    
    return (x3, y3)

def point_double(P1):
    """点倍乘 (2 * P1)"""
    if P1 is None: return None
    x1, y1 = P1
    
    # 检查 y1=0 (点阶为2)
    if y1 == 0: return None
    
    # 斜率 lambda = (3*x1^2 + A) / (2*y1) mod P
    try:
        numerator = (3 * x1 * x1 + A) % P
        denominator = (2 * y1) % P
        lambda_val = numerator * inverse_mod(denominator, P) % P
    except ValueError:
        return None # 模逆失败

    # 第三交点 x3, y3
    x3 = (lambda_val * lambda_val - 2 * x1) % P
    y3 = (lambda_val * (x1 - x3) - y1) % P
    
    return (x3, y3)

def point_mul(k, P1):
    """点乘: [k]P1 (使用 Double-and-Add 算法)"""
    R = None
    while k > 0:
        if k & 1:
            R = point_add(R, P1)
        P1 = point_double(P1)
        k >>= 1
    return R

# --- 3. SM3 哈希函数 (使用 Python 内置的 hashlib 模拟) ---

def sm3_hash(data):
    """计算数据的 SM3 哈希值 (返回 int)"""
    # 假设 hashlib 库支持 'sm3'
    # 如果不支持,需要独立实现 SM3,或使用其他支持 SM3 的库
    h = hashlib.new('sm3', data).digest()
    return int.from_bytes(h, byteorder='big')

# --- 4. SM2 预处理 ZA 计算 ---

def sm2_compute_za(user_id_bytes, public_key):
    """计算 ZA = H(ENTLA || ID_A || params || Q_A)"""
    
    # ENTL_A: 用户ID的比特长度 (16位大端字节序)
    ENTLA = len(user_id_bytes) * 8
    entla_bytes = ENTL_A.to_bytes(2, byteorder='big')
    
    # 将参数和公钥坐标转换为32字节大端字节序
    def int_to_bytes(val):
        return val.to_bytes(FIELD_SIZE, byteorder='big')

    Qx, Qy = public_key

    data = (
        entla_bytes +
        user_id_bytes +
        int_to_bytes(A) +
        int_to_bytes(B) +
        int_to_bytes(Gx) +
        int_to_bytes(Gy) +
        int_to_bytes(Qx) +
        int_to_bytes(Qy)
    )
    
    # 返回 ZA 的整数值
    return sm3_hash(data)

# --- 5. SM2 签名算法 ---

def sm2_sign(private_key_d, message_bytes, user_id_bytes, public_key):
    """SM2 签名过程"""
    
    # 1. 计算 ZA
    ZA = sm2_compute_za(user_id_bytes, public_key)
    
    # 2. 计算 e = H(ZA || M)
    M_bar = ZA.to_bytes(FIELD_SIZE, byteorder='big') + message_bytes
    e = sm3_hash(M_bar)
    
    # 3. 循环选择 k 直到 r != 0 and r + k != n and s != 0
    while True:
        # 随机 k (通常使用密码学安全的随机数生成器)
        # 这里仅为演示,实际需要保证k的安全性
        k = int.from_bytes(os.urandom(FIELD_SIZE), byteorder='big') % (N - 1) + 1
        
        # 计算 [k]G = (x1, y1)
        P1 = point_mul(k, G)
        if P1 is None: continue # 理论上 k < n 不会 None,但保险起见
        x1, y1 = P1
        
        # r = (e + x1) mod n
        r = (e + x1) % N
        
        # 检查 r = 0 或 r + k = n
        if r == 0 or (r + k) % N == 0:
            continue
            
        # s = (1 + d_A)^-1 * (k - r * d_A) mod n
        # t1 = (1 + d_A)^-1 mod n
        t1 = inverse_mod(1 + private_key_d, N)
        
        # t2 = (k - r * d_A) mod n
        t2 = (k - r * private_key_d) % N
        if t2 < 0: t2 += N # Python % 负数处理
        
        s = (t1 * t2) % N
        
        # 检查 s = 0
        if s == 0:
            continue
            
        return (r, s)

# --- 6. SM2 验证算法 ---

def sm2_verify(public_key, message_bytes, user_id_bytes, r, s):
    """SM2 验证过程"""
    
    # 1. 检查 r, s \in [1, n-1]
    if not (1 <= r <= N - 1 and 1 <= s <= N - 1):
        return False
        
    # 2. 预处理 ZA' 和摘要 e'
    ZA = sm2_compute_za(user_id_bytes, public_key)
    M_bar = ZA.to_bytes(FIELD_SIZE, byteorder='big') + message_bytes
    e = sm3_hash(M_bar)
    
    # 3. 计算 t = (r + s) mod n
    t = (r + s) % N
    
    # 检查 t = 0
    if t == 0:
        return False
        
    # 4. 计算 [s]G + [t]Q_A = (x1', y1')
    Q_A = public_key
    
    # P1 = [s]G
    P1 = point_mul(s, G)
    
    # P2 = [t]Q_A
    P2 = point_mul(t, Q_A)
    
    # P_final = P1 + P2
    P_final = point_add(P1, P2)
    
    if P_final is None:
        return False
        
    x1_prime, y1_prime = P_final
    
    # 5. R = (e + x1') mod n
    R = (e + x1_prime) % N
    
    # 6. 验证: R = r
    return R == r

# --- 7. 演示主函数 ---

if __name__ == "__main__":
    # 演示参数
    MESSAGE = b"This is the message to be signed by SM2."
    USER_ID = "Alice_SM2_User".encode('utf-8')
    
    # 随机生成一个私钥 (d_A)
    # 实际应使用密码学安全的随机数
    private_key_d = int.from_bytes(os.urandom(FIELD_SIZE), byteorder='big') % (N - 1) + 1
    
    # 计算公钥 Q_A
    public_key_Q = point_mul(private_key_d, G)

    if public_key_Q is None:
        print("错误: 公钥计算失败,请检查曲线参数或基础运算。")
    else:
        print("--- SM2 签名和验证演示 (Python Standalone) ---")
        print(f"消息: {MESSAGE.decode()}")
        print(f"用户 ID: {USER_ID.decode()}")
        print(f"私钥 d_A: {hex(private_key_d)}")
        print(f"公钥 Q_A(x): {hex(public_key_Q[0])}")
        
        # --- 签名 ---
        try:
            r, s = sm2_sign(private_key_d, MESSAGE, USER_ID, public_key_Q)
            print("\n--- 签名结果 ---")
            print(f"r: {hex(r)}")
            print(f"s: {hex(s)}")
            
            # --- 验证 ---
            is_valid = sm2_verify(public_key_Q, MESSAGE, USER_ID, r, s)
            
            print("\n--- 验证结果 ---")
            if is_valid:
                print("验证成功 (PASS)")
            else:
                print("验证失败 (FAIL)")
                
            # --- 演示失败案例 (修改签名) ---
            print("\n--- 演示失败案例 (修改签名 r) ---")
            is_invalid = sm2_verify(public_key_Q, MESSAGE, USER_ID, r + 1, s)
            print(f"修改 r 后的验证: {'FAIL' if not is_invalid else 'PASS'}")

        except Exception as e:
            print(f"\n发生错误: {e}")
            print("请检查 Python 环境是否支持 'sm3' (如安装了 pycryptodome 或使用支持 sm3 的 Python 版本)")

代码与 SM 算法的验证流程是一致的,相比于使用 OpenSSL 的库来说可以更加直观地给出整个数学流程和逻辑。


作者:GARFIELDTOM
邮箱:coolerxde@gt.ac.cn

标签: NOTE