LJgibbs · 2020年04月17日

以SM3算法为例,构建一个软硬协作算法加速器:SM3 软件实现篇

本文是本系列第二篇,我们将通过分析一个 SM3 的开源软件实现,来进一步了解算法的实现流程和软件实现思路
首发知乎:https://zhuanlan.zhihu.com/p/89264895

在 GitHub 上搜索 SM3 可以得到很多,各种语言实现的结果(当然也有 Verilog,比较少就是了)。
v2-5be255a92a64f7ff6930b5e8d1ee46ee_1440w.jpg

     Github SM3 搜索结果

这里我们选择点赞数最高的 GmSSL,支持国密SM2/SM3/SM4/SM9/ZUC/SSL的OpenSSL分支

http://gmssl.org/

在以下目录可以找到独立的 SM3 实现源代码

<u><font color="#0b0118"></font></u>https://github.com/guanzhi/GmSSL/tree/master/crypto/sm3

SM3 的实现都在 sm3.c 中。sm3\_hmac.c 中实现的是基于 sm3 实现的上层加密认证协议,这里我们暂不研究。
v2-fd9a0a51967ef9a538c7d927a791a116_1440w.jpg
                                      cropty/sm3

首次运行

PS:笔者看了下,目前的代码相比笔者当时研究的代码已经有了一些改动,但应该没有太大区别,还是以我当时的代码为准。

准备头文件,运行还需要两个头文件。从 openssl/sm3.h ; internal/byteorder.h 下载头文件

因为我们只需要运行 SM3 这部分,所以可以直接把头文件下载后放到当前目录,并修改 SM3.c 的头文件 include 部分。

编译时会产生很多错误。

这是因为使用的 uint32\_t 在运行环境中没有定义,这里可以手动使用 unsigned int 代替,可以使用 sizeof( unsigned int ) 查看变量长度,一般平台上就是 32 字节的,所以没有问题。
v2-2fa5f3cf14ab16f36e37d94bae33f9ee_1440w.png
上方的报错是因为我们独立编译 SM3.c 没有定义主函数导致的,这里我们增加一个测试用的主函数。

int main(int argc, char *argv[])
{
    unsigned int len = 16;
    unsigned char *str = "1234567812345678";
    unsigned char res[32];

    unsigned long long start,end;
    //start = clock();
    sm3(str, len, res);

    //end = clock();
    //printf("time=%lld\n",end-start);
    unsigned long long *long_ptr = (unsigned long long*)res;
    printf("i=%d\n", len);
    for (int j = 0; j < 4; j++)
    {
        printf("%llX:\n",(*long_ptr));
                long_ptr+=1;
        }

    system("pause");
}

这样就可以得到运行结果:
v2-25323bc279ec24ddb4ca26509336e5d6_1440w.jpg

数据结构与宏定义

sm3\_ctx\_t 结构体用于存储 SM3 运算流程中的信息和中间变量

其中包括:

  • 8 个 32 位寄存器,组成 digest[8] 数组,在迭代期间缓存寄存器值。
  • 消息块数量 nblocks,使用一个 64 位变量存储,理论上支持最多 2^64 个数据块。
  • 缓存当前消息块的 block[64] 数组, 存有当前 512bit 消息块。
  • num 是一个中间临时变量,用作数组的索引,指向消息的特定字节

宏定义中定义

  • 常量初值
  • 迭代函数 P0 ,P1,FF00,FF16等
  • 循环移位函数 ROTL

用于后续的迭代压缩函数。

代码结构

主函数中调用 SM3 函数开始一次计算,传入消息指针,消息的长度以及存放结果的指针,SM3 函数中依次调用 init,update,以及 final 函数进行消息的填充分块,消息扩展以及压缩,并将杂凑值存到结果指针中。

SM3 函数相当于 SM3 运算的接口,依次调用函数,中间变量使用 sm3\_ctx\_t 结构体传递。

sm3\_init 函数在第一个消息块时使用,使用 SM3 规定的初始值初始化 8 个寄存器。
v2-987f3d6edef4fa45abc9ec491f86b1ae_1440w.jpg

sm3\_update 函数处理最后一个之前的数据块的迭代压缩,这些数据块不用考虑消息扩展,只需要对总消息分块,进行迭代压缩即可。

