diff --git a/paramiko/common.py b/paramiko/common.py index 89b34cf..f4a4d81 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 6c559e0..0000000 --- a/paramiko/osrandom.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/python -# -*- coding: ascii -*- -# Copyright (C) 2008 Dwayne C. Litzenberger -# -# 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 , - # 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 0000000..46329d1 --- /dev/null +++ b/paramiko/rng.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# -*- coding: ascii -*- +# Copyright (C) 2008 Dwayne C. Litzenberger +# +# 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 0000000..1e6d72c --- /dev/null +++ b/paramiko/rng_posix.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: ascii -*- +# Copyright (C) 2008 Dwayne C. Litzenberger +# 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 0000000..3cb8b84 --- /dev/null +++ b/paramiko/rng_win32.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: ascii -*- +# Copyright (C) 2008 Dwayne C. Litzenberger +# 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 , + # 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: