You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
353 lines
12 KiB
353 lines
12 KiB
|
|
# HG changeset patch |
|
# User Benjamin Peterson <benjamin@python.org> |
|
# Date 1399849904 25200 |
|
# Node ID b40f1a00b13460cc089450028280c4e52dd24a64 |
|
# Parent 951775c68b1b7782750c213b0fce1f61d46b2f51 |
|
backport hmac.compare_digest to partially implement PEP 466 (closes #21306) |
|
|
|
Backport from Alex Gaynor. |
|
|
|
diff --git a/Doc/library/hmac.rst b/Doc/library/hmac.rst |
|
--- 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 |
|
--- a/Lib/hmac.py |
|
+++ b/Lib/hmac.py |
|
@@ -5,6 +5,9 @@ Implements the HMAC algorithm as describ |
|
|
|
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 |
|
--- 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/Modules/operator.c b/Modules/operator.c |
|
--- 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! |
|
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 */ |
|
|
|
}; |
|
|
|
|