diff --git a/Lib/hashlib.py b/Lib/hashlib.py --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -130,9 +130,13 @@ __get_hash = __get_openssl_constructor algorithms_available = algorithms_available.union( _hashlib.openssl_md_meth_names) + timingsafe_cmp = _hashlib.timingsafe_cmp except ImportError: new = __py_new __get_hash = __get_builtin_constructor + import _hashlibfb + timingsafe_cmp = _hashlibfb.timingsafe_cmp + for __func_name in __always_supported: # try them all, some may not work due to the OpenSSL diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -372,8 +372,82 @@ self.assertEqual(expected_hash, hasher.hexdigest()) + +class TimingSafeCompareTestCase(unittest.TestCase): + def test_timingsafe_cmp(self): + # Testing bytes of different lengths + a, b = b"foobar", b"foo" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + a, b = b"\xde\xad\xbe\xef", b"\xde\xad" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + + # Testing bytes of same lengths, different values + a, b = b"foobar", b"foobaz" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + a, b = b"\xde\xad\xbe\xef", b"\xab\xad\x1d\xea" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + + # Testing bytes of same lengths, same values + a, b = b"foobar", b"foobar" + self.assertTrue(hashlib.timingsafe_cmp(a, b)) + a, b = b"\xde\xad\xbe\xef", b"\xde\xad\xbe\xef" + self.assertTrue(hashlib.timingsafe_cmp(a, b)) + + # Testing bytearrays of same lengths, same values + a, b = bytearray(b"foobar"), bytearray(b"foobar") + self.assertTrue(hashlib.timingsafe_cmp(a, b)) + + # Testing bytearrays of diffeent lengths + a, b = bytearray(b"foobar"), bytearray(b"foo") + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + + # Testing bytearrays of same lengths, different values + a, b = bytearray(b"foobar"), bytearray(b"foobaz") + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + + # Testing byte and bytearray of same lengths, same values + a, b = bytearray(b"foobar"), b"foobar" + self.assertTrue(hashlib.timingsafe_cmp(a, b)) + self.assertTrue(hashlib.timingsafe_cmp(b, a)) + + # Testing byte bytearray of diffeent lengths + a, b = bytearray(b"foobar"), b"foo" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + self.assertFalse(hashlib.timingsafe_cmp(b, a)) + + # Testing byte and bytearray of same lengths, different values + a, b = bytearray(b"foobar"), b"foobaz" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + self.assertFalse(hashlib.timingsafe_cmp(b, a)) + + # Testing str of same lengths + a, b = "foobar", "foobar" + self.assertTrue(hashlib.timingsafe_cmp(a, b)) + + # Testing str of diffeent lengths + a, b = "foo", "foobar" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + + # Testing bytes of same lengths, different values + a, b = "foobar", "foobaz" + self.assertFalse(hashlib.timingsafe_cmp(a, b)) + + # Testing error cases + a, b = "foobar", b"foobar" + self.assertRaises(TypeError, hashlib.timingsafe_cmp, a, b) + a, b = b"foobar", "foobar" + self.assertRaises(TypeError, hashlib.timingsafe_cmp, a, b) + a, b = b"foobar", 1 + self.assertRaises(TypeError, hashlib.timingsafe_cmp, a, b) + a, b = 100, 200 + self.assertRaises(TypeError, hashlib.timingsafe_cmp, a, b) + a, b = "fooä", "fooä" + self.assertRaises(TypeError, hashlib.timingsafe_cmp, a, b) + + def test_main(): support.run_unittest(HashLibTestCase) + support.run_unittest(TimingSafeCompareTestCase) if __name__ == "__main__": test_main() diff --git a/Modules/_hashlibfb.c b/Modules/_hashlibfb.c new file mode 100644 --- /dev/null +++ b/Modules/_hashlibfb.c @@ -0,0 +1,42 @@ +/* Fallback for _hashlib on Systems without OpenSSL */ +#define PY_SSIZE_T_CLEAN + +#include "Python.h" +#define TIMINGSAFE_COMPARE 1 +#include "hashlib.h" +#undef TIMINGSAFE_COMPARE + +/* List of functions exported by this module */ + +static struct PyMethodDef _hashlibfb_functions[] = { + {"timingsafe_cmp", (PyCFunction)timingsafe_cmp, METH_VARARGS, + timingsafe_cmp__doc__}, + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +static struct PyModuleDef _hashlibfbmodule = { + PyModuleDef_HEAD_INIT, + "_hashlibfb", + NULL, + -1, + _hashlibfb_functions, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit__hashlibfb(void) +{ + PyObject *m; + + m = PyModule_Create(&_hashlibfbmodule); + if (m == NULL) + return NULL; + + return m; +} diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -15,7 +15,9 @@ #include "Python.h" #include "structmember.h" +#define TIMINGSAFE_COMPARE 1 #include "hashlib.h" +#undef TIMINGSAFE_COMPARE #ifdef WITH_THREAD #include "pythread.h" @@ -605,7 +607,7 @@ /* List of functions exported by this module */ -static struct PyMethodDef EVP_functions[] = { +static struct PyMethodDef _hashlib_functions[] = { {"new", (PyCFunction)EVP_new, METH_VARARGS|METH_KEYWORDS, EVP_new__doc__}, CONSTRUCTOR_METH_DEF(md5), CONSTRUCTOR_METH_DEF(sha1), @@ -615,6 +617,8 @@ CONSTRUCTOR_METH_DEF(sha384), CONSTRUCTOR_METH_DEF(sha512), #endif + {"timingsafe_cmp", (PyCFunction)timingsafe_cmp, METH_VARARGS, + timingsafe_cmp__doc__}, {NULL, NULL} /* Sentinel */ }; @@ -627,7 +631,7 @@ "_hashlib", NULL, -1, - EVP_functions, + _hashlib_functions, NULL, NULL, NULL, diff --git a/Modules/hashlib.h b/Modules/hashlib.h --- a/Modules/hashlib.h +++ b/Modules/hashlib.h @@ -26,3 +26,153 @@ return NULL; \ } \ } while(0); + + +#ifdef TIMINGSAFE_COMPARE + +/* + * 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; + right = b; + + /* don't use else here */ + 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(timingsafe_cmp__doc__, +"timingsafe_cmp(a, b) -> bool\n" +"\n" +"Return the equivalent of 'a == b', but avoid any short circuiting to\n" +"counterfeit timing analysis of input data. The function should be used to\n" +"compare cryptographic secrets.\n" +"\n" +"a and b must both be either of type byte, str or support the buffer\n" +"protocol at the same time. Subclasses of str are not supported,\n" +"subclasses of bytes fall back to the buffer protocol.\n" +"\n" +"Note: In case of an error or different lengths the function may disclose\n" +"some timing information about the types and lengths of a and b.\n"); + + +PyObject* +timingsafe_cmp(PyObject *self, PyObject *args) +{ + PyObject *a, *b; + int rc; + PyObject *result; + + if (!PyArg_ParseTuple(args, "OO:timingsafe_cmp", &a, &b)) { + return NULL; + } + + /* most common case first */ + if ((PyBytes_CheckExact(a) != 0) & (PyBytes_CheckExact(b) != 0)) { + rc = _tscmp((const unsigned char*)PyBytes_AS_STRING(a), + (const unsigned char*)PyBytes_AS_STRING(b), + PyBytes_GET_SIZE(a), + PyBytes_GET_SIZE(b)); + } + /* ASCII unicode string */ + else if((PyUnicode_CheckExact(a) != 0) & (PyUnicode_CheckExact(b) != 0)) { + if (PyUnicode_READY(a) == -1 || PyUnicode_READY(b) == -1) { + assert(0 && "unicode_eq ready fail"); + PyErr_SetString(PyExc_TypeError, + "unicode not ready"); + return NULL; + } + if (!PyUnicode_IS_COMPACT_ASCII(a) || !PyUnicode_IS_COMPACT_ASCII(b)) { + PyErr_SetString(PyExc_TypeError, + "comparing strings with non-ASCII characters is " + "not supported"); + return NULL; + } + + rc = _tscmp(_PyUnicode_COMPACT_DATA(a), + _PyUnicode_COMPACT_DATA(b), + PyUnicode_GET_LENGTH(a), + PyUnicode_GET_LENGTH(b)); + } + /* fallback to buffer interface */ + 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); + } + + result = PyBool_FromLong(rc); + Py_INCREF(result); + return result; +} + +#endif /* TIMINGSAFE_COMPARE */ diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -749,6 +749,10 @@ openssl_ver) missing.append('_hashlib') + # _hashlib fallback for supplementary function + exts.append( Extension('_hashlibfb', ['_hashlibfb.c'], + depends=['hashlib.h']) ) + # We always compile these even when OpenSSL is available (issue #14693). # It's harmless and the object code is tiny (40-50 KB per module, # only loaded when actually used).