一、前言
SM3 算法是中国国家密码管理局于 2010 年发布的一种密码杂凑算法,广泛地应用于数据的完整性校验、数字签名、消息认证码、密钥交换和数据加密等。密码杂凑算法需要满足三种基本属性:抗原像攻击、抗第二原像攻击、抗碰撞攻击,这三种基本属性分别对应三个问题:
- 原像问题,已知哈希值 y y y 找出 x x x 使得 h ( x ) = y h(x)=y h(x)=y,其中 h ( ) h() h() 为哈希函数;
- 第二原像问题,已知 x x x,找出 x ′ x^{\prime} x′ 满足 h ( x ) = h ( x ′ ) h(x) = h(x^{\prime}) h(x)=h(x′) ;
- 碰撞问题,找出 x x x与 x ′ x^{\prime} x′ ,使其 h ( x ) = h ( x ′ ) h(x) = h(x^{\prime}) h(x)=h(x′) 。
SM3 算法采用了类似于 SHA-256 算法的 Merkle-Damgård 结构,并且使用了置换压缩和消息扩展算法,以提高算法的安全性和效率。SM3 算法的安全性已经得到了广泛认可,并且已被国际标准化组织 ISO/IEC 和国际电信联盟 ITU-T 接受为国际标准。
二、迭代哈希函数
迭代哈希函数是一种常见的密码杂凑函数,其基本思想是将输入数据分成固定长度的数据块,并对每个数据块进行迭代计算,最终输出一个固定长度的哈希值。迭代哈希函数的安全性和效率都比较高,因此在实际应用中得到了广泛的应用。
迭代哈希函数通常分为以下几个部分:
- 消息填充,将输入数据按照一定规则进行填充,使其长度符合特定的要求。
- 消息分组,将填充后的数据分成固定长度的数据块。
- 压缩函数,对每个数据块进行压缩计算,得到一个中间状态值。
- 消息扩展,根据中间状态值进行消息扩展,得到下一个数据块的输入。
- 输出变换,将最后一个数据块的中间状态值进行处理,得到固定长度的哈希值。
[3] 给出压缩函数进行迭代压缩计算的示例:
 
 
 其中, 
     
      
       
        
        
          z 
         
        
          i 
         
        
       
      
        z_i 
       
      
    zi 为压缩计算的中间状态值, 
     
      
       
        
        
          y 
         
        
          i 
         
        
       
      
        y_i 
       
      
    yi 为消息分组后的数据块。
迭代哈希函数的安全性主要依赖于消息填充、压缩函数和消息扩展算法的设计。消息填充需要满足一定的安全性要求,以防止恶意攻击者对填充后的数据进行修改。压缩函数需要满足抗碰撞和抗原像性的要求,以保证哈希值的安全性。消息扩展算法需要满足一定的扩展性和安全性要求,以保证哈希函数的效率和安全性。迭代哈希函数的优点是输出长度固定,且能够处理任意长度的输入数据,具有高效性和安全性。常见的迭代哈希函数包括 MD5、SHA-1、SHA-2 和 SM3 等。在实际应用中,迭代哈希函数通常用于数字签名、消息认证码、密钥交换和数据加密等领域。
Merkle-Damgård 结构是一种常见的迭代哈希函数的实现方式,它将输入数据分成固定长度的数据块,并对每个数据块进行迭代计算,最终输出一个固定长度的哈希值。Merkle-Damgård 结构的主要特点是简单、高效、安全,因此在实际应用中得到了广泛的应用。Merkle-Damgård 结构主要由以下两个部分组成:
- 压缩函数:对每个数据块进行压缩计算,得到一个中间状态值。
- 消息扩展:根据中间状态值进行消息扩展,得到下一个数据块的输入。
Merkle-Damgård 结构将输入数据分成固定长度的数据块,然后对每个数据块调用压缩函数进行计算,得到一个中间状态值。中间状态值被用作消息扩展的输入,以生成下一个数据块的输入。迭代计算的过程一直持续到最后一个数据块处理完毕,然后将最后一个数据块的中间状态值进行处理,得到固定长度的哈希值。
Merkle-Damgård 结构的优点是简单、高效、安全,能够处理任意长度的输入数据,且输出长度固定。在实际应用中,Merkle-Damgård 结构通常用于实现 MD5、SHA-1、SHA-2 和 SM3 等常见的哈希函数。
三、SM3 算法
算法介绍
SM3 算法的设计目标是满足密码学的强度要求,具有高效性、安全性和灵活性。SM3 算法的输入为任意长度的消息,输出为一个 256 位的杂凑值。SM3 算法主要包括以下几个部分:
- 消息填充,将消息进行填充,使其长度满足 512 位的倍数。
- 消息分组,将填充后的消息分按512 比特进行分组。
- 压缩函数,并对每个数据块进行压缩计算,得到中间状态值。
- 消息扩展:根据中间状态值进行消息扩展,得到下一个数据块的输入。
- 输出变换,将最后一个数据块的中间状态值进行处理,得到 256 比特的杂凑值。

 
SM3 密码杂凑算法特点:SM3 压缩函数整体结构与SHA-256 类似,但是增加了许多新的设计技术,包括:增加16步全异或操作,消息双字介入、增加快速雪崩效应的P置换等;能有效率地避免高概率的局部碰撞,有效地抵抗强碰撞性的差分分析、弱碰撞性的线性分析和比特追踪法等密码分析 [2]。
详细介绍与示例请参看SM3算法标准文档[1] 。
源码分析
GmSSL 库实现SM3 算法,相关源码分析如下:
// SM3 哈希长度,32字节=512比特
#define SM3_DIGEST_SIZE		32
// SM3 处理的消息分组为512比特,对应字节数为64
#define SM3_BLOCK_SIZE		64
#define SM3_STATE_WORDS		8
#define SM3_HMAC_SIZE		(SM3_DIGEST_SIZE)
/*
 * SM3 上下文信息
 */
