直到 Python 2.5,哈希和摘要是在它们自己的模块中实现的(例如[Python 2.Docs]: md5 - MD5消息摘要算法)。
从v2.5开始,添加了[Python 2.6.Docs]: hashlib - 安全哈希和消息摘要。它的目的是:
提供一种统一的访问哈希/摘要的方法(通过它们的名称)
切换(默认情况下)到外部加密提供程序(似乎将委托给某个专门从事该领域的实体是合理的步骤,因为维护所有这些算法可能会过度)。当时,OpenSSL是最好的选择:足够成熟,已知且兼容(有一堆类似的Java提供者,但那些都没什么用)
作为
#2. 的副作用,
Python 实现被隐藏在公共的
API 中(将它们重命名为:
_md5、
_sha1、
_sha256、
_sha512,并添加了后者:
_blake2、
_sha3),因为冗余通常会引起混淆。
但是,另一个副作用是 _hashlib.so 依赖于 OpenSSL 的 libcrypto*.so(这是特定于
Nix(至少是
Linux)的,在
Win 上,静态的
libeay32.lib 被链接到
_hashlib.pyd 中,还有
_ssl.pyd(我认为这很糟糕),直到
v3.7+,其中
OpenSSL 的
.dll 是
Python 安装的一部分)。
可能在超过
90% 的机器上,情况都很顺利,因为
OpenSSL 默认已安装,但对于那些没有安装的机器,许多东西可能会出问题,因为例如
hashlib 被许多模块导入(其中一个例子是
random,它本身被许多其他模块导入),所以
与加密完全无关的琐碎代码(至少在第一眼看上去是这样)将停止工作。这就是为什么旧的实现被保留下来(但再次强调,它们只是备用方案,因为
OpenSSL 版本正在/应该得到更好的维护)。
[cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q059955854]> ~/sopr.sh
[064bit-prompt]> python3 -c "import sys, hashlib as hl, _md5, ssl;print(\"{0:}\n{1:}\n{2:}\n{3:}\".format(sys.version, _md5, hl._hashlib, ssl.OPENSSL_VERSION))"
3.5.2 (default, Oct 8 2019, 13:06:37)
[GCC 5.4.0 20160609]
<module '_md5' (built-in)>
<module '_hashlib' from '/usr/lib/python3.5/lib-dynload/_hashlib.cpython-35m-x86_64-linux-gnu.so'>
OpenSSL 1.0.2g 1 Mar 2016
[064bit-prompt]>
[064bit-prompt]> ldd /usr/lib/python3.5/lib-dynload/_hashlib.cpython-35m-x86_64-linux-gnu.so
linux-vdso.so.1 => (0x00007fffa7d0b000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f50d9e4d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f50d9a83000)
libcrypto.so.1.0.0 => /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 (0x00007f50d963e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f50da271000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f50d943a000)
[064bit-prompt]>
[064bit-prompt]> openssl version -a
OpenSSL 1.0.2g 1 Mar 2016
built on: reproducible build, date unspecified
platform: debian-amd64
options: bn(64,64) rc4(16x,int) des(idx,cisc,16,int) blowfish(idx)
compiler: cc -I. -I.. -I../include -fPIC -DOPENSSL_PIC -DOPENSSL_THREADS -D_REENTRANT -DDSO_DLFCN -DHAVE_DLFCN_H -m64 -DL_ENDIAN -g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -Wl,-Bsymbolic-functions -Wl,-z,relro -Wa,--noexecstack -Wall -DMD32_REG_T=int -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DMD5_ASM -DAES_ASM -DVPAES_ASM -DBSAES_ASM -DWHIRLPOOL_ASM -DGHASH_ASM -DECP_NISTZ256_ASM
OPENSSLDIR: "/usr/lib/ssl"
[064bit-prompt]>
[064bit-prompt]> python3 -c "import _md5, hashlib as hl;print(_md5.md5(b\"A\").hexdigest(), hl.md5(b\"A\").hexdigest())"
7fc56270e7a70fa81a5935b72eacbe29 7fc56270e7a70fa81a5935b72eacbe29
根据
[Python 3.Docs]: hashlib.algorithms_guaranteed:
包含哈希算法名称的集合,保证在所有平台上都由此模块支持。请注意,尽管某些上游供应商提供了奇怪的“符合FIPS”的Python版本,但“md5”仍在此列表中。
下面是一个
自定义的Python 2.7安装示例(我相当长时间以前构建的,值得一提的是它动态链接到
OpenSSL .dll)。
[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q059955854]> sopr.bat
### Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ###
[prompt]> "F:\Install\pc064\HPE\OPSWpython\2.7.10__00\python.exe" -c "import sys, ssl;print(\"{0:}\n{1:}\".format(sys.version, ssl.OPENSSL_VERSION))"
2.7.10 (default, Mar 8 2016, 15:02:46) [MSC v.1600 64 bit (AMD64)]
OpenSSL 1.0.2j-fips 26 Sep 2016
[prompt]> "F:\Install\pc064\HPE\OPSWpython\2.7.10__00\python.exe" -c "import hashlib as hl;print(hl.md5(\"A\").hexdigest())"
7fc56270e7a70fa81a5935b72eacbe29
[prompt]> "F:\Install\pc064\HPE\OPSWpython\2.7.10__00\python.exe" -c "import ssl;ssl.FIPS_mode_set(True);import hashlib as hl;print(hl.md5(\"A\").hexdigest())"
Traceback (most recent call last):
File "<string>", line 1, in <module>
ValueError: error:060A80A3:digital envelope routines:FIPS_DIGESTINIT:disabled for fips
关于速度问题,我只能做出推测:
考虑到以上因素,前者(至少对于小消息,在这种情况下,开销(函数调用和其他
Python底层操作)占总时间的比例相当大)会更快一些。还有其他因素需要考虑(例如是否使用了
OpenSSL汇编器加速)。
更新#0
以下是我自己的一些基准测试。
code00.py:
import sys
import timeit
from hashlib import md5 as md5_openssl
from _md5 import md5 as md5_builtin
MD5S = (
md5_openssl,
md5_builtin,
)
def main(*argv):
base_text = b"A"
number = 1000000
print("timeit attempts number: {:d}".format(number))
for count in range(0, 16):
factor = 2 ** count
text = base_text * factor
globals_dict = {"text": text}
print("\nUsing a {:8d} (2 ** {:2d}) bytes message".format(len(text), count))
for func in MD5S:
globals_dict["md5"] = func
t = timeit.timeit(stmt="md5(text)", globals=globals_dict, number=number)
print(" {:12s} took: {:11.6f} seconds".format(func.__name__, t))
if __name__ == "__main__":
print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
64 if sys.maxsize > 0x100000000 else 32, sys.platform))
rc = main(*sys.argv[1:])
print("\nDone.\n")
sys.exit(rc)
输出: