diff options
Diffstat (limited to 'paramiko')
-rw-r--r-- | paramiko/__init__.py | 14 | ||||
-rw-r--r-- | paramiko/agent.py | 6 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 2 | ||||
-rw-r--r-- | paramiko/channel.py | 2 | ||||
-rw-r--r-- | paramiko/client.py | 20 | ||||
-rw-r--r-- | paramiko/common.py | 4 | ||||
-rw-r--r-- | paramiko/dsskey.py | 7 | ||||
-rw-r--r-- | paramiko/hostkeys.py | 2 | ||||
-rw-r--r-- | paramiko/kex_gex.py | 5 | ||||
-rw-r--r-- | paramiko/kex_group1.py | 5 | ||||
-rw-r--r-- | paramiko/packet.py | 53 | ||||
-rw-r--r-- | paramiko/pkey.py | 31 | ||||
-rw-r--r-- | paramiko/primes.py | 12 | ||||
-rw-r--r-- | paramiko/rng.py | 112 | ||||
-rw-r--r-- | paramiko/rng_posix.py | 97 | ||||
-rw-r--r-- | paramiko/rng_win32.py | 121 | ||||
-rw-r--r-- | paramiko/rsakey.py | 3 | ||||
-rw-r--r-- | paramiko/sftp_attr.py | 10 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 15 | ||||
-rw-r--r-- | paramiko/transport.py | 39 |
20 files changed, 122 insertions, 438 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py index ac0d559..96b5943 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2003-2011 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -20,7 +20,7 @@ I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend") is a module for python 2.3 or greater that implements the SSH2 protocol for secure (encrypted and authenticated) connections to remote machines. Unlike -SSL (aka TLS), the SSH2 protocol does not require heirarchical certificates +SSL (aka TLS), the SSH2 protocol does not require hierarchical certificates signed by a powerful central authority. You may know SSH2 as the protocol that replaced C{telnet} and C{rsh} for secure access to remote shells, but the protocol also includes the ability to open arbitrary channels to remote @@ -47,7 +47,7 @@ released under the GNU Lesser General Public License (LGPL). Website: U{http://www.lag.net/paramiko/} -@version: 1.7.6 (Fanny) +@version: 1.7.7.1 (George) @author: Robey Pointer @contact: robeypointer@gmail.com @license: GNU Lesser General Public License (LGPL) @@ -60,13 +60,13 @@ if sys.version_info < (2, 2): __author__ = "Robey Pointer <robeypointer@gmail.com>" -__date__ = "1 Nov 2009" -__version__ = "1.7.6 (Fanny)" -__version_info__ = (1, 7, 6) +__date__ = "21 May 2011" +__version__ = "1.7.7.1 (George)" +__version_info__ = (1, 7, 7, 1) __license__ = "GNU Lesser General Public License (LGPL)" -from transport import randpool, SecurityOptions, Transport +from transport import SecurityOptions, Transport from client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy from auth_handler import AuthHandler from channel import Channel, ChannelFile diff --git a/paramiko/agent.py b/paramiko/agent.py index 71de8b8..3bb9426 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -55,6 +55,7 @@ class Agent: @raise SSHException: if an SSH agent is found, but speaks an incompatible protocol """ + self.conn = None self.keys = () if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'): conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -87,7 +88,8 @@ class Agent: """ Close the SSH agent connection. """ - self.conn.close() + if self.conn is not None: + self.conn.close() self.conn = None self.keys = () @@ -139,7 +141,7 @@ class AgentKey(PKey): def get_name(self): return self.name - def sign_ssh_data(self, randpool, data): + def sign_ssh_data(self, rng, data): msg = Message() msg.add_byte(chr(SSH2_AGENTC_SIGN_REQUEST)) msg.add_string(self.blob) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 0f2e4f6..e3bd82d 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -206,7 +206,7 @@ class AuthHandler (object): m.add_string(self.private_key.get_name()) m.add_string(str(self.private_key)) blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) - sig = self.private_key.sign_ssh_data(self.transport.randpool, blob) + sig = self.private_key.sign_ssh_data(self.transport.rng, blob) m.add_string(str(sig)) elif self.auth_method == 'keyboard-interactive': m.add_string('') diff --git a/paramiko/channel.py b/paramiko/channel.py index 4694eef..6d895fe 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -364,7 +364,7 @@ class Channel (object): if auth_protocol is None: auth_protocol = 'MIT-MAGIC-COOKIE-1' if auth_cookie is None: - auth_cookie = binascii.hexlify(self.transport.randpool.get_bytes(16)) + auth_cookie = binascii.hexlify(self.transport.rng.read(16)) m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) diff --git a/paramiko/client.py b/paramiko/client.py index 023b405..4a65477 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -123,6 +123,7 @@ class SSHClient (object): self._log_channel = None self._policy = RejectPolicy() self._transport = None + self._agent = None def load_system_host_keys(self, filename=None): """ @@ -226,7 +227,8 @@ class SSHClient (object): self._policy = policy def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None, - key_filename=None, timeout=None, allow_agent=True, look_for_keys=True): + key_filename=None, timeout=None, allow_agent=True, look_for_keys=True, + compress=False): """ Connect to an SSH server and authenticate to it. The server's host key is checked against the system host keys (see L{load_system_host_keys}) @@ -267,6 +269,8 @@ class SSHClient (object): @param look_for_keys: set to False to disable searching for discoverable private key files in C{~/.ssh/} @type look_for_keys: bool + @param compress: set to True to turn on compression + @type compress: bool @raise BadHostKeyException: if the server's host key could not be verified @@ -281,7 +285,8 @@ class SSHClient (object): addr = sockaddr break else: - raise SSHException('No suitable address family for %s' % hostname) + # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( + af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) sock = socket.socket(af, socket.SOCK_STREAM) if timeout is not None: try: @@ -290,7 +295,7 @@ class SSHClient (object): pass sock.connect(addr) t = self._transport = Transport(sock) - + t.use_compression(compress=compress) if self._log_channel is not None: t.set_log_channel(self._log_channel) t.start_client() @@ -335,6 +340,10 @@ class SSHClient (object): self._transport.close() self._transport = None + if self._agent != None: + self._agent.close() + self._agent = None + def exec_command(self, command, bufsize=-1): """ Execute a command on the SSH server. A new L{Channel} is opened and @@ -432,7 +441,10 @@ class SSHClient (object): saved_exception = e if allow_agent: - for key in Agent().get_keys(): + if self._agent == None: + self._agent = Agent() + + for key in self._agent.get_keys(): try: self._log(DEBUG, 'Trying SSH agent key %s' % hexlify(key.get_fingerprint())) self._transport.auth_publickey(username, key) diff --git a/paramiko/common.py b/paramiko/common.py index 7a37463..3323f0a 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 rng import StrongLockingRandomPool +from Crypto import Random # keep a crypto-strong PRNG nearby -randpool = StrongLockingRandomPool() +rng = Random.new() import sys if sys.version_info < (2, 3): diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index eecfa69..53ca92b 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -91,13 +91,13 @@ class DSSKey (PKey): def can_sign(self): return self.x is not None - def sign_ssh_data(self, rpool, data): + def sign_ssh_data(self, rng, data): digest = SHA.new(data).digest() dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) # generate a suitable k qsize = len(util.deflate_long(self.q, 0)) while True: - k = util.inflate_long(rpool.get_bytes(qsize), 1) + k = util.inflate_long(rng.read(qsize), 1) if (k > 2) and (k < self.q): break r, s = dss.sign(util.inflate_long(digest, 1), k) @@ -161,8 +161,7 @@ class DSSKey (PKey): @return: new private key @rtype: L{DSSKey} """ - randpool.stir() - dsa = DSA.generate(bits, randpool.get_bytes, progress_func) + dsa = DSA.generate(bits, rng.read, progress_func) key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key.x = dsa.x return key diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 9ceef43..70ccf43 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -303,7 +303,7 @@ class HostKeys (UserDict.DictMixin): @rtype: str """ if salt is None: - salt = randpool.get_bytes(SHA.digest_size) + salt = rng.read(SHA.digest_size) else: if salt.startswith('|1|'): salt = salt.split('|')[2] diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index c6be638..9c98339 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -101,8 +101,7 @@ class KexGex (object): qhbyte <<= 1 qmask >>= 1 while True: - self.transport.randpool.stir() - x_bytes = self.transport.randpool.get_bytes(bytes) + x_bytes = self.transport.rng.read(bytes) x_bytes = chr(ord(x_bytes[0]) & qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): @@ -207,7 +206,7 @@ class KexGex (object): H = SHA.new(str(hm)).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.randpool, H) + sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) # send reply m = Message() m.add_byte(chr(_MSG_KEXDH_GEX_REPLY)) diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 4228dd9..1386cf3 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -79,8 +79,7 @@ class KexGroup1(object): # potential x where the first 63 bits are 1, because some of those will be # larger than q (but this is a tiny tiny subset of potential x). while 1: - self.transport.randpool.stir() - x_bytes = self.transport.randpool.get_bytes(128) + x_bytes = self.transport.rng.read(128) x_bytes = chr(ord(x_bytes[0]) & 0x7f) + x_bytes[1:] if (x_bytes[:8] != '\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF') and \ (x_bytes[:8] != '\x00\x00\x00\x00\x00\x00\x00\x00'): @@ -125,7 +124,7 @@ class KexGroup1(object): H = SHA.new(str(hm)).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.randpool, H) + sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) # send reply m = Message() m.add_byte(chr(_MSG_KEXDH_REPLY)) diff --git a/paramiko/packet.py b/paramiko/packet.py index 9072fbe..391c5d5 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -59,7 +59,7 @@ class Packetizer (object): # they should probably be lower. REKEY_PACKETS = pow(2, 30) REKEY_BYTES = pow(2, 30) - + def __init__(self, socket): self.__socket = socket self.__logger = None @@ -68,14 +68,14 @@ class Packetizer (object): self.__need_rekey = False self.__init_count = 0 self.__remainder = '' - + # used for noticing when to re-key: self.__sent_bytes = 0 self.__sent_packets = 0 self.__received_bytes = 0 self.__received_packets = 0 self.__received_packets_overflow = 0 - + # current inbound/outbound ciphering: self.__block_size_out = 8 self.__block_size_in = 8 @@ -99,13 +99,13 @@ class Packetizer (object): self.__keepalive_interval = 0 self.__keepalive_last = time.time() self.__keepalive_callback = None - + def set_log(self, log): """ Set the python log object to use for logging. """ self.__logger = log - + def set_outbound_cipher(self, block_engine, block_size, mac_engine, mac_size, mac_key): """ Switch outbound data cipher. @@ -122,7 +122,7 @@ class Packetizer (object): if self.__init_count == 3: self.__init_count = 0 self.__need_rekey = False - + def set_inbound_cipher(self, block_engine, block_size, mac_engine, mac_size, mac_key): """ Switch inbound data cipher. @@ -140,26 +140,26 @@ class Packetizer (object): if self.__init_count == 3: self.__init_count = 0 self.__need_rekey = False - + def set_outbound_compressor(self, compressor): self.__compress_engine_out = compressor - + def set_inbound_compressor(self, compressor): self.__compress_engine_in = compressor - + def close(self): self.__closed = True self.__socket.close() def set_hexdump(self, hexdump): self.__dump_packets = hexdump - + def get_hexdump(self): return self.__dump_packets - + def get_mac_size_in(self): return self.__mac_size_in - + def get_mac_size_out(self): return self.__mac_size_out @@ -168,11 +168,11 @@ class Packetizer (object): Returns C{True} if a new set of keys needs to be negotiated. This will be triggered during a packet read or write, so it should be checked after every read or write, or at least after every few. - + @return: C{True} if a new set of keys needs to be negotiated """ return self.__need_rekey - + def set_keepalive(self, interval, callback): """ Turn on/off the callback keepalive. If C{interval} seconds pass with @@ -182,11 +182,11 @@ class Packetizer (object): self.__keepalive_interval = interval self.__keepalive_callback = callback self.__keepalive_last = time.time() - + def read_all(self, n, check_rekey=False): """ Read as close to N bytes as possible, blocking as long as necessary. - + @param n: number of bytes to read @type n: int @return: the data read @@ -262,7 +262,7 @@ class Packetizer (object): break out = out[n:] return - + def readline(self, timeout): """ Read a line from the socket. We assume no data is pending after the @@ -277,7 +277,7 @@ class Packetizer (object): if (len(buf) > 0) and (buf[-1] == '\r'): buf = buf[:-1] return buf - + def send_message(self, data): """ Write a block of data using the current cipher, as an SSH block. @@ -311,9 +311,6 @@ class Packetizer (object): self.__sent_bytes += len(out) self.__sent_packets += 1 - if (self.__sent_packets % 100) == 0: - # stirring the randpool takes 30ms on my ibook!! - randpool.stir() if ((self.__sent_packets >= self.REKEY_PACKETS) or (self.__sent_bytes >= self.REKEY_BYTES)) \ and not self.__need_rekey: # only ask once for rekeying @@ -328,7 +325,7 @@ class Packetizer (object): """ Only one thread should ever be in this function (no other locking is done). - + @raise SSHException: if the packet is mangled @raise NeedRekeyException: if the transport should rekey """ @@ -359,7 +356,7 @@ class Packetizer (object): raise SSHException('Mismatched MAC') padding = ord(packet[0]) payload = packet[1:packet_size - padding] - randpool.add_event() + if self.__dump_packets: self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) @@ -369,7 +366,7 @@ class Packetizer (object): msg = Message(payload[1:]) msg.seqno = self.__sequence_number_in self.__sequence_number_in = (self.__sequence_number_in + 1) & 0xffffffffL - + # check for rekey self.__received_bytes += packet_size + self.__mac_size_in + 4 self.__received_packets += 1 @@ -398,8 +395,8 @@ class Packetizer (object): ########## protected - - + + def _log(self, level, msg): if self.__logger is None: return @@ -418,7 +415,7 @@ class Packetizer (object): if now > self.__keepalive_last + self.__keepalive_interval: self.__keepalive_callback() self.__keepalive_last = now - + def _py22_read_all(self, n, out): while n > 0: r, w, e = select.select([self.__socket], [], [], 0.1) @@ -476,7 +473,7 @@ class Packetizer (object): packet = struct.pack('>IB', len(payload) + padding + 1, padding) packet += payload if self.__block_engine_out is not None: - packet += randpool.get_bytes(padding) + packet += rng.read(padding) else: # cute trick i caught openssh doing: if we're not encrypting, # don't waste random bytes for the padding diff --git a/paramiko/pkey.py b/paramiko/pkey.py index bb8c83c..3e71222 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -25,7 +25,7 @@ from binascii import hexlify, unhexlify import os from Crypto.Hash import MD5 -from Crypto.Cipher import DES3 +from Crypto.Cipher import DES3, AES from paramiko.common import * from paramiko import util @@ -40,7 +40,8 @@ class PKey (object): # known encryption types for private key files: _CIPHER_TABLE = { - 'DES-EDE3-CBC': { 'cipher': DES3, 'keysize': 24, 'blocksize': 8, 'mode': DES3.MODE_CBC } + 'AES-128-CBC': { 'cipher': AES, 'keysize': 16, 'blocksize': 16, 'mode': AES.MODE_CBC }, + 'DES-EDE3-CBC': { 'cipher': DES3, 'keysize': 24, 'blocksize': 8, 'mode': DES3.MODE_CBC }, } @@ -143,13 +144,13 @@ class PKey (object): """ return base64.encodestring(str(self)).replace('\n', '') - def sign_ssh_data(self, randpool, data): + def sign_ssh_data(self, rng, data): """ Sign a blob of data with this private key, and return a L{Message} representing an SSH signature message. - @param randpool: a secure random number generator. - @type randpool: L{Crypto.Util.randpool.RandomPool} + @param rng: a secure random number generator. + @type rng: L{Crypto.Util.rng.RandomPool} @param data: the data to sign. @type data: str @return: an SSH signature message. @@ -171,7 +172,7 @@ class PKey (object): @rtype: boolean """ return False - + def from_private_key_file(cls, filename, password=None): """ Create a key object by reading a private key file. If the private @@ -204,7 +205,7 @@ class PKey (object): object. If the private key is encrypted and C{password} is not C{None}, the given password will be used to decrypt the key (otherwise L{PasswordRequiredException} is thrown). - + @param file_obj: the file to read from @type file_obj: file @param password: an optional password to use to decrypt the key, if it's @@ -212,7 +213,7 @@ class PKey (object): @type password: str @return: a new key object based on the given private key @rtype: L{PKey} - + @raise IOError: if there was an error reading the key @raise PasswordRequiredException: if the private key file is encrypted, and C{password} is C{None} @@ -236,17 +237,17 @@ class PKey (object): @raise SSHException: if the key is invalid """ raise Exception('Not implemented in PKey') - + def write_private_key(self, file_obj, password=None): """ Write private key contents into a file (or file-like) object. If the password is not C{None}, the key is encrypted before writing. - + @param file_obj: the file object to write into @type file_obj: file @param password: an optional password to use to encrypt the key @type password: str - + @raise IOError: if there was an error writing to the file @raise SSHException: if the key is invalid """ @@ -279,7 +280,7 @@ class PKey (object): data = self._read_private_key(tag, f, password) f.close() return data - + def _read_private_key(self, tag, f, password=None): lines = f.readlines() start = 0 @@ -350,7 +351,7 @@ class PKey (object): os.chmod(filename, 0600) self._write_private_key(tag, f, data, password) f.close() - + def _write_private_key(self, tag, f, data, password=None): f.write('-----BEGIN %s PRIVATE KEY-----\n' % tag) if password is not None: @@ -360,11 +361,11 @@ class PKey (object): keysize = self._CIPHER_TABLE[cipher_name]['keysize'] blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] mode = self._CIPHER_TABLE[cipher_name]['mode'] - salt = randpool.get_bytes(8) + salt = rng.read(8) key = util.generate_key_bytes(MD5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize - #data += randpool.get_bytes(n) + #data += rng.read(n) # that would make more sense ^, but it confuses openssh. data += '\0' * n data = cipher.new(key, mode, salt).encrypt(data) diff --git a/paramiko/primes.py b/paramiko/primes.py index 1cf7905..9ebfec1 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -26,12 +26,12 @@ from paramiko import util from paramiko.ssh_exception import SSHException -def _generate_prime(bits, randpool): +def _generate_prime(bits, rng): "primtive attempt at prime generation" hbyte_mask = pow(2, bits % 8) - 1 while True: # loop catches the case where we increment n into a higher bit-range - x = randpool.get_bytes((bits+7) // 8) + x = rng.read((bits+7) // 8) if hbyte_mask > 0: x = chr(ord(x[0]) & hbyte_mask) + x[1:] n = util.inflate_long(x, 1) @@ -43,7 +43,7 @@ def _generate_prime(bits, randpool): break return n -def _roll_random(rpool, n): +def _roll_random(rng, n): "returns a random # from 0 to N-1" bits = util.bit_length(n-1) bytes = (bits + 7) // 8 @@ -56,7 +56,7 @@ def _roll_random(rpool, n): # fits, so i can't guarantee that this loop will ever finish, but the odds # of it looping forever should be infinitesimal. while True: - x = rpool.get_bytes(bytes) + x = rng.read(bytes) if hbyte_mask > 0: x = chr(ord(x[0]) & hbyte_mask) + x[1:] num = util.inflate_long(x, 1) @@ -75,7 +75,7 @@ class ModulusPack (object): # pack is a hash of: bits -> [ (generator, modulus) ... ] self.pack = {} self.discarded = [] - self.randpool = rpool + self.rng = rpool def _parse_modulus(self, line): timestamp, mod_type, tests, tries, size, generator, modulus = line.split() @@ -147,5 +147,5 @@ class ModulusPack (object): if min > good: good = bitsizes[-1] # now pick a random modulus of this bitsize - n = _roll_random(self.randpool, len(self.pack[good])) + n = _roll_random(self.rng, len(self.pack[good])) return self.pack[good][n] diff --git a/paramiko/rng.py b/paramiko/rng.py deleted file mode 100644 index 46329d1..0000000 --- a/paramiko/rng.py +++ /dev/null @@ -1,112 +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 -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 deleted file mode 100644 index c4c9691..0000000 --- a/paramiko/rng_posix.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/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 deleted file mode 100644 index 3cb8b84..0000000 --- a/paramiko/rng_win32.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/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: diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index a665279..1e2d8f9 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -137,8 +137,7 @@ class RSAKey (PKey): @return: new private key @rtype: L{RSAKey} """ - randpool.stir() - rsa = RSA.generate(bits, randpool.get_bytes, progress_func) + rsa = RSA.generate(bits, rng.read, progress_func) key = RSAKey(vals=(rsa.e, rsa.n)) key.d = rsa.d key.p = rsa.p diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index 26290be..1f09421 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -39,7 +39,7 @@ class SFTPAttributes (object): are stored in a dict named C{attr}. Occasionally, the filename is also stored, in C{filename}. """ - + FLAG_SIZE = 1 FLAG_UIDGID = 2 FLAG_PERMISSIONS = 4 @@ -89,7 +89,7 @@ class SFTPAttributes (object): ### internals... - + def _from_msg(cls, msg, filename=None, longname=None): attr = cls() attr._unpack(msg) @@ -200,7 +200,7 @@ class SFTPAttributes (object): else: ks = '?---------' # compute display date - if (self.st_mtime is None) or (self.st_mtime == 0xffffffff): + if (self.st_mtime is None) or (self.st_mtime == 0xffffffffL): # shouldn't really happen datestr = '(unknown date)' else: @@ -210,7 +210,7 @@ class SFTPAttributes (object): else: datestr = time.strftime('%d %b %H:%M', time.localtime(self.st_mtime)) filename = getattr(self, 'filename', '?') - + # not all servers support uid/gid uid = self.st_uid gid = self.st_gid @@ -218,6 +218,6 @@ class SFTPAttributes (object): uid = 0 if gid is None: gid = 0 - + return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 1f11075..79a7761 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -533,7 +533,7 @@ class SFTPClient (BaseSFTP): """ return self._cwd - def put(self, localpath, remotepath, callback=None): + def put(self, localpath, remotepath, callback=None, confirm=True): """ Copy a local file (C{localpath}) to the SFTP server as C{remotepath}. Any exception raised by operations will be passed through. This @@ -549,6 +549,10 @@ class SFTPClient (BaseSFTP): transferred so far and the total bytes to be transferred (since 1.7.4) @type callback: function(int, int) + @param confirm: whether to do a stat() on the file afterwards to + confirm the file size (since 1.7.7) + @type confirm: bool + @return: an object containing attributes about the given file (since 1.7.4) @rtype: SFTPAttributes @@ -574,9 +578,12 @@ class SFTPClient (BaseSFTP): fr.close() finally: fl.close() - s = self.stat(remotepath) - if s.st_size != size: - raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) + if confirm: + s = self.stat(remotepath) + if s.st_size != size: + raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) + else: + s = SFTPAttributes() return s def get(self, remotepath, localpath, callback=None): diff --git a/paramiko/transport.py b/paramiko/transport.py index 50e78e7..30de295 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -45,11 +45,7 @@ from paramiko.server import ServerInterface from paramiko.sftp_client import SFTPClient from paramiko.ssh_exception import SSHException, BadAuthenticationType, ChannelException -# these come from PyCrypt -# http://www.amk.ca/python/writing/pycrypt/ -# i believe this on the standards track. -# PyCrypt compiled for Win32 can be downloaded from the HashTar homepage: -# http://nitace.bsd.uchicago.edu:8080/hashtar +from Crypto import Random from Crypto.Cipher import Blowfish, AES, DES3, ARC4 from Crypto.Hash import SHA, MD5 try: @@ -198,7 +194,7 @@ class Transport (threading.Thread): """ _PROTO_ID = '2.0' - _CLIENT_ID = 'paramiko_1.7.6' + _CLIENT_ID = 'paramiko_1.7.7.1' _preferred_ciphers = ( 'aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256' ) @@ -285,19 +281,25 @@ class Transport (threading.Thread): if type(sock) is tuple: # connect to the given (host, port) hostname, port = sock + reason = 'No suitable address family' for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): if socktype == socket.SOCK_STREAM: af = family addr = sockaddr - break + sock = socket.socket(af, socket.SOCK_STREAM) + try: + sock.connect((hostname, port)) + except socket.error, e: + reason = str(e) + else: + break else: - raise SSHException('No suitable address family for %s' % hostname) - sock = socket.socket(af, socket.SOCK_STREAM) - sock.connect((hostname, port)) + raise SSHException( + 'Unable to connect to %s: %s' % (hostname, reason)) # okay, normal socket-ish flow here... threading.Thread.__init__(self) self.setDaemon(True) - self.randpool = randpool + self.rng = rng self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB try: @@ -450,6 +452,7 @@ class Transport (threading.Thread): # synchronous, wait for a result self.completion_event = event = threading.Event() self.start() + Random.atfork() while True: event.wait(0.1) if not self.active: @@ -585,7 +588,7 @@ class Transport (threading.Thread): @note: This has no effect when used in client mode. """ - Transport._modulus_pack = ModulusPack(randpool) + Transport._modulus_pack = ModulusPack(rng) # places to look for the openssh "moduli" file file_list = [ '/etc/ssh/moduli', '/usr/local/etc/moduli' ] if filename is not None: @@ -706,10 +709,8 @@ class Transport (threading.Thread): @raise SSHException: if the request is rejected or the session ends prematurely """ - chan = None if not self.active: - # don't bother trying to allocate a channel - return None + raise SSHException('SSH session not active') self.lock.acquire() try: chanid = self._next_channel() @@ -839,10 +840,9 @@ class Transport (threading.Thread): """ m = Message() m.add_byte(chr(MSG_IGNORE)) - randpool.stir() if bytes is None: - bytes = (ord(randpool.get_bytes(1)) % 32) + 10 - m.add_bytes(randpool.get_bytes(bytes)) + bytes = (ord(rng.read(1)) % 32) + 10 + m.add_bytes(rng.read(bytes)) self._send_user_message(m) def renegotiate_keys(self): @@ -1676,10 +1676,9 @@ class Transport (threading.Thread): else: available_server_keys = self._preferred_keys - randpool.stir() m = Message() m.add_byte(chr(MSG_KEXINIT)) - m.add_bytes(randpool.get_bytes(16)) + m.add_bytes(rng.read(16)) m.add_list(self._preferred_kex) m.add_list(available_server_keys) m.add_list(self._preferred_ciphers) |