Skip to content

Commit dc6f616

Browse files
committed
add blake3 to hashlib
blake3_impl.c and blake3module.c are adapted from the existing BLAKE2 module. This involves a lot of copy-paste, and hopefully someone who knows this code better can help me clean them up. (In particular, BLAKE2 relies on clinic codegen to share code between BLAKE2b and BLAKE2s, but BLAKE3 has no need for that.) blake3_dispatch.c, which is vendored from upstream, includes runtime CPU feature detection to choose the appropriate SIMD instruction set for the current platform (x86 only). In this model, the build should include all instruction sets, and here I unconditionally include the Unix assembly files (*_unix.S) as `extra_objects` in setup.py. This "works on my box", but is currently incomplete in several ways: - It needs some Windows-specific build logic. There are two additional assembly flavors included for each instruction set, *_windows_gnu.S and *_windows_msvc.asm. I need to figure out how to include the right flavor based on the target OS/ABI. - I need to figure out how to omit these files on non-x86-64 platforms. x86-32 will require some explicit preprocessor definitions to restrict blake3_dispatch.c to portable code. (Unless we vendor intrinsics-based implementations for 32-bit support. More on this below.) - It's not going to work on compilers that are too old to recognize these instruction sets, particularly AVX-512. (Question: What's the oldest GCC version that CPython supports?) Maybe compiler feature detection could be added to ./configure and somehow plumbed through to setup.py. I'm hoping someone more experienced with the build system can help me narrow down the best solution for each of those. This also raises the higher level question of whether the CPython project feels comfortable about including assembly files in general. As a possible alternative, the upstream BLAKE3 project also provides intrinsics-based implementations of the same optimizations. The upsides of these are 1) that they don't require Unix/Windows platform detection, 2) that they support 32-bit x86 targets, and 3) that C is easier to audit than assembly. However, the downsides of these are 1) that they're ~10% slower than the hand-written assembly, 2) that their performance is less consistent and worse on older compilers, and 3) that they take noticeably longer to compile. We recommend the assembly implementations for these reasons, but intrinsics are a viable option if assembly violates CPython's requirements.
1 parent 69f06a4 commit dc6f616

File tree

8 files changed

+627
-12
lines changed

8 files changed

+627
-12
lines changed

Lib/hashlib.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
than using new(name):
1313
1414
md5(), sha1(), sha224(), sha256(), sha384(), sha512(), blake2b(), blake2s(),
15-
sha3_224, sha3_256, sha3_384, sha3_512, shake_128, and shake_256.
15+
blake3(), sha3_224, sha3_256, sha3_384, sha3_512, shake_128, and shake_256.
1616
1717
More algorithms may be available on your platform but the above are guaranteed
1818
to exist. See the algorithms_guaranteed and algorithms_available attributes
@@ -56,7 +56,7 @@
5656
# This tuple and __get_builtin_constructor() must be modified if a new
5757
# always available algorithm is added.
5858
__always_supported = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
59-
'blake2b', 'blake2s',
59+
'blake2b', 'blake2s', 'blake3',
6060
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
6161
'shake_128', 'shake_256')
6262

@@ -75,8 +75,9 @@
7575
# implementations neither support keyed blake2 (blake2 MAC) nor advanced
7676
# features like salt, personalization, or tree hashing. OpenSSL hash-only
7777
# variants are available as 'blake2b512' and 'blake2s256', though.
78+
# OpenSSL 1.1.0 does not support blake3.
7879
__block_openssl_constructor = {
79-
'blake2b', 'blake2s',
80+
'blake2b', 'blake2s', 'blake3'
8081
}
8182

8283
def __get_builtin_constructor(name):
@@ -103,6 +104,9 @@ def __get_builtin_constructor(name):
103104
import _blake2
104105
cache['blake2b'] = _blake2.blake2b
105106
cache['blake2s'] = _blake2.blake2s
107+
elif name in {'blake3'}:
108+
import _blake3
109+
cache['blake3'] = _blake3.blake3
106110
elif name in {'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512'}:
107111
import _sha3
108112
cache['sha3_224'] = _sha3.sha3_224

Lib/test/test_hashlib.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class HashLibTestCase(unittest.TestCase):
9292
supported_hash_names = ( 'md5', 'MD5', 'sha1', 'SHA1',
9393
'sha224', 'SHA224', 'sha256', 'SHA256',
9494
'sha384', 'SHA384', 'sha512', 'SHA512',
95-
'blake2b', 'blake2s',
95+
'blake2b', 'blake2s', 'blake3',
9696
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
9797
'shake_128', 'shake_256')
9898

@@ -172,6 +172,9 @@ def add_builtin_constructor(name):
172172
if _blake2:
173173
add_builtin_constructor('blake2s')
174174
add_builtin_constructor('blake2b')
175+
_blake3 = self._conditional_import_module('_blake3')
176+
if _blake3:
177+
add_builtin_constructor('blake3')
175178

