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
|
||||
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`
|
||||
|
|
|
@ -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)])
|
||||
|
||||
|
|
|
@ -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__":
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue