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


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

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

对原创代码的修改内容

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

注释者及联系邮箱

Paul Lee
paul_lee0919@163.com

国标(GMT 0003.3-2012, 以下简称“国标”) 规定的SM2算法秘钥交换协议

/*国标(GMT 0003.3-2012, 以下简称“国标”) 规定的SM2算法秘钥交换协议
*/// ExchangeResult 为国标规定的最后推导出的秘钥交换协议的结果:
// Key 为共享秘钥,比如SM4秘钥
// S1 为校验B用户身份的可选中间参数,其哈希函数输入参数的头部为0x02
// S2 为校验A用户身份的可选中间参数,其哈希函数输入参数的头部为0x03
type ExchangeResult struct {Key []byteS1  []byteS2  []byte
}

ExchangeResult 为国标规定的最后推导出的秘钥交换协议的结果, 其中:
(1) Key 为共享秘钥,比如SM4秘钥
(2) S1 为校验B用户(应答用户)身份的可选中间参数,其哈希函数输入参数的头部为0x02
(3) S2 为校验A用户(发起用户)身份的可选中间参数,其哈希函数输入参数的头部为0x03

// reduce 为国密算法中获取(x拔)的中间函数, 详见国标6.1的A4/A6和B3/B5,其中:
// 1. Lsh() 为左位移方法,将整数1左移w位,相当于获取2^w
// 2. SetBit(x, i, b) 为设定整数x第i位为b的函数,当b为1时,相当于x | (1<<i)
// 3. x拔在国标中的定义为: 2^w + (x & (2^w - 1)), 其中:
// (1) 2^w二进制表示为w位为1,后续其他位均为0,因此
// (2) 若A = 2^w - 1, 则计算结果A的第w位的值必定为0,因此
// (2) 若B = x & A, 则与运算结果B的第w位也必定为0, 因此
// (3) SetBit (reulst, w, 1) 相当于result + 2^w
// 综上,reduce的计算结果就是 (x拔) =  2^w + (x & (2^w - 1))
func reduce(x *big.Int, w int) *big.Int {intOne := new(big.Int).SetInt64(1)result := util.Lsh(intOne, uint(w))result = util.Sub(result, intOne)result = util.And(x, result)result = util.SetBit(result, w, 1)return result
}

reduce 为国密算法中获取(x拔)的中间函数, 详见国标6.1的A4/A6和B3/B5,其中:

  1. Lsh() 为左位移方法,将整数1左移w位,相当于获取2w
  2. SetBit(x, i, b) 为设定整数x第i位为b的函数,当b为1时,相当于x | (1<<i)
  3. x拔在国标中的定义为: 2w+ (x & (2w - 1)), 其中:
    // (1) 2w二进制表示为: w位为1, 后续其他位均为0,因此
    // (2) 若A = 2w - 1, 则计算结果A的第w位的值必定为0,因此
    // (2) 若B = x & A, 则与运算结果B的第w位也必定为0, 因此
    // (3) SetBit (reulst, w, 1) 相当于result + 2w
    // 综上,reduce的计算结果就是 (x拔) = 2w + (x & (2w - 1))
  4. 另外,分析w取值,其实为基点G阶数n二进制比特位数长度的一半,x & (2w - 1) 其实是按照n位数长度一半截取x的值,再加上2w, 相当于在w位加标签“1”
