Issue #15061: Don't oversell the capabilities of the new non-shortcircuiting comparison function in hmac

This commit is contained in:
Nick Coghlan 2012-06-15 21:14:08 +10:00
parent 307693a8bb
commit 807770ec1b
4 changed files with 72 additions and 56 deletions

View file

@ -42,8 +42,8 @@ An HMAC object has the following methods:
When comparing the output of :meth:`digest` to an externally-supplied When comparing the output of :meth:`digest` to an externally-supplied
digest during a verification routine, it is recommended to use the digest during a verification routine, it is recommended to use the
:func:`hmac.secure_compare` function instead of the ``==`` operator :func:`compare_digest` function instead of the ``==`` operator
to avoid potential timing attacks. to reduce the vulnerability to timing attacks.
.. method:: HMAC.hexdigest() .. method:: HMAC.hexdigest()
@ -54,10 +54,11 @@ An HMAC object has the following methods:
.. warning:: .. warning::
When comparing the output of :meth:`hexdigest` to an externally-supplied The output of :meth:`hexdigest` should not be compared directly to an
digest during a verification routine, it is recommended to use the externally-supplied digest during a verification routine. Instead, the
:func:`hmac.secure_compare` function instead of the ``==`` operator externally supplied digest should be converted to a :class:`bytes`
to avoid potential timing attacks. value and compared to the output of :meth:`digest` with
:func:`compare_digest`.
.. method:: HMAC.copy() .. method:: HMAC.copy()
@ -68,20 +69,28 @@ An HMAC object has the following methods:
This module also provides the following helper function: This module also provides the following helper function:
.. function:: secure_compare(a, b) .. function:: compare_digest(a, b)
Returns the equivalent of ``a == b``, but using a time-independent Returns the equivalent of ``a == b``, but avoids content based
comparison method. Comparing the full lengths of the inputs *a* and *b*, short circuiting behaviour to reduce the vulnerability to timing
instead of short-circuiting the comparison upon the first unequal byte, analysis. The inputs must be :class:`bytes` instances.
prevents leaking information about the inputs being compared and mitigates
potential timing attacks. The inputs must be either :class:`str` or Using a short circuiting comparison (that is, one that terminates as soon
:class:`bytes` instances. as it finds any difference between the values) to check digests for
correctness can be problematic, as it introduces a potential
vulnerability when an attacker can control both the message to be checked
*and* the purported signature value. By keeping the plaintext consistent
and supplying different signature values, an attacker may be able to use
timing variations to search the signature space for the expected value in
O(n) time rather than the desired O(2**n).
.. note:: .. note::
While the :func:`hmac.secure_compare` function prevents leaking the While this function reduces the likelihood of leaking the contents of
contents of the inputs via a timing attack, it does leak the length the expected digest via a timing attack, it still uses short circuiting
of the inputs. However, this generally is not a security risk. behaviour based on the *length* of the inputs. It is assumed that the
expected length of the digest is not a secret, as it is typically
published as part of a file format, network protocol or API definition.
.. versionadded:: 3.3 .. versionadded:: 3.3

View file

@ -13,24 +13,24 @@ trans_36 = bytes((x ^ 0x36) for x in range(256))
digest_size = None digest_size = None
def secure_compare(a, b): def compare_digest(a, b):
"""Returns the equivalent of 'a == b', but using a time-independent """Returns the equivalent of 'a == b', but avoids content based short
comparison method to prevent timing attacks.""" circuiting to reduce the vulnerability to timing attacks."""
if not ((isinstance(a, str) and isinstance(b, str)) or # Consistent timing matters more here than data type flexibility
(isinstance(a, bytes) and isinstance(b, bytes))): if not (isinstance(a, bytes) and isinstance(b, bytes)):
raise TypeError("inputs must be strings or bytes") raise TypeError("inputs must be bytes instances")
# We assume the length of the expected digest is public knowledge,
# thus this early return isn't leaking anything an attacker wouldn't
# already know
if len(a) != len(b): if len(a) != len(b):
return False return False
# We assume that integers in the bytes range are all cached,
# thus timing shouldn't vary much due to integer object creation
result = 0 result = 0
if isinstance(a, bytes): for x, y in zip(a, b):
for x, y in zip(a, b): result |= x ^ y
result |= x ^ y
else:
for x, y in zip(a, b):
result |= ord(x) ^ ord(y)
return result == 0 return result == 0

View file

@ -302,40 +302,42 @@ class CopyTestCase(unittest.TestCase):
self.assertEqual(h1.hexdigest(), h2.hexdigest(), self.assertEqual(h1.hexdigest(), h2.hexdigest(),
"Hexdigest of copy doesn't match original hexdigest.") "Hexdigest of copy doesn't match original hexdigest.")
class SecureCompareTestCase(unittest.TestCase): class CompareDigestTestCase(unittest.TestCase):
def test_compare(self): def test_compare(self):
# Testing input type exception handling # Testing input type exception handling
a, b = 100, 200 a, b = 100, 200
self.assertRaises(TypeError, hmac.secure_compare, a, b) self.assertRaises(TypeError, hmac.compare_digest, a, b)
a, b = 100, "foobar" a, b = 100, b"foobar"
self.assertRaises(TypeError, hmac.secure_compare, a, b) self.assertRaises(TypeError, hmac.compare_digest, a, b)
a, b = b"foobar", 200
self.assertRaises(TypeError, hmac.compare_digest, a, b)
a, b = "foobar", b"foobar" a, b = "foobar", b"foobar"
self.assertRaises(TypeError, hmac.secure_compare, a, b) self.assertRaises(TypeError, hmac.compare_digest, a, b)
a, b = b"foobar", "foobar"
# Testing str/bytes of different lengths self.assertRaises(TypeError, hmac.compare_digest, a, b)
a, b = "foobar", "foo"
self.assertFalse(hmac.secure_compare(a, b))
a, b = b"foobar", b"foo"
self.assertFalse(hmac.secure_compare(a, b))
a, b = b"\xde\xad\xbe\xef", b"\xde\xad"
self.assertFalse(hmac.secure_compare(a, b))
# Testing str/bytes of same lengths, different values
a, b = "foobar", "foobaz"
self.assertFalse(hmac.secure_compare(a, b))
a, b = b"foobar", b"foobaz"
self.assertFalse(hmac.secure_compare(a, b))
a, b = b"\xde\xad\xbe\xef", b"\xab\xad\x1d\xea"
self.assertFalse(hmac.secure_compare(a, b))
# Testing str/bytes of same lengths, same values
a, b = "foobar", "foobar" a, b = "foobar", "foobar"
self.assertTrue(hmac.secure_compare(a, b)) self.assertRaises(TypeError, hmac.compare_digest, a, b)
a, b = bytearray(b"foobar"), bytearray(b"foobar")
self.assertRaises(TypeError, hmac.compare_digest, a, b)
# Testing bytes of different lengths
a, b = b"foobar", b"foo"
self.assertFalse(hmac.compare_digest(a, b))
a, b = b"\xde\xad\xbe\xef", b"\xde\xad"
self.assertFalse(hmac.compare_digest(a, b))
# Testing bytes of same lengths, different values
a, b = b"foobar", b"foobaz"
self.assertFalse(hmac.compare_digest(a, b))
a, b = b"\xde\xad\xbe\xef", b"\xab\xad\x1d\xea"
self.assertFalse(hmac.compare_digest(a, b))
# Testing bytes of same lengths, same values
a, b = b"foobar", b"foobar" a, b = b"foobar", b"foobar"
self.assertTrue(hmac.secure_compare(a, b)) self.assertTrue(hmac.compare_digest(a, b))
a, b = b"\xde\xad\xbe\xef", b"\xde\xad\xbe\xef" a, b = b"\xde\xad\xbe\xef", b"\xde\xad\xbe\xef"
self.assertTrue(hmac.secure_compare(a, b)) self.assertTrue(hmac.compare_digest(a, b))
def test_main(): def test_main():
support.run_unittest( support.run_unittest(
@ -343,7 +345,7 @@ def test_main():
ConstructorTestCase, ConstructorTestCase,
SanityTestCase, SanityTestCase,
CopyTestCase, CopyTestCase,
SecureCompareTestCase CompareDigestTestCase
) )
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -21,6 +21,11 @@ Core and Builtins
Library Library
------- -------
- Issue #15061: The inappropriately named hmac.secure_compare has been
renamed to hash.compare_digest, restricted to operating on bytes inputs
only and had its documentation updated to more acurrately reflect both its
intent and its limitations
- Issue #13841: Make child processes exit using sys.exit() on Windows. - Issue #13841: Make child processes exit using sys.exit() on Windows.
- Issue #14936: curses_panel was converted to PEP 3121 and PEP 384 API. - Issue #14936: curses_panel was converted to PEP 3121 and PEP 384 API.