diff options
-rw-r--r-- | .travis.yml | 3 | ||||
-rw-r--r-- | README.rst | 14 | ||||
-rw-r--r-- | paramiko/_version.py | 2 | ||||
-rw-r--r-- | paramiko/_winapi.py | 6 | ||||
-rw-r--r-- | paramiko/channel.py | 5 | ||||
-rw-r--r-- | paramiko/client.py | 21 | ||||
-rw-r--r-- | paramiko/dsskey.py | 123 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 113 | ||||
-rw-r--r-- | paramiko/packet.py | 6 | ||||
-rw-r--r-- | paramiko/pkey.py | 72 | ||||
-rw-r--r-- | paramiko/primes.py | 4 | ||||
-rw-r--r-- | paramiko/rsakey.py | 158 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 60 | ||||
-rw-r--r-- | paramiko/sftp_file.py | 36 | ||||
-rw-r--r-- | paramiko/ssh_exception.py | 10 | ||||
-rw-r--r-- | paramiko/ssh_gss.py | 18 | ||||
-rw-r--r-- | paramiko/transport.py | 69 | ||||
-rw-r--r-- | paramiko/util.py | 34 | ||||
-rw-r--r-- | setup.py | 25 | ||||
-rw-r--r-- | sites/www/changelog.rst | 57 | ||||
-rw-r--r-- | sites/www/index.rst | 5 | ||||
-rw-r--r-- | sites/www/installing-1.x.rst | 94 | ||||
-rw-r--r-- | sites/www/installing.rst | 110 | ||||
-rwxr-xr-x | test.py | 4 | ||||
-rw-r--r-- | tests/test_auth.py | 12 | ||||
-rw-r--r-- | tests/test_client.py | 65 | ||||
-rw-r--r-- | tests/test_packetizer.py | 29 | ||||
-rw-r--r-- | tests/test_pkey.py | 48 | ||||
-rwxr-xr-x | tests/test_sftp.py | 4 | ||||
-rw-r--r-- | tests/test_ssh_exception.py | 31 | ||||
-rw-r--r-- | tox-requirements.txt | 3 | ||||
-rw-r--r-- | tox.ini | 2 |
32 files changed, 756 insertions, 487 deletions
diff --git a/.travis.yml b/.travis.yml index 55cba46d..3b7b2b42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: python sudo: false +cache: + directories: + - $HOME/.cache/pip python: - "2.6" - "2.7" @@ -42,10 +42,9 @@ that should have come with this archive. Requirements ------------ -- `Python <http://www.python.org/>`_ 2.6, 2.7, or 3.3+ (3.2 should also work, - but it is not recommended) -- `pycrypto <https://www.dlitz.net/software/pycrypto/>`_ 2.1+ -- `ecdsa <https://pypi.python.org/pypi/ecdsa>`_ 0.11+ +- `Python <http://www.python.org/>`_ 2.6, 2.7, or 3.3+ +- `Cryptography <https://cryptography.io>`_ 0.8 or better +- `pyasn1 <https://pypi.python.org/pypi/pyasn1>`_ 0.1.7 or better Installation @@ -66,13 +65,6 @@ Paramiko primarily supports POSIX platforms with standard OpenSSH implementations, and is most frequently tested on Linux and OS X. Windows is supported as well, though it may not be as straightforward. -Some Windows users whose Python is 64-bit have found that the PyCrypto -dependency ``winrandom`` may not install properly, leading to an -``ImportError``. In this scenario, you may need to compile ``winrandom`` -yourself. See `Fabric #194 <https://github.com/fabric/fabric/issues/194>`_ -for info. - - Bugs & Support -------------- diff --git a/paramiko/_version.py b/paramiko/_version.py index e82b8667..4b78efad 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 16, 0) +__version_info__ = (1, 17, 0) __version__ = '.'.join(map(str, __version_info__)) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index 9a8bdedd..77e0129c 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -1,6 +1,6 @@ """ Windows API functions implemented as ctypes functions and classes as found -in jaraco.windows (3.3). +in jaraco.windows (3.4.1). If you encounter issues with this module, please consider reporting the issues in jaraco.windows and asking the author to port the fixes back here. @@ -158,7 +158,7 @@ class MemoryMap(object): if self.pos + n >= self.length: # A little safety. raise ValueError("Refusing to write %d bytes" % n) dest = self.view + self.pos - length = ctypes.wintypes.SIZE(n) + length = ctypes.c_size_t(n) ctypes.windll.kernel32.RtlMoveMemory(dest, msg, length) self.pos += n @@ -168,7 +168,7 @@ class MemoryMap(object): """ out = ctypes.create_string_buffer(n) source = self.view + self.pos - length = ctypes.wintypes.SIZE(n) + length = ctypes.c_size_t(n) ctypes.windll.kernel32.RtlMoveMemory(out, source, length) self.pos += n return out.raw diff --git a/paramiko/channel.py b/paramiko/channel.py index 4ce4f286..3c43eb10 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -88,15 +88,20 @@ class Channel (ClosingContextManager): :param int chanid: the ID of this channel, as passed by an existing `.Transport`. """ + #: Channel ID self.chanid = chanid + #: Remote channel ID self.remote_chanid = 0 + #: `.Transport` managing this channel self.transport = None + #: Whether the connection is presently active self.active = False self.eof_received = 0 self.eof_sent = 0 self.in_buffer = BufferedPipe() self.in_stderr_buffer = BufferedPipe() self.timeout = None + #: Whether the connection has been closed self.closed = False self.ultra_debug = False self.lock = threading.Lock() diff --git a/paramiko/client.py b/paramiko/client.py index 8d899a15..e3d3780e 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -164,10 +164,23 @@ class SSHClient (ClosingContextManager): def set_missing_host_key_policy(self, policy): """ - Set the policy to use when connecting to a server that doesn't have a - host key in either the system or local `.HostKeys` objects. The - default policy is to reject all unknown servers (using `.RejectPolicy`). - You may substitute `.AutoAddPolicy` or write your own policy class. + Set policy to use when connecting to servers without a known host key. + + Specifically: + + * A **policy** is an instance of a "policy class", namely some subclass + of `.MissingHostKeyPolicy` such as `.RejectPolicy` (the default), + `.AutoAddPolicy`, `.WarningPolicy`, or a user-created subclass. + + .. note:: + This method takes class **instances**, not **classes** themselves. + Thus it must be called as e.g. + ``.set_missing_host_key_policy(WarningPolicy())`` and *not* + ``.set_missing_host_key_policy(WarningPolicy)``. + + * A host key is **known** when it appears in the client object's cached + host keys structures (those manipulated by `load_system_host_keys` + and/or `load_host_keys`). :param .MissingHostKeyPolicy policy: the policy to use when receiving a host key from a diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index d7dd6275..7e14422c 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -20,21 +20,23 @@ DSS keys. """ -import os -from hashlib import sha1 - -from Crypto.PublicKey import DSA +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import dsa +from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_rfc6979_signature, encode_rfc6979_signature +) from paramiko import util from paramiko.common import zero_byte -from paramiko.py3compat import long from paramiko.ssh_exception import SSHException from paramiko.message import Message from paramiko.ber import BER, BERException from paramiko.pkey import PKey -class DSSKey (PKey): +class DSSKey(PKey): """ Representation of a DSS key which can be used to sign an verify SSH2 data. @@ -98,15 +100,21 @@ class DSSKey (PKey): return self.x is not None def sign_ssh_data(self, data): - digest = sha1(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(os.urandom(qsize), 1) - if (k > 2) and (k < self.q): - break - r, s = dss.sign(util.inflate_long(digest, 1), k) + key = dsa.DSAPrivateNumbers( + x=self.x, + public_numbers=dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, + q=self.q, + g=self.g + ) + ) + ).private_key(backend=default_backend()) + signer = key.signer(hashes.SHA1()) + signer.update(data) + r, s = decode_rfc6979_signature(signer.finalize()) + m = Message() m.add_string('ssh-dss') # apparently, in rare cases, r or s may be shorter than 20 bytes! @@ -132,27 +140,65 @@ class DSSKey (PKey): # pull out (r, s) which are NOT encoded as mpints sigR = util.inflate_long(sig[:20], 1) sigS = util.inflate_long(sig[20:], 1) - sigM = util.inflate_long(sha1(data).digest(), 1) - dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) - return dss.verify(sigM, (sigR, sigS)) - - def _encode_key(self): - if self.x is None: - raise SSHException('Not enough key information') - keylist = [0, self.p, self.q, self.g, self.y, self.x] + signature = encode_rfc6979_signature(sigR, sigS) + + key = dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, + q=self.q, + g=self.g + ) + ).public_key(backend=default_backend()) + verifier = key.verifier(signature, hashes.SHA1()) + verifier.update(data) try: - b = BER() - b.encode(keylist) - except BERException: - raise SSHException('Unable to create ber encoding of key') - return b.asbytes() + verifier.verify() + except InvalidSignature: + return False + else: + return True def write_private_key_file(self, filename, password=None): - self._write_private_key_file('DSA', filename, self._encode_key(), password) + key = dsa.DSAPrivateNumbers( + x=self.x, + public_numbers=dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, + q=self.q, + g=self.g + ) + ) + ).private_key(backend=default_backend()) + + self._write_private_key_file( + filename, + key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password + ) def write_private_key(self, file_obj, password=None): - self._write_private_key('DSA', file_obj, self._encode_key(), password) + key = dsa.DSAPrivateNumbers( + x=self.x, + public_numbers=dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, + q=self.q, + g=self.g + ) + ) + ).private_key(backend=default_backend()) + + self._write_private_key( + file_obj, + key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password + ) @staticmethod def generate(bits=1024, progress_func=None): @@ -161,14 +207,19 @@ class DSSKey (PKey): generate a new host key or authentication key. :param int bits: number of bits the generated key should be. - :param function progress_func: - an optional function to call at key points in key generation (used - by ``pyCrypto.PublicKey``). + :param function progress_func: Unused :return: new `.DSSKey` private key """ - dsa = DSA.generate(bits, os.urandom, progress_func) - key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) - key.x = dsa.x + numbers = dsa.generate_private_key( + bits, backend=default_backend() + ).private_numbers() + key = DSSKey(vals=( + numbers.public_numbers.parameter_numbers.p, + numbers.public_numbers.parameter_numbers.q, + numbers.public_numbers.parameter_numbers.g, + numbers.public_numbers.y + )) + key.x = numbers.x return key ### internals... diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 8827a1db..c69bef73 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -21,18 +21,23 @@ ECDSA keys """ import binascii -from hashlib import sha256 -from ecdsa import SigningKey, VerifyingKey, der, curves +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_rfc6979_signature, encode_rfc6979_signature +) -from paramiko.common import four_byte, one_byte +from paramiko.common import four_byte from paramiko.message import Message from paramiko.pkey import PKey -from paramiko.py3compat import byte_chr, u from paramiko.ssh_exception import SSHException +from paramiko.util import deflate_long, inflate_long -class ECDSAKey (PKey): +class ECDSAKey(PKey): """ Representation of an ECDSA key which can be used to sign and verify SSH2 data. @@ -65,9 +70,13 @@ class ECDSAKey (PKey): if pointinfo[0:1] != four_byte: raise SSHException('Point compression is being used: %s' % binascii.hexlify(pointinfo)) - self.verifying_key = VerifyingKey.from_string(pointinfo[1:], - curve=curves.NIST256p, - validate_point=validate_point) + curve = ec.SECP256R1() + numbers = ec.EllipticCurvePublicNumbers( + x=inflate_long(pointinfo[1:1 + curve.key_size // 8], always_positive=True), + y=inflate_long(pointinfo[1 + curve.key_size // 8:], always_positive=True), + curve=curve + ) + self.verifying_key = numbers.public_key(backend=default_backend()) self.size = 256 def asbytes(self): @@ -76,8 +85,15 @@ class ECDSAKey (PKey): m.add_string('ecdsa-sha2-nistp256') m.add_string('nistp256') - point_str = four_byte + key.to_string() + numbers = key.public_numbers() + x_bytes = deflate_long(numbers.x, add_sign_padding=False) + x_bytes = b'\x00' * (len(x_bytes) - key.curve.key_size // 8) + x_bytes + + y_bytes = deflate_long(numbers.y, add_sign_padding=False) + y_bytes = b'\x00' * (len(y_bytes) - key.curve.key_size // 8) + y_bytes + + point_str = four_byte + x_bytes + y_bytes m.add_string(point_str) return m.asbytes() @@ -86,8 +102,8 @@ class ECDSAKey (PKey): def __hash__(self): h = hash(self.get_name()) - h = h * 37 + hash(self.verifying_key.pubkey.point.x()) - h = h * 37 + hash(self.verifying_key.pubkey.point.y()) + h = h * 37 + hash(self.verifying_key.public_numbers().x) + h = h * 37 + hash(self.verifying_key.public_numbers().y) return hash(h) def get_name(self): @@ -100,34 +116,52 @@ class ECDSAKey (PKey): return self.signing_key is not None def sign_ssh_data(self, data): - sig = self.signing_key.sign_deterministic( - data, sigencode=self._sigencode, hashfunc=sha256) + signer = self.signing_key.signer(ec.ECDSA(hashes.SHA256())) + signer.update(data) + sig = signer.finalize() + r, s = decode_rfc6979_signature(sig) + m = Message() m.add_string('ecdsa-sha2-nistp256') - m.add_string(sig) + m.add_string(self._sigencode(r, s)) return m def verify_ssh_sig(self, data, msg): if msg.get_text() != 'ecdsa-sha2-nistp256': return False sig = msg.get_binary() - - # verify the signature by SHA'ing the data and encrypting it - # using the public key. - hash_obj = sha256(data).digest() - return self.verifying_key.verify_digest(sig, hash_obj, - sigdecode=self._sigdecode) + sigR, sigS = self._sigdecode(sig) + signature = encode_rfc6979_signature(sigR, sigS) + + verifier = self.verifying_key.verifier( + signature, ec.ECDSA(hashes.SHA256()) + ) + verifier.update(data) + try: + verifier.verify() + except InvalidSignature: + return False + else: + return True def write_private_key_file(self, filename, password=None): - key = self.signing_key or self.verifying_key - self._write_private_key_file('EC', filename, key.to_der(), password) + self._write_private_key_file( + filename, + self.signing_key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password + ) def write_private_key(self, file_obj, password=None): - key = self.signing_key or self.verifying_key - self._write_private_key('EC', file_obj, key.to_der(), password) + self._write_private_key( + file_obj, + self.signing_key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password + ) @staticmethod - def generate(curve=curves.NIST256p, progress_func=None): + def generate(curve=ec.SECP256R1(), progress_func=None): """ Generate a new private ECDSA key. This factory function can be used to generate a new host key or authentication key. @@ -135,9 +169,8 @@ class ECDSAKey (PKey): :param function progress_func: Not used for this type of key. :returns: A new private key (`.ECDSAKey`) object """ - signing_key = SigningKey.generate(curve) - key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) - return key + private_key = ec.generate_private_key(curve, backend=default_backend()) + return ECDSAKey(vals=(private_key, private_key.public_key())) ### internals... @@ -149,27 +182,25 @@ class ECDSAKey (PKey): data = self._read_private_key('EC', file_obj, password) self._decode_key(data) - ALLOWED_PADDINGS = [one_byte, byte_chr(2) * 2, byte_chr(3) * 3, byte_chr(4) * 4, - byte_chr(5) * 5, byte_chr(6) * 6, byte_chr(7) * 7] - def _decode_key(self, data): - s, padding = der.remove_sequence(data) - if padding: - if padding not in self.ALLOWED_PADDINGS: - raise ValueError("weird padding: %s" % u(binascii.hexlify(data))) - data = data[:-len(padding)] - key = SigningKey.from_der(data) + try: + key = serialization.load_der_private_key( + data, password=None, backend=default_backend() + ) + except ValueError as e: + raise SSHException(str(e)) + self.signing_key = key - self.verifying_key = key.get_verifying_key() - self.size = 256 + self.verifying_key = key.public_key() + self.size = key.curve.key_size - def _sigencode(self, r, s, order): + def _sigencode(self, r, s): msg = Message() msg.add_mpint(r) msg.add_mpint(s) return msg.asbytes() - def _sigdecode(self, sig, order): + def _sigdecode(self, sig): msg = Message(sig) r = msg.get_mpint() s = msg.get_mpint() diff --git a/paramiko/packet.py b/paramiko/packet.py index 89a514d1..00cf5657 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -353,7 +353,7 @@ class Packetizer (object): self._log(DEBUG, 'Write packet <%s>, length %d' % (cmd_name, orig_len)) self._log(DEBUG, util.format_binary(packet, 'OUT: ')) if self.__block_engine_out is not None: - out = self.__block_engine_out.encrypt(packet) + out = self.__block_engine_out.update(packet) else: out = packet # + mac @@ -386,7 +386,7 @@ class Packetizer (object): """ header = self.read_all(self.__block_size_in, check_rekey=True) if self.__block_engine_in is not None: - header = self.__block_engine_in.decrypt(header) + header = self.__block_engine_in.update(header) if self.__dump_packets: self._log(DEBUG, util.format_binary(header, 'IN: ')) packet_size = struct.unpack('>I', header[:4])[0] @@ -399,7 +399,7 @@ class Packetizer (object): packet = buf[:packet_size - len(leftover)] post_packet = buf[packet_size - len(leftover):] if self.__block_engine_in is not None: - packet = self.__block_engine_in.decrypt(packet) + packet = self.__block_engine_in.update(packet) if self.__dump_packets: self._log(DEBUG, util.format_binary(packet, 'IN: ')) packet = leftover + packet diff --git a/paramiko/pkey.py b/paramiko/pkey.py index e95d60ba..0637a6f0 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -21,27 +21,39 @@ Common API for all public keys. """ import base64 -from binascii import hexlify, unhexlify +from binascii import unhexlify import os from hashlib import md5 -from Crypto.Cipher import DES3, AES +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher from paramiko import util -from paramiko.common import o600, zero_byte +from paramiko.common import o600 from paramiko.py3compat import u, encodebytes, decodebytes, b from paramiko.ssh_exception import SSHException, PasswordRequiredException -class PKey (object): +class PKey(object): """ Base class for public keys. """ # known encryption types for private key files: _CIPHER_TABLE = { - '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}, + 'AES-128-CBC': { + 'cipher': algorithms.AES, + 'keysize': 16, + 'blocksize': 16, + 'mode': modes.CBC + }, + 'DES-EDE3-CBC': { + 'cipher': algorithms.TripleDES, + 'keysize': 24, + 'blocksize': 8, + 'mode': modes.CBC + }, } def __init__(self, msg=None, data=None): @@ -301,9 +313,12 @@ class PKey (object): mode = self._CIPHER_TABLE[encryption_type]['mode'] salt = unhexlify(b(saltstr)) key = util.generate_key_bytes(md5, salt, password, keysize) - return cipher.new(key, mode, salt).decrypt(data) + decryptor = Cipher( + cipher(key), mode(salt), backend=default_backend() + ).decryptor() + return decryptor.update(data) + decryptor.finalize() - def _write_private_key_file(self, tag, filename, data, password=None): + def _write_private_key_file(self, filename, key, format, password=None): """ Write an SSH2-format private key file in a form that can be read by paramiko or openssh. If no password is given, the key is written in @@ -321,31 +336,16 @@ class PKey (object): with open(filename, 'w', o600) as f: # grrr... the mode doesn't always take hold os.chmod(filename, o600) - self._write_private_key(tag, f, data, password) - - def _write_private_key(self, tag, f, data, password=None): - f.write('-----BEGIN %s PRIVATE KEY-----\n' % tag) - if password is not None: - cipher_name = list(self._CIPHER_TABLE.keys())[0] - cipher = self._CIPHER_TABLE[cipher_name]['cipher'] - keysize = self._CIPHER_TABLE[cipher_name]['keysize'] - blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] - mode = self._CIPHER_TABLE[cipher_name]['mode'] - salt = os.urandom(blocksize) - key = util.generate_key_bytes(md5, salt, password, keysize) - if len(data) % blocksize != 0: - n = blocksize - len(data) % blocksize - #data += os.urandom(n) - # that would make more sense ^, but it confuses openssh. - data += zero_byte * n - data = cipher.new(key, mode, salt).encrypt(data) - f.write('Proc-Type: 4,ENCRYPTED\n') - f.write('DEK-Info: %s,%s\n' % (cipher_name, u(hexlify(salt)).upper())) - f.write('\n') - s = u(encodebytes(data)) - # re-wrap to 64-char lines - s = ''.join(s.split('\n')) - s = '\n'.join([s[i: i + 64] for i in range(0, len(s), 64)]) - f.write(s) - f.write('\n') - f.write('-----END %s PRIVATE KEY-----\n' % tag) + self._write_private_key(f, key, format) + + def _write_private_key(self, f, key, format, password=None): + if password is None: + encryption = serialization.NoEncryption() + else: + encryption = serialization.BestEncryption(password) + + f.write(key.private_bytes( + serialization.Encoding.PEM, + format, + encryption + ).decode()) diff --git a/paramiko/primes.py b/paramiko/primes.py index 7415c182..d0e17575 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -113,12 +113,12 @@ class ModulusPack (object): good = -1 # find nearest bitsize >= preferred for b in bitsizes: - if (b >= prefer) and (b < max) and (b < good or good == -1): + if (b >= prefer) and (b <= max) and (b < good or good == -1): good = b # if that failed, find greatest bitsize >= min if good == -1: for b in bitsizes: - if (b >= min) and (b < max) and (b > good): + if (b >= min) and (b <= max) and (b > good): good = b if good == -1: # their entire (min, max) range has no intersection with our range. diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 4ebd8354..bc9053f5 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -20,34 +20,24 @@ RSA keys. """ -import os -from hashlib import sha1 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa, padding -from Crypto.PublicKey import RSA - -from paramiko import util -from paramiko.common import max_byte, zero_byte, one_byte from paramiko.message import Message -from paramiko.ber import BER, BERException from paramiko.pkey import PKey -from paramiko.py3compat import long from paramiko.ssh_exception import SSHException -SHA1_DIGESTINFO = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' - -class RSAKey (PKey): +class RSAKey(PKey): """ Representation of an RSA key which can be used to sign and verify SSH2 data. """ - def __init__(self, msg=None, data=None, filename=None, password=None, vals=None, file_obj=None): - self.n = None - self.e = None - self.d = None - self.p = None - self.q = None + def __init__(self, msg=None, data=None, filename=None, password=None, key=None, file_obj=None): + self.key = None if file_obj is not None: self._from_private_key(file_obj, password) return @@ -56,22 +46,33 @@ class RSAKey (PKey): return if (msg is None) and (data is not None): msg = Message(data) - if vals is not None: - self.e, self.n = vals + if key is not None: + self.key = key else: if msg is None: raise SSHException('Key object may not be empty') if msg.get_text() != 'ssh-rsa': raise SSHException('Invalid key') - self.e = msg.get_mpint() - self.n = msg.get_mpint() - self.size = util.bit_length(self.n) + self.key = rsa.RSAPublicNumbers( + e=msg.get_mpint(), n=msg.get_mpint() + ).public_key(default_backend()) + + @property + def size(self): + return self.key.key_size + + @property + def public_numbers(self): + if isinstance(self.key, rsa.RSAPrivateKey): + return self.key.private_numbers().public_numbers + else: + return self.key.public_numbers() def asbytes(self): m = Message() m.add_string('ssh-rsa') - m.add_mpint(self.e) - m.add_mpint(self.n) + m.add_mpint(self.public_numbers.e) + m.add_mpint(self.public_numbers.n) return m.asbytes() def __str__(self): @@ -79,8 +80,8 @@ class RSAKey (PKey): def __hash__(self): h = hash(self.get_name()) - h = h * 37 + hash(self.e) - h = h * 37 + hash(self.n) + h = h * 37 + hash(self.public_numbers.e) + h = h * 37 + hash(self.public_numbers.n) return hash(h) def get_name(self): @@ -90,12 +91,16 @@ class RSAKey (PKey): return self.size def can_sign(self): - return self.d is not None + return isinstance(self.key, rsa.RSAPrivateKey) def sign_ssh_data(self, data): - digest = sha1(data).digest() - rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) - sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) + signer = self.key.signer( + padding=padding.PKCS1v15(), + algorithm=hashes.SHA1(), + ) + signer.update(data) + sig = signer.finalize() + m = Message() m.add_string('ssh-rsa') m.add_string(sig) @@ -104,32 +109,38 @@ class RSAKey (PKey): def verify_ssh_sig(self, data, msg): if msg.get_text() != 'ssh-rsa': return False - sig = util.inflate_long(msg.get_binary(), True) - # verify the signature by SHA'ing the data and encrypting it using the - # public key. some wackiness ensues where we "pkcs1imify" the 20-byte - # hash into a string as long as the RSA key. - hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True) - rsa = RSA.construct((long(self.n), long(self.e))) - return rsa.verify(hash_obj, (sig,)) - - def _encode_key(self): - if (self.p is None) or (self.q is None): - raise SSHException('Not enough key info to write private key file') - keylist = [0, self.n, self.e, self.d, self.p, self.q, - self.d % (self.p - 1), self.d % (self.q - 1), - util.mod_inverse(self.q, self.p)] + key = self.key + if isinstance(key, rsa.RSAPrivateKey): + key = key.public_key() + + verifier = key.verifier( + signature=msg.get_binary(), + padding=padding.PKCS1v15(), + algorithm=hashes.SHA1(), + ) + verifier.update(data) try: - b = BER() - b.encode(keylist) - except BERException: - raise SSHException('Unable to create ber encoding of key') - return b.asbytes() + verifier.verify() + except InvalidSignature: + return False + else: + return True def write_private_key_file(self, filename, password=None): - self._write_private_key_file('RSA', filename, self._encode_key(), password) + self._write_private_key_file( + filename, + self.key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password + ) def write_private_key(self, file_obj, password=None): - self._write_private_key('RSA', file_obj, self._encode_key(), password) + self._write_private_key( + file_obj, + self.key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password + ) @staticmethod def generate(bits, progress_func=None): @@ -138,29 +149,16 @@ class RSAKey (PKey): generate a new host key or authentication key. :param int bits: number of bits the generated key should be. - :param function progress_func: - an optional function to call at key points in key generation (used - by ``pyCrypto.PublicKey``). + :param function progress_func: Unused :return: new `.RSAKey` private key """ - rsa = RSA.generate(bits, os.urandom, progress_func) - key = RSAKey(vals=(rsa.e, rsa.n)) - key.d = rsa.d - key.p = rsa.p - key.q = rsa.q - return key + key = rsa.generate_private_key( + public_exponent=65537, key_size=bits, backend=default_backend() + ) + return RSAKey(key=key) ### internals... - def _pkcs1imify(self, data): - """ - turn a 20-byte SHA1 hash into a blob of data as large as the key's N, - using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre. - """ - size = len(util.deflate_long(self.n, 0)) - filler = max_byte * (size - len(SHA1_DIGESTINFO) - len(data) - 3) - return zero_byte + one_byte + filler + zero_byte + SHA1_DIGESTINFO + data - def _from_private_key_file(self, filename, password): data = self._read_private_key_file('RSA', filename, password) self._decode_key(data) @@ -170,18 +168,12 @@ class RSAKey (PKey): self._decode_key(data) def _decode_key(self, data): - # private key file contains: - # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } try: - keylist = BER(data).decode() - except BERException: - raise SSHException('Unable to parse key file') - if (type(keylist) is not list) or (len(keylist) < 4) or (keylist[0] != 0): - raise SSHException('Not a valid RSA private key file (bad ber encoding)') - self.n = keylist[1] - self.e = keylist[2] - self.d = keylist[3] - # not really needed - self.p = keylist[4] - self.q = keylist[5] - self.size = util.bit_length(self.n) + key = serialization.load_der_private_key( + data, password=None, backend=default_backend() + ) + except ValueError as e: + raise SSHException(str(e)) + + assert isinstance(key, rsa.RSAPrivateKey) + self.key = key diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 57225558..0df94389 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -592,6 +592,18 @@ class SFTPClient(BaseSFTP, ClosingContextManager): # TODO: make class initialize with self._cwd set to self.normalize('.') return self._cwd and u(self._cwd) + def _transfer_with_callback(self, reader, writer, file_size, callback): + size = 0 + while True: + data = reader.read(32768) + writer.write(data) + size += len(data) + if len(data) == 0: + break + if callback is not None: + callback(size, file_size) + return size + def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): """ Copy the contents of an open file object (``fl``) to the SFTP server as @@ -621,15 +633,9 @@ class SFTPClient(BaseSFTP, ClosingContextManager): """ with self.file(remotepath, 'wb') as fr: fr.set_pipelined(True) - size = 0 - while True: - data = fl.read(32768) - fr.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - if len(data) == 0: - break + size = self._transfer_with_callback( + reader=fl, writer=fr, file_size=file_size, callback=callback + ) if confirm: s = self.stat(remotepath) if s.st_size != size: @@ -689,16 +695,10 @@ class SFTPClient(BaseSFTP, ClosingContextManager): file_size = self.stat(remotepath).st_size with self.open(remotepath, 'rb') as fr: fr.prefetch(file_size) + return self._transfer_with_callback( + reader=fr, writer=fl, file_size=file_size, callback=callback + ) - size = 0 - while True: - data = fr.read(32768) - fl.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - if len(data) == 0: - break return size def get(self, remotepath, localpath, callback=None): @@ -748,10 +748,10 @@ class SFTPClient(BaseSFTP, ClosingContextManager): raise Exception('unknown type for %r type %r' % (item, type(item))) num = self.request_number self._expecting[num] = fileobj - self._send_packet(t, msg) self.request_number += 1 finally: self._lock.release() + self._send_packet(t, msg) return num def _read_response(self, waitfor=None): @@ -762,15 +762,19 @@ class SFTPClient(BaseSFTP, ClosingContextManager): raise SSHException('Server connection dropped: %s' % str(e)) msg = Message(data) num = msg.get_int() - if num not in self._expecting: - # might be response for a file that was closed before responses came back - self._log(DEBUG, 'Unexpected response #%d' % (num,)) - if waitfor is None: - # just doing a single check - break - continue - fileobj = self._expecting[num] - del self._expecting[num] + self._lock.acquire() + try: + if num not in self._expecting: + # might be response for a file that was closed before responses came back + self._log(DEBUG, 'Unexpected response #%d' % (num,)) + if waitfor is None: + # just doing a single check + break + continue + fileobj = self._expecting[num] + del self._expecting[num] + finally: + self._lock.release() if num == waitfor: # synchronous if t == CMD_STATUS: diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 3b584dbe..fdf667cd 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -389,7 +389,7 @@ class SFTPFile (BufferedFile): """ self.pipelined = pipelined - def prefetch(self, file_size): + def prefetch(self, file_size=None): """ Pre-fetch the remaining contents of this file in anticipation of future `.read` calls. If reading the entire file, pre-fetching can @@ -401,8 +401,24 @@ class SFTPFile (BufferedFile): data may be read in a random order (using `.seek`); chunks of the buffer that haven't been read will continue to be buffered. + :param int file_size: + When this is ``None`` (the default), this method calls `stat` to + determine the remote file size. In some situations, doing so can + cause exceptions or hangs (see `#562 + <https://github.com/paramiko/paramiko/pull/562>`_); as a + workaround, one may call `stat` explicitly and pass its value in + via this parameter. + .. versionadded:: 1.5.1 + .. versionchanged:: 1.16.0 + The ``file_size`` parameter was added (with no default value). + .. versionchanged:: 1.16.1 + The ``file_size`` parameter was made optional for backwards + compatibility. """ + if file_size is None: + file_size = self.stat().st_size; + # queue up async reads for the rest of the file chunks = [] n = self._realpos @@ -469,8 +485,8 @@ class SFTPFile (BufferedFile): # do these read requests in a temporary thread because there may be # a lot of them, so it may block. for offset, length in chunks: + num = self.sftp._async_request(self, CMD_READ, self.handle, long(offset), int(length)) with self._prefetch_lock: - num = self.sftp._async_request(self, CMD_READ, self.handle, long(offset), int(length)) self._prefetch_extents[num] = (offset, length) def _async_response(self, t, msg, num): @@ -484,12 +500,16 @@ class SFTPFile (BufferedFile): if t != CMD_DATA: raise SFTPError('Expected data') data = msg.get_string() - with self._prefetch_lock: - offset, length = self._prefetch_extents[num] - self._prefetch_data[offset] = data - del self._prefetch_extents[num] - if len(self._prefetch_extents) == 0: - self._prefetch_done = True + while True: + with self._prefetch_lock: + # spin if in race with _prefetch_thread + if num in self._prefetch_extents: + offset, length = self._prefetch_extents[num] + self._prefetch_data[offset] = data + del self._prefetch_extents[num] + if len(self._prefetch_extents) == 0: + self._prefetch_done = True + break def _check_exception(self): """if there's a saved exception, raise & clear it""" diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 016a411e..ed36a952 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -164,12 +164,18 @@ class NoValidConnectionsError(socket.error): :param dict errors: The errors dict to store, as described by class docstring. """ - addrs = errors.keys() + addrs = sorted(errors.keys()) body = ', '.join([x[0] for x in addrs[:-1]]) tail = addrs[-1][0] - msg = "Unable to connect to port {0} on {1} or {2}" + if body: + msg = "Unable to connect to port {0} on {1} or {2}" + else: + msg = "Unable to connect to port {0} on {2}" super(NoValidConnectionsError, self).__init__( None, # stand-in for errno msg.format(addrs[0][1], body, tail) ) self.errors = errors + + def __reduce__(self): + return (self.__class__, (self.errors, )) diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index e9b13a66..e906a851 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -39,22 +39,8 @@ import sys """ GSS_AUTH_AVAILABLE = True -try: - from pyasn1.type.univ import ObjectIdentifier - from pyasn1.codec.der import encoder, decoder -except ImportError: - GSS_AUTH_AVAILABLE = False - class ObjectIdentifier(object): - def __init__(self, *args): - raise NotImplementedError("Module pyasn1 not importable") - - class decoder(object): - def decode(self): - raise NotImplementedError("Module pyasn1 not importable") - - class encoder(object): - def encode(self): - raise NotImplementedError("Module pyasn1 not importable") +from pyasn1.type.univ import ObjectIdentifier +from pyasn1.codec.der import encoder, decoder from paramiko.common import MSG_USERAUTH_REQUEST from paramiko.ssh_exception import SSHException diff --git a/paramiko/transport.py b/paramiko/transport.py index 5b440a4d..d6acb4aa 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -29,6 +29,9 @@ import time import weakref from hashlib import md5, sha1, sha256, sha512 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes + import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler @@ -55,7 +58,7 @@ from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey from paramiko.message import Message from paramiko.packet import Packetizer, NeedRekeyException from paramiko.primes import ModulusPack -from paramiko.py3compat import string_types, long, byte_ord, b +from paramiko.py3compat import string_types, long, byte_ord, b, input from paramiko.rsakey import RSAKey from paramiko.ecdsakey import ECDSAKey from paramiko.server import ServerInterface @@ -64,12 +67,6 @@ from paramiko.ssh_exception import (SSHException, BadAuthenticationType, ChannelException, ProxyCommandFailure) from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value -from Crypto.Cipher import Blowfish, AES, DES3 -try: - from Crypto.Util import Counter -except ImportError: - from paramiko.util import Counter - # for thread cleanup _active_threads = [] @@ -92,6 +89,9 @@ class Transport (threading.Thread, ClosingContextManager): Instances of this class may be used as context managers. """ + _ENCRYPT = object() + _DECRYPT = object() + _PROTO_ID = '2.0' _CLIENT_ID = 'paramiko_%s' % paramiko.__version__ @@ -130,55 +130,56 @@ class Transport (threading.Thread, ClosingContextManager): _cipher_info = { 'aes128-ctr': { - 'class': AES, - 'mode': AES.MODE_CTR, + 'class': algorithms.AES, + 'mode': modes.CTR, 'block-size': 16, 'key-size': 16 }, 'aes192-ctr': { - 'class': AES, - 'mode': AES.MODE_CTR, + 'class': algorithms.AES, + 'mode': modes.CTR, 'block-size': 16, 'key-size': 24 }, 'aes256-ctr': { - 'class': AES, - 'mode': AES.MODE_CTR, + 'class': algorithms.AES, + 'mode': modes.CTR, 'block-size': 16, 'key-size': 32 }, 'blowfish-cbc': { - 'class': Blowfish, - 'mode': Blowfish.MODE_CBC, + 'class': algorithms.Blowfish, + 'mode': modes.CBC, 'block-size': 8, 'key-size': 16 }, 'aes128-cbc': { - 'class': AES, - 'mode': AES.MODE_CBC, + 'class': algorithms.AES, + 'mode': modes.CBC, 'block-size': 16, 'key-size': 16 }, 'aes192-cbc': { - 'class': AES, - 'mode': AES.MODE_CBC, + 'class': algorithms.AES, + 'mode': modes.CBC, 'block-size': 16, 'key-size': 24 }, 'aes256-cbc': { - 'class': AES, - 'mode': AES.MODE_CBC, + 'class': algorithms.AES, + 'mode': modes.CBC, 'block-size': 16, 'key-size': 32 }, '3des-cbc': { - 'class': DES3, - 'mode': DES3.MODE_CBC, + 'class': algorithms.TripleDES, + 'mode': modes.CBC, 'block-size': 8, 'key-size': 24 }, } + _mac_info = { 'hmac-sha1': {'class': sha1, 'size': 20}, 'hmac-sha1-96': {'class': sha1, 'size': 12}, @@ -1381,7 +1382,7 @@ class Transport (threading.Thread, ClosingContextManager): print(instructions.strip()) for prompt,show_input in prompt_list: print(prompt.strip(),end=' ') - answers.append(raw_input()) + answers.append(input()) return answers return self.auth_interactive(username, handler, submethods) @@ -1619,15 +1620,19 @@ class Transport (threading.Thread, ClosingContextManager): sofar += digest return out[:nbytes] - def _get_cipher(self, name, key, iv): + def _get_cipher(self, name, key, iv, operation): if name not in self._cipher_info: raise SSHException('Unknown client cipher ' + name) - if name.endswith("-ctr"): - # CTR modes, we need a counter - counter = Counter.new(nbits=self._cipher_info[name]['block-size'] * 8, initial_value=util.inflate_long(iv, True)) - return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv, counter) else: - return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv) + cipher = Cipher( + self._cipher_info[name]['class'](key), + self._cipher_info[name]['mode'](iv), + backend=default_backend(), + ) + if operation is self._ENCRYPT: + return cipher.encryptor() + else: + return cipher.decryptor() def _set_forward_agent_handler(self, handler): if handler is None: @@ -2023,7 +2028,7 @@ class Transport (threading.Thread, ClosingContextManager): else: IV_in = self._compute_key('B', block_size) key_in = self._compute_key('D', self._cipher_info[self.remote_cipher]['key-size']) - engine = self._get_cipher(self.remote_cipher, key_in, IV_in) + engine = self._get_cipher(self.remote_cipher, key_in, IV_in, self._DECRYPT) mac_size = self._mac_info[self.remote_mac]['size'] mac_engine = self._mac_info[self.remote_mac]['class'] # initial mac keys are done in the hash's natural size (not the @@ -2050,7 +2055,7 @@ class Transport (threading.Thread, ClosingContextManager): else: IV_out = self._compute_key('A', block_size) key_out = self._compute_key('C', self._cipher_info[self.local_cipher]['key-size']) - engine = self._get_cipher(self.local_cipher, key_out, IV_out) + engine = self._get_cipher(self.local_cipher, key_out, IV_out, self._ENCRYPT) mac_size = self._mac_info[self.local_mac]['size'] mac_engine = self._mac_info[self.local_mac]['class'] # initial mac keys are done in the hash's natural size (not the diff --git a/paramiko/util.py b/paramiko/util.py index 855e5757..5ad1e3fd 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -22,7 +22,6 @@ Useful functions used by the rest of paramiko. from __future__ import generators -import array import errno import sys import struct @@ -31,7 +30,7 @@ import threading import logging from paramiko.common import DEBUG, zero_byte, xffffffff, max_byte -from paramiko.py3compat import PY2, long, byte_ord, b, byte_chr +from paramiko.py3compat import PY2, long, byte_chr, byte_ord, b from paramiko.config import SSHConfig @@ -273,37 +272,6 @@ def retry_on_signal(function): raise -class Counter (object): - """Stateful counter for CTR mode crypto""" - def __init__(self, nbits, initial_value=long(1), overflow=long(0)): - self.blocksize = nbits / 8 - self.overflow = overflow - # start with value - 1 so we don't have to store intermediate values when counting - # could the iv be 0? - if initial_value == 0: - self.value = array.array('c', max_byte * self.blocksize) - else: - x = deflate_long(initial_value - 1, add_sign_padding=False) - self.value = array.array('c', zero_byte * (self.blocksize - len(x)) + x) - - def __call__(self): - """Increament the counter and return the new value""" - i = self.blocksize - 1 - while i > -1: - c = self.value[i] = byte_chr((byte_ord(self.value[i]) + 1) % 256) - if c != zero_byte: - return self.value.tostring() - i -= 1 - # counter reset - x = deflate_long(self.overflow, add_sign_padding=False) - self.value = array.array('c', zero_byte * (self.blocksize - len(x)) + x) - return self.value.tostring() - - @classmethod - def new(cls, nbits, initial_value=long(1), overflow=long(0)): - return cls(nbits, initial_value=initial_value, overflow=overflow) - - def constant_time_bytes_eq(a, b): if len(a) != len(b): return False @@ -24,30 +24,16 @@ connections between python scripts. All major ciphers and hash methods are supported. SFTP client and server mode are both supported too. Required packages: - pyCrypto + Cryptography To install the `in-development version <https://github.com/paramiko/paramiko/tarball/master#egg=paramiko-dev>`_, use `pip install paramiko==dev`. ''' -# if someday we want to *require* setuptools, uncomment this: -# (it will cause setuptools to be automatically downloaded) -#import ez_setup -#ez_setup.use_setuptools() - import sys -try: - from setuptools import setup - kw = { - 'install_requires': [ - 'pycrypto>=2.1,!=2.4', - 'ecdsa>=0.11', - ], - } -except ImportError: - from distutils.core import setup - kw = {} +from setuptools import setup + if sys.platform == 'darwin': import setup_helper @@ -89,5 +75,8 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ], - **kw + install_requires=[ + 'cryptography>=0.8', + 'pyasn1>=0.1.7', + ], ) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index eaf3cd57..7cca1840 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,58 @@ Changelog ========= +* :bug:`676` (via :issue:`677`) Fix a backwards incompatibility issue that + cropped up in `SFTPFile.prefetch <~paramiko.sftp_file.prefetch>` re: the + erroneously non-optional ``file_size`` parameter. Should only affect users + who manually call ``prefetch``. Thanks to ``@stevevanhooser`` for catch & + patch. +* :feature:`394` Replace PyCrypto with the Python Cryptographic Authority + (PyCA) 'Cryptography' library suite. This improves security, installability, + and performance; adds PyPy support; and much more. + + There aren't enough ways to thank Alex Gaynor for all of his work on this, + and then his patience while the maintainer let his PR grow moss for a year + and change. Paul Kehrer came in with an assist, and I think I saw Olle + Lundberg, ``@techtonik`` and ``@johnthagen`` supplying backup as well. Thanks + to all! + + .. warning:: + **This is a backwards incompatible change.** + + However, **it should only affect installation** requirements; **no API + changes are intended or expected**. Please report any such breakages as + bugs. + + See our updated :doc:`installation docs <installing>` for details on what + is now required to install Paramiko; many/most users should be able to + simply ``pip install -U paramiko`` (especially if you **upgrade to pip + 8**). + +* :bug:`577` (via :issue:`578`; should also fix :issue:`718`, :issue:`560`) Fix + stalled/hung SFTP downloads by cleaning up some threading lock issues. Thanks + to Stephen C. Pope for the patch. +* :bug:`716` Fix a Python 3 compatibility issue when handling two-factor + authentication. Thanks to Mateusz Kowalski for the catch & original patch. +* :support:`729 backported` Clean up ``setup.py`` to always use ``setuptools``, + not doing so was a historical artifact from bygone days. Thanks to Alex + Gaynor. +* :bug:`649 major` Update the module in charge of handling SSH moduli so it's + consistent with OpenSSH behavior re: prime number selection. Thanks to Damien + Tournoud for catch & patch. +* :bug:`617` (aka `fabric/fabric#1429 + <https://github.com/fabric/fabric/issues/1429>`_; via :issue:`679`; related: + :issue:`678`, :issue:`685`, :issue:`615` & :issue:`616`) Fix up + `~paramiko.ssh_exception.NoValidConnectionsError` so it pickles correctly, + and fix a related Python 3 compatibility issue. Thanks to Rebecca Schlussel + for the report & Marius Gedminas for the patch. +* :bug:`613` (via :issue:`619`) Update to ``jaraco.windows`` 3.4.1 to fix some + errors related to ``ctypes`` on Windows platforms. Credit to Jason R. Coombs. +* :support:`621 backported` Annotate some public attributes on + `~paramiko.channel.Channel` such as ``.closed``. Thanks to Sergey Vasilyev + for the report. +* :bug:`632` Fix logic bug in the SFTP client's callback-calling functionality; + previously there was a chance the given callback would fire twice at the end + of a transfer. Thanks to ``@ab9-er`` for catch & original patch. * :support:`612` Identify & work around a race condition in the test for handshake timeouts, which was causing frequent test failures for a subset of contributors as well as Travis-CI (usually, but not always, limited to Python @@ -155,8 +207,9 @@ Changelog well) would hang due to incorrect values passed into the new window size arguments for `.Transport` (thanks to a botched merge). This has been corrected. Thanks to Dylan Thacker-Smith for the report & patch. -* :feature:`167` Add `.SSHConfig.get_hostnames` for easier introspection of a - loaded SSH config file or object. Courtesy of Søren Løvborg. +* :feature:`167` Add `~paramiko.config.SSHConfig.get_hostnames` for easier + introspection of a loaded SSH config file or object. Courtesy of Søren + Løvborg. * :release:`1.15.0 <2014-09-18>` * :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor. diff --git a/sites/www/index.rst b/sites/www/index.rst index 8e7562af..b09ab589 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -3,8 +3,9 @@ Welcome to Paramiko! Paramiko is a Python (2.6+, 3.3+) implementation of the SSHv2 protocol [#]_, providing both client and server functionality. While it leverages a Python C -extension for low level cryptography (`PyCrypto <http://pycrypto.org>`_), -Paramiko itself is a pure Python interface around SSH networking concepts. +extension for low level cryptography +(`Cryptography <https://cryptography.io>`_), Paramiko itself is a pure Python +interface around SSH networking concepts. This website covers project information for Paramiko such as the changelog, contribution guidelines, development roadmap, news/blog, and so forth. Detailed diff --git a/sites/www/installing-1.x.rst b/sites/www/installing-1.x.rst new file mode 100644 index 00000000..0c2424bb --- /dev/null +++ b/sites/www/installing-1.x.rst @@ -0,0 +1,94 @@ +Installing (1.x) +================ + +.. note:: Installing Paramiko 2.0 or above? See :doc:`installing` instead. + +This document includes legacy notes on installing Paramiko 1.x (specifically, +1.13 and up). Users are strongly encouraged to upgrade to 2.0 when possible; +PyCrypto (the dependency covered below) is no longer maintained and contains +security vulnerabilities. + +General install notes +===================== + +* Python 2.6+ and 3.3+ are supported; Python <=2.5 and 3.0-3.2 are **not + supported**. +* See the note in the main install doc about :ref:`release-lines` for details + on specific versions you may want to install. + + .. note:: 1.x will eventually be entirely end-of-lifed. +* Paramiko 1.7-1.14 have only one dependency: :ref:`pycrypto`. +* Paramiko 1.15+ (not including 2.x and above) add a second, pure-Python + dependency: the ``ecdsa`` module, trivially installable via PyPI. +* Paramiko 1.15+ (again, not including 2.x and up) also allows you to + optionally install a few more dependencies to gain support for + :ref:`GSS-API/Kerberos <gssapi-on-1x>`. +* Users on Windows may want to opt for the :ref:`pypm` approach. + + +.. _pycrypto: + +PyCrypto +======== + +`PyCrypto <https://www.dlitz.net/software/pycrypto/>`__ provides the low-level +(C-based) encryption algorithms we need to implement the SSH protocol. There +are a couple gotchas associated with installing PyCrypto: its compatibility +with Python's package tools, and the fact that it is a C-based extension. + +C extension +----------- + +Unless you are installing from a precompiled source such as a Debian apt +repository or RedHat RPM, or using :ref:`pypm <pypm>`, you will also need the +ability to build Python C-based modules from source in order to install +PyCrypto. Users on **Unix-based platforms** such as Ubuntu or Mac OS X will +need the traditional C build toolchain installed (e.g. Developer Tools / XCode +Tools on the Mac, or the ``build-essential`` package on Ubuntu or Debian Linux +-- basically, anything with ``gcc``, ``make`` and so forth) as well as the +Python development libraries, often named ``python-dev`` or similar. + +For **Windows** users we recommend using :ref:`pypm`, installing a C +development environment such as `Cygwin <http://cygwin.com>`_ or obtaining a +precompiled Win32 PyCrypto package from `voidspace's Python modules page +<http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_. + +.. note:: + Some Windows users whose Python is 64-bit have found that the PyCrypto + dependency ``winrandom`` may not install properly, leading to ImportErrors. + In this scenario, you'll probably need to compile ``winrandom`` yourself + via e.g. MS Visual Studio. See `Fabric #194 + <https://github.com/fabric/fabric/issues/194>`_ for info. + + +.. _pypm: + +ActivePython and PyPM +===================== + +Windows users who already have ActiveState's `ActivePython +<http://www.activestate.com/activepython/downloads>`_ distribution installed +may find Paramiko is best installed with `its package manager, PyPM +<http://code.activestate.com/pypm/>`_. Below is example output from an +installation of Paramiko via ``pypm``:: + + C:\> pypm install paramiko + The following packages will be installed into "%APPDATA%\Python" (2.7): + paramiko-1.7.8 pycrypto-2.4 + Get: [pypm-free.activestate.com] paramiko 1.7.8 + Get: [pypm-free.activestate.com] pycrypto 2.4 + Installing paramiko-1.7.8 + Installing pycrypto-2.4 + C:\> + + +.. _gssapi-on-1x: + +Optional dependencies for GSS-API / SSPI / Kerberos +=================================================== + +First, see the main install doc's notes: :ref:`gssapi` - everything there is +required for Paramiko 1.x as well. + +Additionally, users of Paramiko 1.x, on all platforms, need a final dependency: +`pyasn1 <https://pypi.python.org/pypi/pyasn1>`_ ``0.1.7`` or better. diff --git a/sites/www/installing.rst b/sites/www/installing.rst index a657c3fc..5a41a76b 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -2,6 +2,13 @@ Installing ========== + +.. note:: + These instructions cover Paramiko 2.0 and above. If you're looking to + install Paramiko 1.x, see :doc:`installing-1.x`. However, **the 1.x line + relies on insecure dependencies** so upgrading is strongly encouraged. + + .. _paramiko-itself: Paramiko itself @@ -16,17 +23,15 @@ via `pip <http://pip-installer.org>`_:: Users who want the bleeding edge can install the development version via ``pip install paramiko==dev``. -We currently support **Python 2.6, 2.7 and 3.3+** (Python **3.2** should also -work but has a less-strong compatibility guarantee from us.) Users on Python -2.5 or older are urged to upgrade. +We currently support **Python 2.6, 2.7, 3.3+, and PyPy**. Users on Python 2.5 +or older (or 3.2 or older) are urged to upgrade. -Paramiko has two hard dependencies: the pure-Python ECDSA module ``ecdsa``, and the -PyCrypto C extension. ``ecdsa`` is easily installable from wherever you -obtained Paramiko's package; PyCrypto may require more work. Read on for -details. +Paramiko has only one direct hard dependency: the Cryptography library. See +:ref:`cryptography`. If you need GSS-API / SSPI support, see :ref:`the below subsection on it -<gssapi>` for details on additional dependencies. +<gssapi>` for details on its optional dependencies. + .. _release-lines: @@ -37,71 +42,52 @@ Users desiring stability may wish to pin themselves to a specific release line once they first start using Paramiko; to assist in this, we guarantee bugfixes for the last 2-3 releases including the latest stable one. -If you're unsure which version to install, we have suggestions: +This typically spans major & minor versions, so even if e.g. 3.1 is the latest +stable release, it's likely that bugfixes will occasionally come out for the +latest 2.x and perhaps even 1.x releases, as well as for 3.0. New feature +releases for previous major-version lines are less likely but not unheard of. + +If you're unsure which version to install: * **Completely new users** should always default to the **latest stable release** (as above, whatever is newest / whatever shows up with ``pip install paramiko``.) -* **Users upgrading from a much older version** (e.g. the 1.7.x line) should - probably get the **oldest actively supported line** (see the paragraph above - this list for what that currently is.) +* **Users upgrading from a much older version** (e.g. 1.7.x through 1.10.x) + should probably get the **oldest actively supported line** (check the + :doc:`changelog` for recent releases). * **Everybody else** is hopefully already "on" a given version and can carefully upgrade to whichever version they care to, when their release line stops being supported. -PyCrypto -======== - -`PyCrypto <https://www.dlitz.net/software/pycrypto/>`_ provides the low-level -(C-based) encryption algorithms we need to implement the SSH protocol. There -are a couple gotchas associated with installing PyCrypto: its compatibility -with Python's package tools, and the fact that it is a C-based extension. +.. _cryptography: -C extension ------------ +Cryptography +============ -Unless you are installing from a precompiled source such as a Debian apt -repository or RedHat RPM, or using :ref:`pypm <pypm>`, you will also need the -ability to build Python C-based modules from source in order to install -PyCrypto. Users on **Unix-based platforms** such as Ubuntu or Mac OS X will -need the traditional C build toolchain installed (e.g. Developer Tools / XCode -Tools on the Mac, or the ``build-essential`` package on Ubuntu or Debian Linux --- basically, anything with ``gcc``, ``make`` and so forth) as well as the -Python development libraries, often named ``python-dev`` or similar. +`Cryptography <https://cryptography.io>`__ provides the low-level (C-based) +encryption algorithms we need to implement the SSH protocol. It has detailed +`installation instructions`_ (and an `FAQ +<https://cryptography.io/en/latest/faq/>`_) which you should read carefully. -For **Windows** users we recommend using :ref:`pypm`, installing a C -development environment such as `Cygwin <http://cygwin.com>`_ or obtaining a -precompiled Win32 PyCrypto package from `voidspace's Python modules page -<http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_. +In general, you'll need one of the following setups: -.. note:: - Some Windows users whose Python is 64-bit have found that the PyCrypto - dependency ``winrandom`` may not install properly, leading to ImportErrors. - In this scenario, you'll probably need to compile ``winrandom`` yourself - via e.g. MS Visual Studio. See `Fabric #194 - <https://github.com/fabric/fabric/issues/194>`_ for info. - - -.. _pypm: - -ActivePython and PyPM -===================== +* On Windows or Mac OS X, provided your ``pip`` is modern (8.x+): nothing else + is required. ``pip`` will install statically compiled binary archives of + Cryptography & its dependencies. +* On Linux, or on other platforms with older versions of ``pip``: you'll need a + C build toolchain, plus development headers for Python, OpenSSL and + ``libffi``. Again, see `Cryptography's install docs`_; these requirements may + occasionally change. -Windows users who already have ActiveState's `ActivePython -<http://www.activestate.com/activepython/downloads>`_ distribution installed -may find Paramiko is best installed with `its package manager, PyPM -<http://code.activestate.com/pypm/>`_. Below is example output from an -installation of Paramiko via ``pypm``:: + .. warning:: + If you go this route, note that **OpenSSL 1.0.1 or newer is effectively + required**. Cryptography 1.3 and older technically allow OpenSSL 0.9.8, but + 1.4 and newer - which Paramiko will gladly install or upgrade, if you e.g. + ``pip install -U`` - drop that support. - C:\> pypm install paramiko - The following packages will be installed into "%APPDATA%\Python" (2.7): - paramiko-1.7.8 pycrypto-2.4 - Get: [pypm-free.activestate.com] paramiko 1.7.8 - Get: [pypm-free.activestate.com] pycrypto 2.4 - Installing paramiko-1.7.8 - Installing pycrypto-2.4 - C:\> +.. _installation instructions: +.. _Cryptography's install docs: https://cryptography.io/en/latest/installation/ .. _gssapi: @@ -115,8 +101,6 @@ due to their infrequent utility & non-platform-agnostic requirements): * It hopefully goes without saying but **all platforms** need **a working installation of GSS-API itself**, e.g. Heimdal. -* **All platforms** need `pyasn1 <https://pypi.python.org/pypi/pyasn1>`_ - ``0.1.7`` or better. * **Unix** needs `python-gssapi <https://pypi.python.org/pypi/python-gssapi/>`_ ``0.6.1`` or better. @@ -130,3 +114,9 @@ due to their infrequent utility & non-platform-agnostic requirements): delegation, make sure that the target host is trusted for delegation in the active directory configuration. For details see: http://technet.microsoft.com/en-us/library/cc738491%28v=ws.10%29.aspx + + +.. toctree:: + :hidden: + + installing-1.x @@ -43,8 +43,9 @@ from tests.test_kex import KexTest from tests.test_packetizer import PacketizerTest from tests.test_auth import AuthTest from tests.test_transport import TransportTest +from tests.test_ssh_exception import NoValidConnectionsErrorTest from tests.test_client import SSHClientTest -from test_client import SSHClientTest +from test_client import SSHClientTest # XXX why shadow the above import? from test_gssapi import GSSAPITest from test_ssh_gss import GSSAuthTest from test_kex_gss import GSSKexTest @@ -156,6 +157,7 @@ def main(): if options.use_transport: suite.addTest(unittest.makeSuite(AuthTest)) suite.addTest(unittest.makeSuite(TransportTest)) + suite.addTest(unittest.makeSuite(NoValidConnectionsErrorTest)) suite.addTest(unittest.makeSuite(SSHClientTest)) if options.use_sftp: suite.addTest(unittest.makeSuite(SFTPTest)) diff --git a/tests/test_auth.py b/tests/test_auth.py index ec78e3ce..23517790 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -83,13 +83,13 @@ class NullServer (ServerInterface): return AUTH_SUCCESSFUL return AUTH_PARTIALLY_SUCCESSFUL return AUTH_FAILED - + def check_auth_interactive(self, username, submethods): if username == 'commie': self.username = username return InteractiveQuery('password', 'Please enter a password.', ('Password', False)) return AUTH_FAILED - + def check_auth_interactive_response(self, responses): if self.username == 'commie': if (len(responses) == 1) and (responses[0] == 'cat'): @@ -111,7 +111,7 @@ class AuthTest (unittest.TestCase): self.ts.close() self.socks.close() self.sockc.close() - + def start_server(self): host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) self.public_host_key = RSAKey(data=host_key.asbytes()) @@ -120,7 +120,7 @@ class AuthTest (unittest.TestCase): self.server = NullServer() self.assertTrue(not self.event.is_set()) self.ts.start_server(self.event, self.server) - + def verify_finished(self): self.event.wait(1.0) self.assertTrue(self.event.is_set()) @@ -156,7 +156,7 @@ class AuthTest (unittest.TestCase): self.assertTrue(issubclass(etype, AuthenticationException)) self.tc.auth_password(username='slowdive', password='pygmalion') self.verify_finished() - + def test_3_multipart_auth(self): """ verify that multipart auth works. @@ -187,7 +187,7 @@ class AuthTest (unittest.TestCase): self.assertEqual(self.got_prompts, [('Password', False)]) self.assertEqual([], remain) self.verify_finished() - + def test_5_interactive_auth_fallback(self): """ verify that a password auth attempt will fallback to "interactive" diff --git a/tests/test_client.py b/tests/test_client.py index f71efd5a..f42d79d9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -22,6 +22,8 @@ Some unit tests for SSHClient. from __future__ import with_statement +import gc +import platform import socket from tempfile import mkstemp import threading @@ -31,8 +33,9 @@ import warnings import os import time from tests.util import test_path + import paramiko -from paramiko.common import PY2, b +from paramiko.common import PY2 from paramiko.ssh_exception import SSHException @@ -87,6 +90,12 @@ class SSHClientTest (unittest.TestCase): self.sockl.bind(('localhost', 0)) self.sockl.listen(1) self.addr, self.port = self.sockl.getsockname() + self.connect_kwargs = dict( + hostname=self.addr, + port=self.port, + username='slowdive', + look_for_keys=False, + ) self.event = threading.Event() def tearDown(self): @@ -124,7 +133,7 @@ class SSHClientTest (unittest.TestCase): self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) # Actual connection - self.tc.connect(self.addr, self.port, username='slowdive', **kwargs) + self.tc.connect(**dict(self.connect_kwargs, **kwargs)) # Authentication successful? self.event.wait(1.0) @@ -229,7 +238,7 @@ class SSHClientTest (unittest.TestCase): self.tc = paramiko.SSHClient() self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.assertEqual(0, len(self.tc.get_host_keys())) - self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') + self.tc.connect(password='pygmalion', **self.connect_kwargs) self.event.wait(1.0) self.assertTrue(self.event.is_set()) @@ -272,19 +281,18 @@ class SSHClientTest (unittest.TestCase): transport's packetizer) is closed. """ # Unclear why this is borked on Py3, but it is, and does not seem worth - # pursuing at the moment. + # pursuing at the moment. Skipped on PyPy because it fails on travis + # for unknown reasons, works fine locally. # XXX: It's the release of the references to e.g packetizer that fails # in py3... - if not PY2: + if not PY2 or platform.python_implementation() == "PyPy": return threading.Thread(target=self._run).start() - host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) - public_host_key = paramiko.RSAKey(data=host_key.asbytes()) self.tc = paramiko.SSHClient() self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.assertEqual(0, len(self.tc.get_host_keys())) - self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') + self.tc.connect(**dict(self.connect_kwargs, password='pygmalion')) self.event.wait(1.0) self.assertTrue(self.event.is_set()) @@ -295,14 +303,10 @@ class SSHClientTest (unittest.TestCase): self.tc.close() del self.tc - # hrm, sometimes p isn't cleared right away. why is that? - #st = time.time() - #while (time.time() - st < 5.0) and (p() is not None): - # time.sleep(0.1) - - # instead of dumbly waiting for the GC to collect, force a collection - # to see whether the SSHClient object is deallocated correctly - import gc + # force a collection to see whether the SSHClient object is deallocated + # correctly. 2 GCs are needed to make sure it's really collected on + # PyPy + gc.collect() gc.collect() self.assertTrue(p() is None) @@ -312,14 +316,12 @@ class SSHClientTest (unittest.TestCase): verify that an SSHClient can be used a context manager """ threading.Thread(target=self._run).start() - host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) - public_host_key = paramiko.RSAKey(data=host_key.asbytes()) with paramiko.SSHClient() as tc: self.tc = tc self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.assertEquals(0, len(self.tc.get_host_keys())) - self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') + self.tc.connect(**dict(self.connect_kwargs, password='pygmalion')) self.event.wait(1.0) self.assertTrue(self.event.is_set()) @@ -341,12 +343,29 @@ class SSHClientTest (unittest.TestCase): self.tc = paramiko.SSHClient() self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) # Connect with a half second banner timeout. + kwargs = dict(self.connect_kwargs, banner_timeout=0.5) self.assertRaises( paramiko.SSHException, self.tc.connect, - self.addr, - self.port, - username='slowdive', + **kwargs + ) + + def test_8_auth_trickledown(self): + """ + Failed key auth doesn't prevent subsequent pw auth from succeeding + """ + # NOTE: re #387, re #394 + # If pkey module used within Client._auth isn't correctly handling auth + # errors (e.g. if it allows things like ValueError to bubble up as per + # midway thru #394) client.connect() will fail (at key load step) + # instead of succeeding (at password step) + kwargs = dict( + # Password-protected key whose passphrase is not 'pygmalion' (it's + # 'television' as per tests/test_pkey.py). NOTE: must use + # key_filename, loading the actual key here with PKey will except + # immediately; we're testing the try/except crap within Client. + key_filename=[test_path('test_rsa_password.key')], + # Actual password for default 'slowdive' user password='pygmalion', - banner_timeout=0.5 ) + self._test_connection(**kwargs) diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py index 8faec03c..ccfe26bd 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -23,9 +23,10 @@ Some unit tests for the ssh2 protocol in Transport. import unittest from hashlib import sha1 -from tests.loop import LoopSocket +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes -from Crypto.Cipher import AES +from tests.loop import LoopSocket from paramiko import Message, Packetizer, util from paramiko.common import byte_chr, zero_byte @@ -43,8 +44,12 @@ class PacketizerTest (unittest.TestCase): p = Packetizer(wsock) p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) - cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20) + encryptor = Cipher( + algorithms.AES(zero_byte * 16), + modes.CBC(x55 * 16), + backend=default_backend() + ).encryptor() + p.set_outbound_cipher(encryptor, 16, sha1, 12, x1f * 20) # message has to be at least 16 bytes long, so we'll have at least one # block of data encrypted that contains zero random padding bytes @@ -66,8 +71,12 @@ class PacketizerTest (unittest.TestCase): p = Packetizer(rsock) p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) - cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_inbound_cipher(cipher, 16, sha1, 12, x1f * 20) + decryptor = Cipher( + algorithms.AES(zero_byte * 16), + modes.CBC(x55 * 16), + backend=default_backend() + ).decryptor() + p.set_inbound_cipher(decryptor, 16, sha1, 12, x1f * 20) wsock.send(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59') cmd, m = p.read_message() self.assertEqual(100, cmd) @@ -82,8 +91,12 @@ class PacketizerTest (unittest.TestCase): p = Packetizer(wsock) p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) - cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20) + encryptor = Cipher( + algorithms.AES(zero_byte * 16), + modes.CBC(x55 * 16), + backend=default_backend() + ).encryptor() + p.set_outbound_cipher(encryptor, 16, sha1, 12, x1f * 20) # message has to be at least 16 bytes long, so we'll have at least one # block of data encrypted that contains zero random padding bytes diff --git a/tests/test_pkey.py b/tests/test_pkey.py index f673254f..ec128140 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -42,34 +42,34 @@ SIGNED_RSA = '20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97: RSA_PRIVATE_OUT = """\ -----BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKCAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAM -s6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZ -v3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cCASMC -ggCAEiI6plhqipt4P05L3PYr0pHZq2VPEbE4k9eI/gRKo/c1VJxY3DJnc1cenKsk -trQRtW3OxCEufqsX5PNec6VyKkW+Ox6beJjMKm4KF8ZDpKi9Nw6MdX3P6Gele9D9 -+ieyhVFljrnAqcXsgChTBOYlL2imqCs3qRGAJ3cMBIAx3VsCQQD3pIFVYW398kE0 -n0e1icEpkbDRV4c5iZVhu8xKy2yyfy6f6lClSb2+Ub9uns7F3+b5v0pYSHbE9+/r -OpRq83AfAkEA2rMZlr8SnMXgnyka2LuggA9QgMYy18hyao1dUxySubNDa9N+q2QR -mwDisTUgRFHKIlDHoQmzPbXAmYZX1YlDmQJBAPCRLS5epV0XOAc7pL762OaNhzHC -veAfQKgVhKBt105PqaKpGyQ5AXcNlWQlPeTK4GBTbMrKDPna6RBkyrEJvV8CQBK+ -5O+p+kfztCrmRCE0p1tvBuZ3Y3GU1ptrM+KNa6mEZN1bRV8l1Z+SXJLYqv6Kquz/ -nBUeFq2Em3rfoSDugiMCQDyG3cxD5dKX3IgkhLyBWls/FLDk4x/DQ+NUTu0F1Cu6 -JJye+5ARLkL0EweMXf0tmIYfWItDLsWB0fKg/56h0js= +MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz +oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/ +d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB +gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0 +EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon +soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H +tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU +avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA +4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g +H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv +qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV +HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc +nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7 -----END RSA PRIVATE KEY----- """ DSS_PRIVATE_OUT = """\ -----BEGIN DSA PRIVATE KEY----- -MIIBvgIBAAKCAIEA54GmA2d9HOv+3CYBBG7ZfBYCncIW2tWe6Dqzp+DCP+guNhtW -2MDLqmX+HQQoJbHat/Uh63I2xPFaueID0jod4OPrlfUXIOSDqDy28Kdo0Hxen9RS -G7Me4awwiKlHEHHD0sXrTwSplyPUTfK2S2hbkHk5yOuQSjPfEbsL6ukiNi8CFQDw -z4UnmsGiSNu5iqjn3uTzwUpshwKCAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25c -PzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq -1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+X -FDxlqZo8Y+wCggCARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lY -ukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+N -wacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgECFGI9 -QPSch9pT9XHqn+1rZ4bK+QGA +MIIBuwIBAAKBgQDngaYDZ30c6/7cJgEEbtl8FgKdwhba1Z7oOrOn4MI/6C42G1bY +wMuqZf4dBCglsdq39SHrcjbE8Vq54gPSOh3g4+uV9Rcg5IOoPLbwp2jQfF6f1FIb +sx7hrDCIqUcQccPSxetPBKmXI9RN8rZLaFuQeTnI65BKM98Ruwvq6SI2LwIVAPDP +hSeawaJI27mKqOfe5PPBSmyHAoGBAJMXxXmPD9sGaQ419DIpmZecJKBUAy9uXD8x +gbgeDpwfDaFJP8owByCKREocPFfi86LjCuQkyUKOfjYMN6iHIf1oEZjB8uJAatUr +FzI0ArXtUqOhwTLwTyFuUojE5own2WYsOAGByvgfyWjsGhvckYNhI4ODpNdPlxQ8 +ZamaPGPsAoGARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmn +jO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacI +BlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgECFGI9QPSc +h9pT9XHqn+1rZ4bK+QGA -----END DSA PRIVATE KEY----- """ @@ -121,7 +121,7 @@ class KeyTest (unittest.TestCase): self.assertEqual(exp_rsa, my_rsa) self.assertEqual(PUB_RSA.split()[1], key.get_base64()) self.assertEqual(1024, key.get_bits()) - + def test_4_load_dss(self): key = DSSKey.from_private_key_file(test_path('test_dss.key')) self.assertEqual('ssh-dss', key.get_name()) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 53b73ee0..e4c2c3a3 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -611,7 +611,7 @@ class SFTPTest (unittest.TestCase): with sftp.open(FOLDER + '/bunny.txt', 'rb') as f: self.assertEqual(text, f.read(128)) - self.assertEqual((41, 41), saved_progress[-1]) + self.assertEqual([(41, 41)], saved_progress) os.unlink(localname) fd, localname = mkstemp() @@ -621,7 +621,7 @@ class SFTPTest (unittest.TestCase): with open(localname, 'rb') as f: self.assertEqual(text, f.read(128)) - self.assertEqual((41, 41), saved_progress[-1]) + self.assertEqual([(41, 41)], saved_progress) os.unlink(localname) sftp.unlink(FOLDER + '/bunny.txt') diff --git a/tests/test_ssh_exception.py b/tests/test_ssh_exception.py new file mode 100644 index 00000000..18f2a97d --- /dev/null +++ b/tests/test_ssh_exception.py @@ -0,0 +1,31 @@ +import pickle +import unittest + +from paramiko.ssh_exception import NoValidConnectionsError + + +class NoValidConnectionsErrorTest (unittest.TestCase): + + def test_pickling(self): + # Regression test for https://github.com/paramiko/paramiko/issues/617 + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception()}) + new_exc = pickle.loads(pickle.dumps(exc)) + self.assertEqual(type(exc), type(new_exc)) + self.assertEqual(str(exc), str(new_exc)) + self.assertEqual(exc.args, new_exc.args) + + def test_error_message_for_single_host(self): + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception()}) + assert "Unable to connect to port 22 on 127.0.0.1" in str(exc) + + def test_error_message_for_two_hosts(self): + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception(), + ('::1', '22'): Exception()}) + assert "Unable to connect to port 22 on 127.0.0.1 or ::1" in str(exc) + + def test_error_message_for_multiple_hosts(self): + exc = NoValidConnectionsError({('127.0.0.1', '22'): Exception(), + ('::1', '22'): Exception(), + ('10.0.0.42', '22'): Exception()}) + exp = "Unable to connect to port 22 on 10.0.0.42, 127.0.0.1 or ::1" + assert exp in str(exc) diff --git a/tox-requirements.txt b/tox-requirements.txt index 26224ce6..47ddd792 100644 --- a/tox-requirements.txt +++ b/tox-requirements.txt @@ -1,2 +1,3 @@ # Not sure why tox can't just read setup.py? -pycrypto +cryptography >= 0.8 +pyasn1 >= 0.1.7 @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33,py34 +envlist = py26,py27,py33,py34,pypy [testenv] commands = pip install -q -r tox-requirements.txt |