用Python实战SM2协同签名:从数学公式到可运行代码的完整指南
在密码学领域,SM2作为我国自主研发的椭圆曲线公钥密码算法标准,其协同签名机制因能实现多方安全计算而备受关注。但当你翻阅理论文档时,是否曾被那些抽象的数学符号和冗长的参数计算步骤劝退?本文将带你用Python一步步拆解SM2协同签名的每个环节,把晦涩的公式转化为可执行的代码。
1. 环境准备与基础工具
实现SM2协同签名需要几个核心组件:椭圆曲线运算库、大整数处理工具和哈希函数。我们选择gmssl库作为基础,它原生支持SM2算法曲线参数。
首先安装必要依赖:
pip install gmssl pycryptodome初始化SM2椭圆曲线参数(基于GMT 0003.5-2012标准):
from gmssl import sm2, func import secrets # 标准SM2曲线参数 SM2_p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF SM2_a = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC SM2_b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93 SM2_n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123 SM2_Gx = 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7 SM2_Gy = 0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0注意:实际开发中应使用标准库提供的曲线参数,此处仅为演示原理。
gmssl内部已内置这些参数。
2. 密钥生成与参数交换
协同签名的核心在于客户端和服务端各自持有部分私钥,通过安全交互完成签名。我们先实现密钥对生成:
def generate_key_pair(): """生成SM2密钥对""" private_key = secrets.randbelow(SM2_n) public_key = sm2.base_point_mult(private_key) return private_key, public_key # 客户端密钥 d1, P1 = generate_key_pair() # 服务端密钥 d2, P2 = generate_key_pair()公共公钥P的计算需要双方交换公钥:
def compute_shared_public_key(P1, P2): """计算协同签名公共公钥 P = (d1*d2-1)*G """ # 等效于 P = (P1 * d2) - G 或 (P2 * d1) - G temp_point = sm2.point_mult(d1, P2) # P1 = d1*G, 所以 P1*d2 = d1*d2*G shared_pub = sm2.point_sub(temp_point, (SM2_Gx, SM2_Gy)) return shared_pub P = compute_shared_public_key(P1, P2)3. 签名过程分步实现
3.1 客户端第一阶段计算
客户端生成随机数K1并计算相关参数:
def client_phase1(d1, P2): K1 = secrets.randbelow(SM2_n) R1 = sm2.base_point_mult(K1) # R1 = K1*G R1_ = sm2.point_mult(K1, P2) # R1_ = K1*P2 return K1, R1, R1_ K1, R1, R1_ = client_phase1(d1, P2)服务端收到后需验证R1_ == sm2.point_mult(d2, R1),因为:
R1_ = K1*P2 = K1*d2*G d2*R1 = d2*K1*G = K1*d2*G3.2 服务端响应计算
服务端生成K2并计算响应参数:
def server_phase(d2, P1, R1, R1_): # 验证客户端参数 if not sm2.point_equal(R1_, sm2.point_mult(d2, R1)): raise ValueError("Client parameter verification failed") K2 = secrets.randbelow(SM2_n) R2_ = sm2.base_point_mult(K2) # R2_ = K2*G R2 = sm2.point_mult(K2, P1) # R2 = K2*P1 return K2, R2_, R2 K2, R2_, R2 = server_phase(d2, P1, R1, R1_)3.3 客户端签名计算
客户端验证服务端参数并计算部分签名:
def client_sign(m, d1, K1, K2, R2_, R2): # 验证服务端参数 if not sm2.point_equal(R2, sm2.point_mult(d1, R2_)): raise ValueError("Server parameter verification failed") # 计算签名随机数K K = (K1 + K2 * d1) % SM2_n # 计算r = (H(M) + x1) mod n x1, _ = sm2.base_point_mult(K) e = int.from_bytes(func.sm3_hash(m.encode()), 'big') r = (e + x1) % SM2_n # 计算s_ = (K1 + r) / d1 mod n s_ = (K1 + r) * pow(d1, -1, SM2_n) % SM2_n return r, s_ message = "plaintext" r, s_ = client_sign(message, d1, K1, K2, R2_, R2)4. 服务端完成签名
服务端收到s_后计算最终签名分量:
def server_complete_sign(d2, K2, s_, r): # 计算 t = (s_ + K2) / d2 mod n t = (s_ + K2) * pow(d2, -1, SM2_n) % SM2_n # 客户端最终计算 s = (t - r) mod n s = (t - r) % SM2_n return t, s t, s = server_complete_sign(d2, K2, s_, r)现在我们已经得到完整签名(r, s)。验证签名的正确性:
def verify_signature(P, message, signature): sm2_crypt = sm2.CryptSM2(private_key="", public_key=P) return sm2_crypt.verify(signature, message) signature = (r, s) print("Signature valid:", verify_signature(P, message, signature))5. 关键问题与调试技巧
在实际实现过程中,可能会遇到以下典型问题:
模逆运算失败
SM2签名涉及多个模逆计算(如pow(d1, -1, SM2_n))。当d1与n不互质时,会抛出异常。解决方案:
def safe_inv(a, n): try: return pow(a, -1, n) except ValueError: # 极低概率事件,可重新生成密钥 raise ValueError("No modular inverse exists, regenerate keys")参数验证失败
协同签名过程中有两次关键验证:
- 服务端验证
R1_ == d2*R1 - 客户端验证
R2 == d1*R2_
验证失败通常意味着:
- 随机数生成不符合规范
- 参数在传输过程中被篡改
- 密钥对生成逻辑有误
签名验证不通过
如果最终签名验证失败,可按以下步骤排查:
| 检查项 | 验证方法 | 可能原因 |
|---|---|---|
| 公共公钥P | 比较P1d2-G与P2d1-G | 密钥交换错误 |
| 随机数K | 检查K=K1+K2*d1 mod n | 参数传递错误 |
| 哈希值e | 对比标准SM3实现 | 消息编码不一致 |
6. 性能优化实践
当需要高频执行协同签名时,可考虑以下优化手段:
预计算加速
椭圆曲线点乘是最耗时的操作,对固定参数可预先计算:
# 客户端预计算d1的逆元 d1_inv = pow(d1, -1, SM2_n) # 服务端预计算d2的逆元 d2_inv = pow(d2, -1, SM2_n)并行计算
客户端和服务端的部分计算可并行执行:
from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor() as executor: # 客户端并行计算R1和R1_ future_R1 = executor.submit(sm2.base_point_mult, K1) future_R1_ = executor.submit(sm2.point_mult, K1, P2) R1, R1_ = future_R1.result(), future_R1_.result()缓存机制
对于长期合作的客户端-服务端对,可缓存部分中间结果减少重复计算。