背景
上周车厂向我们提出了一个问题:已卖出的部分车辆,上报给平台的排放数据中验签失败。
经过内部确认是我们的流程设计问题。先简单描述一下设备的签名流程,如下图:
流程:
- 模组将待验签数据通过
i2c
串口发给SE加密芯片进行签名。 - SE 加密芯片用自身的
userID_A
和预定的私钥按照SM2标准签名接口进行签名。并将签名值返回给4G模组。 - 4G模组将签名数据、
userID_B
、约定好的公钥、签名信息上报给平台。
由上述流程可知,SE签名使用的userID_A
与模组上报的userID_B
不一致。导致了平台验签不通过。
解决方案:
-
4G模组上报的
userID_B
改为userID_A
。可惜上报给平台的userID
车厂有需求,必须按照指定格式上报。【不通过】; -
4G模组需要将
userID
传递给SE加密芯片。【通过】;
理论上到这里就可以解决该问题了。但是方案二需要修改串口协议以及更新SE芯片的内部程序,而已经卖出的车无法进行远程升级SE程序,仅支持远程升级4G模组程序。
为了解决已经卖出车问题,我们不得不准备临时方案:4G模组进行验签,不依赖SE加密芯片。
gmssl
经过查阅了解到,国密SM签名不能依赖开源的OpenSSL库,而是依赖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
虽然签名、验签最终的结果是成功的。但存在两个问题:
- 该验证方式是命令行形式,并且非对称密钥都是证书形式。与我们期望的不一致(api形式,且公钥是64字节数组,私钥是32字节数组)。
- 虽然验证成功,但是并不能确保该签名、验签流程符合国家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);
由上可知:
- 非对称密钥对象是
SM2_KEY key
,是一种结构体形式。定义为:
typedef struct {
SM2_Z256_POINT public_key;
sm2_z256_t private_key;
} SM2_KEY;
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验签接口:
将上述示例程序中的公钥、userId、数据、签名值输入。如下:
注意:因为SM签名内部用到了随机数,即使userID、私钥、签名数据都一样。每次生成的签名也会不一样。
集成
通过上述测试示例验证接口可用,接下来就是根据业务场景封装接口,并集成到项目中。一般情况需要两个步骤:
- 开源代码交叉编译
- 接口封装
交叉编译
交叉编译的前提是要知道自己需要什么。比如,我们工程中实际上需要一个GmSLL
静态库。但是该开源项目默认生成动态库。因此需要修改一下cmake
,如下:
---:add_library(gmssl ${src}) # 默认为动态库
+++:add_library(gmssl STATIC ${src}) # 明确指示生成静态库
操作流程:
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$
- 编译
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
- 查看生成的目标文件是否交叉编译成功
- 将生成的静态库
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
)
编译、提交代码。
总结
问题最终得以解决了,似乎看起来也并不困难。其中的酸楚,估计也就只有经历过的人才了解。面对类似未接触过的问题,我的建议是:
- 找到可以验证正确性的方式。本文中就是SM2 在线验签工具
- 确定方向可行后,花心思去研究。比如,我在通过
gmssl
命令行进行SM2签名、验签通过后,就认为该开源库应该是满足要求的。 - 对于不了解的内容,遇到困难不要轻言放弃,转换思路,投机取巧。
希望我的经验能够帮助你。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途