当前位置:   article > 正文

国密算法Go语言实现(详解)(八) ——SM2(椭圆曲线公钥密码算法)_golang sm2

golang sm2

国密算法Go语言实现(详解)(八) ——SM2(椭圆曲线公钥密码算法)


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

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

对原创代码的修改内容

  1. 修改了部分常量、变量、结构体属性的名称, 以便与GO语言标准包规范相统一
  2. 加入中文注释,解释代码逻辑

注释者及联系邮箱

Paul Lee
paul_lee0919@163.com

// xor 函数是将国标3-6.1.A5和3-6.1.A6两步结合到一起的异或函数,
// 其计算结果返回调用来源函数kdf(),
// 从而可在计算中间变量t的同时异或、拼接获得C2:
// (1) data 为输入明文消息M
// (2) kdfOut[] 为秘钥派生函数输出缓存buf[]
// (3) dRemaining 为KDF()函数中标注输入消息数组encData[]每次调用xor()时,
// 阶段性“读”动作读取的字节数组元素个数
func xor(data []byte, kdfOut []byte, dRemaining int) {
	for i := 0; i != dRemaining; i++ {
		data[i] ^= kdfOut[i]
	}
}

// kdf 为SM2公钥加密算法中调用秘钥派生函数的操作步骤(国标4-6.1.A5):
// (1) 按照哈希摘要字节长度创设缓存切片buf[]
// (2) 以公钥P的k倍点坐标(c1x, c1y)和输入明文消息M(长度为klen位)为输入参数
// (3) 按照国标4-5.4.3定义的秘钥派生函KDF()和国标第4-6.1.A5规定的算法推算中间变量t
// (4) t=KDF(c1x||c1y, klen), 该算法核心是迭代调用Hash(c1x||c1y||ct),其中:
//     (a) ct为32位整数计数器, 从1起算
//     (b) 调用次数为klen/v向上取整次
//     (c) v代表哈希摘要的位数长度(SM3为256位)
//     (d) 最后一次调用若明文M剩余长度小于v, 则取有值的字节
// (5) C2=M^t, 即通过xor()在计算中间变量t的过程中将中间结果与M的对应字节进行异或运算
func kdf(digest hash.Hash, c1x *big.Int, c1y *big.Int, encData []byte) {
	// 4个字节为32位字长
	bufSize := 4
	if bufSize < digest.Size() {
		// SM3哈希算法的摘要长度为32字节(256位),所以,此处取值将为32
		bufSize = digest.Size()
	}
	buf := make([]byte, bufSize)

	// 输入消息的字节数组长度,根据国标第2部分5.4.3定义,其值应小于(2^32-1)*v
	// 鉴于SM3的哈希值长度v为256位,所以,klen应当小于(2^32-1)*2^8
	encDataLen := len(encData)

	// 加密算法中,(c1x, c1y)为公钥P的k倍点(k为加密过程中产生随机整数)
	c1xBytes := c1x.Bytes()
	c1yBytes := c1y.Bytes()

	// encData[]元素序号“读”指针
	off := 0
	// 32位计数器
	ct := uint32(0)
	for off < encDataLen {
		digest.Reset()
		digest.Write(c1xBytes)
		digest.Write(c1yBytes)
		ct++
		binary.BigEndian.PutUint32(buf, ct)
		//ct为32位计数器,占4个字节,所以Write()方法仅需要读取到buf[:4]
		digest.Write(buf[:4])

		// 循环写入哈希值H(c1x || c2y || ct)到缓存数组buf[]
		tmp := digest.Sum(nil)
		copy(buf[:bufSize], tmp[:bufSize])

		xorLen := encDataLen - off
		if xorLen > digest.Size() {
			xorLen = digest.Size()
		}
		xor(encData[off:], buf, xorLen)
		off += xorLen
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

kdf( ) 为SM2公钥加密算法中调用秘钥派生函数的操作步骤(国标4-6.1.A5):

  • (1) 按照哈希摘要字节长度创设缓存切片buf[]
  • (2) 以公钥P的k倍点坐标(c1x, c1y)和输入明文消息M(长度为klen位)为输入参数
  • (3) 按照国标4-5.4.3定义的秘钥派生函KDF()和国标第4-6.1.A5规定的算法推算中间变量t
  • (4) t=KDF(c1x||c1y, klen), 该算法核心是迭代调用Hash(c1x||c1y||ct),其中:
  • ----(a) ct为32位整数计数器, 从1起算
  • ----(b) 调用次数为klen/v向上取整次
  • ----( c ) v代表哈希摘要的位数长度(SM3为256位)
  • ----(d) 最后一次调用若明文M剩余长度小于v, 则取有值的字节
  • (5) C2=M^t, 即通过xor()在计算中间变量t的过程中将中间结果与M的对应字节进行异或运算
// notEncrypted 为国标3-6.1.A5中判断中间变量t是否为全0比特串的判断函数。
// 如果C2与输入消息M每个字节都相等,就意味着在3-6.1.A6进行异或计算(C2=M^t)时,中间变量t所有字节均为0。
// 此时,应当重新选择随机数k,重启加密计算流程。
func notEncrypted(encData []byte, in []byte) bool {
	encDataLen := len(encData)
	for i := 0; i != encDataLen; i++ {
		if encData[i] != in[i] {
			return false
		}
	}
	return true
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

notEncrypted( ) 为国标3-6.1.A5中判断中间变量t是否为全0比特串的判断函数。如果C2与输入消息M每个字节都相等,就意味着在3-6.1.A6进行异或计算(C2=M^t)时,中间变量t所有字节均为0。此时,应当重新选择随机数k,重启加密计算流程。

// Encrypt 为SM2加密函数:
// (1) 输入参数为: 公钥PB点(pub.X, pub.Y), 明文消息字节数组 in[], 密文类别标识 cipherTextType
// (2) 生成随机数k, k属于区间[1,N-1]
// (3) 利用标准包elliptic的方法CurveParams.ScalarBaseMult()生成倍点C1=kG=(c1x, c1y)
// (4) 由于SM2推荐曲线为素数域椭圆曲线,其余因子h=1,此时,点S=[h]PB就是公钥PB点,不可能为无穷远点O,
// 所以,国标4-6.1.A3被省略
// (5) 利用标准包elliptic的方法CurveParams.ScalarBaseMult()生成倍点kPB=(kPBx, kPBy)
// (6) 调用改进后的秘钥派生函数kdf(), 生成C2
func Encrypt(pub *PublicKey, in []byte, cipherTextType CipherTextType) ([]byte, error) {
	c2 := make([]byte, len(in))
	copy(c2, in)
	var c1 []byte
	digest := sm3.New()
	var kPBx, kPBy *big.Int
	for {
		// 利用标准库crypto/rand获取随机数k
		k, err := nextK(rand.Reader, pub.Curve.N)
		if err != nil {
			return nil, err
		}
		kBytes := k.Bytes()
		// 利用标准库elliptic的方法CurveParams.ScalarBaseMult()计算倍点C1=kG=(c1x, c1y)
		c1x, c1y := pub.Curve.ScalarBaseMult(kBytes)

		// 将公钥曲线与C1点的坐标参数序列化。
		c1 = elliptic.Marshal(pub.Curve, c1x, c1y)

		// 利用标准库elliptic的方法CurveParams.ScalarMult()计算倍点kPB=(kPBx, kPBy)
		kPBx, kPBy = pub.Curve.ScalarMult(pub.X, pub.Y, kBytes)

		// 利用改造后的秘钥派生函数推算C2
		kdf(digest, kPBx, kPBy, c2)

		// 若中间变量t全部字节均为0则重启加密运算(详见国标4-6.1.A5)
		if !notEncrypted(c2, in) {
			break
		}
	}

	// 推算C3=Hash(kPBx || M || kPBy),详见国标4-6.1.A7
	digest.Reset()
	digest.Write(kPBx.Bytes())
	digest.Write(in)
	digest.Write(kPBy.Bytes())
	c3 := digest.Sum(nil)

	// 根据密文格式标识的选择输出密文(C1C3C2新国准,或C1C2C3旧国标)
	c1Len := len(c1)
	c2Len := len(c2)
	c3Len := len(c3)
	result := make([]byte, c1Len+c2Len+c3Len)
	if cipherTextType == C1C2C3 {
		copy(result[:c1Len], c1)
		copy(result[c1Len:c1Len+c2Len], c2)
		copy(result[c1Len+c2Len:], c3)
	} else if cipherTextType == C1C3C2 {
		copy(result[:c1Len], c1)
		copy(result[c1Len:c1Len+c3Len], c3)
		copy(result[c1Len+c3Len:], c2)
	} else {
		return nil, errors.New("unknown cipherTextType:" + string(cipherTextType))
	}
	return result, nil
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

Encrypt( ) 为SM2加密函数:

  • (1) 输入参数为: 公钥PB点(pub.X, pub.Y), 明文消息字节数组 in[ ], 密文类别标识 cipherTextType
  • (2) 生成随机数k, k属于区间[1,N-1]
  • (3) 利用标准包elliptic的方法CurveParams.ScalarBaseMult()生成倍点C1=kG=(c1x, c1y)
  • (4) 由于SM2推荐曲线为素数域椭圆曲线,其余因子h=1,此时,点S=[h]PB就是公钥PB点,不可能为无穷远点O,所以,国标4-6.1.A3被省略
  • (5) 利用标准包elliptic的方法CurveParams.ScalarBaseMult()生成倍点kPB=(kPBx, kPBy)
  • (6) 调用调整后的秘钥派生函数kdf(), 生成C2
// Decrypt 为SM2算法利用私钥解密(国标4-7.1)的函数:
// (1) 读取C1
// (2) 反序列化同时校验C1点是否位于私钥曲线上
// (3) 校验S点(S=[h]C1)是否为无穷远点O
// (4) 私钥推算倍点[d]C1
// (5) 采用改造后的kdf()函数,计算并获取解密后的明文消息M'=C2^t
// (6) 计算u=Hash(c1x || M' || c2y)并与C3诸位比较
// (7) 返回解密后的明文消息M'
func Decrypt(priv *PrivateKey, in []byte, cipherTextType CipherTextType) ([]byte, error) {
	// 根据算法字长读取C1
	c1Len := ((priv.Curve.BitSize+7)/8)*2 + 1
	c1 := make([]byte, c1Len)
	copy(c1, in[:c1Len])

	// 读取C1点坐标(c1x, c1y),并校验是否位于曲线上(标准库方法elliptic.Unmarshal()内部调用)
	c1x, c1y := elliptic.Unmarshal(priv.Curve, c1)

	// 校验S点是否为无穷远点(SM2推荐曲线h为1,S点即为C1点, 本步骤可忽略)
	sx, sy := priv.Curve.ScalarMult(c1x, c1y, sm2H.Bytes())
	if util.IsEcPointInfinity(sx, sy) {
		return nil, errors.New("[h]C1 at infinity")
	}

	// 根据私钥(priv.D)和曲线计算倍点[priv.D]C1=(c1x, c1y)
	c1x, c1y = priv.Curve.ScalarMult(c1x, c1y, priv.D.Bytes())

	// 根据密文格式,分别读取C2和C3
	digest := sm3.New()
	c3Len := digest.Size()
	c2Len := len(in) - c1Len - c3Len
	c2 := make([]byte, c2Len)
	c3 := make([]byte, c3Len)
	if cipherTextType == C1C2C3 {
		copy(c2, in[c1Len:c1Len+c2Len])
		copy(c3, in[c1Len+c2Len:])
	} else if cipherTextType == C1C3C2 {
		copy(c3, in[c1Len:c1Len+c3Len])
		copy(c2, in[c1Len+c3Len:])
	} else {
		return nil, errors.New("unknown cipherTextType:" + string(cipherTextType))
	}

	// 采用改造后的kdf()函数,计算并获取解密后的明文消息M'=C2^t(国标4-7.1.B4-B5)
	kdf(digest, c1x, c1y, c2)

	// 计算u=Hash(c1x || M' || c2y)(国标4-7.1.B6-1)
	digest.Reset()
	digest.Write(c1x.Bytes())
	digest.Write(c2)
	digest.Write(c1y.Bytes())
	newC3 := digest.Sum(nil)

	// 将u与C3逐位比较(国标4-7.1.B6-2)
	if !bytes.Equal(newC3, c3) {
		return nil, errors.New("invalid cipher text")
	}

	// 返回明文消息M'
	return c2, nil
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

Decrypt( ) 为SM2算法利用私钥解密(国标4-7.1)的函数:

  • (1) 读取C1
  • (2) 反序列化同时校验C1点是否位于私钥曲线上
  • (3) 校验S点(S=[h]C1)是否为无穷远点O
  • (4) 私钥推算倍点[d]C1
  • (5) 采用改造后的kdf()函数,计算并获取解密后的明文消息M’=C2^t
  • (6) 计算u=Hash(c1x || M’ || c2y)并与C3诸位比较
  • (7) 返回解密后的明文消息M’

(未完待续)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/483321
推荐阅读
相关标签
  

闽ICP备14008679号