Post

Gmssl实现SM2签名验签

包括SM3WithSM2摘要流程,SM2签名,SM2验签

Gmssl实现SM2签名验签

官方资料

Gmssl
GmSsl 的 EVP 接口

个人经历

  • 官方做签名时虽然 EVP_PKEY 结构体可以适配 SM2,但是示例用 EVP_sha256()作为摘要算法,顺利避开了 SM3WithSM2 的问题。(刚接触 SM2/SM3 的朋友可能不太知道问题在哪,下面会说到)
  • 那么,SM2 结合 SM3 的代码应该怎么写。第一时间当然是使用网站上的示例,修改 EVP_sha256()为 EVP_sm3(),这样代码可以兼容 SM2 和 RSA 的签名验签了,但是这样坐下来,发现我可以自签自验,但是无法验证第三方签名。
  • 忽略查源码,查资料的过程,我发现这一套 EVP 接口内部并没有帮我做 SM3WithSM2 的摘要,只是单纯的 SM3。
  • 我一直以为 EVP 接口内部将能帮我实现这部分内容,然而…… 进而我查阅源码发现 test 文件夹(哎~发现得有点晚)里面就有相关代码。test\sm2test.c : test_sm2_sign

个人代码以及解释

源码中 test 里的代码其实够用了,但是一下贴一下本人的版本,主要是为了讲解一下 SM3WithSM2 的摘要流程,所以 copy 了 test 代码,写一个比较精简的示例 编译命令,注意 ssl 指 gmssl 的动态库,不是 openssl g++ -o test sm2Test.cc -lssl

  • 普通 SM3 摘要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int hashForSM3(unsigned char* clearText, int clearTextLen, unsigned char* sm3Data){
    int ret = -1;
    //初始化摘要结构体
    EVP_MD_CTX *mdctx = EVP_MD_CTX_create();
    if(!mdctx)
        return -1;
    EVP_MD_CTX_init(mdctx);

    //设置摘要算法和密码算法引擎
    if(!EVP_DigestInit_ex(mdctx, EVP_sm3(), NULL))
        goto ERR;

    //输入原文clearText
    if(!EVP_DigestUpdate(mdctx,clearText, clearTextLen))
        goto ERR;

    //输出摘要值,外部判断ret是否为32作为成功条件
    if(!EVP_DigestFinal(mdctx, sm3Data, (unsigned int*)&ret))
        goto ERR;
ERR:
    EVP_MD_CTX_destroy(mdctx);
    return ret;
}
  • SM3WithSM2 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int hashForSM3WithSM2(unsigned char* clearText, int clearTextLen, unsigned char* puk, int pukLen, unsigned char* sm3Data){
    //以下为国密标准推荐参数,id="1234567812345678",长度是128bit则0x0080
    unsigned char sm2_par_dig[210] = {//idlen[2]+id[16]+parm[128]+puk[64]
        0x00,0x80,
        0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,
        0xFF,0xFF,0xFF,0xFE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
        0xFF,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFC,
        0x28,0xE9,0xFA,0x9E,0x9D,0x9F,0x5E,0x34,0x4D,0x5A,0x9E,0x4B,0xCF,0x65,0x09,0xA7,
        0xF3,0x97,0x89,0xF5,0x15,0xAB,0x8F,0x92,0xDD,0xBC,0xBD,0x41,0x4D,0x94,0x0E,0x93,
        0x32,0xC4,0xAE,0x2C,0x1F,0x19,0x81,0x19,0x5F,0x99,0x04,0x46,0x6A,0x39,0xC9,0x94,
        0x8F,0xE3,0x0B,0xBF,0xF2,0x66,0x0B,0xE1,0x71,0x5A,0x45,0x89,0x33,0x4C,0x74,0xC7,
        0xBC,0x37,0x36,0xA2,0xF4,0xF6,0x77,0x9C,0x59,0xBD,0xCE,0xE3,0x6B,0x69,0x21,0x53,
        0xD0,0xA9,0x87,0x7C,0xC6,0x2A,0x47,0x40,0x02,0xDF,0x32,0xE5,0x21,0x39,0xF0,0xA0,
        };
    //对应规范pdf中的章节8.1
    memcpy(sm2_par_dig + 2 + 16 + 128, puk, pukLen);
    unsigned char* sm3_e = new unsigned char[32 + clearTextLen];
    if(32 != hashForSM3(sm2_par_dig, 210, sm3_e)){
        delete[] sm3_e;
        return -1;
    }
    //对应规范pdf中的章节8.2
    memcpy(sm3_e + 32, clearText, clearTextLen);
    if(32 != hashForSM3(sm3_e, 32 + clearTextLen, sm3Data)){
        delete[] sm3_e;
        return -1;
    }
    delete[] sm3_e;
    return 0;
}
  • main 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