// calculateU 为推导共享秘钥(曲线上关键点U)的函数,其中:
// x1 为己方临时公钥点R1的x值所对应的x拔: x1 =  2^w + (x1 & (2^w - 1))
// x2 为对方临时公钥点R2的x值所对应的x拔: x2 = 2^w + (x2 & (2^w - 1))
// tA 为己方临时私钥r1乘x1加永久私钥d1,之后对基点阶数n取模所得的tA = (d1 + x1.r1) mod n
// sm2H 为SM2曲线余因子h, 应当为曲线点个数#E(Fq)除以基点G阶数n的商,对SM2推荐曲线而言,h=1
// 关键点U = h*tA*(P2 + x2*R2) = h*tA*P2 + h*tA*x2*R2 = k1*P2 + k2*R2
// 值得注意的是,取模计算被从tA计算,挪到了k1和k2步骤,从取模运算的乘法交换律来看,结果并没有影响,
// 但可以尽量让k2模运算后的结果更小,进而降低后续步骤的运算压力。
func calculateU(w int, selfStaticPriv *PrivateKey, selfEphemeralPriv *PrivateKey, selfEphemeralPub *PublicKey,otherStaticPub *PublicKey, otherEphemeralPub *PublicKey) (x *big.Int, y *big.Int) {x1 := reduce(selfEphemeralPub.X, w)x2 := reduce(otherEphemeralPub.X, w)tA := util.Mul(x1, selfEphemeralPriv.D)tA = util.Add(selfStaticPriv.D, tA)k1 := util.Mul(sm2H, tA)k1 = util.Mod(k1, selfStaticPriv.Curve.N)k2 := util.Mul(k1, x2)k2 = util.Mod(k2, selfStaticPriv.Curve.N)p1x, p1y := selfStaticPriv.Curve.ScalarMult(otherStaticPub.X, otherStaticPub.Y, k1.Bytes())p2x, p2y := selfStaticPriv.Curve.ScalarMult(otherEphemeralPub.X, otherEphemeralPub.Y, k2.Bytes())x, y = selfStaticPriv.Curve.Add(p1x, p1y, p2x, p2y)return
}

calculateU 为推导共享秘钥(曲线上关键点U)的函数,其中:

  1. x1 为己方临时公钥点R1的x值所对应的x拔: x1 = 2w + (x1 & (2w - 1))
  2. x2 为对方临时公钥点R2的x值所对应的x拔: x2 = 2w + (x2 & (2w - 1))
  3. tA 为己方临时私钥r1乘x1加永久私钥d1,之后对基点阶数n取模所得的tA = (d1 + x1 * r1) mod n
  4. sm2H 为SM2曲线余因子h, 应当为曲线点个数#E(Fq)除以基点G阶数n的商,对SM2推荐曲线而言,h=1
  5. 关键点U(xu, yu) = h * tA * (P2 + [x2]*R2) = h * tA * P2 + h * t A * x2 * R2 = k1 * P2 + k2 * R2
  6. 值得注意的是,取模计算被从国标规定的tA计算步骤,挪到了k1和k2步骤,从取模运算的乘法交换律来看,结果并没有影响,但可以尽量让k2模运算后的结果更小,进而降低后续步骤的运算压力。
// kdfForExch 为秘钥派生函数,其中输入参数为:
// (1) digest 为哈希函数哈希值实例,采用SM3算法,产生256位哈希值
// (2) ux, uy 为共享秘钥关键点U的有限域坐标
// (3) za, zb 为交换秘钥双方按国标规定生成的识别字, 例如:Za = H256 (ENTLa || IDa || a || b || XG || YG || xa || ya)
// (4) keyBits 为秘钥位数长度
// 输出值为: 长度为keyBit的秘钥位串
// 算法核心逻辑:
// (1) 按目标输出秘钥位数长度klen整除哈希算法输出值位数v的次数(klen/v向上取整),取输入值Z加“盐”的哈希,
// (2) 每次哈希运算所加入的“盐”,为32位计数器ct所计次数(i=1,2...ceiling(klen/v))
// (3) 将历次哈希运算结果首位相接,形成位串
// (4) 对于运算结果超出klen目标长度的部分,截尾丢弃,或者说,最后的位串,靠左取值
// 例如:SM3输出256位哈希值,而SM4秘钥为128位,则仅需要进行1次加盐哈希运算,然后,将哈希值取前128位,即可输出
func kdfForExch(digest hash.Hash, ux, uy *big.Int, za, zb []byte, keyBits int) []byte {bufSize := 4                      // 4字节,32位运算的字长if bufSize < digest.BlockSize() { // 分组字节长度,对于SM3而言,为32bufSize = digest.BlockSize()}buf := make([]byte, bufSize)rv := make([]byte, (keyBits+7)/8) // 按klen向上取整创设字节数组, result valuervLen := len(rv)                  // 对SM4而言,为16,代表128位长度uxBytes := ux.Bytes()             // 将big.Int转换成[]byteuyBytes := uy.Bytes()off := 0ct := uint32(0)for off < rvLen {digest.Reset()digest.Write(uxBytes)digest.Write(uyBytes)digest.Write(za)digest.Write(zb)ct++binary.BigEndian.PutUint32(buf, ct) // 缓存数组buf用了两次: 转换计数器 + 周转哈希值digest.Write(buf[:4])tmp := digest.Sum(nil) // len(tmp) == bufSizecopy(buf[:bufSize], tmp[:bufSize])if rvlen <= bufSize {copyLen := rvLen - off // 仅适用于 rvlen <= bufSize 情形copy(rv[off:off+copyLen], buf[:copyLen])off += copyLen} else {copyLen := bufSize // 新增加内容,其实,国密算法中,哈希函数SM3为256位,而SM2为256位、SM4为128位,因此,不适用copy(rv[off:off+copyLen], buf[:copyLen])off += bufSize}}return rv
}

