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)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容