这个函数的第一部分,笔者觉得在现有的工作模式应该是不可达的,欢迎大家讨论。

之后对数据分块,计算杂凑值,此时的数据块不需要考虑填充。记录剩下的数据量,即最后一个消息块的数量,拷贝最后一个消息块到结构体中
v2-987f3d6edef4fa45abc9ec491f86b1ae_1440w.jpg
sm3\_final 函数处理最后一个数据块,最后一个数据首先需要扩展,再对扩展后的数据块进行压缩。

消息扩展时首先填 1,然后填0, 有两种情况:
1.填0后消息<448b,当前块为最后一个消息块
2.填0后消息>448b,当前块为倒数第2个消息块,全部填充0后压缩,消息长度添加在后一个块中

最后填充消息长度,并压缩最后一个块。通过指针输出最终杂凑值,转为小端。

sm3\_compress 函数进行 64 轮迭代压缩,使用到的置乱函数通过宏定义的方式已经预先定义好了。在函数中进行消息扩展和迭代。迭代分为两个阶段,两阶段只是置乱函数 FF GG 定义有所不同,可以见宏定义。

最后输出摘要值。

void sm3_compress(unsigned int digest[8], const unsigned char block[64])
{
    unsigned int A = digest[0];
    unsigned int B = digest[1];
    unsigned int C = digest[2];
    unsigned int D = digest[3];
    unsigned int E = digest[4];
    unsigned int F = digest[5];
    unsigned int G = digest[6];
    unsigned int H = digest[7];
    const unsigned int *pblock = (const unsigned int *)block;
    unsigned int W[68], W1[64];
    unsigned int SS1, SS2, TT1, TT2;
    int j;

    //消息扩展
    for (j = 0; j < 16; j++)
        W[j] = cpu_to_be32(pblock[j]);

    for (; j < 68; j++)
        W[j] = P1(W[j - 16] ^ W[j - 9] ^ ROTL(W[j - 3], 15))
            ^ ROTL(W[j - 13], 7) ^ W[j - 6];

    for(j = 0; j < 64; j++)
        W1[j] = W[j] ^ W[j + 4];

    //迭代压缩阶段1
    for (j = 0; j < 16; j++) {
        SS1 = ROTL((ROTL(A, 12) + E + ROTL(T00, j)), 7);
        SS2 = SS1 ^ ROTL(A, 12);
        TT1 = FF00(A, B, C) + D + SS2 + W1[j];
        TT2 = GG00(E, F, G) + H + SS1 + W[j];
        D = C;
        C = ROTL(B, 9);
        B = A;
        A = TT1;
        H = G;
        G = ROTL(F, 19);
        F = E;
        E = P0(TT2);
    }
    //迭代压缩阶段2 使用的 FF 和 GG 函数不同
    for (; j < 64; j++) {
        SS1 = ROTL((ROTL(A, 12) + E + ROTL(T16, j % 32)), 7);
        SS2 = SS1 ^ ROTL(A, 12);
        TT1 = FF16(A, B, C) + D + SS2 + W1[j];
        TT2 = GG16(E, F, G) + H + SS1 + W[j];
        D = C;
        C = ROTL(B, 9);
        B = A;
        A = TT1;
        H = G;
        G = ROTL(F, 19);
        F = E;
        E = P0(TT2);
    }

    //输出摘要值
    digest[0] ^= A;
    digest[1] ^= B;
    digest[2] ^= C;
    digest[3] ^= D;
    digest[4] ^= E;
    digest[5] ^= F;
    digest[6] ^= G;
    digest[7] ^= H;
}

结语

本文我们通过学习一个 SM3 软件开源项目的实现,进一步了解了 SM3 的实现过程。同时也了解软件实现的思路:首先依次将数据分块,缓存一块后迭代计算,在最后一块上填充消息后迭代计算。是一种基于顺序计算和缓存的软件思维。我们后面将分析一个开源硬件代码,包有你今天得到的软件思维,之后我们再来个软硬思维的对比。

系列篇:
以SM3算法为例,构建一个软硬协作算法加速器:算法篇

更多FPGA相关知识请关注我的FPGA 的逻辑专栏
推荐阅读
关注数
10579
内容数
550
FPGA Logic 二三事
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息