Repository: urllib3/urllib3

Issue:
SSL cert digest check uses non-constant-time comparison
This is the problematic line:
https://github.com/shazow/urllib3/blob/master/urllib3/util/ssl_.py#L137

The digests being compared are strings, and `==` is not constant-time, which could potentially allow a timing attack. While it may not immediately be obvious whether an adversary could gain any advantage by successfully timing this line, I am a firm believer in "better safe than sorry."

The best way to fix this is to use a constant-time digest comparison function, such as the built-in `hmac.compare_digest`. However, this function was only introduced in Python 2.7.7, while this library supports Python 2.6+. The next best solution would be to use something like the `constant_time_compare` function from the following package:
https://github.com/mitsuhiko/itsdangerous

Using a constant-time comparison is semantically equivalent to using `==`, and the performance impact is negligible and comes along with improved security. I believe this is a good tradeoff.



Discussion:

Resolving patch:
diff --git a/urllib3/util/ssl_.py b/urllib3/util/ssl_.py
--- a/urllib3/util/ssl_.py
+++ b/urllib3/util/ssl_.py
@@ -17,6 +17,25 @@
 
 import errno
 import warnings
+import hmac
+
+
+def _const_compare_digest_backport(a, b):
+    """
+    Compare two digests of equal length in constant time.
+
+    The digests must be of type str/bytes.
+    Returns True if the digests match, and False otherwise.
+    """
+    result = abs(len(a) - len(b))
+    for l, r in zip(bytearray(a), bytearray(b)):
+        result |= l ^ r
+    return result == 0
+
+
+_const_compare_digest = getattr(hmac, 'compare_digest',
+                                _const_compare_digest_backport)
+
 
 try:  # Test for SSL features
     import ssl
@@ -134,7 +153,7 @@ def assert_fingerprint(cert, fingerprint):
 
     cert_digest = hashfunc(cert).digest()
 
-    if cert_digest != fingerprint_bytes:
+    if not _const_compare_digest(cert_digest, fingerprint_bytes):
         raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".'
                        .format(fingerprint, hexlify(cert_digest)))
 


Test patch:
diff --git a/test/test_util.py b/test/test_util.py
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -1,3 +1,4 @@
+import hashlib
 import warnings
 import logging
 import unittest
@@ -18,6 +19,7 @@
 from urllib3.util.ssl_ import (
     resolve_cert_reqs,
     ssl_wrap_socket,
+    _const_compare_digest_backport,
 )
 from urllib3.exceptions import (
     LocationParseError,
@@ -412,3 +414,16 @@ def test_ssl_wrap_socket_with_no_sni(self):
         ssl_wrap_socket(ssl_context=mock_context, sock=socket)
         mock_context.wrap_socket.assert_called_once_with(socket)
         ssl_.HAS_SNI = HAS_SNI
+
+    def test_const_compare_digest_fallback(self):
+        target = hashlib.sha256(b'abcdef').digest()
+        self.assertTrue(_const_compare_digest_backport(target, target))
+
+        prefix = target[:-1]
+        self.assertFalse(_const_compare_digest_backport(target, prefix))
+
+        suffix = target + b'0'
+        self.assertFalse(_const_compare_digest_backport(target, suffix))
+
+        incorrect = hashlib.sha256(b'xyz').digest()
+        self.assertFalse(_const_compare_digest_backport(target, incorrect))