sm3算法实现

在实现sm3算法之前,先来回顾一下,sm3算法的执行过程:

SM3执行过程

整个过程分为带入初始IV,中间部分(包括消息填充、消息分组、消息扩展、迭代压缩)、输出结果。把这三部分分别定义为:initupdatedone,其中:

  1. init部分: 代表初始化操作,主要是初始化A,B,C,D,E,F,G,H,这8个字寄存器
  2. update部分: 包括消息分组、消息扩展、迭代压缩,以及到了最后一组的时候,进行消息填充。
  3. done部分: 主要是输出结果

1. 算法实现疑问

1.1 为何拆分成三部分

可能会有同学会有疑问,为啥要分成三部分来实现,直接封装成一个函数不好吗?

这个问题,本身是没有问题的,只是角度不同,提出这个问题的同学,我猜测这位同学大部分使用sm3的场景是针对数据量不大,比如就是字节级或KB级的数据,这样直接封装成一个函数是没问题的。但考虑另外一个使用场景,如果我们要对一个GB级或更大的文件,计算其摘要值,怎么办。如果只是封装成一个函数来调用,就意味着,要事先将全部的文件内容读出来,再调用,内存就会爆。

所以就要实现成,读文件一部分内容出来,计算一次,意思就是update部分要可以循环调用。比如每次都读文件的512字节出来,读一次调用一次update,最后等文件全部读完后,调用done来计算最终的摘要值。

而sm3的算法特性正好可以满足计算大文件的要求,它本来就是分组算法,就是来一组计算一组,一组的大小是64字节,计算结果保存在A,B,C,D,E,F,G,H这8个字寄存器中,这样内存就不可能会爆了。

1.2 消息填充实现位置

按规范文档的理解,是在进行消息分组前,就要对消息进行填充,但现在分成了三部分来实现,中间的update部分是循环调用的,来一部分消息,调用一次。而且也算法自身也无法事先知道消息的部大小是多少,所以消息的填充就要放到最后去做。也就是调用done函数时,此时表示消息都读完了,要输出结果了,此时算法已经处理了多少消息是可以用变量来记录的,然后再进行消息填充,再执行迭代压缩,最后输出结果。

2. 算法实现

通过上述解惑,了解到了为何要将sm3的实现拆分成三部分来做。那既然拆分成了三部分,其中update部分还是可以循环调用的,肯定就保存一些中间状态,比如已经压缩了多少消息,当前A,B,C,D,E,F,G,H这8个寄存器的值是多少之类的,我们用一个结构体来当上下文:

typedef struct {
    unsigned int state[8]; // 寄存器中间状态
    unsigned char buf[64]; // 待压缩消息
    uint64_t cur_buf_len; // 当前待压缩消息长度(字节)
    uint64_t compressed_len; // 已压缩消息长度(比特)
} gm_sm3_context;

sm3.h中定义gm_sm3_context这个结构体,用来记录sm3算法执行过程中的一些状态及中间结果,其中:

  1. state: 用来保存A,B,C,D,E,F,G,H这8个字寄存器的值,每执行完一轮后,暂存在里面。
  2. buf: 用来保存待压缩的消息,因为调用update传过来的消息长度可能不满一组,即不满64字节,此时就不能进行消息扩展、迭代压缩的操作。
  3. cur_buf_len: 用来记录buf中待压缩的消息长度,单位是字节。
  4. compressed_len: 用来记录已压缩的消息长度,单位比特。

这里compressed_len记录的是比特,因为消息填充时,长度需要的是比特单位的长度。

下面以全局视角,先来看看initupdatedone这三部分函数的定义:

/**
 * 摘要算法初始化
 * @param ctx 上下文
 */
void gm_sm3_init(gm_sm3_context * ctx);

/**
 * 添加消息
 * @param ctx 上下文
 * @param input 消息
 * @param iLen 消息长度(字节)
 */
void gm_sm3_update(gm_sm3_context * ctx, const unsigned char * input, unsigned int iLen);

/**
 * 计算摘要
 * @param ctx 上下文
 * @param output 输出摘要结果
 */
void gm_sm3_done(gm_sm3_context * ctx, unsigned char output[32]);

