summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/common.py4
-rw-r--r--paramiko/osrandom.py159
-rw-r--r--paramiko/rng.py112
-rw-r--r--paramiko/rng_posix.py97
-rw-r--r--paramiko/rng_win32.py121
5 files changed, 332 insertions, 161 deletions
diff --git a/paramiko/common.py b/paramiko/common.py
index 89b34cf5..f4a4d814 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -95,10 +95,10 @@ CONNECTION_FAILED_CODE = {
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
-from osrandom import OSRandomPool
+from rng import StrongLockingRandomPool
# keep a crypto-strong PRNG nearby
-randpool = OSRandomPool()
+randpool = StrongLockingRandomPool()
import sys
if sys.version_info < (2, 3):
diff --git a/paramiko/osrandom.py b/paramiko/osrandom.py
deleted file mode 100644
index 6c559e02..00000000
--- a/paramiko/osrandom.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/python
-# -*- coding: ascii -*-
-# Copyright (C) 2008 Dwayne C. Litzenberger <dlitz@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
-
-##
-## Find potential random number sources
-##
-
-# Try to open /dev/urandom now so that paramiko will be able to access
-# it even if os.chroot() is invoked later.
-try:
- _dev_urandom = open("/dev/urandom", "rb", 0)
-except EnvironmentError:
- _dev_urandom = None
-
-# Try to import the "winrandom" module
-try:
- from Crypto.Util import winrandom
-except ImportError:
- winrandom = None
-
-# Lastly, try to get the plain "RandomPool"
-# (sometimes windows doesn't even have winrandom!)
-try:
- from Crypto.Util.randpool import RandomPool
-except ImportError:
- RandomPool = None
-
-
-##
-## Define RandomPool classes
-##
-
-def _workaround_windows_cryptgenrandom_bug(self):
- # 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.
- self.get_bytes(128*1024) # discard 128 KiB of output
-
-
-class BaseOSRandomPool(object):
- def __init__(self, numbytes=160, cipher=None, hash=None):
- pass
-
- def stir(self, s=''):
- pass
-
- def randomize(self, N=0):
- self.stir()
-
- def add_event(self, s=None):
- pass
-
-
-class WinRandomPool(BaseOSRandomPool):
- """RandomPool that uses the C{winrandom} module for input"""
- def __init__(self, numbytes=160, cipher=None, hash=None):
- self._wr = winrandom.new()
- self.get_bytes = self._wr.get_bytes
- self.randomize()
-
- def stir(self, s=''):
- _workaround_windows_cryptgenrandom_bug(self)
-
-
-class DevUrandomPool(BaseOSRandomPool):
- """RandomPool that uses the C{/dev/urandom} special device node for input"""
- def __init__(self, numbytes=160, cipher=None, hash=None):
- self.randomize()
-
- def get_bytes(self, n):
- bytes = ""
- while len(bytes) < n:
- bytes += _dev_urandom.read(n - len(bytes))
- return bytes
-
-
-class FallbackRandomPool (BaseOSRandomPool):
- def __init__(self):
- self._wr = RandomPool()
- self.randomize()
-
- def get_bytes(self, n):
- return self._wr.get_bytes(n)
-
-
-##
-## Detect default random number source
-##
-osrandom_source = None
-
-# Try /dev/urandom
-if osrandom_source is None and _dev_urandom is not None:
- osrandom_source = "/dev/urandom"
- DefaultRandomPoolClass = DevUrandomPool
-
-# Try winrandom
-if osrandom_source is None and winrandom is not None:
- osrandom_source = "winrandom"
- DefaultRandomPoolClass = WinRandomPool
-
-# Try final fallback
-if osrandom_source is None and RandomPool is not None:
- osrandom_source = "randompool"
- DefaultRandomPoolClass = FallbackRandomPool
-
-# Give up
-if osrandom_source is None:
- raise ImportError("Cannot find OS entropy source")
-
-
-##
-## Define wrapper class
-##
-
-class OSRandomPool(object):
- """RandomPool wrapper.
-
- The C{randpool} attribute of this object may be modified by users of this class at runtime.
- """
-
- def __init__(self, instance=None):
- if instance is None:
- instance = DefaultRandomPoolClass()
- self.randpool = instance
-
- def stir(self, s=''):
- self.randpool.stir(s)
-
- def randomize(self, N=0):
- self.randpool.randomize(N)
-
- def add_event(self, s=None):
- self.randpool.add_event(s)
-
- def get_bytes(self, N):
- return self.randpool.get_bytes(N)
-
-# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/paramiko/rng.py b/paramiko/rng.py
new file mode 100644
index 00000000..46329d1e
--- /dev/null
+++ b/paramiko/rng.py
@@ -0,0 +1,112 @@
+#!/usr/bin/python
+# -*- coding: ascii -*-
+# Copyright (C) 2008 Dwayne C. Litzenberger <dlitz@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
+import threading
+from Crypto.Util.randpool import RandomPool as _RandomPool
+
+try:
+ import platform
+except ImportError:
+ platform = None # Not available using Python 2.2
+
+def _strxor(a, b):
+ assert len(a) == len(b)
+ return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), a, b))
+
+##
+## Find a strong random entropy source, depending on the detected platform.
+## WARNING TO DEVELOPERS: This will fail on some systems, but do NOT use
+## Crypto.Util.randpool.RandomPool as a fall-back. RandomPool will happily run
+## with very little entropy, thus _silently_ defeating any security that
+## Paramiko attempts to provide. (This is current as of PyCrypto 2.0.1).
+## See http://www.lag.net/pipermail/paramiko/2008-January/000599.html
+## and http://www.lag.net/pipermail/paramiko/2008-April/000678.html
+##
+
+if ((platform is not None and platform.system().lower() == 'windows') or
+ sys.platform == 'win32'):
+ # MS Windows
+ from paramiko import rng_win32
+ rng_device = rng_win32.open_rng_device()
+else:
+ # Assume POSIX (any system where /dev/urandom exists)
+ from paramiko import rng_posix
+ rng_device = rng_posix.open_rng_device()
+
+
+class StrongLockingRandomPool(object):
+ """Wrapper around RandomPool guaranteeing strong random numbers.
+
+ Crypto.Util.randpool.RandomPool will silently operate even if it is seeded
+ with little or no entropy, and it provides no prediction resistance if its
+ state is ever compromised throughout its runtime. It is also not thread-safe.
+
+ This wrapper augments RandomPool by XORing its output with random bits from
+ the operating system, and by controlling access to the underlying
+ RandomPool using an exclusive lock.
+ """
+
+ def __init__(self, instance=None):
+ if instance is None:
+ instance = _RandomPool()
+ self.randpool = instance
+ self.randpool_lock = threading.Lock()
+ self.entropy = rng_device
+
+ # Stir 256 bits of entropy from the RNG device into the RandomPool.
+ self.randpool.stir(self.entropy.read(32))
+ self.entropy.randomize()
+
+ def stir(self, s=''):
+ self.randpool_lock.acquire()
+ try:
+ self.randpool.stir(s)
+ finally:
+ self.randpool_lock.release()
+ self.entropy.randomize()
+
+ def randomize(self, N=0):
+ self.randpool_lock.acquire()
+ try:
+ self.randpool.randomize(N)
+ finally:
+ self.randpool_lock.release()
+ self.entropy.randomize()
+
+ def add_event(self, s=''):
+ self.randpool_lock.acquire()
+ try:
+ self.randpool.add_event(s)
+ finally:
+ self.randpool_lock.release()
+
+ def get_bytes(self, N):
+ self.randpool_lock.acquire()
+ try:
+ randpool_data = self.randpool.get_bytes(N)
+ finally:
+ self.randpool_lock.release()
+ entropy_data = self.entropy.read(N)
+ result = _strxor(randpool_data, entropy_data)
+ assert len(randpool_data) == N and len(entropy_data) == N and len(result) == N
+ return result
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/paramiko/rng_posix.py b/paramiko/rng_posix.py
new file mode 100644
index 00000000..1e6d72c2
--- /dev/null
+++ b/paramiko/rng_posix.py
@@ -0,0 +1,97 @@
+#!/usr/bin/python
+# -*- coding: ascii -*-
+# Copyright (C) 2008 Dwayne C. Litzenberger <dlitz@dlitz.net>
+# Copyright (C) 2008 Open Systems Canada Limited
+#
+# 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 os
+import stat
+
+class error(Exception):
+ pass
+
+class _RNG(object):
+ def __init__(self, file):
+ self.file = file
+
+ def read(self, bytes):
+ return self.file.read(bytes)
+
+ def close(self):
+ return self.file.close()
+
+ def randomize(self):
+ return
+
+def open_rng_device(device_path=None):
+ """Open /dev/urandom and perform some sanity checks."""
+
+ f = None
+ g = None
+
+ if device_path is None:
+ device_path = "/dev/urandom"
+
+ try:
+ # Try to open /dev/urandom now so that paramiko will be able to access
+ # it even if os.chroot() is invoked later.
+ try:
+ f = open(device_path, "rb", 0)
+ except EnvironmentError:
+ raise error("Unable to open /dev/urandom")
+
+ # Open a second file descriptor for sanity checking later.
+ try:
+ g = open(device_path, "rb", 0)
+ except EnvironmentError:
+ raise error("Unable to open /dev/urandom")
+
+ # Check that /dev/urandom is a character special device, not a regular file.
+ st = os.fstat(f.fileno()) # f
+ if stat.S_ISREG(st.st_mode) or not stat.S_ISCHR(st.st_mode):
+ raise error("/dev/urandom is not a character special device")
+
+ st = os.fstat(g.fileno()) # g
+ if stat.S_ISREG(st.st_mode) or not stat.S_ISCHR(st.st_mode):
+ raise error("/dev/urandom is not a character special device")
+
+ # Check that /dev/urandom always returns the number of bytes requested
+ x = f.read(20)
+ y = g.read(20)
+ if len(x) != 20 or len(y) != 20:
+ raise error("Error reading from /dev/urandom: input truncated")
+
+ # Check that different reads return different data
+ if x == y:
+ raise error("/dev/urandom is broken; returning identical data: %r == %r" % (x, y))
+
+ # Close the duplicate file object
+ g.close()
+
+ # Return the first file object
+ return _RNG(f)
+
+ except error:
+ if f is not None:
+ f.close()
+ if g is not None:
+ g.close()
+ raise
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
+
diff --git a/paramiko/rng_win32.py b/paramiko/rng_win32.py
new file mode 100644
index 00000000..3cb8b84c
--- /dev/null
+++ b/paramiko/rng_win32.py
@@ -0,0 +1,121 @@
+#!/usr/bin/python
+# -*- coding: ascii -*-
+# Copyright (C) 2008 Dwayne C. Litzenberger <dlitz@dlitz.net>
+# Copyright (C) 2008 Open Systems Canada Limited
+#
+# 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.
+
+class error(Exception):
+ pass
+
+# Try to import the "winrandom" module
+try:
+ from Crypto.Util import winrandom as _winrandom
+except ImportError:
+ _winrandom = None
+
+# Try to import the "urandom" module
+try:
+ from os import urandom as _urandom
+except ImportError:
+ _urandom = None
+
+
+class _RNG(object):
+ def __init__(self, readfunc):
+ self.read = readfunc
+
+ def randomize(self):
+ # 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.
+ self.read(128*1024) # discard 128 KiB of output
+
+def _open_winrandom():
+ if _winrandom is None:
+ raise error("Crypto.Util.winrandom module not found")
+
+ # Check that we can open the winrandom module
+ try:
+ r0 = _winrandom.new()
+ r1 = _winrandom.new()
+ except Exception, exc:
+ raise error("winrandom.new() failed: %s" % str(exc), exc)
+
+ # Check that we can read from the winrandom module
+ try:
+ x = r0.get_bytes(20)
+ y = r1.get_bytes(20)
+ except Exception, exc:
+ raise error("winrandom get_bytes failed: %s" % str(exc), exc)
+
+ # Check that the requested number of bytes are returned
+ if len(x) != 20 or len(y) != 20:
+ raise error("Error reading from winrandom: input truncated")
+
+ # Check that different reads return different data
+ if x == y:
+ raise error("winrandom broken: returning identical data")
+
+ return _RNG(r0.get_bytes)
+
+def _open_urandom():
+ if _urandom is None:
+ raise error("os.urandom function not found")
+
+ # Check that we can read from os.urandom()
+ try:
+ x = _urandom(20)
+ y = _urandom(20)
+ except Exception, exc:
+ raise error("os.urandom failed: %s" % str(exc), exc)
+
+ # Check that the requested number of bytes are returned
+ if len(x) != 20 or len(y) != 20:
+ raise error("os.urandom failed: input truncated")
+
+ # Check that different reads return different data
+ if x == y:
+ raise error("os.urandom failed: returning identical data")
+
+ return _RNG(_urandom)
+
+def open_rng_device():
+ # Try using the Crypto.Util.winrandom module
+ try:
+ return _open_winrandom()
+ except error:
+ pass
+
+ # Several versions of PyCrypto do not contain the winrandom module, but
+ # Python >= 2.4 has os.urandom, so try to use that.
+ try:
+ return _open_urandom()
+ except error:
+ pass
+
+ # SECURITY NOTE: DO NOT USE Crypto.Util.randpool.RandomPool HERE!
+ # If we got to this point, RandomPool will silently run with very little
+ # entropy. (This is current as of PyCrypto 2.0.1).
+ # See http://www.lag.net/pipermail/paramiko/2008-January/000599.html
+ # and http://www.lag.net/pipermail/paramiko/2008-April/000678.html
+
+ raise error("Unable to find a strong random entropy source. You cannot run this software securely under the current configuration.")
+
+# vim:set ts=4 sw=4 sts=4 expandtab: