终于解决了!!! 基于GmSSL的SM2签名算法及思路分享

在这里插入图片描述

背景

上周车厂向我们提出了一个问题:已卖出的部分车辆,上报给平台的排放数据中验签失败

经过内部确认是我们的流程设计问题。先简单描述一下设备的签名流程,如下图:

在这里插入图片描述

流程:

  1. 模组将待验签数据通过i2c串口发给SE加密芯片进行签名。
  2. SE 加密芯片用自身的userID_A和预定的私钥按照SM2标准签名接口进行签名。并将签名值返回给4G模组。
  3. 4G模组将签名数据、userID_B、约定好的公钥、签名信息上报给平台。

由上述流程可知,SE签名使用的userID_A与模组上报的userID_B不一致。导致了平台验签不通过

解决方案:

  1. 4G模组上报的userID_B改为userID_A。可惜上报给平台的userID车厂有需求,必须按照指定格式上报。【不通过】;

  2. 4G模组需要将userID传递给SE加密芯片。【通过】;

在这里插入图片描述

理论上到这里就可以解决该问题了。但是方案二需要修改串口协议以及更新SE芯片的内部程序,而已经卖出的车无法进行远程升级SE程序,仅支持远程升级4G模组程序

为了解决已经卖出车问题,我们不得不准备临时方案:4G模组进行验签,不依赖SE加密芯片

gmssl

经过查阅了解到,国密SM签名不能依赖开源的OpenSSL库,而是依赖GmSSL开源库。官网地址如下:

GmSSL

经过官网指导,编译、安装后:

$ unzip GmSSL-master.zip
$ cd GmSSL-master
$ mkdir build
$ cd build
$ cmake ..
$ make
$ make test
$ sudo make install
$
// 查看安装是否成功
$ gmssl version
GmSSL 3.1.0 Dev

再通过命令行验证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

$ echo hello | gmssl sm2encrypt -pubkey sm2pub.pem -out sm2.der
$ gmssl sm2decrypt -key sm2.pem -pass 1234 -in sm2.der

虽然签名、验签最终的结果是成功的。但存在两个问题:

  1. 该验证方式是命令行形式,并且非对称密钥都是证书形式。与我们期望的不一致(api形式,且公钥是64字节数组,私钥是32字节数组)
  2. 虽然验证成功,但是并不能确保该签名、验签流程符合国家1239平台

针对第一个问题,我是通过分析GmSLL源码库去理解的。

私钥、公钥如何转换为字节数组

由命令行gmssl sm2keygen -pass 1234 -out sm2.pem -pubout sm2pub.pem可知,gmssl通过sm2keygen接口生成私钥与公钥证书。于是我从分析sm2keygen接口开始。通过源码分析,大致明确流程:

/** 生成SM2非对称密钥*/
SM2_KEY key;
sm2_key_generate(&key);

/** 通过SM2非对称密钥生成私钥证书*/
char* pass = NULL;
FILE* outfp = NULL;
...
sm2_private_key_info_encrypt_to_pem(&key, pass, outfp);

/** 通过SM2非对称密钥生成公钥证书*/
FILE* puboutfp = NULL;
sm2_public_key_info_to_pem(&key, puboutfp);

由上可知:

  1. 非对称密钥对象是SM2_KEY key,是一种结构体形式。定义为:
typedef struct {
	SM2_Z256_POINT public_key;
	sm2_z256_t private_key;
} SM2_KEY;
  1. sm2_private_key_info_encrypt_to_pem接口内部应该是将SM2_KEY经过转换,生成PEM格式的证书。同理,公钥证书也应该如此。

于是我继续分析sm2_private_key_info_encrypt_to_pem接口。最终分析到如下代码,基本确定,该接口得到的就是32字节长度数组。

uint8_t prikey[32];
sm2_z256_to_bytes(key->private_key, prikey);

同理:64字节长度的公钥数组转换如下:

uint8_t octets[65];
out[0] = SM2_point_uncompressed;
(void)sm2_z256_point_to_bytes(&key->public_key, octets + 1);

通过以上接口,即可获取私钥、公钥的字节数组。

签名流程

如法炮制,SM2签名流程可从分析sm2sign接口入手,因为我们最终想要得到的是签名信息(32字节的r值数组、32字节的s值数组)。大致流程如下:

/** 初始化SM2 签名对象*/
SM2_SIGN_CTX sign_ctx;
SM2_KEY key;
char * id = NULL;
...
sm2_sign_init(&sign_ctx, &key, id, strlen(id));

/** 插入待签名信息*/
char* buf = NULL;
int len = 0;
...
sm2_sign_update(&sign_ctx, buf, len);

/** 签名*/
uint8_t dgst[SM3_DIGEST_SIZE];
SM2_SIGNATURE signature; //签名信息

sm3_finish(&sign_ctx.sm3_ctx, dgst);

if (sign_ctx.num_pre_comp == 0) {
    if (sm2_fast_sign_pre_compute(sign_ctx.pre_comp) != 1) {
        printf("sm2_fast_sign failed");
        return -1;
    }
    sign_ctx.num_pre_comp = SM2_SIGN_PRE_COMP_COUNT;
}

sign_ctx.num_pre_comp--;
if (sm2_fast_sign(sign_ctx.fast_sign_private, &sign_ctx.pre_comp[sign_ctx.num_pre_comp],
    dgst, &signature) != 1) {
    printf("sm2_fast_sign failed");
    return -1;
}

通过以上分析思路,输出测试例程:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <gmssl/mem.h>
#include <gmssl/sm2.h>

int main()
{
    
    SM2_KEY key;
    uint8_t prikey[32];
    uint8_t pubkey[65];

    SM2_SIGN_CTX sign_ctx;
    /**
     * 1. 生成密钥对
     */
    if (sm2_key_generate(&key) != 1)
    {
        printf("sm2_key_generate failed\n");
    }

    /** 获取私钥 */
    sm2_z256_to_bytes(key.private_key, prikey);

    /** 获取公钥 */
    sm2_z256_point_to_uncompressed_octets(&(key.public_key), pubkey);

    printf("prikey:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",prikey[i]);
    }
    printf("\n");

    printf("pubkey:");
    for(int i = 1 ; i < 65 ; i++)
    {
        printf("%02x ",pubkey[i]);
    }
    printf("\n");

    const char* userId = "1234567812345678";
    /** sm2 初始化*/
    if (sm2_sign_init(&sign_ctx, &key, userId, strlen(userId)) != 1) 
    {
        printf("sm2_sign_init failed\n");
        return -1;
    }

    /** 插入签名数据 */
    char data[] = {0x68,0x74,0x74,0x70,0x73,0x3A,0x2F,0x2F,0x63,0x6F,0x6E,0x73,0x74,0x2E,0x6E,0x65,0x74,0x2E,0x63,0x6E,0x2F};
    if (sm2_sign_update(&sign_ctx, data, sizeof(data)) != 1) 
    {
        printf("sm2_sign_update\n");
        return -1;
	}
   
    /** 签名 */
    uint8_t dgst[SM3_DIGEST_SIZE];
	SM2_SIGNATURE signature;

	sm3_finish(&sign_ctx.sm3_ctx, dgst);

	if (sign_ctx.num_pre_comp == 0) {
		if (sm2_fast_sign_pre_compute(sign_ctx.pre_comp) != 1) {
			printf("sm2_fast_sign failed");
			return -1;
		}
		sign_ctx.num_pre_comp = SM2_SIGN_PRE_COMP_COUNT;
	}

	sign_ctx.num_pre_comp--;
	if (sm2_fast_sign(sign_ctx.fast_sign_private, &sign_ctx.pre_comp[sign_ctx.num_pre_comp],
		dgst, &signature) != 1) {
		printf("sm2_fast_sign failed");
		return -1;
	}

    printf("sign-r:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature.r[i]);
    }
    printf("\n");

    printf("sign-s:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature.s[i]);
    }
    printf("\n");

    return 0;
}

经编译验证输出如下:

xieyihua@xieyihua:~/GmSSL-master$ gcc test.c -o 1 -lgmssl
xieyihua@xieyihua:~/GmSSL-master$ ./1 
prikey:83 ce bc 99 57 95 4a 62 1c 59 89 fa ca 05 c1 b8 47 b1 b4 4f 32 3f 8c 12 b8 12 c4 36 98 32 8e 03 
pubkey:19 66 23 f7 5e 17 43 7d 19 8f 77 fe cb 7f f8 a9 61 f6 80 50 2f f7 cb 50 26 9d aa 62 56 4c e4 8b 61 91 c2 1d 82 05 17 2b bd 85 29 b5 ba 58 f5 fe 0b d1 ae a7 ce 0c 2c 13 e1 48 2b 96 a2 d2 08 24 
sign-r:ac cd 90 46 c0 9f 1f b9 76 7c a9 4a 75 31 85 91 09 df 61 00 61 e3 86 d2 da 2d 4e 08 74 e6 5c 86 
sign-s:1b 04 8f 7e da 1d 78 7d 63 6d 01 fe 47 1c f1 e5 42 dc 57 f3 43 27 2b 5b 65 95 9c 1d 25 49 27 11 
xieyihua@xieyihua:~/GmSSL-master$ 

如何确保该签名方式与平台验签匹配呢?

通过车厂人员指导,可通过下列在线SM2验签接口:

SM2 在线验签工具

将上述示例程序中的公钥、userId、数据、签名值输入。如下:

在这里插入图片描述

注意:因为SM签名内部用到了随机数,即使userID、私钥、签名数据都一样。每次生成的签名也会不一样

集成

通过上述测试示例验证接口可用,接下来就是根据业务场景封装接口,并集成到项目中。一般情况需要两个步骤:

  1. 开源代码交叉编译
  2. 接口封装

交叉编译

交叉编译的前提是要知道自己需要什么。比如,我们工程中实际上需要一个GmSLL静态库。但是该开源项目默认生成动态库。因此需要修改一下cmake,如下:

---:add_library(gmssl ${src}) # 默认为动态库
+++:add_library(gmssl STATIC ${src}) # 明确指示生成静态库

操作流程:

  1. source环境变量:
xieyihua@xieyihua:~/GmSSL-master$ source ~/3503-MPU/sdk/ql-ol-crosstool/ql-ol-crosstool-env-init
QUECTEL_PROJECT_NAME      =AG35CENFAN
QUECTEL_PROJECT_REV       =AG35CENFNR07A02M4G_OCPU
xieyihua@xieyihua:~/GmSSL-master$
  1. 编译GmSLL
xieyihua@xieyihua:~/GmSSL-master$ rm build/ -rf
xieyihua@xieyihua:~/GmSSL-master$ mkdir build
xieyihua@xieyihua:~/GmSSL-master$ cd build/
xieyihua@xieyihua:~/GmSSL-master/build$ cmake ..
-- The C compiler identification is GNU 4.9.3
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /home/xieyihua/3503-MPU/sdk/ql-ol-crosstool/sysroots/x86_64-oesdk-linux/usr/bin/arm-oe-linux-gnueabi/arm-oe-linux-gnueabi-gcc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- ENABLE_ASM_UNDERSCORE_PREFIX is ON
-- ENABLE_SM4_ECB is ON
-- ENABLE_SM4_OFB is ON
-- ENABLE_SM4_CFB is ON
-- ENABLE_SM4_CCM is ON
-- ENABLE_SM4_XTS is ON
-- ENABLE_SM3_XMSS is ON
-- ENABLE_SHA1 is ON
-- ENABLE_SHA2 is ON
-- ENABLE_AES is ON
-- ENABLE_CHACHA20 is ON
-- ENABLE_SM4_CBC_MAC is ON
-- Looking for getentropy
-- Looking for getentropy - not found
-- ENABLE_SDF is ON
-- Detected Linux, configuring /etc/ld.so.conf.d/gmssl.conf
-- Configuring done (1.3s)
-- Generating done (0.4s)
-- Build files have been written to: /home/xieyihua/GmSSL-master/build
xieyihua@xieyihua:~/GmSSL-master/build$ make -j8
  1. 查看生成的目标文件是否交叉编译成功

在这里插入图片描述

  1. 将生成的静态库libgmssl.a及头文件include放到工程中对应目录中即可。

工程封装

根据1239协议及业务需求,最终接口封装如下:

//gmsslSign.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <gmssl/mem.h>
#include <gmssl/sm2.h>

const uint8_t g_prikey[32] = {0x83,0xce,0xbc,0x99,0x57,0x95,0x4a,0x62,0x1c,0x59,0x89,0xfa,0xca,0x05,0xc1,0xb8,0x47,0xb1,0xb4,0x4f,0x32,0x3f,0x8c,0x12,0xb8,0x12,0xc4,0x36,0x98,0x32,0x8e,0x03};


const uint8_t g_pubkey[65] = {0x04,0x19,0x66,0x23,0xF7,0x5E,0x17,0x43,0x7D,
0x19,0x8F,0x77,0xFE,0xCB,0x7F,0xF8,0xA9,
0x61,0xF6,0x80,0x50,0x2F,0xF7,0xCB,0x50,
0x26,0x9D,0xAA,0x62,0x56,0x4C,0xE4,0x8B,
0x61,0x91,0xC2,0x1D,0x82,0x05,0x17,0x2B,
0xBD,0x85,0x29,0xB5,0xBA,0x58,0xF5,0xFE,
0x0B,0xD1,0xAE,0xA7,0xCE,0x0C,0x2C,0x13,
0xE1,0x48,0x2B,0x96,0xA2,0xD2,0x08,0x24};
/**
 * @brief 通过gmssl 开源库,实现SM2 软签名
 * 
 * @param pcUserId  [in] userID
 * @param userIdLen [in] userID 长度
 * @param pcData    [in] 待验签数据
 * @param datalen   [in] 验签数据长度
 * @param sign      [out] 签名数组,内存由调用者申请必须大于64Byte
 * @param signLen   [out] 签名长度,默认64
 * 
 * @return 0:成功  非0:失败
 * 
 * @note
 */
