backport hmac.compare_digest to partially implement PEP 466 (closes #21306)

Backport from Alex Gaynor.
This commit is contained in:
Benjamin Peterson 2014-05-11 16:11:44 -07:00
parent e9314e4a3c
commit 629026aecc
5 changed files with 278 additions and 1 deletions

View File

@ -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`

View File

@ -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)])

View File

@ -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__":

View File

@ -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.

View File

@ -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 */
};