176179
_sha3 = self._conditional_import_module('_sha3')
177180
if _sha3:
@@ -342,25 +345,25 @@ def test_large_update(self):
342345
self.assertEqual(m1.digest(*args), m4_copy.digest(*args))
343346
self.assertEqual(m4.digest(*args), m4_digest)
344347

345-
def check(self, name, data, hexdigest, shake=False, **kwargs):
348+
def check(self, name, data, hexdigest, set_length=False, **kwargs):
346349
length = len(hexdigest)//2
347350
hexdigest = hexdigest.lower()
348351
constructors = self.constructors_to_test[name]
349352
# 2 is for hashlib.name(...) and hashlib.new(name, ...)
350353
self.assertGreaterEqual(len(constructors), 2)
351354
for hash_object_constructor in constructors:
352355
m = hash_object_constructor(data, **kwargs)
353-
computed = m.hexdigest() if not shake else m.hexdigest(length)
356+
computed = m.hexdigest() if not set_length else m.hexdigest(length)
354357
self.assertEqual(
355358
computed, hexdigest,
356359
"Hash algorithm %s constructed using %s returned hexdigest"
357360
" %r for %d byte input data that should have hashed to %r."
358361
% (name, hash_object_constructor,
359362
computed, len(data), hexdigest))
360-
computed = m.digest() if not shake else m.digest(length)
363+
computed = m.digest() if not set_length else m.digest(length)
361364
digest = bytes.fromhex(hexdigest)
362365
self.assertEqual(computed, digest)
363-
if not shake:
366+
if not set_length:
364367
self.assertEqual(len(digest), m.digest_size)
365368

366369
def check_no_unicode(self, algorithm_name):
@@ -776,6 +779,73 @@ def test_blake2s_vectors(self):
776779
key = bytes.fromhex(key)
777780
self.check('blake2s', msg, md, key=key)
778781

782+
def test_case_blake3_0(self):
783+
self.check('blake3', b"",
784+
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262")
785+
786+
def test_case_blake3_1(self):
787+
self.check('blake3', b"abc",
788+
"6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85")
789+
790+
def test_case_blake3_keyed(self):
791+
self.check('blake3', b"abc",
792+
"6da54495d8152f2bcba87bd7282df70901cdb66b4448ed5f4c7bd2852b8b5532",
793+
key=bytes(range(32)))
794+
795+
def test_case_blake3_derive_key(self):
796+
self.check('blake3', b"super secret key material",
797+
"dbf0a1433e0137fb11b71d3ae3c138bff46445936dd5d4f01f403c23abd5660a",
798+
derive_key_context="hardcoded, globally unique, application-specific context string")
799+
800+
def test_case_blake3_length(self):
801+
self.check('blake3', b"",
802+
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" +
803+
"e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a" +
804+
"26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda" +
805+
"7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421" +
806+
"cce14d",
807+
# True here means that digest() and hexdigest() get a length
808+
# argument. This shares the variable length test codepath with
809+
# shake_128 and shake_256.
810+
True)
811+
812+
def test_case_blake3_seek(self):
813+
# None of the other hashes support a seek parameter. Rather than
814+
# hacking this into self.check(), just invoke blake3() explicitly.
815+
output_hex = (
816+
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262" +
817+
"e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a" +
818+
"26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda" +
819+
"7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421" +
820+
"cce14d")
821+
output_bytes = unhexlify(output_hex)
822+
# Test a few interesting seek points, including one with length=0.
823+
for seek in [0, 1, len(output_bytes)//2, len(output_bytes)-1, len(output_bytes)]:
824+
length = len(output_bytes) - seek
825+
expected_bytes = output_bytes[seek:]
826+
expected_hex = output_hex[2*seek:]
827+
# positional
828+
assert expected_bytes == hashlib.blake3().digest(length, seek)
829+
assert expected_hex == hashlib.blake3().hexdigest(length, seek)
830+
# keywords
831+
assert expected_bytes == hashlib.blake3().digest(length=length, seek=seek)
832+
assert expected_hex == hashlib.blake3().hexdigest(length=length, seek=seek)
833+
834+
def test_case_blake3_key_must_be_32_bytes(self):
835+
for length in [0, 1, 31, 33]:
836+
try:
837+
hashlib.blake3(key=b"\0"*31)
838+
assert False, "the line above should raise an exception"
839+
except ValueError as e:
840+
assert str(e) == "key must be 32 bytes"
841+
842+
def test_case_blake3_keyed_derive_key_exclusive(self):
843+
try:
844+
hashlib.blake3(key=b"\0"*32, derive_key_context="foo")
845+
assert False, "the line above should raise an exception"
846+
except ValueError as e:
847+
assert str(e) == "key and derive_key_context can't be used together"
848+
779849
def test_case_sha3_224_0(self):
780850
self.check('sha3_224', b"",
781851
"6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7")

0 commit comments

Comments
 (0)