0. 写在前面
工作的第32天。
这周网关那边的同事安排我了解一下SM2算法,学完后写个学习笔记巩固一下知识,也方便之后汇报。
文末附上了学习过程中使用到且经筛选后的参考资料,以作补充。纯小白,如有不足,敬请指正。
后续准备基于AT32的板子做一个wifi桌面智能小摆件,会在博客同步更新学习经验分享。另外,如果时间充裕会写一个医学影像相关的3D图像分割的技术分享。
1. SM2算法
首先,通过下图对算法有个整体的认识。
SM2是基于椭圆曲线密码学(ECC)的一种加密算法,其曲线定义与参数设置如下:
椭圆曲线:y^3 = x^3 + ax +b
default_ecc_table = {
'n': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',
'p': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF',
'g': '32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7'
'bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0',
'a': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC',
'b': '28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93',
}
SM2算法中的倍点操作也是ECC的核心部分,用于实现公钥的生成、签名的验证等关键步骤。
(注:剩余三个操作函数本文不作详细介绍)
ECC算法可以简单描述为:
K = G * k
G为初始定义的G点
k为密钥
K为公钥
理解ECC算法需要先明确两个推广
- 曲线上两点A+B推广为点A自身加法,即A+B->A+A
- 乘法作为多次加法的推广,即A+A+…A-> k*A
然后需要理解A点与B点加法如何在几何上实现
- 定义无穷远点0,得出A+B+C =0 -> A+B= -C= D
- C为AB连线后与椭圆曲线的交点
- D与C沿x轴对称
A+A的情况时,则过A点做曲线的切线
这时最终点为2A,操作数k为2,同理反复进行n次,我们也能得到k=n时最终点的位置。
即等式表示的:K = G * k。
接着,我们将上述ECC部分内容与私钥dA、公钥pA、G点进行联系:
K = G * k -> pA = G * dA
pA为公开的私钥,映射到曲线上为终止点位置
G为预设的椭圆上一点,映射到曲线上为点A
dA私钥,映射到曲线上为需要执行几次点加操作
可以发现,已知操作次数(dA)与椭圆上一点(G),易得终止点位置(pA)。但是,若只知道最终点位置(pA)与椭圆上一点(G),难以求出操作次数(dA)。
1.0 密钥对生成
椭圆的连续域处理成离散域,然后在该离散域进行私钥的随机生成。
得到私钥后,通过之前介绍的pA = G * dA,进行公钥的计算。
class PrivateKey:
def __init__(self, curve=sm2p256v1, secret=None):
self.curve = curve
self.secret = secret or SystemRandom().randrange(1, curve.N)
def publicKey(self): # xQ,yQ
curve = self.curve
xPublicKey, yPublicKey = multiply((curve.Gx, curve.Gy), self.secret, A=curve.A, P=curve.P, N=curve.N)
return PublicKey(xPublicKey, yPublicKey, curve)
def toString(self):
return "{}".format(str(hex(self.secret))[2:].zfill(64))
1.1 加/解密
加/解密过程关注将msg进行拆分后的三个分支C1,C2,C3。两过程实际为查看加密计算的点(x′1; y′1) 和解密计算的点 ( x1, y1 )是否相等。
该部分为验证过程的计算展示,通过等式代入推导展现加/解密过程,实际为验证加密计算的点(x′1; y′1) 和解密计算的点 ( x1, y1 )相等。
encrypt:
C1 = k * G (1)
(x1,y1) = k * pA (2)
decrypt:
(x1′,y1′)
= dA * C1
= dA * k * G # 带入(1)
= k * dA * G
= k * pA ==(x1,x2)
python代码实现,可对应流程图理解。
def encrypt(self, data): #public_key
# 加密函数,data消息(bytes)
msg = data.hex() # 消息转化为16进制字符串 k是每次随机的
k = func.random_hex(self.para_len) #1.k随机生成
print('k:',k)
C1 = self._kg(int(k, 16), self.ecc_table['g']) # 2.得到 C1 k*G C1 随机数K与G进行倍乘 记作(kx,ky)
xy = self._kg(int(k, 16), self.public_key) # 3.得到C2 k*public
x2 = xy[0:self.para_len]
y2 = xy[self.para_len:2*self.para_len] #分别提取x y
ml = len(msg)
# 1.复用计算c1时产生的随机数k
# 2.计算kpk(x, y),
# 3.根据data的长度与kpx, kpy生成与data等长的密钥流,用于c2的最终计算
t = sm3.sm3_kdf(xy.encode('utf8'), ml/2) #这个t是密钥流
if int(t, 16) == 0:
return None #不能生成0 生成0要重新分配k
else:
form = '%%0%dx' % ml
C2 = form % (int(msg, 16) ^ int(t, 16)) # 3.得到C2 msg|t C2操作 密钥流异或data 。这步C2获得了数据密文
#4. c3 = HASH(kpx, data, kpy)
C3 = sm3.sm3_hash([
i for i in bytes.fromhex('%s%s%s' % (x2, msg, y2)) #C3 使用SM3对kx,ky,data的hash值,用于解密时校验
])
#5. C = C1 || C2 || C3
# 加密过程就是去计算这三个东西,操作就是倍乘
if self.mode:
return bytes.fromhex('%s%s%s' % (C1, C3, C2))
else:
return bytes.fromhex('%s%s%s' % (C1, C2, C3))
#在解密时需要在解密出原文后计算HASH值做最后确认,确认一致后认定解密成功,不一致则解密失败。
def decrypt(self, data): #private_key
# 解密函数,data密文(bytes)
data = data.hex()
len_2 = 2 * self.para_len
len_3 = len_2 + 64
print('len2:',len_2) #len2==128
C1 = data[0:len_2] # 1.密文中分离出c1
print('decode c1:',C1)
if self.mode:
C3 = data[len_2:len_3]
C2 = data[len_3:]
else:
C2 = data[len_2:-64]
C3 = data[-64:]
xy = self._kg(int(self.private_key, 16), C1) # 2.私钥与c1计算(cx,cy) by d*c1(x,y)
# print('xy = %s' % xy)
x2 = xy[0:self.para_len]
y2 = xy[self.para_len:len_2]
cl = len(C2)
# 生成需要长度
t = sm3.sm3_kdf(xy.encode('utf8'), cl/2) # 3.根据c2长度 计算密钥流t
# 如果全0解密错误
if int(t, 16) == 0:
return None
else:
form = '%%0%dx' % cl
# 4. M = C2异或t ,M为明文
M = form % (int(C2, 16) ^ int(t, 16))
# 5. 计算u=hash(x2||M||y2)u!=C3 解密错误
u = sm3.sm3_hash([
i for i in bytes.fromhex('%s%s%s' % (x2, M, y2))
])
return bytes.fromhex(M)
1.2 加/验签
直接用过流程图进行过程展示。
主要关注两个分支:
- 通过信息msg的hash操作,获得e
- x1的获取
接着使用二者进行SM2算法的规定运算流程,并验证加签计算的点(x′1; y′1) 和验签计算的点 ( x1, y1 )是否相等。
该部分为验证过程的计算展示,通过等式代入推导展现加/验签过程,实际为验证加签计算的点(x′1; y′1) 和验签计算的点 ( x1, y1 )相等。
sign:
(x1,y1)= k * G (1)
s=((1+dA)^-1 * (k-r*dA)) mod n (2)
vertify:
t = s′ + r′ (3)
pA = G * dA (4)
(x′1; y′1)
= s′ * G + t * pA
= s′ * G + s′ * pA + r′ * pA # 代入(3)
= s′ * G + s′ * dA * G + r′* dA * G # 代入(4)
= [(1+ dA) * s′] * G+(r′ * dA) * G #代入(2)
= [k − r * dA) ] * G+(r′ * dA) * G
= k * G == ( x1, y1 )
python代码实现:
def verify(self, Sign, data):
# 就是为了验证(x1,y1)与之前是否相同
# 验签函数,sign签名r||s,E消息hash,public_key公钥
# 解码签名,得到整数r,s
if self.asn1:
unhex_sign = unhexlify(Sign.encode())
seq_der = DerSequence()
origin_sign = seq_der.decode(unhex_sign)
r = origin_sign[0]
s = origin_sign[1]
else:
r = int(Sign[0:self.para_len], 16)
s = int(Sign[self.para_len:2*self.para_len], 16)
#1. e = hash(M)
e = int(data.hex(), 16)
#2. t = (r+s) mod n (r,s)是之前sign中得到的
t = (r + s) % int(self.ecc_table['n'], base=16)
# t=0则验证失败
if t == 0:
return 0
# 通过曲线点计算 P1 P2
#3.(x1,y1) =s*G+t*pub
P1 = self._kg(s, self.ecc_table['g']) # s*G
P2 = self._kg(t, self.public_key) # t*pub
# print(P1)
# print(P2)
# s、t分别计算P1、P2两个点,根据两个值进行验证
# 有的步骤是只有一个P1进行运算的,然后最后将两部分相加,这跟分为P1、P2算没有区别
if P1 == P2:
P1 = '%s%s' % (P1, 1)
P1 = self._double_point(P1) #自己倍乘,其实就是加
else:
P1 = '%s%s' % (P1, 1)
P1 = self._add_point(P1, P2)
P1 = self._convert_jacb_to_nor(P1)
x = int(P1[0:self.para_len], 16) #提取P1的x后进行r的验证,相等则sign
return r == ((e + x) % int(self.ecc_table['n'], base=16))
def sign(self, data, K):
"""
签名函数, data消息的hash,private_key私钥,K随机数,均为16进制字符串
:param self:
:param data: data消息的hash
:param K: K随机数
:return:
"""
E = data.hex() # 消息转化为16进制字符串
e = int(E, 16) # 1.hash 生成16进制的数字
d = int(self.private_key, 16) # 私钥
k = int(K, 16) # 2.k已经随机生成得到
P1 = self._kg(k, self.ecc_table['g']) #3.计算椭圆曲线上的点(x1,y1 )= k*G
x = int(P1[0:self.para_len], 16)
#4. r=(e+x1) mod n
R = ((e + x) % int(self.ecc_table['n'], base=16))
# 如果r=0或者r+k=n,则重新选择随机数k
if R == 0 or R + k == int(self.ecc_table['n'], base=16):
return None
# 5. 计算s=((1+d)^-1*(k-r*d)) mod n,其中d为私钥,
d_1 = pow(
d+1, int(self.ecc_table['n'], base=16) - 2, int(self.ecc_table['n'], base=16))
S = (d_1*(k + R) - R) % int(self.ecc_table['n'], base=16)
if S == 0:
return None
elif self.asn1:
return DerSequence([DerInteger(R), DerInteger(S)]).encode().hex()
else:
# 6. 签名值为(r, s)
return '%064x%064x' % (R, S)