[pypy-commit] pypy default: Introducing the missing method _compare_digest in the module operator.

agobi noreply at buildbot.pypy.org
Thu Sep 11 22:48:31 CEST 2014


Author: Attila Gobi <attila.gobi at gmail.com>
Branch: 
Changeset: r73474:ac26684c69de
Date: 2014-07-27 13:23 +0200
http://bitbucket.org/pypy/pypy/changeset/ac26684c69de/

Log:	Introducing the missing method _compare_digest in the module
	operator. The method is used in the hmac module. (grafted from
	b453ad72cd9a80f665d4dd4a0565f1321c9bc3be)

diff --git a/pypy/module/operator/__init__.py b/pypy/module/operator/__init__.py
--- a/pypy/module/operator/__init__.py
+++ b/pypy/module/operator/__init__.py
@@ -39,7 +39,7 @@
                     'irshift', 'isub', 'itruediv', 'ixor', '_length_hint']
 
     interpleveldefs = {
-        '_compare_digest': 'interp_operator.compare_digest',
+        '_compare_digest': 'tscmp.compare_digest',
     }
 
     for name in interp_names:
diff --git a/pypy/module/operator/test/test_operator.py b/pypy/module/operator/test/test_operator.py
--- a/pypy/module/operator/test/test_operator.py
+++ b/pypy/module/operator/test/test_operator.py
@@ -334,3 +334,15 @@
         assert operator._compare_digest(a, b)
         a, b = mybytes(b"foobar"), mybytes(b"foobaz")
         assert not operator._compare_digest(a, b)
+
+    def test_compare_digest_buffer(self):
+        import operator
+        assert operator._compare_digest(b'asd', b'asd')
+        assert not operator._compare_digest(b'asd', b'qwe')
+        assert not operator._compare_digest(b'asd', b'asdq')
+
+    def test_compare_digest_ascii(self):
+        import operator
+        assert operator._compare_digest('asd', 'asd')
+        assert not operator._compare_digest('asd', 'qwe')
+        assert not operator._compare_digest('asd', 'asdq')
diff --git a/pypy/module/operator/test/test_tscmp.py b/pypy/module/operator/test/test_tscmp.py
new file mode 100644
--- /dev/null
+++ b/pypy/module/operator/test/test_tscmp.py
@@ -0,0 +1,15 @@
+from pypy.module.operator.tscmp import pypy_tscmp
+from rpython.rtyper.lltypesystem.rffi import scoped_nonmovingbuffer
+
+class TestTimingSafeCompare:
+    def test_tscmp_neq(self):
+        assert not pypy_tscmp('asd', 'qwe', 3, 3)
+
+    def test_tscmp_eq(self):
+        assert pypy_tscmp('asd', 'asd', 3, 3)
+
+    def test_tscmp_len(self):
+        assert pypy_tscmp('asdp', 'asdq', 3, 3)
+
+    def test_tscmp_nlen(self):
+        assert not pypy_tscmp('asd', 'asd', 2, 3)
diff --git a/pypy/module/operator/tscmp.c b/pypy/module/operator/tscmp.c
new file mode 100644
--- /dev/null
+++ b/pypy/module/operator/tscmp.c
@@ -0,0 +1,42 @@
+/* From CPython 3.3.5's operator.c
+ */
+
+#include <stdlib.h>
+#include "tscmp.h"
+
+int
+pypy_tscmp(const unsigned char *a, const unsigned char *b, long len_a, long 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 long length;
+    volatile const unsigned char *left;
+    volatile const unsigned char *right;
+    long 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);
+}
diff --git a/pypy/module/operator/tscmp.h b/pypy/module/operator/tscmp.h
new file mode 100644
--- /dev/null
+++ b/pypy/module/operator/tscmp.h
@@ -0,0 +1,1 @@
+int pypy_tscmp(const unsigned char *a, const unsigned char *b, long len_a, long len_b);
diff --git a/pypy/module/operator/tscmp.py b/pypy/module/operator/tscmp.py
new file mode 100644
--- /dev/null
+++ b/pypy/module/operator/tscmp.py
@@ -0,0 +1,42 @@
+"""
+Provides _compare_digest method, which is a safe comparing to prevent timing
+attacks for the hmac module.
+"""
+import py
+from rpython.rtyper.lltypesystem import lltype, rffi
+from rpython.translator.tool.cbuild import ExternalCompilationInfo
+from pypy.interpreter.error import OperationError
+
+cwd = py.path.local(__file__).dirpath()
+eci = ExternalCompilationInfo(
+    includes=[cwd.join('tscmp.h')],
+    separate_module_files=[cwd.join('tscmp.c')],
+    export_symbols=['pypy_tscmp'])
+
+def llexternal(*args, **kwargs):
+    kwargs.setdefault('compilation_info', eci)
+    kwargs.setdefault('sandboxsafe', True)
+    return rffi.llexternal(*args, **kwargs)
+
+pypy_tscmp = llexternal('pypy_tscmp', [rffi.CCHARP, rffi.CCHARP, rffi.LONG, rffi.LONG], rffi.INT)
+
+def compare_digest(space, w_a, w_b):
+    if space.isinstance_w(w_a, space.w_unicode) and space.isinstance_w(w_b, space.w_unicode):
+        try:
+            a_value = space.call_method(w_a, "encode", space.wrap("ascii"))
+            b_value = space.call_method(w_b, "encode", space.wrap("ascii"))
+            return compare_digest_buffer(space, a_value, b_value)
+        except OperationError as e:
+            if not e.match(space, space.w_UnicodeEncodeError):
+                raise
+            raise OperationError(space.w_TypeError,
+                    space.wrap("comparing strings with non-ASCII characters is not supported"))
+    else:
+        return compare_digest_buffer(space, w_a, w_b)
+
+def compare_digest_buffer(space, w_a, w_b):
+    a = space.bufferstr_w(w_a)
+    b = space.bufferstr_w(w_b)
+    with rffi.scoped_nonmovingbuffer(a) as a_buffer:
+        with rffi.scoped_nonmovingbuffer(b) as b_buffer:
+            return space.wrap(pypy_tscmp(a_buffer, b_buffer, len(a), len(b)))


More information about the pypy-commit mailing list