在实现sm3算法之前,先来回顾一下,sm3算法的执行过程:
整个过程分为带入初始IV,中间部分(包括消息填充、消息分组、消息扩展、迭代压缩)、输出结果。把这三部分分别定义为:init
、update
、done
,其中:
- init部分: 代表初始化操作,主要是初始化
A,B,C,D,E,F,G,H
,这8个字寄存器 - update部分: 包括消息分组、消息扩展、迭代压缩,以及到了最后一组的时候,进行消息填充。
- 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算法执行过程中的一些状态及中间结果,其中:
- state: 用来保存
A,B,C,D,E,F,G,H
这8个字寄存器的值,每执行完一轮后,暂存在里面。 - buf: 用来保存待压缩的消息,因为调用update传过来的消息长度可能不满一组,即不满64字节,此时就不能进行消息扩展、迭代压缩的操作。
- cur_buf_len: 用来记录buf中待压缩的消息长度,单位是字节。
- compressed_len: 用来记录已压缩的消息长度,单位比特。
这里compressed_len
记录的是比特,因为消息填充时,长度需要的是比特单位的长度。
下面以全局视角,先来看看init
、update
、done
这三部分函数的定义:
/**
* 摘要算法初始化
* @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]);
函数定义说明:
- gm_sm3_init: 初始化,带入个上下文就行,sm3算法本身不需要传入密钥之类的,所以就没有其它参数需要带入了。
- gm_sm3_update: 需要带入上下文,待压缩的消息及消息长度。还没执行到最终状态,中间状态都保存到上下文中,所以也没有中间结果需要输出。
- 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;
}
}
}
代码解析:
- 不满一轮时,将消息暂存到buf缓冲区中。
- 当满一轮时,调用
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} Wj←P1(Wj−16⊕Wj−9⊕(Wj−3⋘15))⊕(Wj−13⋘7)⊕Wj−6
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′=Wj⊕Wj+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);
}
代码解析:
-
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字节整型,然后转换为字节的形式,方便添加到消息的尾部。
-
计算填充内容的长度,以及填充
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
。 -
最后的输出就是将字转换为字节。