SM3是一种密码杂凑算法,输出的杂凑值是256位(MD5是128位,SHA-1是160位),用于替换MD5/SHA-1等国际算法。实现SM3的python库主要是python-gmssl库和snowland-smx(pysmx)库,二者都对SM2(仅公钥加解密和数字签名)、SM3、SM4进行了细致而优雅的实现。在耗时测试中,还引入了国际算法MD5和SHA-256作为对比,采用的是成熟高效的Crypto(PyCryptodome)库。 它们的来源如下:
1.GMSSL. https://github.com/duanhongyi/gmssl
2.snowland-smx. https://gitee.com/snowlandltd/ snowland-smx-python
3.PyCryptodome. https://www.pycryptodome.org
话不多说,老套路,先上能完整运行的SM3实现代码(运行不了是没装够依赖库):
from array import array
Tj_rl = array('L', ((0x79cc4519 << j | 0x79cc4519 >> 32-j) & 0xffffffff for j in range(16)))
Tj_rl.extend((0x7a879d8a << (j & 31) | 0x7a879d8a >> (32 - j & 31)) & 0xffffffff for j in range(16, 64))
V0 = array('L', [0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600, 0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e])
def CF(V, B):
W = array('L', B)
for j in range(16, 68):
X = W[j-16] ^ W[j-9] ^ (W[j-3] << 15 | W[j-3] >> 17) & 0xffffffff
W.append((X ^ (X << 15 | X >> 17) ^ (X << 23 | X >> 9) ^ (W[j-13] << 7 | W[j-13] >> 25) ^ W[j-6]) & 0xffffffff)
W_ = array('L', (W[j] ^ W[j+4] for j in range(64)))
A, B, C, D, E, F, G, H = V
for j in range(64):
A_rl12 = A << 12 | A >> 20
tmp = (A_rl12 + E + Tj_rl[j]) & 0xffffffff
SS1 = (tmp << 7 | tmp >> 25)
SS2 = SS1 ^ A_rl12
if j & 0x30: # 16 <= j
FF, GG = A & B | A & C | B & C, E & F | ~E & G
else:
FF, GG = A ^ B ^ C, E ^ F ^ G
TT1, TT2 = (FF + D + SS2 + W_[j]) & 0xffffffff, (GG + H + SS1 + W[j]) & 0xffffffff
C, D, G, H = (B << 9 | B >> 23) & 0xffffffff, C, (F << 19 | F >> 13) & 0xffffffff, G
A, B, E, F = TT1, A, (TT2 ^ (TT2 << 9 | TT2 >> 23) ^ (TT2 << 17 | TT2 >> 15)) & 0xffffffff, E
return A ^ V[0], B ^ V[1], C ^ V[2], D ^ V[3], E ^ V[4], F ^ V[5], G ^ V[6], H ^ V[7]
def digest(data):
# 填充
pad_num = 64 - (len(data) + 1 & 0x3f)
data += b'\x80' + (len(data) << 3).to_bytes(pad_num if pad_num >= 8 else pad_num + 64, 'big')
V, B = V0, array('L', data)
B.byteswap()
# 迭代压缩
for i in range(0, len(B), 16):
V = CF(V, B[i:i+16])
V = array('L', V)
V.byteswap()
return V.tobytes()
from Crypto.Hash import MD5, SHA3_256
from pysmx.SM3 import digest as SM3_pysmx
from gmssl.sm3 import sm3_hash
import time, os
def SM3_gmssl(data: bytes) -> bytes:
return bytes.fromhex(sm3_hash([i for i in data]))
def SM3_my(data: bytes) -> bytes:
return digest(data)
def sm3_compare_test():
print('—————————————————————首次Hash测试—————————————————————')
# 随机生成消息
long_data = os.urandom(128)
print('消息长度:%dB 单位:μs' % (len(long_data)))
print('算法库名\t\t\t首次Hash\t\t再次Hash')
# Crypto - MD5
time_1 = time.perf_counter()
MD5.new(long_data).digest()
time_2 = time.perf_counter()
MD5.new(long_data).digest()
time_3 = time.perf_counter()
print('Crypto-MD5\t\t%.1f\t\t%.1f' % ((time_2 - time_1) * 1000000, (time_3 - time_2) * 1000000))
# Crypto - SHA256
time_1 = time.perf_counter()
SHA3_256.new(long_data).digest()
time_2 = time.perf_counter()
SHA3_256.new(long_data).digest()
time_3 = time.perf_counter()
print('Crypto-SHA256\t%.1f\t\t%.1f' % ((time_2 - time_1) * 1000000, (time_3 - time_2) * 1000000))
# gmssl - SM3
time_1 = time.perf_counter()
SM3_gmssl(long_data)
time_2 = time.perf_counter()
hash2 = SM3_gmssl(long_data)
time_3 = time.perf_counter()
print('gmssl-SM3\t\t%.1f\t\t%.1f' % ((time_2 - time_1) * 1000000, (time_3 - time_2) * 1000000))
# pysmx - SM3
time_1 = time.perf_counter()
SM3_pysmx(long_data)
time_2 = time.perf_counter()
hash1 = SM3_pysmx(long_data)
time_3 = time.perf_counter()
print('pysmx-SM3\t\t%.1f\t\t%.1f' % ((time_2 - time_1) * 1000000, (time_3 - time_2) * 1000000))
assert hash1 == hash2
# my - SM3
time_1 = time.perf_counter()
SM3_my(long_data)
time_2 = time.perf_counter()
hash2 = SM3_my(long_data)
time_3 = time.perf_counter()
print('my-SM3\t\t\t%.1f\t\t%.1f' % ((time_2 - time_1) * 1000000, (time_3 - time_2) * 1000000))
assert hash1 == hash2
print('\n—————————————————————连续Hash测试—————————————————————')
test_num = 100 # 测试次数
# 随机生成消息
short_data = [os.urandom(28) for i in range(test_num)] # 短消息列表
long_data = [os.urandom(1128) for i in range(test_num)] # 长消息列表
hash_data = [b''] * test_num
hash_data1 = [b''] * test_num
hash_data2 = [b''] * test_num
hash_data3 = [b''] * test_num
hash_data4 = [b''] * test_num
print('短消息长度:%dB 长消息长度:%dB 测试次数:%d 单位:ms' % (len(short_data[0]), len(long_data[0]), test_num))
print('算法库名\t\t\t短消息Hash\t长消息Hash')
# Crypto - MD5
time_1 = time.perf_counter()
for i in range(test_num):
hash_data[i] = MD5.new(short_data[i]).digest() # 短消息Hash
time_2 = time.perf_counter()
for i in range(test_num):
hash_data[i] = MD5.new(long_data[i]).digest() # 长消息Hash
time_3 = time.perf_counter()
print('Crypto-MD5\t\t%.2f\t\t%.2f' % ((time_2 - time_1) * 1000, (time_3 - time_2) * 1000))
# Crypto - SHA256
time_1 = time.perf_counter()
for i in range(test_num):
hash_data[i] = SHA3_256.new(short_data[i]).digest() # 短消息Hash
time_2 = time.perf_counter()
for i in range(test_num):
hash_data[i] = SHA3_256.new(long_data[i]).digest() # 长消息Hash
time_3 = time.perf_counter()
print('Crypto-SHA256\t%.2f\t\t%.2f' % ((time_2 - time_1) * 1000, (time_3 - time_2) * 1000))
# gmssl - SM3
time_1 = time.perf_counter()
for i in range(test_num):
hash_data1[i] = SM3_gmssl(short_data[i]) # 短消息Hash
time_2 = time.perf_counter()
for i in range(test_num):
hash_data2[i] = SM3_gmssl(long_data[i]) # 长消息Hash
time_3 = time.perf_counter()
print('gmssl-SM3\t\t%.2f\t\t%.2f' % ((time_2 - time_1) * 1000, (time_3 - time_2) * 1000))
time_aim1 = time_3 - time_1
# pysmx - SM3
time_1 = time.perf_counter()
for i in range(test_num):
hash_data3[i] = SM3_pysmx(short_data[i]) # 短消息Hash
time_2 = time.perf_counter()
for i in range(test_num):
hash_data4[i] = SM3_pysmx(long_data[i]) # 长消息Hash
time_3 = time.perf_counter()
print('pysmx-SM3\t\t%.2f\t\t%.2f' % ((time_2 - time_1) * 1000, (time_3 - time_2) * 1000))
time_aim2 = time_3 - time_1
assert hash_data1 == hash_data3 and hash_data2 == hash_data4
# my - SM3
time_1 = time.perf_counter()
for i in range(test_num):
hash_data1[i] = SM3_my(short_data[i]) # 短消息Hash
time_2 = time.perf_counter()
for i in range(test_num):
hash_data2[i] = SM3_my(long_data[i]) # 长消息Hash
time_3 = time.perf_counter()
print('my-SM3\t\t\t%.2f\t\t%.2f' % ((time_2 - time_1) * 1000, (time_3 - time_2) * 1000))
time_my = time_3 - time_1
print('总耗时为pysmx的%.2f%%、gmssl的%.2f%%' % (time_my / time_aim2 * 100, time_my / time_aim1 * 100))
assert hash_data1 == hash_data3 and hash_data2 == hash_data4
if __name__ == "__main__":
sm3_compare_test()
SM3的核心代码只有前40行,后面是测试用的。核心函数是digest,输入输出均为bytes类型。不像其他库提供以字符串和16进制串作为输入的实现,我的代码只处理bytes,我认为调用者才最清楚如何将各种类型的源数据高效转换成bytes类型。
虽然代码是最简短的一次(相比SM2和SM4),可过程真是一言难尽。。。还是想吐槽一下/(ㄒoㄒ)/~~我用的密码学教材描述SM3算法的部分出了三个错误,搞得我不得不在一大堆运算和循环中,通过将中间值一步一步输出,与正确程序逐一对比,才发现可能是教材错了,费劲!好在调通了(๑•̀ㅂ•́)و✧
运行上面的代码,看一下性能对比测试结果:
连续Hash测试中,结果数值为总耗时,不是平均耗时。不同的计算机配置下结果可能有些许差别,我的计算机配置:
可见,调用机器代码的Crypto库依旧吊打我们这些倔强的纯python,和SM4的测试结果类似,头一次加载链接库比较慢,但从第二次开始就起飞了。我的实现比另两个国密算法库快了一点,主要是做了以下改进:
1.统一用array数组作为中间值,最大限度减少bytes、list、int之间的类型转换。
2.运算中经常用到"& 0xffffffff"确保值是32位,因为我们的计算机是64位,有时让中间值超过32位也无妨,仅在最后赋值时再进行"& 0xffffffff"运算,这样可减少运算量。但连续两次循环移位之间不能省略"& 0xffffffff",否则会得到错误结果。
3.将循环移位运算从pysmx的“一次模除、一次乘法、一次加法”调整为“两次移位、一次按位或”,位运算比乘除法快。
当然,尽管优化了一点,还是被机器代码轰成渣。下一步还是要用numba对代码进行改写,在分享SM4文章的末尾已经见识到了numba的威力,效率已跟Crypto库不相上下,不知道这一次能达到什么效果,拭目以待吧!写完了会分享。
自从去年阴差阳错地改进了SM2,到之前用python做基于国密算法的加密系统发现咱自己的SM4效率比国际成熟库差了数量级,又改进SM4,再到现在写完SM3,惊喜地发现自己实现的SM2、SM3、SM4已经构成了一个完整的体系,包括非对称密码、杂凑函数和分组密码,下步考虑把它们都做到跟国际成熟库(类似Crypto库)对等的效率,封装成库,供大家pip安装。毕竟用python的开发者有那么多而且越来越多,要是能解决python平台国密算法耗时太高的痛点,也算是功德一件!
现有的国密python库或多或少存在效率、易用性和实现不完整等方面的问题。当然,没有前辈的工作,就没有我的努力方向,更没有目前的成果。中学数学老师说过的话让我印象深刻:“先谈会不会,再谈好不好”。gmssl库和pysmx库首先解决了有没有的问题,然后再通过我们共同努力让它变得更好。再次向国密算法的设计者、国密算法库的开发者、国密算法的广大使用者以及高瞻远瞩的各级领导致以崇高的敬意!
正是大家的坚持,我们才能将网络信息安全一步一步掌握在自己手中。