int sa_gmssl_SM2_sign(const char* pcUserId,
                        size_t userIdLen, 
                        const uint8_t * pcData, 
                        size_t datalen, 
                        uint8_t* sign, 
                        int32_t * signLen)
{
    SM2_KEY key;
    uint8_t prikey[32];
    uint8_t pubkey[65];

    SM2_SIGN_CTX sign_ctx;

    sm2_z256_from_bytes(key.private_key, g_prikey);

    sm2_z256_point_from_octets(&key.public_key, g_pubkey, 65);

#ifdef DEGUG
     /** 获取私钥 */
    sm2_z256_to_bytes(key.private_key, prikey);

    /** 获取公钥 */
    sm2_z256_point_to_uncompressed_octets(&(key.public_key), pubkey);

    printf("prikey:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",prikey[i]);
    }
    printf("\n");

    printf("pubkey:");
    for(int i = 1 ; i < 65 ; i++)
    {
        printf("%02x ",pubkey[i]);
    }
    printf("\n");
#endif
    /** sm2 初始化*/
    if (sm2_sign_init(&sign_ctx, &key, pcUserId, userIdLen) != 1) 
    {
        printf("sm2_sign_init failed\n");
        return -1;
    }

    /** 插入签名数据 */
    if (sm2_sign_update(&sign_ctx, pcData, datalen) != 1) 
    {
        printf("sm2_sign_update\n");
        return -1;
	}
   
    /** 签名 */
    uint8_t dgst[SM3_DIGEST_SIZE];
	SM2_SIGNATURE signature;

	sm3_finish(&sign_ctx.sm3_ctx, dgst);

	if (sign_ctx.num_pre_comp == 0) {
		if (sm2_fast_sign_pre_compute(sign_ctx.pre_comp) != 1) {
			printf("sm2_fast_sign failed");
			return -1;
		}
		sign_ctx.num_pre_comp = SM2_SIGN_PRE_COMP_COUNT;
	}

	sign_ctx.num_pre_comp--;
	if (sm2_fast_sign(sign_ctx.fast_sign_private, &sign_ctx.pre_comp[sign_ctx.num_pre_comp],
		dgst, &signature) != 1) {
		printf("sm2_fast_sign failed");
		return -1;
	}
#ifdef DEGUG
    printf("sign-r:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature.r[i]);
    }
    printf("\n");

    printf("sign-s:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature.s[i]);
    }
    printf("\n");
#endif
/** 设置sign-r*/
    memcpy(sign,signature.r,32);
    /** 设置sign-s*/
    memcpy(sign+32,signature.s,32);
    *signLen = 64;
    
    return 0;
}
//gmsslSign.h
#include <stdint.h>

#ifndef __GMSSL_SIGN_H__
#define __GMSSL_SIGN_H__

#ifdef __cplusplus
extern "C"{
#endif
/**
 * @brief 通过gmssl 开源库,实现SM2 软签名
 * 
 * @param pcUserId  [in] userID
 * @param userIdLen [in] userID 长度
 * @param pcData    [in] 待验签数据
 * @param datalen   [in] 验签数据长度
 * @param sign      [out] 签名数组,内存由调用者申请必须大于64Byte
 * @param signLen   [out] 签名长度,默认64
 * 
 * @return 0:成功  非0:失败
 * 
 * @note
 */
int sa_gmssl_SM2_sign(const char* pcUserId,
                        size_t userIdLen, 
                        const uint8_t * pcData, 
                        size_t datalen, 
                        uint8_t* sign, 
                        int32_t * signLen);

#ifdef __cplusplus
extern }
#endif

#endif

其中extern "C"的原因是:该源文件是.c,而我们的工程存在c++c之间的混合编写。避免在.cpp文件中引用,导致编译不过,需要添加该声明

修改cmake

源码和头文件添加到工程后,就是需要将其编译到工程中,供其他源文件调用。于是就需要修改cmake。

# 设置头文件查找路径
include_directories(
    ${CMAKE_SOURCE_DIR}/lib/gmssl/include
    )

# 将源码编译至工程
set(LIB_SRC
    ${CMAKE_SOURCE_DIR}/soc/stTsp/Acl16Api/gmsslSign.c
    )

# 设置库查找路径
link_directories(${CMAKE_SOURCE_DIR}/lib/gmssl/lib)

# 显式连接静态路libgmssl.a
target_link_libraries(stTsp
                        libgmssl.a
)

编译、提交代码。

总结

问题最终得以解决了,似乎看起来也并不困难。其中的酸楚,估计也就只有经历过的人才了解。面对类似未接触过的问题,我的建议是:

  1. 找到可以验证正确性的方式。本文中就是SM2 在线验签工具
  2. 确定方向可行后,花心思去研究。比如,我在通过gmssl命令行进行SM2签名、验签通过后,就认为该开源库应该是满足要求的。
  3. 对于不了解的内容,遇到困难不要轻言放弃,转换思路,投机取巧。

希望我的经验能够帮助你。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途

在这里插入图片描述
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢艺华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值