[Checkins] SVN: Zope/branches/2.12/ LP #1071067: Use a stronger random number generator and a constant time comparison function.

Hano Schlichting cvs-admin at zope.org
Wed Oct 31 14:13:08 UTC 2012


Log message for revision 128158:
  LP #1071067: Use a stronger random number generator and a constant time comparison function.
  

Changed:
  U   Zope/branches/2.12/doc/CHANGES.rst
  U   Zope/branches/2.12/setup.py
  U   Zope/branches/2.12/src/AccessControl/AuthEncoding.py
  U   Zope/branches/2.12/src/Products/Sessions/BrowserIdManager.py
  U   Zope/branches/2.12/versions.cfg

-=-
Modified: Zope/branches/2.12/doc/CHANGES.rst
===================================================================
--- Zope/branches/2.12/doc/CHANGES.rst	2012-10-31 14:10:50 UTC (rev 128157)
+++ Zope/branches/2.12/doc/CHANGES.rst	2012-10-31 14:13:07 UTC (rev 128158)
@@ -5,9 +5,12 @@
 Change information for previous versions of Zope can be found at
 http://docs.zope.org/zope2/releases/.
 
-2.12.26 (unreleased)
+2.12.26 (2012-10-31)
 --------------------
 
+- LP #1071067: Use a stronger random number generator and a constant time
+  comparison function.
+
 - LP #930812: Scrub headers a bit more.
 
 2.12.25 (2012-09-18)

Modified: Zope/branches/2.12/setup.py
===================================================================
--- Zope/branches/2.12/setup.py	2012-10-31 14:10:50 UTC (rev 128157)
+++ Zope/branches/2.12/setup.py	2012-10-31 14:13:07 UTC (rev 128158)
@@ -16,7 +16,7 @@
 from setuptools import setup, find_packages, Extension
 
 setup(name='Zope2',
-    version='2.12.26dev',
+    version='2.12.26',
     url='http://www.zope.org',
     license='ZPL 2.1',
     description='Zope2 application server / web framework',

Modified: Zope/branches/2.12/src/AccessControl/AuthEncoding.py
===================================================================
--- Zope/branches/2.12/src/AccessControl/AuthEncoding.py	2012-10-31 14:10:50 UTC (rev 128157)
+++ Zope/branches/2.12/src/AccessControl/AuthEncoding.py	2012-10-31 14:13:07 UTC (rev 128158)
@@ -11,18 +11,59 @@
 #
 ##############################################################################
 
-__version__='$Revision: 1.9 $'[11:-2]
+import binascii
+from binascii import b2a_base64, a2b_base64
+from hashlib import sha1 as sha
+from hashlib import sha256
+from os import getpid
+import time
 
+# Use the system PRNG if possible
+import random
 try:
-    from hashlib import sha1 as sha
-except:
-    from sha import new as sha
+    random = random.SystemRandom()
+    using_sysrandom = True
+except NotImplementedError:
+    using_sysrandom = False
 
-import binascii
-from binascii import b2a_base64, a2b_base64
-from random import choice, randrange
 
+def _reseed():
+    if not using_sysrandom:
+        # This is ugly, and a hack, but it makes things better than
+        # the alternative of predictability. This re-seeds the PRNG
+        # using a value that is hard for an attacker to predict, every
+        # time a random string is required. This may change the
+        # properties of the chosen random sequence slightly, but this
+        # is better than absolute predictability.
+        random.seed(sha256(
+            "%s%s%s" % (random.getstate(), time.time(), getpid())
+        ).digest())
 
+
+def _choice(c):
+    _reseed()
+    return random.choice(c)
+
+
+def _randrange(r):
+    _reseed()
+    return random.randrange(r)
+
+
+def constant_time_compare(val1, val2):
+    """
+    Returns True if the two strings are equal, False otherwise.
+
+    The time taken is independent of the number of characters that match.
+    """
+    if len(val1) != len(val2):
+        return False
+    result = 0
+    for x, y in zip(val1, val2):
+        result |= ord(x) ^ ord(y)
+    return result == 0
+
+
 class PasswordEncryptionScheme:  # An Interface
 
     def encrypt(pw):
@@ -40,12 +81,14 @@
 
 _schemes = []
 
+
 def registerScheme(id, s):
     '''
     Registers an LDAP password encoding scheme.
     '''
     _schemes.append((id, '{%s}' % id, s))
 
+
 def listSchemes():
     r = []
     for id, prefix, scheme in _schemes:
@@ -67,7 +110,7 @@
         # All 256 characters are available.
         salt = ''
         for n in range(7):
-            salt += chr(randrange(256))
+            salt += chr(_randrange(256))
         return salt
 
     def encrypt(self, pw):
@@ -83,7 +126,7 @@
             return 0
         salt = ref[20:]
         compare = b2a_base64(sha(attempt + salt).digest() + salt)[:-1]
-        return (compare == reference)
+        return constant_time_compare(compare, reference)
 
 registerScheme('SSHA', SSHADigestScheme())
 
@@ -95,7 +138,7 @@
 
     def validate(self, reference, attempt):
         compare = b2a_base64(sha(attempt).digest())[:-1]
-        return (compare == reference)
+        return constant_time_compare(compare, reference)
 
 registerScheme('SHA', SHADigestScheme())
 
@@ -114,14 +157,14 @@
             choices = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                        "abcdefghijklmnopqrstuvwxyz"
                        "0123456789./")
