SM2数字签名算法

签名算法的作用是用于证明“这些消息由我生成并发送的”,对于发送方而言就是用私钥 d 去和发送的消息做绑定,对于接收方而言就是确定发送方真的知道私钥 d。但是私钥是不能在网络上传输的,但是可以利用椭圆曲线离散对数问题,将 d 包装成一个点坐标,接收方使用若能够使用公钥去验证出相同的坐标,就能确定消息确实真的和 d 是绑定的。

发送方使用 SM2 签名算法会得到一个 (r,s) 元组,接收方在获取了 r、s、公钥 PAP_A 后,使用国标设计的验证方程

[s]G+[r+s]PA=[k]G[s]G + [r+s]P_A = [k]G

来判断签名是否有效(实际情况由于接收方无法得知 k,验证的其实是 [k]G[k]G 的横坐标)。很多人学习这个算法直接开始看 r、s 的表达式,其实没必要。因为在设计算法标准的时候,最先设计出来的其实是验证方程和 r,s 其实是通过方程和 r 直接求出表达式的,没有承载什么实际的设计思想。与其思考为什么 r 的式子是怎么得出来的,不如想想:为什么验证方程是它。

为什么是这个方程 ?

接收方验证签名只为了一件事:对于一个公开的公钥 PAP_A,发送方如何证明公钥是自己的私钥生成的。既然为了验证公钥,式子中肯定要存在 PAP_A。然后发送方会使用一个 r 来参与运算,r 通过 SM3 杂凑算法与发送方的身份标识、私钥绑定,可以看作是私钥套了个壳。按照这个思路理论上得出的最简单的验证方程应该是

[r]PA=[k]G[r]P_A=[k]G

国标为了提高安全性,避免通过偶然得知 k 直接解出私钥等特殊情况,最后优化成了现在的版本,也就是增加了 s 变量。

发送方生成签名

由于签名的格式是 (r,s) 元组,需要分开探讨。

计算 r

我可以直接把 r 的计算式子写出来

r=(e+x1)mod(n)r=(e+x_1)mod (n)

计算 r 还需要一个参数 e (消息摘要),这个 e 其实就是一连串有关发送方的信息 ZAZ_A和 明文 M 杂凑的结果。由于 e 和 x1x_1都是大数,很容易就超出 SM2 规定的范围,这里的取模运算能够限制最终的结果在 [0,n1][0,n-1] 上。

e=SM3(ZAM)e=SM3(Z_A||M)

而这里的ZAZ_A 就是发送方的唯一标识符 ID 和各种固定参数杂凑后的结果。

ZA=SM3(ENTLIDabxGyGxAyA)Z_A=SM3(ENTL||ID||a||b||x_G||y_G||x_A||y_A)

其中,ID 是发送者的身份标识,可以是身份证号、 UUID 等,前面的 ENTL 用于标识 ID 的长度。a 和 b 是椭圆曲线 y2=x3+ax+by^2=x^3+ax+b 对应对参数,xGx_GyGy_G 是基点的坐标,这几个参数都在国标中被规定了固定的值。xAx_AyAy_A是签名者公钥,也就是通过私钥dAdA 和基点计算出的一个秘密点,也就是 A=[dA]GA=[dA]G

这里为什么要把 ZAZ_A 和 e 分开求杂凑呢?因为ZAZ_A 使用的参数基本都是不变的,可以被缓存。出于性能考虑,这样对于不同的明文也可以快速地计算出 e。

然后通过前面 SM2 公钥加密算法的学习可以知道,每次传输都希望使用不同的“临时密钥”来提高安全性,而参数 x1x_1 就体现了这个原则,得到它的式子是

x1=[k]Gx_1=[k]G

其中 k 是由随机数生成器生成的随机数。这样,通过 e 和 x1x_1 就可以将发送方的信息、当前会话的标识 k 和发送的数据进行绑定。

计算 s

经过前面的对前面算法的剖析,s 这个值其实就是方程

[s]G+[r+s]PA=[k]G[s]G + [r+s]P_A = [k]G

把 r、公钥带入后得到的式子,也就是

s=(kr×dA)(1+dA)1mod(n)s = (k - r × dA) · (1 + dA)^{-1} mod(n)

真的没什么好讲的,就是移项计算,毕竟 s 本身就是一个用于优化算法的变量。

思考

SM2 数字签名算法借助椭圆曲线难题,巧妙地将 PA=[dA]GP_A=[d_A]G 问题转化为了 [s]G+[r+s]PA=[k]G[s]G + [r+s]P_A = [k]G 方程,利用数学方法避免了直接传输私钥却能证明公钥的归属。