kdfForExch 为秘钥派生函数(国标5.4.3),其中:

  1. 输入参数为:
    (1) digest 为哈希函数哈希值实例,采用SM3算法,产生256位哈希值
    (2) ux, uy 为共享秘钥关键点U的有限域坐标
    (3) za, zb 为交换秘钥双方按国标规定生成的识别字, 例如:Za = H256 (ENTLa || IDa || a || b || XG || YG || xa || ya)
    (4) keyBits 为秘钥位数长度
  2. 输出值为: 长度为keyBit的秘钥位串
  3. 算法核心逻辑:
    (1) 按目标输出秘钥位数长度klen整除哈希算法输出值位数v的次数(klen/v向上取整),取输入值Z加“盐”的哈希,
    (2) 每次哈希运算所加入的“盐”,为32位计数器ct所计次数(i=1,2…ceiling(klen/v))
    (3) 将历次哈希运算结果首位相接,形成位串
    (4) 对于运算结果超出klen目标长度的部分,截尾丢弃,或者说,最后的位串,靠左取值
    例如:SM3输出256位哈希值,而SM4秘钥为128位,则仅需要进行1次加盐哈希运算,然后,将哈希值取前128位,即可输出。
// s1 为根据协商应答者临时公钥、公钥、Z值等参数推算的可选校验值,其输入值头部标签为0x02
func s1(digest hash.Hash, uy *big.Int, innerHash []byte) []byte {digest.Reset()digest.Write([]byte{0x02})digest.Write(uy.Bytes())digest.Write(innerHash)return digest.Sum(nil)
}// s2 为根据协商发起者临时公钥、公钥、Z值等参数推算的可选校验值,其输入值头部标签为0x03
func s2(digest hash.Hash, uy *big.Int, innerHash []byte) []byte {digest.Reset()digest.Write([]byte{0x03})digest.Write(uy.Bytes())digest.Write(innerHash)return digest.Sum(nil)
}// CalculateKeyWithConfirmation 为SM2秘钥交换算法的主函数入口,其中:
// 1. 前部为准备函数, 基于用户ID、ENTL、基础曲线参数和公钥,准备Z值
// 2. 后半部按国标算法,推算关键点U,进而推算Key、S1和S2
// 3. 当协商发起人调用时,应当已经获得对方应答的Sb值,进而需要校验Sb == S1
// 4. 若不是发起人,则调用时仅需要计算得出Key、S1、Sb,无需在本函数中校验S值
func CalculateKeyWithConfirmation(initiator bool, keyBits int, confirmationTag []byte,selfStaticPriv *PrivateKey, selfEphemeralPriv *PrivateKey, selfId []byte,otherStaticPub *PublicKey, otherEphemeralPub *PublicKey, otherId []byte) (*ExchangeResult, error) {if selfId == nil {selfId = make([]byte, 0)}if otherId == nil {otherId = make([]byte, 0)}if initiator && confirmationTag == nil {return nil, errors.New("if initiating, confirmationTag must be set")}selfStaticPub := CalculatePubKey(selfStaticPriv)digest := sm3.New()za := getZ(digest, &selfStaticPriv.Curve, selfStaticPub.X, selfStaticPub.Y, selfId)zb := getZ(digest, &selfStaticPriv.Curve, otherStaticPub.X, otherStaticPub.Y, otherId)w := selfStaticPriv.Curve.BitSize/2 - 1selfEphemeralPub := CalculatePubKey(selfEphemeralPriv)ux, uy := calculateU(w, selfStaticPriv, selfEphemeralPriv, selfEphemeralPub, otherStaticPub, otherEphemeralPub)if initiator {rv := kdfForExch(digest, ux, uy, za, zb, keyBits)innerHash := calculateInnerHash(digest, ux, za, zb, selfEphemeralPub.X, selfEphemeralPub.Y,otherEphemeralPub.X, otherEphemeralPub.Y)s1 := s1(digest, uy, innerHash)if !bytes.Equal(s1, confirmationTag) { // 比较Sb和S1值是否相等return nil, errors.New("confirmation tag mismatch")}s2 := s2(digest, uy, innerHash)return &ExchangeResult{Key: rv, S2: s2}, nil} else {rv := kdfForExch(digest, ux, uy, zb, za, keyBits)innerHash := calculateInnerHash(digest, ux, zb, za, otherEphemeralPub.X, otherEphemeralPub.Y,selfEphemeralPub.X, selfEphemeralPub.Y)s1 := s1(digest, uy, innerHash)s2 := s2(digest, uy, innerHash)return &ExchangeResult{Key: rv, S1: s1, S2: s2}, nil}
}// ResponderConfim 为秘钥协商应答主体调用的S值校验函数(国标6.1.B10)
func ResponderConfirm(responderS2 []byte, initiatorS2 []byte) bool {return bytes.Equal(responderS2, initiatorS2)
}

CalculateKeyWithConfirmation 为SM2秘钥交换算法的主函数入口,其中:

  1. 前部为准备函数, 基于用户ID、ENTL、基础曲线参数和公钥,准备Z值
  2. 后半部按国标算法,推算关键点U,进而推算Key、S1和S2
  3. 当协商发起人调用时,应当已经获得对方应答的Sb值,进而需要校验Sb == S1
  4. 若不是发起人,而是应答方调用本函数时,则仅需要计算得出Key、S1、S2,无需在本函数中校验S值
  5. 在ResponderConfirm函数中,应答方可校验S2 == Sa

国密算法Go语言实现(详解)(十) ——SM2(椭圆曲线公钥密码算法)相关推荐

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

    国密算法Go语言实现(详解)(九) --SM2(椭圆曲线公钥密码算法) 原创代码:https://github.com/ZZMarquis/gm 引用时,请导入原创代码库.本文仅以注释方式详解代码逻辑 ...

  2. SM2椭圆曲线公钥密码算法的C语言实现(基于Miracl大数运算库)

    SM2椭圆曲线公钥密码算法的C语言实现(基于Miracl大数运算库) 实验环境 预备知识 FpF_pFp​ 及椭圆曲线 素域 FpF_pFp​ FpF_pFp​ 上的椭圆曲线 FpF_pFp​ 上椭圆 ...

  3. SM2椭圆曲线公钥密码算法(Python实现)

    本文目录 一.实验目的(包括实验环境.实现目标等等) 1. 实验环境 2. 实现目标 3. 实验中需要导入的库 二.方案设计(包括背景.原理.必要的公式.图表.算法步骤等等) 1. 实验背景 2. 实 ...

  4. 12、SM2椭圆曲线公钥密码算法

    参考推荐: 国家密码管理局关于发布<SM2椭圆曲线公钥密码算法>公告(国密局公告第21号)_国家密码管理局 https://blog.csdn.net/u013137970/article ...

  5. GMSSL :SM2椭圆曲线公钥密码算法——数字签名算法1

    2021SC@SDUSC 一.相关术语以及定义 二.数字签名算法 1.辅助函数 密码杂凑算法和随机数发生器 杂凑运算:这个输出串称为该消息的杂凑值. 就是一种可将一个 key 对应到一个索引的函数,一 ...

  6. SM2椭圆曲线公钥密码算法的JAVA实现

    2019独角兽企业重金招聘Python工程师标准>>> package com.zpc.cryptography;import java.io.ByteArrayInputStrea ...

  7. c语言动画原理,动画详解十大经典排序算法(C语言版)

    排序算法是程序员必备的基础知识,弄明白它们的原理和实现很有必要.本文中将通过非常细节的动画展示出算法的原理,配合代码更容易理解. 概述 由于待排序的元素数量不同,使得排序过程中涉及的存储器不同,可将排 ...

  8. GMSSL :SM2椭圆曲线公钥密码算法——数字签名算法4

    2021SC@SDUSC 目录 一.ECDSA介绍 二.代码分析 一.ECDSA介绍 ECDSA的全名是Elliptic Curve DSA,即椭圆曲线DSA.它是Digital Signature ...

  9. GMSSL :SM2椭圆曲线公钥密码算法-密钥交换协议

    2021SC@SDUSC 目录 一.整体架构 二.具体分析 前一篇文章写了密钥交换协议的基本流程,这一篇文章看一下代码实现 一.整体架构 整体来看这一部分有6个函数,相对重要的是序号3,4代表的函数 ...

最新文章

  1. ECMAScript 6 入门
  2. 使用squid在阿里云服务器(centos7)上搭建自己的代理服务器
  3. VTK:Qt之SideBySideRenderWindowsQt
  4. mysql把游标数据存入表中_利用Python爬股票数据并存入数据库Mysql
  5. Wpf控件ListBox使用实例2
  6. ABP vNext 自动注入,暗藏天坑如斯
  7. 【第十六章】模板实参推断
  8. 剑指offer不用加减乘除做加法_剑指Offer-不用加减乘除做加法
  9. NeHe OpenGL第三十二课:拾取游戏
  10. java单链表删除重复元素_用加头节点实现链表的删除重复元素、分割、翻转(Java实现)...
  11. 我精心梳了犀利的发型,胡子刮得一干二净,去字节跳动面试,结果被怼了!...
  12. 美图秀秀拼接渐变过渡_如何使用Web组件创建渐变过渡
  13. 《SQL145题》正式发布!
  14. 卷积神经网络实现人脸表情识别
  15. switch细节讨论
  16. 买电脑时,i5和i7处理器有什么区别?
  17. 论文阅读笔记五十:CornerNet: Detecting Objects as Paired Keypoints(ECCV2018)
  18. 通俗理解卷积神经网络
  19. kernelbase故障模块_kernelbase.dll故障修复 故障模块名称kernelbase
  20. 前端页面导入json格式数据

热门文章

  1. Windows下Redis安装和使用
  2. JavaScript基础知识总结 17:JavaScript表单脚本
  3. 12306的技术革命
  4. Corgi_Engine_----_2D_25D_Platformer_5.4 最新版本
  5. 支付宝发的计算机,支付宝电脑支付沙箱配置(JAVA)
  6. pcre 安装 linux,Linux下安装PCRE
  7. springboot profiles多环境打包
  8. macos支持exfat吗_360压缩发布免费,界面简洁的MacOS版本压缩软件
  9. Android开发:最详细的 Toolbar 开发实践总结
  10. 怎么查询2021年广东高考成绩,2021年广东省高考成绩查询入口官网http://eea.gd.gov.cn/...