backport hmac.compare_digest to partially implement PEP 466 (closes #21306)
Backport from Alex Gaynor.
This commit is contained in:
parent
e9314e4a3c
commit
629026aecc
|
@ -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
|
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.
|
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()
|
.. 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
|
containing only hexadecimal digits. This may be used to exchange the value
|
||||||
safely in email or other non-binary environments.
|
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()
|
.. 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.
|
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::
|
.. seealso::
|
||||||
|
|
||||||
Module :mod:`hashlib`
|
Module :mod:`hashlib`
|
||||||
|
|
|
@ -5,6 +5,9 @@ Implements the HMAC algorithm as described by RFC 2104.
|
||||||
|
|
||||||
import warnings as _warnings
|
import warnings as _warnings
|
||||||
|
|
||||||
|
from operator import _compare_digest as compare_digest
|
||||||
|
|
||||||
|
|
||||||
trans_5C = "".join ([chr (x ^ 0x5C) for x in xrange(256)])
|
trans_5C = "".join ([chr (x ^ 0x5C) for x in xrange(256)])
|
||||||
trans_36 = "".join ([chr (x ^ 0x36) for x in xrange(256)])
|
trans_36 = "".join ([chr (x ^ 0x36) for x in xrange(256)])
|
||||||
|
|
||||||
|
|
|
@ -302,12 +302,122 @@ class CopyTestCase(unittest.TestCase):
|
||||||
self.assertTrue(h1.hexdigest() == h2.hexdigest(),
|
self.assertTrue(h1.hexdigest() == h2.hexdigest(),
|
||||||
"Hexdigest of copy doesn't match original 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():
|
def test_main():
|
||||||
test_support.run_unittest(
|
test_support.run_unittest(
|
||||||
TestVectorsTestCase,
|
TestVectorsTestCase,
|
||||||
ConstructorTestCase,
|
ConstructorTestCase,
|
||||||
SanityTestCase,
|
SanityTestCase,
|
||||||
CopyTestCase
|
CopyTestCase,
|
||||||
|
CompareDigestTestCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -49,6 +49,9 @@ Core and Builtins
|
||||||
Library
|
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
|
- Issue #21321: itertools.islice() now releases the reference to the source
|
||||||
iterator when the slice is exhausted. Patch by Anton Afanasyev.
|
iterator when the slice is exhausted. Patch by Anton Afanasyev.
|
||||||
|
|
||||||
|
|
|
@ -235,6 +235,132 @@ op_delslice(PyObject *s, PyObject *a)
|
||||||
#define spam2o(OP,ALTOP,DOC) {#OP, op_##OP, METH_O, PyDoc_STR(DOC)}, \
|
#define spam2o(OP,ALTOP,DOC) {#OP, op_##OP, METH_O, PyDoc_STR(DOC)}, \
|
||||||
{#ALTOP, 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[] = {
|
static struct PyMethodDef operator_methods[] = {
|
||||||
|
|
||||||
spam1o(isCallable,
|
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(gt,__gt__, "gt(a, b) -- Same as a>b.")
|
||||||
spam2(ge,__ge__, "ge(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 */
|
{NULL, NULL} /* sentinel */
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue