diff --git a/Doc/library/hmac.rst b/Doc/library/hmac.rst index f88a03e0f97..09e819ae5a2 100644 --- a/Doc/library/hmac.rst +++ b/Doc/library/hmac.rst @@ -38,6 +38,13 @@ An HMAC object has the following methods: This string will be the same length as the *digest_size* of the digest given to the constructor. It may contain non-ASCII characters, including NUL bytes. + .. warning:: + + When comparing the output of :meth:`digest` to an externally-supplied + digest during a verification routine, it is recommended to use the + :func:`compare_digest` function instead of the ``==`` operator + to reduce the vulnerability to timing attacks. + .. method:: HMAC.hexdigest() @@ -45,6 +52,13 @@ An HMAC object has the following methods: containing only hexadecimal digits. This may be used to exchange the value safely in email or other non-binary environments. + .. warning:: + + When comparing the output of :meth:`hexdigest` to an externally-supplied + digest during a verification routine, it is recommended to use the + :func:`compare_digest` function instead of the ``==`` operator + to reduce the vulnerability to timing attacks. + .. method:: HMAC.copy() @@ -52,6 +66,25 @@ An HMAC object has the following methods: compute the digests of strings that share a common initial substring. +This module also provides the following helper function: + +.. function:: compare_digest(a, b) + + Return ``a == b``. This function uses an approach designed to prevent + timing analysis by avoiding content-based short circuiting behaviour, + making it appropriate for cryptography. *a* and *b* must both be of the + same type: either :class:`unicode` or a :term:`bytes-like object`. + + .. note:: + + If *a* and *b* are of different lengths, or if an error occurs, + a timing attack could theoretically reveal information about the + types and lengths of *a* and *b*--but not their values. + + + .. versionadded:: 2.7.7 + + .. seealso:: Module :mod:`hashlib` diff --git a/Lib/hmac.py b/Lib/hmac.py index 538810630cf..9cd1a9fd913 100644 --- a/Lib/hmac.py +++ b/Lib/hmac.py @@ -5,6 +5,9 @@ Implements the HMAC algorithm as described by RFC 2104. import warnings as _warnings +from operator import _compare_digest as compare_digest + + trans_5C = "".join ([chr (x ^ 0x5C) for x in xrange(256)]) trans_36 = "".join ([chr (x ^ 0x36) for x in xrange(256)]) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index cd148e9d0a1..3f6f7567e69 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -302,12 +302,122 @@ class CopyTestCase(unittest.TestCase): self.assertTrue(h1.hexdigest() == h2.hexdigest(), "Hexdigest of copy doesn't match original hexdigest.") + +class CompareDigestTestCase(unittest.TestCase): + + def test_compare_digest(self): + # Testing input type exception handling + a, b = 100, 200 + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = 100, b"foobar" + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = b"foobar", 200 + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = u"foobar", b"foobar" + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = b"foobar", u"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" + self.assertTrue(hmac.compare_digest(a, b)) + a, b = b"\xde\xad\xbe\xef", b"\xde\xad\xbe\xef" + self.assertTrue(hmac.compare_digest(a, b)) + + # Testing bytearrays of same lengths, same values + a, b = bytearray(b"foobar"), bytearray(b"foobar") + self.assertTrue(hmac.compare_digest(a, b)) + + # Testing bytearrays of diffeent lengths + a, b = bytearray(b"foobar"), bytearray(b"foo") + self.assertFalse(hmac.compare_digest(a, b)) + + # Testing bytearrays of same lengths, different values + a, b = bytearray(b"foobar"), bytearray(b"foobaz") + self.assertFalse(hmac.compare_digest(a, b)) + + # Testing byte and bytearray of same lengths, same values + a, b = bytearray(b"foobar"), b"foobar" + self.assertTrue(hmac.compare_digest(a, b)) + self.assertTrue(hmac.compare_digest(b, a)) + + # Testing byte bytearray of diffeent lengths + a, b = bytearray(b"foobar"), b"foo" + self.assertFalse(hmac.compare_digest(a, b)) + self.assertFalse(hmac.compare_digest(b, a)) + + # Testing byte and bytearray of same lengths, different values + a, b = bytearray(b"foobar"), b"foobaz" + self.assertFalse(hmac.compare_digest(a, b)) + self.assertFalse(hmac.compare_digest(b, a)) + + # Testing str of same lengths + a, b = "foobar", "foobar" + self.assertTrue(hmac.compare_digest(a, b)) + + # Testing str of diffeent lengths + a, b = "foo", "foobar" + self.assertFalse(hmac.compare_digest(a, b)) + + # Testing bytes of same lengths, different values + a, b = "foobar", "foobaz" + self.assertFalse(hmac.compare_digest(a, b)) + + # Testing error cases + a, b = u"foobar", b"foobar" + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = b"foobar", u"foobar" + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = b"foobar", 1 + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = 100, 200 + self.assertRaises(TypeError, hmac.compare_digest, a, b) + a, b = "fooä", "fooä" + self.assertTrue(hmac.compare_digest(a, b)) + + # subclasses are supported by ignore __eq__ + class mystr(str): + def __eq__(self, other): + return False + + a, b = mystr("foobar"), mystr("foobar") + self.assertTrue(hmac.compare_digest(a, b)) + a, b = mystr("foobar"), "foobar" + self.assertTrue(hmac.compare_digest(a, b)) + a, b = mystr("foobar"), mystr("foobaz") + self.assertFalse(hmac.compare_digest(a, b)) + + class mybytes(bytes): + def __eq__(self, other): + return False + + a, b = mybytes(b"foobar"), mybytes(b"foobar") + self.assertTrue(hmac.compare_digest(a, b)) + a, b = mybytes(b"foobar"), b"foobar" + self.assertTrue(hmac.compare_digest(a, b)) + a, b = mybytes(b"foobar"), mybytes(b"foobaz") + self.assertFalse(hmac.compare_digest(a, b)) + + def test_main(): test_support.run_unittest( TestVectorsTestCase, ConstructorTestCase, SanityTestCase, - CopyTestCase + CopyTestCase, + CompareDigestTestCase, ) if __name__ == "__main__": diff --git a/Misc/NEWS b/Misc/NEWS index 57d1f247640..65489562da5 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -49,6 +49,9 @@ Core and Builtins Library ------- +- Issue #21306: Backport hmac.compare_digest from Python 3. This is part of PEP + 466. + - Issue #21321: itertools.islice() now releases the reference to the source iterator when the slice is exhausted. Patch by Anton Afanasyev. diff --git a/Modules/operator.c b/Modules/operator.c index 3f005884426..ac3caf8291c 100644 --- a/Modules/operator.c +++ b/Modules/operator.c @@ -235,6 +235,132 @@ op_delslice(PyObject *s, PyObject *a) #define spam2o(OP,ALTOP,DOC) {#OP, op_##OP, METH_O, PyDoc_STR(DOC)}, \ {#ALTOP, op_##OP, METH_O, PyDoc_STR(DOC)}, + + +/* compare_digest **********************************************************/ + +/* + * timing safe compare + * + * Returns 1 of the strings are equal. + * In case of len(a) != len(b) the function tries to keep the timing + * dependent on the length of b. CPU cache locally may still alter timing + * a bit. + */ +static int +_tscmp(const unsigned char *a, const unsigned char *b, + Py_ssize_t len_a, Py_ssize_t len_b) +{ + /* The volatile type declarations make sure that the compiler has no + * chance to optimize and fold the code in any way that may change + * the timing. + */ + volatile Py_ssize_t length; + volatile const unsigned char *left; + volatile const unsigned char *right; + Py_ssize_t i; + unsigned char result; + + /* loop count depends on length of b */ + length = len_b; + left = NULL; + right = b; + + /* don't use else here to keep the amount of CPU instructions constant, + * volatile forces re-evaluation + * */ + if (len_a == length) { + left = *((volatile const unsigned char**)&a); + result = 0; + } + if (len_a != length) { + left = b; + result = 1; + } + + for (i=0; i < length; i++) { + result |= *left++ ^ *right++; + } + + return (result == 0); +} + +PyDoc_STRVAR(compare_digest__doc__, +"compare_digest(a, b) -> bool\n" +"\n" +"Return 'a == b'. This function uses an approach designed to prevent\n" +"timing analysis, making it appropriate for cryptography.\n" +"a and b must both be of the same type: either str (ASCII only),\n" +"or any type that supports the buffer protocol (e.g. bytes).\n" +"\n" +"Note: If a and b are of different lengths, or if an error occurs,\n" +"a timing attack could theoretically reveal information about the\n" +"types and lengths of a and b--but not their values.\n"); + +static PyObject* +compare_digest(PyObject *self, PyObject *args) +{ + PyObject *a, *b; + int rc; + + if (!PyArg_ParseTuple(args, "OO:compare_digest", &a, &b)) { + return NULL; + } + + /* Unicode string */ + if (PyUnicode_Check(a) && PyUnicode_Check(b)) { + rc = _tscmp(PyUnicode_AS_DATA(a), + PyUnicode_AS_DATA(b), + PyUnicode_GET_DATA_SIZE(a), + PyUnicode_GET_DATA_SIZE(b)); + } + /* fallback to buffer interface for bytes, bytesarray and other */ + else { + Py_buffer view_a; + Py_buffer view_b; + + if ((PyObject_CheckBuffer(a) == 0) & (PyObject_CheckBuffer(b) == 0)) { + PyErr_Format(PyExc_TypeError, + "unsupported operand types(s) or combination of types: " + "'%.100s' and '%.100s'", + Py_TYPE(a)->tp_name, Py_TYPE(b)->tp_name); + return NULL; + } + + if (PyObject_GetBuffer(a, &view_a, PyBUF_SIMPLE) == -1) { + return NULL; + } + if (view_a.ndim > 1) { + PyErr_SetString(PyExc_BufferError, + "Buffer must be single dimension"); + PyBuffer_Release(&view_a); + return NULL; + } + + if (PyObject_GetBuffer(b, &view_b, PyBUF_SIMPLE) == -1) { + PyBuffer_Release(&view_a); + return NULL; + } + if (view_b.ndim > 1) { + PyErr_SetString(PyExc_BufferError, + "Buffer must be single dimension"); + PyBuffer_Release(&view_a); + PyBuffer_Release(&view_b); + return NULL; + } + + rc = _tscmp((const unsigned char*)view_a.buf, + (const unsigned char*)view_b.buf, + view_a.len, + view_b.len); + + PyBuffer_Release(&view_a); + PyBuffer_Release(&view_b); + } + + return PyBool_FromLong(rc); +} + static struct PyMethodDef operator_methods[] = { spam1o(isCallable, @@ -318,6 +444,8 @@ spam2(ne,__ne__, "ne(a, b) -- Same as a!=b.") spam2(gt,__gt__, "gt(a, b) -- Same as a>b.") spam2(ge,__ge__, "ge(a, b) -- Same as a>=b.") + {"_compare_digest", (PyCFunction)compare_digest, METH_VARARGS, + compare_digest__doc__}, {NULL, NULL} /* sentinel */ };