typedef struct {
    // 字寄存器
	uint32_t digest[SM3_STATE_WORDS];
    // 已参与压缩函数计算数据块的长度(单位为数据块数)
	uint64_t nblocks;
    // 待参与压缩函数计算的数据块
	uint8_t block[SM3_BLOCK_SIZE];
    // 待参与压缩函数计算的数据块的长度(单位为字节)
	size_t num;
} SM3_CTX;
/**
 * 多个数据块迭代压缩,
 * @param digest 出参,压缩计算后字寄存器的状态值
 * @param data 入参,数据块
 * @param blocks 入参,数据块的数量
 */
void sm3_compress_blocks(uint32_t digest[8], const uint8_t *data, size_t blocks) {
    uint32_t A;
    uint32_t B;
    uint32_t C;
    uint32_t D;
    uint32_t E;
    uint32_t F;
    uint32_t G;
    uint32_t H;
    uint32_t W[68];
    uint32_t SS1, SS2, TT1, TT2;
    int j;
#ifdef SM3_SSE3
#endif
    while (blocks--) {
        A = digest[0];
        B = digest[1];
        C = digest[2];
        D = digest[3];
        E = digest[4];
        F = digest[5];
        G = digest[6];
        H = digest[7];
#ifdef SM3_SSE3
#else
        // 消息扩展,生成字 W_i,其中字W'_i 在此处并未计算,在压缩函数中计算
        for (j = 0; j < 16; j++)
            W[j] = GETU32(data + j * 4);
        for (; j < 68; j++)
            W[j] = P1(W[j - 16] ^ W[j - 9] ^ ROL32(W[j - 3], 15))
                   ^ ROL32(W[j - 13], 7) ^ W[j - 6];
#endif
        j = 0;
#define FULL_UNROLL
#ifdef FULL_UNROLL
        /*
         * 压缩函数 V^{i+1} = CF(V^{i}, B^{i})
         * 宏定义等价于下面两个for 循环
         */
        R8(A, B, C, D, E, F, G, H, 00);
        R8(A, B, C, D, E, F, G, H, 00);
        R8(A, B, C, D, E, F, G, H, 16);
        R8(A, B, C, D, E, F, G, H, 16);
        R8(A, B, C, D, E, F, G, H, 16);
        R8(A, B, C, D, E, F, G, H, 16);
        R8(A, B, C, D, E, F, G, H, 16);
        R8(A, B, C, D, E, F, G, H, 16);
#else
#endif
        digest[0] ^= A;
        digest[1] ^= B;
        digest[2] ^= C;
        digest[3] ^= D;
        digest[4] ^= E;
        digest[5] ^= F;
        digest[6] ^= G;
        digest[7] ^= H;
        data += 64;
    }
}
/*
 * 初始化上下文
 */
void sm3_init(SM3_CTX *ctx) {
    memset(ctx, 0, sizeof(*ctx));
    // 初始值IV
    ctx->digest[0] = 0x7380166F;
    ctx->digest[1] = 0x4914B2B9;
    ctx->digest[2] = 0x172442D7;
    ctx->digest[3] = 0xDA8A0600;
    ctx->digest[4] = 0xA96F30BC;
    ctx->digest[5] = 0x163138AA;
    ctx->digest[6] = 0xE38DEE4D;
    ctx->digest[7] = 0xB0FB0E4E;
}
/*
 * 将消息数据更新到上下文中
 */
