[paramiko] [MERGE] insecure use of RandomPool

Dwayne C. Litzenberger dlitz at dlitz.net
Sun Jan 13 20:48:12 PST 2008


Programs using paramiko that meet _either_ (or both) of the following 
criteria may be vulnerable to attacks on paramiko's random number 
generator:

     1. The program maintains multiple simultaneous paramiko connections 
        (Transport instances) via forking or threading; or
     
     2. The program uses certain Win32 builds of PyCrypto, where the 
        Crypt.Util.winrandom module is missing, thus preventing the 
        RandomPool instance from being initialized with sufficient entropy.

This message deals with primarily with the first criterion.

paramiko generates random numbers using a single instance of PyCrypto's 
RandomPool class.  Unfortunately, neither paramiko nor PyCrypto ensure 
that:

     1. after fork(), that the output of randpool.get_bytes() in the parent
        cannot be used to predict the output of randpool.get_bytes() in the 
        child, or vice versa; or
     
     2. that the state of the RandomPool instance is not corrupted by 
        multiple threads accessing it at the same time.

Consider the following Python program, which generates three separate RSA 
keys in three different child processes, and outputs the keys' 
fingerprints:

# -------[ snip here ]------------------------
from paramiko import RSAKey
import os
import time

# time.time() and time.clock() are predictable from a cryptographic standpoint,
# but make them REALLY predictable for the purposes of this demonstration.
class PredictableTime(object):
     def __init__(self):
         self.n = 0
     def __call__(self):
         self.n += 1
         return self.n

time.time = PredictableTime()
time.clock = PredictableTime()

def fork_and_generate_key():
     pid = os.fork()
     if pid != 0:
         # parent process
         os.waitpid(pid, 0)
         return
     else:
         # Child process
         k = RSAKey.generate(512)
         print "[PID %d] %s" % (os.getpid(), k.get_fingerprint().encode('hex'))
         os._exit(0)

fork_and_generate_key()
fork_and_generate_key()
fork_and_generate_key()
# -------[ snip here ]------------------------

Theoretically, each key generated using this program should be independent, 
regardless of whether the outputs of time.clock() and time.time() are 
predictable.  In reality, the program's output looks something like this:

     [PID 18872] bff46933095489d81af86493e4e9ba2f
     [PID 18873] bff46933095489d81af86493e4e9ba2f
     [PID 18874] bff46933095489d81af86493e4e9ba2f

This means that if you're using (for example) Python's 
SocketServer.ForkingTCPServer class to build an SSH server using paramiko, 
then supposedly "random" data from one session can be used to predict 
"random" data in another session.

We could try to make a thread-safe wrapper around RandomPool, and we could 
provide some API to let users randomize the pool after a fork(), but that's 
error-prone.  Also, because of a bug in PyCrypto 2.0.1 (where winrandom is 
not built), this would not be sufficient to fix the problem on Win32.

A better thing to do would be to let the operating system generate our 
random numbers for us, since it is in a far better position than we are to 
guarantee that RNG state is not leaked or reused.

The attached patch creates a new OSRandomPool class that provides a 
RandomPool-like interface, but gets its random numbers directly from the 
operating system.  It also works around the recently-published Windows 
CryptGenRandom vulnerabilities (see http://eprint.iacr.org/2007/419).

-- 
Dwayne C. Litzenberger <dlitz at dlitz.net>
-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: dlitz at dlitz.net-20080114035622-x9sdxlxwtehg6ey8
# target_branch: http://www.lag.net/paramiko/bzr/paramiko/
# testament_sha1: bca5d7f128ed9233a12e6f0c556445cbd19e5f7c
# timestamp: 2008-01-13 22:15:39 -0600
# base_revision_id: robey at lag.net-20071231052950-8h599bnez3sgbf2e
# 
# Begin patch
=== added file 'paramiko/osrandom.py'
--- paramiko/osrandom.py	1970-01-01 00:00:00 +0000
+++ paramiko/osrandom.py	2008-01-14 03:56:22 +0000
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: ascii -*-
+# Copyright (C) 2008  Dwayne C. Litzenberger <dlitz at dlitz.net>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import sys
+
+# Detect an OS random number source
+osrandom_source = None
+
+# Try os.urandom
+if osrandom_source is None:
+    try:
+        from os import urandom
+        osrandom_source = "os.urandom"
+    except ImportError:
+        pass
+
+# Try winrandom
+if osrandom_source is None:
+    try:
+        from Crypto.Util import winrandom
+        osrandom_source = "winrandom"
+    except ImportError:
+        pass
+
+# Try /dev/urandom
+if osrandom_source is None:
+    try:
+        _dev_urandom = open("/dev/urandom", "rb", 0)
+        def urandom(bytes):
+            return _def_urandom.read(bytes)
+        osrandom_source = "/dev/urandom"
+    except (OSError, IOError):
+        pass
+
+# Give up
+if osrandom_source is None:
+    raise ImportError("Cannot find OS entropy source")
+
+class BaseOSRandomPool(object):
+    def __init__(self, numbytes=160, cipher=None, hash=None):
+        pass
+
+    def stir(self, s=''):
+        # According to "Cryptanalysis of the Random Number Generator of the
+        # Windows Operating System", by Leo Dorrendorf and Zvi Gutterman
+        # and Benny Pinkas <http://eprint.iacr.org/2007/419>,
+        # CryptGenRandom only updates its internal state using kernel-provided
+        # random data every 128KiB of output.
+        if osrandom_source == 'winrandom' or sys.platform == 'win32':
+            self.get_bytes(128*1024)    # discard 128 KiB of output
+
+    def randomize(self, N=0):
+        self.stir()
+
+    def add_event(self, s=None):
+        pass
+
+class WinrandomOSRandomPool(BaseOSRandomPool):
+    def __init__(self, numbytes=160, cipher=None, hash=None):
+        self._wr = winrandom.new()
+        self.get_bytes = self._wr.get_bytes
+        self.randomize()
+
+class UrandomOSRandomPool(BaseOSRandomPool):
+    def __init__(self, numbytes=160, cipher=None, hash=None):
+        self.get_bytes = urandom
+        self.randomize()
+
+if osrandom_source in ("urandom", "os.urandom"):
+    OSRandomPool = UrandomOSRandomPool
+elif osrandom_source == "winrandom":
+    OSRandomPool = WinrandomOSRandomPool
+else:
+    raise AssertionError("Unrecognized osrandom_source %r" % (osrandom_source,))
+
+# vim:set ts=4 sw=4 sts=4 expandtab:

=== modified file 'paramiko/common.py'
--- paramiko/common.py	2007-11-19 03:12:09 +0000
+++ paramiko/common.py	2008-01-14 03:56:22 +0000
@@ -95,22 +95,10 @@
 DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
     DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
 
-
-from Crypto.Util.randpool import PersistentRandomPool, RandomPool
+from osrandom import OSRandomPool
 
 # keep a crypto-strong PRNG nearby
-import os
-try:
-    randpool = PersistentRandomPool(os.path.join(os.path.expanduser('~'), '/.randpool'))
-except:
-    # the above will likely fail on Windows - fall back to non-persistent random pool
-    randpool = RandomPool()
-
-try:
-    randpool.randomize()
-except:
-    # earlier versions of pyCrypto (pre-2.0) don't have randomize()
-    pass
+randpool = OSRandomPool()
 
 import sys
 if sys.version_info < (2, 3):

# Begin bundle
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
aCE0iaZInliJ40P2F3JFOFCQwl59rg==


More information about the paramiko mailing list