国密算法Go语言实现(详解)(一) ——SM3(杂凑算法)

原创代码:https://github.com/ZZMarquis/gm

引用时,请导入原创代码库。本文仅以注释方式详解代码逻辑,供学习研究使用。

对原创代码的修改内容

  1. 修改了部分常量、变量、结构体属性的名称, 以便与GO语言标准包规范相统一
  2. 加入中文注释,解释代码逻辑
  3. 在SM3算法中,将常数BlockSize修改为“字节长度”而不是“字长”,与GO语言标准包相统一

注释者及联系邮箱

Paul Lee
paul_lee0919@163.com

出于兴趣和业务拓展的需要,开始对Hyperledger Fabric和GO语言进行系统的学习。目前的一个主要工作目标是对Fabric 2.1进行“国密改造”。

所幸之前已有诸位大牛完成了国密算法Go语言实现的源代码开发,借用开源代码即可,不必再重新发明轮子。只是出于律师的职业病,还是严格依据国密局公布的国家标准,对代码库一一进行核实验证。在验证代码的过程中,发现了大牛ZZMarquis开发的这个GM包,逻辑清晰、充分借用了go语言标准库的既有模块,而且可以看出,基于国密推荐的加密参数结合近世代数理论,已经对算法和代码进行了相当程度的效率优化。

将学习代码逻辑、核对国标验算过程的注释,连同源代码一并在此发布,希望对后来者能有些帮助。

const (
	// Size 代表SM3哈希摘要以“字节”为计量单位核算的长度。
	Size = 32
	// BlockSize 代表迭代压缩前,输入消息分组时,长度相等的分组数据块以“字节”为计量单位核算的长度。
	BlockSize = 64
)

以上为SM3算法的两个常数设置,出于与go语言标准库的一致性考虑,将原创代码的DigestLength改成了Size。

  1. Size 代表SM3哈希摘要的字节长度
  2. BlockSize 代表消息分组的字节长度
// gT 为SM3国密算法中的常量T(j)随下标j变化而进行左移j位运算(详见国标5.3.3获取中间变量SS1的算法)的计算结果数组,其中:
// (1) (0 <= j <= 15)时, T(j) = 0x79CC4519
// (2) (16 <= j <= 63)时, T(j) = 0x7A879D8A
// (3) 左移j位和左移(j mod 32)位的运算结果相同
// (4) 具体实现的算法见函数PrintT()
var gT = []uint32{
	0x79CC4519, 0xF3988A32, 0xE7311465, 0xCE6228CB, 0x9CC45197, 0x3988A32F, 0x7311465E, 0xE6228CBC,
	0xCC451979, 0x988A32F3, 0x311465E7, 0x6228CBCE, 0xC451979C, 0x88A32F39, 0x11465E73, 0x228CBCE6,
	0x9D8A7A87, 0x3B14F50F, 0x7629EA1E, 0xEC53D43C, 0xD8A7A879, 0xB14F50F3, 0x629EA1E7, 0xC53D43CE,
	0x8A7A879D, 0x14F50F3B, 0x29EA1E76, 0x53D43CEC, 0xA7A879D8, 0x4F50F3B1, 0x9EA1E762, 0x3D43CEC5,
	0x7A879D8A, 0xF50F3B14, 0xEA1E7629, 0xD43CEC53, 0xA879D8A7, 0x50F3B14F, 0xA1E7629E, 0x43CEC53D,
	0x879D8A7A, 0x0F3B14F5, 0x1E7629EA, 0x3CEC53D4, 0x79D8A7A8, 0xF3B14F50, 0xE7629EA1, 0xCEC53D43,
	0x9D8A7A87, 0x3B14F50F, 0x7629EA1E, 0xEC53D43C, 0xD8A7A879, 0xB14F50F3, 0x629EA1E7, 0xC53D43CE,
	0x8A7A879D, 0x14F50F3B, 0x29EA1E76, 0x53D43CEC, 0xA7A879D8, 0x4F50F3B1, 0x9EA1E762, 0x3D43CEC5}

gT[ ]为SM3国密算法中的常量Tj随下标j变化而进行左移j位运算(详见国标5.3.3获取中间变量SS1的算法)的计算结果数组,其中:

  1. (0 <= j <= 15)时, 左移运算的初始值T0 = 0x79CC4519
  2. (16 <= j <= 63)时, 左移运算的初始值T16 = 0x7A879D8A
  3. 由于采用32位大端方式存储,所以左移j位和左移(j mod 32)位的运算结果相同
  4. 具体实现的算法见下文中的函数PrintT( )
// digest 为SM3算法哈希摘要类,属于私有类,仅SM3包内可以调用
type digest struct {
	v            [Size / 4]uint32      // v[8] 为迭代压缩运算结果的8个“字”寄存器,其存储值为中间运算结果或最终哈希值。
	inWords      [BlockSize / 4]uint32 // inWords[16] 为输入消息分组数据块(512位)再次拆分所形成的16个输入消息“字”数组,是后续扩展消息、迭代压缩运算的数据基础。
	endOfInWords int32                 // endOfInWords 代表持续写入输入消息时,inWords[]数组非空元素的尾部序号指针。
	w            [68]uint32            // w[68] 代表迭代压缩过程中的消息扩展“字”数组。
	inBuf        [4]byte               // inBuf[4] 为以“字节”为单位暂存输入消息的缓存数组,其目的旨在将输入消息凑够一个“字”的长度,即4个字节。
	endOfInBuf   int32                 // endOfInBuf 为inBuff[]数组非空元素的尾部序号指针。
	lenInBytes   int64                 // lenInBytes 为输入消息以“字节”为单位的长度,在“填充”过程中将折算成以“比特”为单位的长度。
}

digest 为SM3算法哈希摘要类,属于私有类,仅SM3包内可以调用。其中:

  • v[8] 为迭代压缩运算结果的8个“字”寄存器,其存储值为中间运算结果或最终哈希值。
  • inWords[16] 为输入消息分组数据块(512位)再次拆分所形成的16个输入消息“字”数组,是后续扩展消息、迭代压缩运算的数据基础。
  • endOfInWords 代表持续写入输入消息时,inWords[]数组非空元素的尾部序号指针。
  • w[68] 代表迭代压缩过程中的消息扩展“字”数组。
  • inBuf [ ] 为以“字节”为单位暂存输入消息的缓存数组,其目的旨在将输入消息凑够一个“字”的长度,即4个字节。
  • endOfInBuf 为inBuff[]数组非空元素的尾部序号指针。
  • lenInBytes 为输入消息以“字节”为单位的长度,在“填充”过程中将折算成以“比特”为单位的长度。
// New 创建digest的实例,并根据国标(GB/T 32905-2016)规定的初始值(IV)初始化字寄存器。
func New() hash.Hash {
	digest := new(digest)
	digest.Reset()
	return digest
}

New ( ) 函数: 创建digest的实例,并根据国标(GB/T 32905-2016)规定的初始值(IV)初始化字寄存器。

// Sum() 为GO语言hash标准接口类中Sum()方法的实现。
// 其功能是将输入消息与其哈希值连接在一个字节数组中,
// 方便数字指纹核实、数字签名等应用中同时获得输入消息和其哈希值。
func (digest *digest) Sum(b []byte) []byte {
	d1 := digest
	h := d1.checkSum()
	return append(b, h[:]...)
}

Sum( ) 为GO语言标准库hash.Hash接口类中Sum()方法的实现。其功能是将输入消息与其哈希值连接在一个字节数组中。

// Size 方法返回SM3哈希摘要以字节为计量单位核算的长度。
// 为GO语言hash标准接口类中规定的Size()方法的实现。
func (digest *digest) Size() int {
	return Size
}

Size( ) 方法返回SM3哈希摘要以字节为计量单位核算的长度。为GO语言hash标准接口类中规定的Size()方法的实现。

// BlockSize 是为了提高运算效率,给开发者提供的一个查询算法消息分组数据长度的接口。
// 尽管哈希算法可将任意长度的消息进行哈希运算,但如果输入消息的长度为分组数据块长度的整数倍,
// 则可以大大提高运算效率,降低运算次数。
// 返回的是单位消息分组以“字节”为单位核算的消息长度,与GO语言标准包SHA256的口径保持一致。
func (digest *digest) BlockSize() int {
	return BlockSize
}

BlockSize( ) 是为了提高运算效率,给开发者提供的一个查询算法消息分组数据长度的接口。尽管哈希算法可将任意长度的消息进行哈希运算,但如果输入消息的长度为分组数据块长度的整数倍,则可以大大提高运算效率,降低运算次数。返回的是以“字节”为单位核算的分组长度,与GO语言标准包SHA256的口径保持一致。

// Reset 为初始化或重置哈希摘要的方法。
// v[] 的首次赋值为国标规定的初始值IV。
func (digest *digest) Reset() {
	digest.lenInBytes = 0

	digest.endOfInBuf = 0
	for i := 0; i < len(digest.inBuf); i++ {
		digest.inBuf[i] = 0
	}

	for i := 0; i < len(digest.inWords); i++ {
		digest.inWords[i] = 0
	}

	for i := 0; i < len(digest.w); i++ {
		digest.w[i] = 0
	}

	digest.v[0] = 0x7380166F
	digest.v[1] = 0x4914B2B9
	digest.v[2] = 0x172442D7
	digest.v[3] = 0xDA8A0600
	digest.v[4] = 0xA96F30BC
	digest.v[5] = 0x163138AA
	digest.v[6] = 0xE38DEE4D
	digest.v[7] = 0xB0FB0E4E

	digest.endOfInWords = 0
}

Reset( )为初始化或重置哈希摘要的方法。v[ ] 的首次赋值为国标规定的初始值IV。

(待续)