函数定义说明:

  1. gm_sm3_init: 初始化,带入个上下文就行,sm3算法本身不需要传入密钥之类的,所以就没有其它参数需要带入了。
  2. gm_sm3_update: 需要带入上下文,待压缩的消息及消息长度。还没执行到最终状态,中间状态都保存到上下文中,所以也没有中间结果需要输出。
  3. gm_sm3_done: 需要带入上下文以及输出缓存区,因为sm3的结果就是A,B,C,D,E,F,G,H这8个寄存器的值,所以它的长度就是8*4=32字节。这里让用户指定缓冲区,是为了遵循内存谁创建谁管理的原则,不应由算法来分配输出缓冲区内存,要不用户可能会忘记释放或不清楚该不该由用户自己来释放。

2.1 init实现

init部分实现,只需要初始化一下A,B,C,D,E,F,G,H这8个寄存器,以及上下文中其它几个变量即可,实现代码如下:

/**
 * 摘要算法初始化
 * @param ctx 上下文
 */
void gm_sm3_init(gm_sm3_context * ctx) {
    ctx->state[0] = GM_SM3_IV_A;
    ctx->state[1] = GM_SM3_IV_B;
    ctx->state[2] = GM_SM3_IV_C;
    ctx->state[3] = GM_SM3_IV_D;
    ctx->state[4] = GM_SM3_IV_E;
    ctx->state[5] = GM_SM3_IV_F;
    ctx->state[6] = GM_SM3_IV_G;
    ctx->state[7] = GM_SM3_IV_H;
    ctx->cur_buf_len = 0;
    ctx->compressed_len = 0;
}

2.2 update实现

实现代码如下:

/**
 * 添加消息
 * @param ctx 上下文
 * @param input 消息
 * @param iLen 消息长度(字节)
 */
void gm_sm3_update(gm_sm3_context * ctx, const unsigned char * input, unsigned int iLen) {
    while (iLen--) {
        ctx->buf[ctx->cur_buf_len++] = *input++;

        /* 是否满64个字节 */
        if (ctx->cur_buf_len == 64) {
            // 满了,则立即调用压缩函数进行压缩
            gm_sm3_compress(ctx);
            ctx->compressed_len += 512;
            ctx->cur_buf_len = 0;
        }
    }
}

代码解析:

  1. 不满一轮时,将消息暂存到buf缓冲区中。
  2. 当满一轮时,调用gm_sm3_compress函数对消息进行消息扩展以及迭代压缩。同时增加已压缩的消息长度,清空待压缩消息长度。
2.2.1 gm_sm3_compress函数
// 压缩算法
static void gm_sm3_compress(gm_sm3_context * ctx) {
    unsigned int W[68];
    unsigned int W1[64];

    // Bi 扩展为 W
    gm_sm3_BiToW(ctx->buf, W);

    // W 扩展为 W1
    gm_sm3_WToW1(W, W1);

    // 压缩
    gm_sm3_CF(W, W1, ctx);
}

gm_sm3_compress函数需要对消息进行扩展,然后再进行迭代压缩。我们把消息扩展分两部分来实现。

2.2.2 消息扩展为W

先来看第一部分,将消息扩展为W,在讲《sm3算法基本原理》中,消息扩展为W首先需要将消息划分为16个字 W 0 , W 1 , ⋅ ⋅ ⋅ , W 15 W_0 , W_1 , · · · , W_{15} W0,W1,⋅⋅⋅,W15,然后按规范文档中的方法进行扩展。先来实现消息划分:

// 消息扩展,消息Bi -> W
static void gm_sm3_BiToW(const unsigned char * Bi, unsigned int * W) {
    GM_GET_UINT32_BE( W[ 0], Bi,  0 );
    GM_GET_UINT32_BE( W[ 1], Bi,  4 );
    GM_GET_UINT32_BE( W[ 2], Bi,  8 );
    GM_GET_UINT32_BE( W[ 3], Bi, 12 );
    GM_GET_UINT32_BE( W[ 4], Bi, 16 );
    GM_GET_UINT32_BE( W[ 5], Bi, 20 );
    GM_GET_UINT32_BE( W[ 6], Bi, 24 );
    GM_GET_UINT32_BE( W[ 7], Bi, 28 );
    GM_GET_UINT32_BE( W[ 8], Bi, 32 );
    GM_GET_UINT32_BE( W[ 9], Bi, 36 );
    GM_GET_UINT32_BE( W[10], Bi, 40 );
    GM_GET_UINT32_BE( W[11], Bi, 44 );
    GM_GET_UINT32_BE( W[12], Bi, 48 );
    GM_GET_UINT32_BE( W[13], Bi, 52 );
    GM_GET_UINT32_BE( W[14], Bi, 56 );
    GM_GET_UINT32_BE( W[15], Bi, 60 );
}

就是将字节转换为字的操作,参照《sm3算法基本原理》中大小端的解释,再使用《sm3常量及通用函数》中事先实现好的将字节转换为字的宏定义即可。

消息扩展为W,规范原文是:

F O R   j = 16   T O   67 FOR\ j= 16\ TO\ 67 FOR j=16 TO 67
   W j ← P 1 ( W j − 16 ⊕ W j − 9 ⊕ ( W j − 3 ⋘ 15 ) ) ⊕ ( W j − 13 ⋘ 7 ) ⊕ W j − 6 W_j \gets P_1 (W_{j−16} \oplus W_{j−9} \oplus (W_{j−3} \lll 15)) \oplus (W_{j−13} \lll 7) ⊕ W_{j−6} WjP1(Wj16Wj9(Wj315))(Wj137)Wj6
E N D F O R ENDFOR ENDFOR

其中 P 1 P_1 P1在《sm3常量及通用函数》中已经用宏定义实现完了,可以去回顾一下,那要实现将消息扩展为W就简单了:

// 消息扩展,消息Bi -> W
static void gm_sm3_BiToW(const unsigned char * Bi, unsigned int * W) {
    int i;
    unsigned int tmp;

    GM_GET_UINT32_BE( W[ 0], Bi,  0 );
   // 此处省略W1-W14的字节转换为字...
    GM_GET_UINT32_BE( W[15], Bi, 60 );

    for (i = 16; i <= 67; i++) {
        tmp = W[i - 16]    ^ W[i - 9] ^ GM_SM3_ROTL(W[i - 3], 15);
        W[i] = GM_SM3_P_1(tmp) ^ (GM_SM3_ROTL(W[i - 13], 7)) ^ W[i - 6];
    }
}
2.2.3 W扩展为W1

规范原文:

F O R   j = 0   T O   63 FOR\ j=0\ TO\ 63 FOR j=0 TO 63
   W j ′ = W j ⊕ W j + 4 W_j' = W_j \oplus W_{j+4} Wj=WjWj+4
E N D F O R ENDFOR ENDFOR

这个不难,代码实现如下:

// w 扩展算法
static void gm_sm3_WToW1(const unsigned int * W, unsigned int * W1) {
    int i;

    for (i = 0; i <= 63; i++) {
        W1[i] = W[i] ^ W[i + 4];
    }
}
2.2.4 迭代压缩函数

这里我把实现代码贴出来,大家对照着《sm3算法基本原理》来看,理解起来应该没有难度,都是些简单的运算以及讲过的内容了。

// 压缩算法
static void gm_sm3_CF(const unsigned int *W, const unsigned int *W1, gm_sm3_context *ctx) {
    unsigned int SS1;
    unsigned int SS2;
    unsigned int TT1;
    unsigned int TT2;
    unsigned int A, B, C, D, E, F, G, H;
    unsigned int Tj;
    int j;

    // ABCDEFGH = V (i)
    A = ctx->state[0];
    B = ctx->state[1];
    C = ctx->state[2];
    D = ctx->state[3];
    E = ctx->state[4];
    F = ctx->state[5];
    G = ctx->state[6];
    H = ctx->state[7];

    for (j = 0; j < 64; j++) {
        if (j < 16) {
            // if 0 <= j <= 15 Tj = 0x79cc4519
            Tj = GM_SM3_T_0;
        } else {
            // if j > 15 Tj = 0x7a879d8a
            Tj = GM_SM3_T_1;
        }
        // SS1 = ((A <<< 12) + E + (Tj <<< j)) <<< 7
        SS1 = GM_SM3_ROTL((GM_SM3_ROTL(A, 12) + E + GM_SM3_ROTL(Tj, j)), 7);
        // SS2 = SS1 ^ (A <<< 12)
        SS2 = SS1 ^ GM_SM3_ROTL(A, 12);

        // TT1 = FFj(A, B, C) + D + SS2 + Wj1
        // TT2 = GGj(E, F, G) + H + SS1 + Wj
        if (j < 16) {
            TT1 = GM_SM3_FF_0(A, B, C) + D + SS2 + W1[j];
            TT2 = GM_SM3_GG_0(E, F, G) + H + SS1 + W[j];
        } else {
            TT1 = GM_SM3_FF_1(A, B, C) + D + SS2 + W1[j];
            TT2 = GM_SM3_GG_1(E, F, G) + H + SS1 + W[j];
        }

        // D = C
        D = C;
        // C = B <<< 9
        C = GM_SM3_ROTL(B, 9);
        // B = A
        B = A;
        // A = TT1
        A = TT1;
        // H = G
        H = G;
        // G = F <<< 19
        G = GM_SM3_ROTL(F, 19);
        // F = E
        F = E;
        // E = P0(TT2)
        E = GM_SM3_P_0(TT2);
    }

    // V(i+1) = ABCDEFGH ^ V(i)
    ctx->state[0] ^= A;
    ctx->state[1] ^= B;
    ctx->state[2] ^= C;
    ctx->state[3] ^= D;
    ctx->state[4] ^= E;
    ctx->state[5] ^= F;
    ctx->state[6] ^= G;
    ctx->state[7] ^= H;
}

2.3 done实现

先贴代码,再进行代码解析:

static const unsigned char gm_sm3_padding[64] = {
        0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};

/**
 * 计算摘要
 * @param ctx 上下文
 * @param output 输出摘要结果
 */
void gm_sm3_done(gm_sm3_context * ctx, unsigned char output[32]) {
    uint32_t padn;
    unsigned char msglen[8];
    uint64_t total_len, high, low;

    // 消息的总长度(比特) = 剩余未压缩数据的长度(字节) * 8
    total_len = ctx->compressed_len + (ctx->cur_buf_len << 3);
    high = (total_len >> 32) & 0x0FFFFFFFF;
    low = total_len & 0x0FFFFFFFF;

    GM_PUT_UINT32_BE(high, msglen, 0);
    GM_PUT_UINT32_BE(low,  msglen, 4);

    // 计算填充长度,因为事先要添加一比特,故应计算cur_buf_len + 1是否超过56
    padn = ((ctx->cur_buf_len + 1) <= 56) ? (56 - ctx->cur_buf_len) : (120 - ctx->cur_buf_len);

    // 添加填充
    gm_sm3_update(ctx, (unsigned char *) gm_sm3_padding, padn);
    gm_sm3_update(ctx, msglen, 8);

    // output
    GM_PUT_UINT32_BE(ctx->state[0], output,  0);
    GM_PUT_UINT32_BE(ctx->state[1], output,  4);
    GM_PUT_UINT32_BE(ctx->state[2], output,  8);
    GM_PUT_UINT32_BE(ctx->state[3], output, 12);
    GM_PUT_UINT32_BE(ctx->state[4], output, 16);
    GM_PUT_UINT32_BE(ctx->state[5], output, 20);
    GM_PUT_UINT32_BE(ctx->state[6], output, 24);
    GM_PUT_UINT32_BE(ctx->state[7], output, 28);
}

代码解析:

  1. done函数的职能,通过前面的讲述,我们是把消息填充放在这一步了,在进行消息填充之前,先要计算好消息的部比特长度,注意不是字节长度。

    unsigned char msglen[8];
    uint64_t total_len, high, low;
    
    // 消息的总长度(比特) = 剩余未压缩数据的长度(字节) * 8
    total_len = ctx->compressed_len + (ctx->cur_buf_len << 3);
    high = (total_len >> 32) & 0x0FFFFFFFF;
    low = total_len & 0x0FFFFFFFF;
    
    // 规范中是大端存储
    GM_PUT_UINT32_BE(high, msglen, 0);
    GM_PUT_UINT32_BE(low,  msglen, 4);
    

    这部分操作,就是计算消息的总长度,然后将8字节的拆成两个4字节整型,然后转换为字节的形式,方便添加到消息的尾部。

  2. 计算填充内容的长度,以及填充

    uint32_t padn;
    
    // 计算填充长度,因为规范中固定事先要添加一比特,故应计算cur_buf_len + 1是否超过56
    padn = ((ctx->cur_buf_len + 1) <= 56) ? (56 - ctx->cur_buf_len) : (120 - ctx->cur_buf_len);
    
    // 添加填充
    gm_sm3_update(ctx, (unsigned char *) gm_sm3_padding, padn);
    

    这里拿56作为分界点,其实是简化了,一组消息的大小是64字节,那么填充的长度=64 - 消息总长度8 - 未压缩的消息长度ctx->cur_buf_len,简化一下就是填充的长度=56 - ctx->cur_buf_len。如果56<未压缩的长度+1<=64,那么填充的长度=128 - 消息总长度8 - 未压缩的消息长度ctx->cur_buf_len,简化一下就是填充的长度=120 - ctx->cur_buf_len

  3. 最后的输出就是将字转换为字节。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值