From ff74b8cb508199a33e1978d68bd7076f2a536ab0 Mon Sep 17 00:00:00 2001 From: Neil Davenport Date: Sun, 24 Jul 2016 22:12:48 -0700 Subject: [PATCH] hmac: Rewrite of the HMAC module to use uhashlib and additionally provide a safe digest_compare. Includes a comprehensive test suite. --- hmac/hmac.py | 214 ++++++++++++++++++++++++++++++++++------------ hmac/metadata.txt | 3 +- hmac/setup.py | 5 +- hmac/test_hmac.py | 166 ++++++++++++++++++++++++++++++++--- 4 files changed, 312 insertions(+), 76 deletions(-) diff --git a/hmac/hmac.py b/hmac/hmac.py index c2ce23b11..066e58e43 100644 --- a/hmac/hmac.py +++ b/hmac/hmac.py @@ -1,19 +1,23 @@ -"""HMAC (Keyed-Hashing for Message Authentication) Python module. +"""HMAC (Keyed-Hashing for Message Authentication) MicroPython module. Implements the HMAC algorithm as described by RFC 2104. """ -import warnings as _warnings +#import warnings as _warnings #from _operator import _compare_digest as compare_digest -import hashlib as _hashlib -PendingDeprecationWarning = None -RuntimeWarning = None +#import hashlib as _hashlib +#PendingDeprecationWarning = None +#RuntimeWarning = None +import uhashlib as _hashlib trans_5C = bytes((x ^ 0x5C) for x in range(256)) trans_36 = bytes((x ^ 0x36) for x in range(256)) def translate(d, t): - return b''.join([ chr(t[x]).encode('ascii') for x in d ]) + # Using bytes with a throw away array instead of char below + # to avoid ending up with the wrong key when a key in the + # form of b'\xAA' is used. + return b''.join([bytes([t[x]]) for x in d]) # The size of the digests returned by HMAC depends on the underlying # hashing module used. Use digest_size from the instance of HMAC instead. @@ -26,54 +30,72 @@ class HMAC: This supports the API for Cryptographic Hash Functions (PEP 247). """ - blocksize = 64 # 512-bit HMAC; can be changed in subclasses. + blocksize = 64 # 512-bit HMAC; Both sha1 and sha256 have a 512 bits blocksize. - def __init__(self, key, msg = None, digestmod = None): + def __init__(self, key, msg=None, digestmod=None): """Create a new HMAC object. key: key for the keyed hash object. msg: Initial input for the hash, if provided. - digestmod: A module supporting PEP 247. *OR* - A hashlib constructor returning a new hash object. *OR* - A hash name suitable for hashlib.new(). - Defaults to hashlib.md5. - Implicit default to hashlib.md5 is deprecated and will be - removed in Python 3.6. + digestmod: A module supporting PEP 247, *OR* + A hash name suitable for hashlib.new() *OR* + A hashlib constructor returning a new hash object. + Defaults to uhashlib.sha256. Note: key and msg must be a bytes or bytearray objects. """ + self.finished = False + self.digest_bytes = None + self.hex_bytes = None + if not isinstance(key, (bytes, bytearray)): raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__) if digestmod is None: - _warnings.warn("HMAC() without an explicit digestmod argument " - "is deprecated.", PendingDeprecationWarning, 2) - digestmod = _hashlib.md5 + #_warnings.warn("HMAC() without an explicit digestmod argument " + # "is deprecated.", PendingDeprecationWarning, 2) + #digestmod = _hashlib.md5 + digestmod = _hashlib.sha256 if callable(digestmod): self.digest_cons = digestmod elif isinstance(digestmod, str): - self.digest_cons = lambda d=b'': _hashlib.new(digestmod, d) + self.digest_cons = lambda d=b'': getattr(_hashlib, digestmod)(d) + elif isinstance(digestmod, (bytes, bytearray)): + self.digest_cons = lambda d=b'': getattr(_hashlib, str(digestmod)[2:-1:])(d) else: self.digest_cons = lambda d=b'': digestmod.new(d) self.outer = self.digest_cons() self.inner = self.digest_cons() - self.digest_size = self.inner.digest_size - - if hasattr(self.inner, 'block_size'): - blocksize = self.inner.block_size - if blocksize < 16: - _warnings.warn('block_size of %d seems too small; using our ' - 'default of %d.' % (blocksize, self.blocksize), - RuntimeWarning, 2) - blocksize = self.blocksize + #self.digest_size = self.inner.digest_size + + #if hasattr(self.inner, 'block_size'): + # blocksize = self.inner.block_size + # if blocksize < 16: + # _warnings.warn('block_size of %d seems too small; using our ' + # 'default of %d.' % (blocksize, self.blocksize), + # RuntimeWarning, 2) + # blocksize = self.blocksize + + + if str(self.inner) == '': + self.digest_size = 20 + elif str(self.inner) == '': + self.digest_size = 32 else: - _warnings.warn('No block_size attribute on given digest object; ' - 'Assuming %d.' % (self.blocksize), - RuntimeWarning, 2) - blocksize = self.blocksize + #_warnings.warn('No block_size attribute on given digest object; ' + # 'Assuming %d.' % (self.blocksize), + # RuntimeWarning, 2) + #blocksize = self.blocksize + + # uhashlib doesn't provide a digest_size and we only have hardcoded + # values for the two uhashlib hash functions. + self.digest_size = None + + # Both uhashlib supported algorithms have the same blocksize. + blocksize = self.blocksize # self.blocksize is the default blocksize. self.block_size is # effective block size as well as the public API attribute. @@ -90,52 +112,74 @@ def __init__(self, key, msg = None, digestmod = None): @property def name(self): - return "hmac-" + self.inner.name + return "hmac-" + str(self.inner)[1:-1:] def update(self, msg): """Update this hashing object with the string msg. """ - self.inner.update(msg) - - def copy(self): - """Return a separate copy of this hashing object. - - An update to this copy won't affect the original object. - """ + if not self.finished: + self.inner.update(msg) + else: + # MicroPython's uhashlib sha1 and sha256 don't support the + # copy method (yet) so not being able to update after a + # digest is generated is a limitation. + raise ValueError('Currently, a digest can only be generated once. ' + 'This object is now "spent" and cannot be updated.') + #def copy(self): + # """Return a separate copy of this hashing object. + # An update to this copy won't affect the original object. + # """ # Call __new__ directly to avoid the expensive __init__. - other = self.__class__.__new__(self.__class__) - other.digest_cons = self.digest_cons - other.digest_size = self.digest_size - other.inner = self.inner.copy() - other.outer = self.outer.copy() - return other + # other = self.__class__.__new__(self.__class__) + # other.digest_cons = self.digest_cons + # other.digest_size = self.digest_size + # other.inner = self.inner.copy() + # other.outer = self.outer.copy() + # return other def _current(self): """Return a hash object for the current state. To be used only internally with digest() and hexdigest(). """ - h = self.outer.copy() - h.update(self.inner.digest()) - return h + #h = self.outer.copy() + #h.update(self.inner.digest()) + #return h + self.outer.update(self.inner.digest()) + return self.outer def digest(self): """Return the hash value of this hashing object. - This returns a string containing 8-bit data. The object is - not altered in any way by this function; you can continue + This returns a string containing 8-bit data. You cannot continue updating the object after calling this function. """ - h = self._current() - return h.digest() + #h = self._current() + #return h.digest() + if not self.finished: + h = self._current() + self.digest_bytes = h.digest() + import ubinascii + self.hex_bytes = ubinascii.hexlify(self.digest_bytes) + del(ubinascii) + self.finished = True + return self.digest_bytes def hexdigest(self): """Like digest(), but returns a string of hexadecimal digits instead. """ - h = self._current() - return h.hexdigest() - -def new(key, msg = None, digestmod = None): + #h = self._current() + #return h.hexdigest() + if not self.finished: + h = self._current() + self.digest_bytes = h.digest() + import ubinascii + self.hex_bytes = ubinascii.hexlify(self.digest_bytes) + del(ubinascii) + self.finished = True + return self.hex_bytes + +def new(key, msg=None, digestmod=None): """Create a new hashing object and return it. key: The starting key for the hash. @@ -143,7 +187,63 @@ def new(key, msg = None, digestmod = None): state. You can now feed arbitrary strings into the object using its update() - method, and can ask for the hash value at any time by calling its digest() + method, and can ask for the hash value only once by calling its digest() method. """ return HMAC(key, msg, digestmod) + +def compare_digest(a, b, double_hmac=True, digestmod=b'sha256'): + """Test two digests for equality in a more secure way than "==". + + This employs two main defenses, a double HMAC with a nonce (if available) + to blind the timing side channel (to only leak unpredictable information + to the side channel) and a constant time comparison. + https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy + + The comparison is designed to run in constant time to + avoid leaking information through the timing side channel. + The constant time nature of this algorithm could be undermined by current + or future MicroPython optimizations which is why it is (by default) + additionally protected by the double HMAC. + + It takes as input the output of digest() or hexdigest() of two + different HMAC objects, or bytes or a bytearray representing a + precalculated digest. + """ + if not isinstance(a, (bytes, bytearray)) or not isinstance(b, (bytes, bytearray)): + raise TypeError("Expected bytes or bytearray, but got {} and {}".format(type(a).__name__, type(b).__name__)) + + if len(a) != len(b): + raise ValueError("This method is only for comparing digests of equal length") + + if double_hmac: + try: + import uos + nonce = uos.urandom(64) + except ImportError: + double_hmac = False + except AttributeError: + double_hmac = False + + if double_hmac: + a = new(nonce, a, digestmod).digest() + b = new(nonce, b, digestmod).digest() + + result = 0 + for index, byte_value in enumerate(a): + result |= byte_value ^ b[index] + return result == 0 + +def test(): + """Test suite for the HMAC module""" + run_tests = False + try: + from test_hmac import test_sha_vectors, test_sha256_rfc4231, test_compare_digest + run_tests = True + except ImportError: + raise AssertionError('test_hmac not found, skipping all tests.') + + if run_tests: + test_sha_vectors() + test_sha256_rfc4231() + test_compare_digest() diff --git a/hmac/metadata.txt b/hmac/metadata.txt index a311314c2..cca4b9d1d 100644 --- a/hmac/metadata.txt +++ b/hmac/metadata.txt @@ -1,4 +1,3 @@ srctype = cpython type = module -version = 3.4.2-1 -depends = warnings, hashlib +version = 3.4.2-2 diff --git a/hmac/setup.py b/hmac/setup.py index 5db37cbcf..c0d185866 100644 --- a/hmac/setup.py +++ b/hmac/setup.py @@ -6,7 +6,7 @@ setup(name='micropython-hmac', - version='3.4.2-1', + version='3.4.2-2', description='CPython hmac module ported to MicroPython', long_description='This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.', url='https://github.com/micropython/micropython/issues/405', @@ -15,5 +15,4 @@ maintainer='MicroPython Developers', maintainer_email='micro-python@googlegroups.com', license='Python', - py_modules=['hmac'], - install_requires=['micropython-warnings', 'micropython-hashlib']) + py_modules=['hmac']) diff --git a/hmac/test_hmac.py b/hmac/test_hmac.py index a2c1349c0..b321eb5a0 100644 --- a/hmac/test_hmac.py +++ b/hmac/test_hmac.py @@ -1,22 +1,160 @@ -import hmac -from hashlib.sha256 import sha256 -from hashlib.sha512 import sha512 +""" Tests for the MicroPython HMAC module """ +from hmac import HMAC, new, compare_digest +import uhashlib as _hashlib -msg = b'zlutoucky kun upel dabelske ody' +# This is the failUnlessEqual method from unittest.TestCase +def assertEqual(first, second): + """Fail if the two objects are unequal as determined by the '==' + operator. + """ + if not first == second: + raise AssertionError('%r != %r' % (first, second)) -dig = hmac.new(b'1234567890', msg=msg, digestmod=sha256).hexdigest() +# Using the tests from +# https://github.com/python/cpython/blob/3.5/Lib/test/test_hmac.py +# with as few changes as possible to ensure correctness and parity with +# Python stdlib version. +def test_sha_vectors(): + def shatest(key, data, digest): + h = HMAC(key, data, digestmod=_hashlib.sha1) + assertEqual(h.hexdigest().upper(), digest.upper()) + assertEqual(h.name, "hmac-sha1") + assertEqual(h.digest_size, 20) + assertEqual(h.block_size, 64) -print('c735e751e36b08fb01e25794bdb15e7289b82aecdb652c8f4f72f307b39dad39') -print(dig) + h = HMAC(key, data, digestmod='sha1') + assertEqual(h.hexdigest().upper(), digest.upper()) + assertEqual(h.name, "hmac-sha1") + assertEqual(h.digest_size, 20) + assertEqual(h.block_size, 64) -if dig != 'c735e751e36b08fb01e25794bdb15e7289b82aecdb652c8f4f72f307b39dad39': - raise Exception("Error") -dig = hmac.new(b'1234567890', msg=msg, digestmod=sha512).hexdigest() + shatest(b"\x0b" * 20, + b"Hi There", + b"b617318655057264e28bc0b6fb378c8ef146be00") -print('59942f31b6f5473fb4eb630fabf5358a49bc11d24ebc83b114b4af30d6ef47ea14b673f478586f520a0b9c53b27c8f8dd618c165ef586195bd4e98293d34df1a') -print(dig) + shatest(b"Jefe", + b"what do ya want for nothing?", + b"effcdf6ae5eb2fa2d27416d5f184df9c259a7c79") -if dig != '59942f31b6f5473fb4eb630fabf5358a49bc11d24ebc83b114b4af30d6ef47ea14b673f478586f520a0b9c53b27c8f8dd618c165ef586195bd4e98293d34df1a': - raise Exception("Error") + shatest(b"\xAA" * 20, + b"\xDD" * 50, + b"125d7342b9ac11cd91a39af48aa17b4f63f175d3") + shatest(bytes(range(1, 26)), + b"\xCD" * 50, + b"4c9007f4026250c6bc8414f9bf50c86c2d7235da") + + shatest(b"\x0C" * 20, + b"Test With Truncation", + b"4c1a03424b55e07fe7f27be1d58bb9324a9a5a04") + + shatest(b"\xAA" * 80, + b"Test Using Larger Than Block-Size Key - Hash Key First", + b"aa4ae5e15272d00e95705637ce8a3b55ed402112") + + shatest(b"\xAA" * 80, + (b"Test Using Larger Than Block-Size Key " + b"and Larger Than One Block-Size Data"), + b"e8e99d0f45237d786d6bbaa7965c7808bbff1a91") + +def _rfc4231_test_cases(hashfunc, hash_name, digest_size, block_size): + def hmactest(key, data, hexdigests): + hmac_name = "hmac-" + hash_name + h = HMAC(key, data, digestmod=hashfunc) + assertEqual(h.hexdigest().lower(), hexdigests[hashfunc]) + assertEqual(h.name, hmac_name) + assertEqual(h.digest_size, digest_size) + assertEqual(h.block_size, block_size) + + h = HMAC(key, data, digestmod=hash_name) + assertEqual(h.hexdigest().lower(), hexdigests[hashfunc]) + assertEqual(h.name, hmac_name) + assertEqual(h.digest_size, digest_size) + assertEqual(h.block_size, block_size) + + hmactest(key = b'\x0b'*20, + data = b'Hi There', + hexdigests = { + _hashlib.sha256: b'b0344c61d8db38535ca8afceaf0bf12b' + b'881dc200c9833da726e9376c2e32cff7' + }) + + hmactest(key = b'Jefe', + data = b'what do ya want for nothing?', + hexdigests = { + _hashlib.sha256: b'5bdcc146bf60754e6a042426089575c7' + b'5a003f089d2739839dec58b964ec3843' + }) + + hmactest(key = b'\xaa'*20, + data = b'\xdd'*50, + hexdigests = { + _hashlib.sha256: b'773ea91e36800e46854db8ebd09181a7' + b'2959098b3ef8c122d9635514ced565fe' + }) + + hmactest(key = bytes(x for x in range(0x01, 0x19+1)), + data = b'\xcd'*50, + hexdigests = { + _hashlib.sha256: b'82558a389a443c0ea4cc819899f2083a' + b'85f0faa3e578f8077a2e3ff46729665b' + }) + + hmactest(key = b'\xaa'*131, + data = b'Test Using Larger Than Block-Siz' + b'e Key - Hash Key First', + hexdigests = { + _hashlib.sha256: b'60e431591ee0b67f0d8a26aacbf5b77f' + b'8e0bc6213728c5140546040f0ee37f54' + }) + + hmactest(key = b'\xaa'*131, + data = b'This is a test using a larger th' + b'an block-size key and a larger t' + b'han block-size data. The key nee' + b'ds to be hashed before being use' + b'd by the HMAC algorithm.', + hexdigests = { + _hashlib.sha256: b'9b09ffa71b942fcb27635fbcd5b0e944' + b'bfdc63644f0713938a7f51535c3a35e2' + }) + + +def test_sha256_rfc4231(): + _rfc4231_test_cases(_hashlib.sha256, 'sha256', 32, 64) + +def test_compare_digest(): + h = new(b'key', b'message', 'sha256') + i = new(b'key', b'message', 'sha256') + j = new(b'key', b'not the message', 'sha256') + digest = b"n\x9e\xf2\x9bu\xff\xfc[z\xba\xe5'\xd5\x8f\xda\xdb/\xe4.r\x19\x01\x19v\x91sC\x06_X\xedJ" + not_digest = b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa' + hexdigest = b'6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a' + not_hexdigest = b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + # Positive Tests + assertEqual(compare_digest(h.digest(), i.digest()), True) + assertEqual(compare_digest(h.digest(), digest), True) + assertEqual(compare_digest(h.hexdigest(), hexdigest), True) + assertEqual(compare_digest(h.digest(), i.digest(), double_hmac=False), True) + assertEqual(compare_digest(h.digest(), i.digest(), digestmod='sha1'), True) + assertEqual(compare_digest(h.hexdigest(), i.hexdigest(), digestmod='sha1'), True) + assertEqual(compare_digest(h.hexdigest(), hexdigest, digestmod='sha1'), True) + assertEqual(compare_digest(h.digest(), i.digest(), digestmod='sha256'), True) + assertEqual(compare_digest(h.digest(), i.digest(), digestmod=b'sha256'), True) + assertEqual(compare_digest(h.hexdigest(), i.hexdigest(), digestmod='sha256'), True) + assertEqual(compare_digest(h.digest(), digest, digestmod='sha256'), True) + + # Negative Tests + assertEqual(compare_digest(h.digest(), j.digest()), False) + assertEqual(compare_digest(h.digest(), not_digest), False) + assertEqual(compare_digest(h.hexdigest(), not_hexdigest), False) + assertEqual(compare_digest(h.digest(), j.digest(), double_hmac=False), False) + assertEqual(compare_digest(h.digest(), j.digest(), digestmod='sha1'), False) + assertEqual(compare_digest(h.hexdigest(), j.hexdigest(), digestmod='sha1'), False) + assertEqual(compare_digest(h.hexdigest(), not_hexdigest, digestmod='sha1'), False) + assertEqual(compare_digest(h.digest(), j.digest(), digestmod='sha256'), False) + assertEqual(compare_digest(h.digest(), j.digest(), digestmod=b'sha256'), False) + assertEqual(compare_digest(h.hexdigest(), j.hexdigest(), digestmod='sha256'), False) + assertEqual(compare_digest(h.digest(), not_digest, digestmod='sha256'), False)