本篇文章介绍如何一步步实现国密 SM2 签名算法,用于学习目的,目前是第 1 部分。为了简化教程,本文不涉及密钥生成和验签算法,会简单描述 SM2 签名算法的流程,所以我们需要使用 GmSSL 库(https://github.com/guanzhi/GmSSL)来帮助我们生成密钥和测试签名的正确性。
下载源码
$ git clone https://github.com/guanzhi/GmSSL.git
$ cd GmSSL
编译安装
$ mkdir build
$ cd build
$ cmake ..
$ make
$ make test
$ sudo make install
SM2签名及验签
$ gmssl sm2keygen -pass 1234 -out sm2.pem -pubout sm2pub.pem
$ echo hello | gmssl sm2sign -key sm2.pem -pass 1234 -out sm2.sig -id 1234567812345678
$ echo hello | gmssl sm2verify -pubkey sm2pub.pem -sig sm2.sig -id 1234567812345678
由于 GmSSL 库强制对私钥进行了加密,为了让教程更加清晰,我不会在 Python 中对私钥进行解密,所以我们修改一下 GmSSL/tools/sm2sign.c
的源码,因为 gmssl sm2sign
命令第一步就是解密私钥(这里涉及到一个SM2的原理:通过私钥可以计算出公钥):
// 1. 解密私钥
if (sm2_private_key_info_decrypt_from_pem(&key, pass, keyfp) != 1) {
fprintf(stderr, "gmssl %s: private key decryption failure\n", prog);
goto end;
}
我们在它的后面加上打印密钥的逻辑:
...
// 打印解密后的密钥
if (sm2_key_print(stdout, 0, 0, "Decrypted Key", &key) != 1) {
fprintf(stderr, "gmssl %s: failed to print key\n", prog);
goto end;
}
重新运行 echo hello | gmssl sm2sign -key sm2.pem -pass 1234 -out sm2.sig -id 1234567812345678
,我们可以得到类似如下输出:
Decrypted Key
publicKey: CB06F5F57DA022626BA8D5C176CF4078B420E581C5A09F18CD79CCD3260D5AD1019B4186FC66CA86AE123181D3044AE070CF5D12C6001257829ECB29579C382E
privateKey: d14b13327d0b6402267ae053131d519c072f82f90dfe4ecf2897d9d7617040e2
接下来我们将写一个 Python 程序来生成 sm2.sig
,我们定义一下 SM2 签名的调用方式:
from gmssl2 import sm2
# 上一步获取的公钥和私钥,16进制
public_key = 'CB06F5F57DA022626BA8D5C176CF4078B420E581C5A09F18CD79CCD3260D5AD1019B4186FC66CA86AE123181D3044AE070CF5D12C6001257829ECB29579C382E'
private_key = 'D14B13327D0B6402267AE053131D519C072F82F90DFE4ECF2897D9D7617040E2'
# 使用公钥和私钥初始化 SM 类
sm2_obj = sm2.SM2(
public_key=public_key,
private_key=private_key
)
# 待签名消息
data = b"hello\n" # bytes类型
sign = sm2_obj.sign(data)
# 将16进制字符串转换为字节串
sign_bytes = bytes.fromhex(sign)
# 将签名写入文件
with open('sm2.sig', 'wb') as outfp:
outfp.write(sign_bytes)
这里有一个坑需要注意一下,echo
命令会在消息最后追加一个 \n
,所以我们在 Python 的时候也需要追加这个 \n
。
我们在 http://www.gmbz.org.cn/main/bzlb.html 找到《SM2 椭圆曲线公钥密码算法第2部分:数字签名算法》,跳到 6.1 章节:
所以第一步我们需要计算这个 Z_A 值,定义在 5.5 章节:
||
的意思是字符串拼接。为了区分于官方的 GmSSL,命名为 gmssl2,因此我们写出如下代码:
from abc import ABC
# 这段参数是从 GmSSL/src/sm2_z256.c 的 SM2 parameters 部分获取
ECC_PARAMETERS = {
'n': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',
'p': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF',
'g': '32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7'
'bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0',
'a': 'fffffffeffffffffffffffffffffffffffffffff00000000fffffffffffffffc',
'b': '28e9fa9e9d9f5e344d5a9e4bcf6509a7f39789f515ab8f92ddbcbd414d940e93',
}
class SM2(ABC):
def __init__(self, public_key, private_key):
self.public_key = public_key
self.private_key = private_key
def sign(self, data, user_id="1234567812345678"):
z = self.get_z(data, user_id)
print(f"拼接字符串为: {z}")
def get_z(self, data, user_id):
# A, B: 使用公钥密码系统两个用户
# ENTL_A: ID 的比特长度,1 个 ASCII 字符为 1 字节( 8 比特)
ENTL_A = '%04x' % (len(user_id) * 8)
# ID_A: 用户 A 的可辨别标识
ID_A = user_id.encode().hex()
# F_q: 包含 q 个元素的有限域
# a: F_q 中的元素,椭圆曲线的参数之一
a = ECC_PARAMETERS['a']
# b: F_q 中的元素,椭圆曲线的参数之一
b = ECC_PARAMETERS['b']
# G: 椭圆曲线的一个基点,其阶为素数
# G 的坐标: x_G 和 y_G
x_G = ECC_PARAMETERS['g'][:64]
y_G = ECC_PARAMETERS['g'][64:]
# P_A: 用户 A 的公钥
# P_A 的坐标: x_A 和 y_A
x_A = self.public_key[:64]
y_A = self.public_key[64:]
z = ENTL_A + ID_A + a + b + x_G + y_G + x_A + y_A
return z
我们写出调用代码,:
from gmssl2 import sm2
#16进制的公钥和私钥
public_key = 'CB06F5F57DA022626BA8D5C176CF4078B420E581C5A09F18CD79CCD3260D5AD1019B4186FC66CA86AE123181D3044AE070CF5D12C6001257829ECB29579C382E'
private_key = 'D14B13327D0B6402267AE053131D519C072F82F90DFE4ECF2897D9D7617040E2'
sm2_obj = sm2.SM2(
public_key=public_key,
private_key=private_key
)
data = b"hello\n" # bytes类型
sign = sm2_obj.sign(data)
运行得到结果:
拼接字符串为: 008031323334353637383132333435363738fffffffeffffffffffffffffffffffffffffffff00000000fffffffffffffffc28e9fa9e9d9f5e344d5a9e4bcf6509a7f39789f515ab8f92ddbcbd414d940e9332c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0CB06F5F57DA022626BA8D5C176CF4078B420E581C5A09F18CD79CCD3260D5AD1019B4186FC66CA86AE123181D3044AE070CF5D12C6001257829ECB29579C382E
为了简化本教程,我不会实现 SM3 杂凑算法,后续有机会单独讲解,在这里我们会使用 GmSSL-Python(https://github.com/GmSSL/GmSSL-Python)的 SM3 实现:
import binascii
from gmssl import Sm3
def H_256(msg: str):
sm3 = Sm3()
sm3.update(msg)
dgst = sm3.digest()
return dgst.hex()
class SM2(ABC):
def sign(self, data: bytes, user_id: str="1234567812345678"):
z = self.get_z(data, user_id)
# 将16进制字符串转换为字节串
z = binascii.a2b_hex(z)
# H_v: 消息摘要长度为 v 比特的密码杂凑函数,SM2 中一般使用 SM3
Z_A = H_256(z)
# M_ = Z_A || M
# M: 待签名消息
M_ = (Z_A + data.hex()).encode('utf-8')
M_ = binascii.a2b_hex(M_)
e = H_256(M_)
至此,我们得到了文档中第 2 步描述的 e 值:
接下来我们写一个安全的随机数生成函数,用于第 3 步产生随机数:
import os
def random_hex(x: int):
# SM2 需要 256 比特的随机数,即 32 字节
nbytes = (x + 1) // 2 # 向上取整,确保生成足够的字节数
return os.urandom(nbytes).hex()[:x]
# n: 基点 G 的阶
# 产生的随机数区间: [1, n-1]
para_len = len(ECC_PARAMETERS['n'])
random_hex_str = random_hex(para_len)
因为时间关系,SM2 签名的教程暂时写到这里,后续抽空我会把后面几步一并写出来,后面将涉及到大数运算和签名计算(R 和 S)。