void sm3_update(SM3_CTX *ctx, const uint8_t *data, size_t data_len) {
    size_t blocks;
    // 只保留低6位,通过按位与 0x3f=0b111111,限制ctx->num 在区间[0, 64]
    ctx->num &= 0x3f;
    if (ctx->num) {
        // 对于参与压缩函数计算的数据块,计算其凑齐64字节的剩余量 left
        size_t left = SM3_BLOCK_SIZE - ctx->num;
        // 输入数据块的长度小于 left,只拷贝,不参与压缩函数计算
        if (data_len < left) {
            memcpy(ctx->block + ctx->num, data, data_len);
            ctx->num += data_len;
            return;
        }
        // 输入数据块能凑齐64字节,计算1个分组下的压缩函数
        else {
            memcpy(ctx->block + ctx->num, data, left);
            sm3_compress_blocks(ctx->digest, ctx->block, 1);
            ctx->nblocks++;
            data += left;
            data_len -= left;
        }
    }
    // 针对剩下的输入数据块,计算压缩函数
    blocks = data_len / SM3_BLOCK_SIZE;
    if (blocks) {
        sm3_compress_blocks(ctx->digest, data, blocks);
        ctx->nblocks += blocks;
        data += SM3_BLOCK_SIZE * blocks;
        data_len -= SM3_BLOCK_SIZE * blocks;
    }
    // 剩余不足64字节输入数据块
    ctx->num = data_len;
    if (data_len) {
        memcpy(ctx->block, data, data_len);
    }
}
/*
 * 计算消息的哈希值
 */
void sm3_finish(SM3_CTX *ctx, uint8_t *digest) {
    int i;
    ctx->num &= 0x3f;
    // 将0x80 添加到消息末尾
    ctx->block[ctx->num] = 0x80;
    // 有余量添加64比特的消息长度l 二进制,直接添加k 个 0
    if (ctx->num <= SM3_BLOCK_SIZE - 9) {
        memset(ctx->block + ctx->num + 1, 0, SM3_BLOCK_SIZE - ctx->num - 9);
    }
    // 没有余量添加64比特的消息长度l 二进制,先补齐64个字节数据块,再补齐56个字节0,用于与消息长度l 构造下一个数据块
    else {
        memset(ctx->block + ctx->num + 1, 0, SM3_BLOCK_SIZE - ctx->num - 1);
        sm3_compress_blocks(ctx->digest, ctx->block, 1);
        memset(ctx->block, 0, SM3_BLOCK_SIZE - 8);
    }
    // 计算消息长度 l =(nblocks*2^9 + num*2^3) 二进制表示
    // block[56..59] = nblocks 的高9位
    PUTU32(ctx->block + 56, ctx->nblocks >> 23);
    // block[60..63] = nblocks 的低23位与num*2^3 之和
    PUTU32(ctx->block + 60, (ctx->nblocks << 9) + (ctx->num << 3));
    // 压缩函数计算最后一个数据分块
    sm3_compress_blocks(ctx->digest, ctx->block, 1);
    // 将字寄存器值转为256比特(32位 u8)杂凑值
    for (i = 0; i < 8; i++) {
        PUTU32(digest + i * 4, ctx->digest[i]);
    }
    memset(ctx, 0, sizeof(SM3_CTX));
}
/**
 * 计算SM3 哈希摘要
 * @param msg 入参,输入消息
 * @param msglen 入参,输入消息长度
 * @param dgst 出参,哈希摘要
 */
void sm3_digest(const uint8_t *msg, size_t msglen,
                uint8_t dgst[SM3_DIGEST_SIZE]) {
    SM3_CTX ctx;
    sm3_init(&ctx);
    sm3_update(&ctx, msg, msglen);
    sm3_finish(&ctx, dgst);
}
四、参考资料
[1] 国家密码管理局,SM3密码杂凑算法, 2010.
 [2] 王小云,于红波.SM3密码杂凑算法[J].信息安全研究,2016,2(11):983-994.
 [3] Douglas R. Stinson 著,冯登国 等译,密码学原理与实践(第三版),电子工业出版社,2016.

![[C语言][小游戏][猜拳游戏]](https://img-blog.csdnimg.cn/8d9cebd0b7e6408ba0e5ddd973a8cfcb.jpeg)








![常用数据分类算法原理介绍、优缺点分析与代码实现[LR/RF/DT/SVM/NavieBayes/GBDT/XGBoost/DNN/LightGBM等]](https://img-blog.csdnimg.cn/img_convert/efaea374885bff814a48b6cd39b6ac8e.png)