int main(){
    int i = 0;
    BIO *bio_pri = NULL;
    BIO *bio_puk = NULL;
    EVP_PKEY *pkey_pri = NULL;
    EC_KEY *prikey = NULL;
    EVP_PKEY *pkey_puk = NULL;
    EC_KEY *pubkey = NULL;
    X509 *cert = NULL;
    unsigned char* puk = NULL;
    int pukLen = 0;
    unsigned char* clearText = (unsigned char*)"Pancake";
    int clearTextLen = strlen((char*)clearText);
    unsigned char sm3Data[32] = {0};
    int sm3DataLen = 0;
    unsigned char out[1024] = {0};
    int outLen = 0;
    ///////////////////////////////////公钥相关结构
    bio_puk = BIO_new_file("SM2Test.cer", "r");
    if (!bio_puk) goto ERR;

    cert = d2i_X509_bio(bio_puk, NULL);
    if (!cert) goto ERR;

    pkey_puk = X509_get_pubkey(cert);
    if (!pkey_puk) goto ERR;

    pubkey = EVP_PKEY_get0_EC_KEY(pkey_puk);//don't free
    if (!pubkey) goto ERR;

    //要去除04标识
    pukLen = i2d_PublicKey(pkey_puk,(unsigned char**)&puk) - 1;
    if (!puk) goto ERR;

    ///////////////////////////////////私钥相关结构
    bio_pri = BIO_new_file("SM2Test.pem", "r");
    if (!bio_pri) goto ERR;

    pkey_pri = PEM_read_bio_PrivateKey(bio_pri, NULL, NULL, NULL);
    if (!pkey_pri) goto ERR;

    prikey = EVP_PKEY_get0_EC_KEY(pkey_pri);//don't free
    if (!prikey) goto ERR;

    ///////////////////////////////////sm3 sm2 sign/verify
    if(hashForSM3WithSM2(clearText, clearTextLen, puk, pukLen, sm3Data))
        goto ERR;

    if (!SM2_sign(NID_undef, sm3Data, sm3DataLen, out, (unsigned int*)&outLen, prikey))
        goto ERR;

    printf("out[%d]:\n", outLen);
    for (int i = 0; i != outLen; i++) {
        printf("%02x ", *(out + i));
        if ((i != 0 && (i + 1) % 16 == 0) || i == outLen - 1) {
            printf("\n");
        }
    }
    printf("\n");

    if (1 != SM2_verify(NID_undef, sm3Data, sm3DataLen, out, outLen, pubkey))
        goto ERR;

    printf("verify success\n");
ERR:
    if(puk) OPENSSL_free(puk);
    if(bio_pri) BIO_free_all(bio_pri);
    if(bio_puk) BIO_free_all(bio_puk);
    if(pkey_pri) EVP_PKEY_free(pkey_pri);
    if(pkey_puk) EVP_PKEY_free(pkey_puk);
    if(cert) X509_free(cert);
    return 0;
}

代码中需要注意的小细节

  • openssl 的风格中,经常出现 xxxget/xxxget0/xxxget1 几个方法共存,如以上代码用到的
    EVP_PKEY_get0_EC_KEY 官方 doc解释如下:

EVP_PKEY_get1_EC_KEY() return the referenced key in pkey or NULL if the key is not of the correct type.
EVP_PKEY_get0_EC_KEY() also return the referenced key in pkey or NULL if the key is not of the correct type but the reference count of the returned key is not incremented and so must not be freed up after use.

简单来说就是:EVP_PKEY_get0_EC_KEY 不需要 free,而 EVP_PKEY_get1_EC_KEY 需要,看看源码应该很容易区分原理,本人没有仔细研究,大概就是 EVP_PKEY_get0_EC_KEY 直接指向了 EVP_PKEY 中的 EC_KEY,单独 free 掉 EC_KEY 会造成对 EVP_PKEY 进行 free 时出现 double free

  • 代码中出现
1
2
3
unsigned char *puk = NULL;
pukLen = i2d_PublicKey(pkey_puk,(unsigned char**)&puk) - 1;
OPENSSL_free(puk);//普通缓冲区释放方法
  • 这里取一个更有代表性的代码例子:
1
2
3
4
5
unsigned char *pvk = "your pvk pkcs1 data";
const unsigned char *pvk_tmp = pvk.value;
//以下函数将会改变pvk_tmp的指向
//d2i_PrivateKey第二个参数类型为EVP_PKEY**,传NULL可以内部new并作为返回值
EVP_PKEY* pkey = d2i_PrivateKey(evpType, NULL, &pvk_tmp, pvk.size);

这里常见两个问题:
1:openssl 某些接口当传入 NULL 时,内部将为你分配内存,如以上的 puk 和 pkey,你需要手动释放。
2:某些接口传入 2 级指针,很可能会改变指向,所以我习惯只要传二级指针都如上述代码的方法,用一个临时指针传入,免得原数据指针被改变,如 d2i_PrivateKey 就是我确认会改变指向的接口

This post is licensed under CC BY 4.0 by the author.