前面的章节已经实现了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

# 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

# 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

# 单元测试

这里的单元测试是用新的实现方法签名,旧的实现方法验签。旧的实现方法来签名,新的实现方法验签来完成。

因为旧的签名及验签算法已经经过单元测试校验过了,正确性有保障,所以这么来做。

单元测试代码:

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

main函数增加:

if(strcmp(argv[1], "sm2_ctx_sv") == 0) {
    test_sm2_ctx_sv();
}
1
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

# 完结

至此国密SM2、SM3、SM4涉及到的算法基本全部都实现了,有错误的地方请指正,实现过程有优化的地方可加群一同讨论。另外因为是C语言实现(无任何依赖),所以可以很方便的扩展至Android、iOS及其它嵌入式设备中。

算法效率中在手机端完全可以满足,嵌入式设备可以通过汇编去优化一部分代码,也是可以满足使用要求的。