Python 一步步实现国密算法——SM2 签名(1)

本篇文章介绍如何一步步实现国密 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 章节:

4DD787C5C26754762E07A644BA11CE49.png

所以第一步我们需要计算这个 Z_A 值,定义在 5.5 章节:

image.png

|| 的意思是字符串拼接。为了区分于官方的 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 值:

image.png

接下来我们写一个安全的随机数生成函数,用于第 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)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容