前面的章节已经实现了SM2的签名,包括计算消息摘要,再计算签名的都实现了,但计算消息摘要部分是需要将待计算的数据一次性传入,这可能不太满足于文件的签名计算,对于大文件来说,不可能把整个文件内容事先读到内存中,再计算签名值,所以就需要改造一下。
改造思路还是跟之前加解密一样,分阶段来实现,也分三个阶段:init、update、done
SM2的签名其实就是先计算待签名数据的摘要值,前面SM3部分已经实现了分阶段,那么签名分阶段实现就很简单了,update部分其实就是不断的往SM3 Context中添加待计算数据,SM3每满一轮会执行一次计算,所以就算文件再大也不用担心内存不足问题。
# init
实现起来比较简单,就不详细讲了,其实就是借助SM3分阶段部分来写就行了。
/**
* 恢复私钥
* @param ctx SM2上下文
* @param key 私钥
* @param kLen 长度必须为32
*/
static int recover_private_key(gm_sm2_context * ctx, const unsigned char * key, unsigned int kLen) {
if(kLen != 32) {
return 0;
}
gm_bn_from_bytes(ctx->private_key, key);
// check k ∈ [1, n-2]
if(gm_bn_is_zero(ctx->private_key) || gm_bn_cmp(ctx->private_key, GM_BN_N_SUB_ONE) >= 0) {
return 0;
}
// check public key
gm_point_mul(&ctx->public_key, ctx->private_key, GM_MONT_G);
if(gm_sm2_check_public_key(&ctx->public_key) != 1) {
return 0;
}
return 1;
}
/**
* 恢复公钥
* @param ctx SM2上下文
* @param key 公钥PC||x||y或者yTile||x
* @param kLen 公钥长度必须为33或65
*/
static int recover_public_key(gm_sm2_context * ctx, const unsigned char * key, unsigned int kLen) {
if((kLen != 33 && kLen != 65) || (key[0] != 0x04 && key[0] != 0x02 && key[0] != 0x03)) {
return 0;
}
// check public key
gm_point_decode(&ctx->public_key, key);
if(gm_sm2_check_public_key(&ctx->public_key) != 1) {
return 0;
}
return 1;
}
/**
* 签名验签初始化
* @param ctx SM2上下文
* @param key 公钥PC||x||y或者yTile||x用于验签,私钥用于签名
* @param kLen 公钥长度必须为33或65,私钥为32字节
* @param id_bytes userid二进制串
* @param idLen userid长度
* @param forSign 1为签名,否则为验签
* @return 1返回成功,否则为密钥非法
*/
int gm_sm2_sign_init(gm_sm2_context * ctx, const unsigned char * key, unsigned int kLen,
const unsigned char * id_bytes, unsigned int idLen, int forSign) {
if(forSign) {
// 私钥签名
if(recover_private_key(ctx, key, kLen) == 0) {
return 0;
}
}else {
// 公钥验签
if(recover_public_key(ctx, key, kLen) == 0) {
return 0;
}
}
// compute z digest
gm_sm2_compute_z_digest(id_bytes, idLen, &ctx->public_key, ctx->buf);
gm_sm3_init(&ctx->sm3_ctx);
gm_sm3_update(&ctx->sm3_ctx, ctx->buf, 32);
ctx->state = forSign;
return 1;
}
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
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
# update
/**
* 添加待签名验签数据
* @param ctx SM2上下文
* @param input 待处理数据
* @param iLen 待处理数据长度
*/
void gm_sm2_sign_update(gm_sm2_context * ctx, const unsigned char * input, unsigned int iLen) {
gm_sm3_update(&ctx->sm3_ctx, input, iLen);
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# done
/**
* 结束签名或验签
* @param ctx SM2上下文
* @param sig 如果是签名则作为输出缓冲区,如果是验签,则传入签名串用于验签
* @return 1签名或验签成功,否则为失败
*/
int gm_sm2_sign_done(gm_sm2_context * ctx, unsigned char sig[64]) {
return gm_sm2_sign_done_for_test(ctx, sig, NULL);
}
int gm_sm2_sign_done_for_test(gm_sm2_context * ctx, unsigned char sig[64], const gm_bn_t testKey) {
gm_bn_t dgst;
gm_sm3_done(&ctx->sm3_ctx, ctx->buf);
gm_bn_from_bytes(dgst, ctx->buf);
if(ctx->state) {
// forSign
return gm_do_sign_for_test(ctx->private_key, dgst, sig, testKey);
}else {
return gm_do_verify(&ctx->public_key, dgst, sig);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 单元测试
这里的单元测试是用新的实现方法签名,旧的实现方法验签。旧的实现方法来签名,新的实现方法验签来完成。
因为旧的签名及验签算法已经经过单元测试校验过了,正确性有保障,所以这么来做。
单元测试代码:
void test_sm2_ctx_sv() {
unsigned char input[60] = {0};
unsigned char buf[64] = {0};
unsigned char userId[3] = {0x61, 0x62, 0x63};
gm_bn_t k, dgst;
gm_point_t p;
gm_sm2_context ctx;
unsigned char testPrivK[32] = {0};
unsigned char testPubK[65] = {0};
gm_hex2bin("3D325BAA32B2A2437FFB471901FD7C0D218FEF5B9BCF5187431DC4B23330FB16", 64, testPrivK);
gm_hex2bin("04328B2B5CEB896FB409FAD358F8228F8FD17A9AED7F9C78B1D78AAD45D2514EA1CC615C5184B1CA6C8462DC3ED541E2D7666FEB6C5293FB1B7E60CBE8DF203D2F", 130, testPubK);
gm_bn_from_bytes(k, testPrivK);
gm_point_from_bytes(&p, testPubK + 1);
gm_hex2bin("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C791167A5EE1C13B05D6A1ED99AC24C3C33E7981EDDCA6C05061328990",
120, input);
// 旧方法签名
gm_sm2_compute_msg_hash(input, 60, userId, 3, &p, buf);
gm_bn_from_bytes(dgst, buf);
if(gm_do_sign_for_test(k, dgst, buf, k) != 1) {
printf("test result sign: fail\n");
return;
}
//新方法验签
if(gm_sm2_sign_init(&ctx, testPubK, 65, userId, 3, 0) == 0) {
printf("test result sign init: fail\n");
return;
}
gm_sm2_sign_update(&ctx, input, 60);
if(gm_sm2_sign_done_for_test(&ctx, buf, k) != 1) {
printf("test result verify: fail\n");
return;
}
// 新方法签名
if(gm_sm2_sign_init(&ctx, testPrivK, 32, userId, 3, 1) == 0) {
printf("test result sign init: fail1\n");
return;
}
gm_sm2_sign_update(&ctx, input, 60);
if(gm_sm2_sign_done_for_test(&ctx, buf, k) != 1) {
printf("test result sign: fail1\n");
return;
}
// 旧方法验签
if(gm_do_verify(&p, dgst, buf) != 1) {
printf("test result verify: fail1\n");
return;
}
printf("test result: ok\n");
}
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
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
main函数增加:
if(strcmp(argv[1], "sm2_ctx_sv") == 0) {
test_sm2_ctx_sv();
}
1
2
3
2
3
执行结果:
192:c saint$ time ./gm_test sm2_ctx_sv
test result: ok
real 0m0.056s
user 0m0.043s
sys 0m0.003s
1
2
3
4
5
6
2
3
4
5
6
# 完结
至此国密SM2、SM3、SM4涉及到的算法基本全部都实现了,有错误的地方请指正,实现过程有优化的地方可加群一同讨论。另外因为是C语言实现(无任何依赖),所以可以很方便的扩展至Android、iOS及其它嵌入式设备中。
算法效率中在手机端完全可以满足,嵌入式设备可以通过汇编去优化一部分代码,也是可以满足使用要求的。
未经本人同意,禁止转载!