-            return choice(choices) + choice(choices)
+            return _choice(choices) + _choice(choices)
 
         def encrypt(self, pw):
             return crypt(pw, self.generate_salt())
 
         def validate(self, reference, attempt):
             a = crypt(attempt, reference[:2])
-            return (a == reference)
+            return constant_time_compare(a, reference)
 
     registerScheme('CRYPT', CryptDigestScheme())
 
@@ -144,7 +187,7 @@
 
     def validate(self, reference, attempt):
         a = self.encrypt(attempt)
-        return (a == reference)
+        return constant_time_compare(a, reference)
 
 registerScheme('MYSQL', MySQLDigestScheme())
 
@@ -158,8 +201,9 @@
         if reference[:lp] == prefix:
             return scheme.validate(reference[lp:], attempt)
     # Assume cleartext.
-    return (reference == attempt)
+    return constant_time_compare(reference, attempt)
 
+
 def is_encrypted(pw):
     for id, prefix, scheme in _schemes:
         lp = len(prefix)
@@ -167,12 +211,13 @@
             return 1
     return 0
 
+
 def pw_encrypt(pw, encoding='SSHA'):
     """Encrypt the provided plain text password using the encoding if provided
     and return it in an LDAP-style representation."""
     for id, prefix, scheme in _schemes:
         if encoding == id:
             return prefix + scheme.encrypt(pw)
-    raise ValueError, 'Not supported: %s' % encoding
+    raise ValueError('Not supported: %s' % encoding)
 
 pw_encode = pw_encrypt  # backward compatibility

Modified: Zope/branches/2.12/src/Products/Sessions/BrowserIdManager.py
===================================================================
--- Zope/branches/2.12/src/Products/Sessions/BrowserIdManager.py	2012-10-31 14:10:50 UTC (rev 128157)
+++ Zope/branches/2.12/src/Products/Sessions/BrowserIdManager.py	2012-10-31 14:13:07 UTC (rev 128158)
@@ -1,5 +1,5 @@
 ############################################################################
-# 
+#
 # Copyright (c) 2002 Zope Foundation and Contributors.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -13,8 +13,9 @@
 
 import binascii
 from cgi import escape
+from hashlib import sha256
 import logging
-import random
+import os
 import re
 import string
 import sys
@@ -63,6 +64,29 @@
 
 LOG = logging.getLogger('Zope.BrowserIdManager')
 
+# Use the system PRNG if possible
+import random
+try:
+    random = random.SystemRandom()
+    using_sysrandom = True
+except NotImplementedError:
+    using_sysrandom = False
+
+
+def _randint(start, end):
+    if not using_sysrandom:
+        # This is ugly, and a hack, but it makes things better than
+        # the alternative of predictability. This re-seeds the PRNG
+        # using a value that is hard for an attacker to predict, every
+        # time a random string is required. This may change the
+        # properties of the chosen random sequence slightly, but this
+        # is better than absolute predictability.
+        random.seed(sha256(
+            "%s%s%s" % (random.getstate(), time.time(), os.getpid())
+        ).digest())
+    return random.randint(start, end)
+
+
 def constructBrowserIdManager(
     self, id=BROWSERID_MANAGER_NAME, title='', idname='_ZopeId',
     location=('cookies', 'form'), cookiepath='/', cookiedomain='',
@@ -558,7 +582,7 @@
         return None
 
 
-def getNewBrowserId(randint=random.randint, maxint=99999999):
+def getNewBrowserId(randint=_randint, maxint=99999999):
     """ Returns 19-character string browser id
     'AAAAAAAABBBBBBBB'
     where:
@@ -573,5 +597,4 @@
 
     An example is: 89972317A0C3EHnUi90w
     """
-    return '%08i%s' % (randint(0, maxint-1), getB64TStamp())
-
+    return '%08i%s' % (randint(0, maxint - 1), getB64TStamp())

Modified: Zope/branches/2.12/versions.cfg
===================================================================
--- Zope/branches/2.12/versions.cfg	2012-10-31 14:10:50 UTC (rev 128157)
+++ Zope/branches/2.12/versions.cfg	2012-10-31 14:13:07 UTC (rev 128158)
@@ -2,7 +2,7 @@
 versions = versions
 
 [versions]
-Zope2 =
+Zope2 = 2.12.26
 Acquisition = 2.13.8
 buildout.dumppickedversions = 0.4
 ClientForm = 0.2.10



More information about the checkins mailing list