diff options
-rw-r--r-- | paramiko/__init__.py | 1 | ||||
-rw-r--r-- | paramiko/_version.py | 2 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 9 | ||||
-rw-r--r-- | paramiko/channel.py | 2 | ||||
-rw-r--r-- | paramiko/client.py | 58 | ||||
-rw-r--r-- | paramiko/dsskey.py | 8 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 11 | ||||
-rw-r--r-- | paramiko/ed25519key.py | 205 | ||||
-rw-r--r-- | paramiko/hostkeys.py | 3 | ||||
-rw-r--r-- | paramiko/kex_ecdh_nist.py | 126 | ||||
-rw-r--r-- | paramiko/kex_gss.py | 17 | ||||
-rw-r--r-- | paramiko/rsakey.py | 7 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 37 | ||||
-rw-r--r-- | paramiko/sftp_server.py | 6 | ||||
-rw-r--r-- | paramiko/sftp_si.py | 19 | ||||
-rw-r--r-- | paramiko/transport.py | 19 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | sites/www/changelog.rst | 67 | ||||
-rw-r--r-- | sites/www/index.rst | 1 | ||||
-rw-r--r-- | sites/www/installing-1.x.rst | 1 | ||||
-rw-r--r-- | sites/www/installing.rst | 6 | ||||
-rw-r--r-- | tests/stub_sftp.py | 12 | ||||
-rw-r--r-- | tests/test_auth.py | 19 | ||||
-rw-r--r-- | tests/test_client.py | 47 | ||||
-rw-r--r-- | tests/test_ed25519.key | 8 | ||||
-rw-r--r-- | tests/test_ed25519_password.key | 8 | ||||
-rw-r--r-- | tests/test_kex.py | 76 | ||||
-rw-r--r-- | tests/test_kex_gss.py | 20 | ||||
-rw-r--r-- | tests/test_pkey.py | 28 | ||||
-rw-r--r-- | tests/test_sftp.py | 34 |
30 files changed, 784 insertions, 80 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py index d70129bc..22505402 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -52,6 +52,7 @@ from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery from paramiko.rsakey import RSAKey from paramiko.dsskey import DSSKey from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key from paramiko.sftp import SFTPError, BaseSFTP from paramiko.sftp_client import SFTP, SFTPClient from paramiko.sftp_server import SFTPServer diff --git a/paramiko/_version.py b/paramiko/_version.py index 17fd0032..96e885f5 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 1, 6) +__version_info__ = (2, 2, 4) __version__ = ".".join(map(str, __version_info__)) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 646e9195..6a7b8bdd 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -21,6 +21,8 @@ """ import weakref +import time + from paramiko.common import ( cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, @@ -57,7 +59,6 @@ from paramiko.common import ( MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES, ) - from paramiko.message import Message from paramiko.py3compat import bytestring from paramiko.ssh_exception import ( @@ -214,6 +215,9 @@ class AuthHandler(object): return m.asbytes() def wait_for_response(self, event): + max_ts = None + if self.transport.auth_timeout is not None: + max_ts = time.time() + self.transport.auth_timeout while True: event.wait(0.1) if not self.transport.is_active(): @@ -223,6 +227,9 @@ class AuthHandler(object): raise e if event.is_set(): break + if max_ts is not None and max_ts <= time.time(): + raise AuthenticationException("Authentication timeout.") + if not self.is_authenticated(): e = self.transport.get_exception() if e is None: diff --git a/paramiko/channel.py b/paramiko/channel.py index b2e8edd1..fd61bd65 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -25,6 +25,8 @@ import os import socket import time import threading + +# TODO: switch as much of py3compat.py to 'six' as possible, then use six.wraps from functools import wraps from paramiko import util diff --git a/paramiko/client.py b/paramiko/client.py index bb6c7ff4..c959b433 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -22,6 +22,7 @@ SSH client & key policies from binascii import hexlify import getpass +import inspect import os import socket import warnings @@ -32,6 +33,7 @@ from paramiko.common import DEBUG from paramiko.config import SSH_PORT from paramiko.dsskey import DSSKey from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key from paramiko.hostkeys import HostKeys from paramiko.py3compat import string_types from paramiko.rsakey import RSAKey @@ -171,16 +173,10 @@ class SSHClient(ClosingContextManager): 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 **policy** is a "policy class" (or instance thereof), namely some + subclass of `.MissingHostKeyPolicy` such as `.RejectPolicy` (the + default), `.AutoAddPolicy`, `.WarningPolicy`, or a user-created + subclass. * 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`). @@ -189,6 +185,8 @@ class SSHClient(ClosingContextManager): the policy to use when receiving a host key from a previously-unknown server """ + if inspect.isclass(policy): + policy = policy() self._policy = policy def _families_and_addresses(self, hostname, port): @@ -233,6 +231,7 @@ class SSHClient(ClosingContextManager): gss_deleg_creds=True, gss_host=None, banner_timeout=None, + auth_timeout=None, ): """ Connect to an SSH server and authenticate to it. The server's host key @@ -284,6 +283,8 @@ class SSHClient(ClosingContextManager): The targets name in the kerberos database. default: hostname :param float banner_timeout: an optional timeout (in seconds) to wait for the SSH banner to be presented. + :param float auth_timeout: an optional timeout (in seconds) to wait for + an authentication response. :raises: `.BadHostKeyException` -- if the server's host key could not be @@ -345,6 +346,8 @@ class SSHClient(ClosingContextManager): t.set_log_channel(self._log_channel) if banner_timeout is not None: t.banner_timeout = banner_timeout + if auth_timeout is not None: + t.auth_timeout = auth_timeout if port == SSH_PORT: server_hostkey_name = hostname @@ -586,7 +589,7 @@ class SSHClient(ClosingContextManager): if not two_factor: for key_filename in key_filenames: - for pkey_class in (RSAKey, DSSKey, ECDSAKey): + for pkey_class in (RSAKey, DSSKey, ECDSAKey, Ed25519Key): try: key = pkey_class.from_private_key_file( key_filename, password @@ -631,25 +634,20 @@ class SSHClient(ClosingContextManager): if not two_factor: keyfiles = [] - rsa_key = os.path.expanduser("~/.ssh/id_rsa") - dsa_key = os.path.expanduser("~/.ssh/id_dsa") - ecdsa_key = os.path.expanduser("~/.ssh/id_ecdsa") - if os.path.isfile(rsa_key): - keyfiles.append((RSAKey, rsa_key)) - if os.path.isfile(dsa_key): - keyfiles.append((DSSKey, dsa_key)) - if os.path.isfile(ecdsa_key): - keyfiles.append((ECDSAKey, ecdsa_key)) - # look in ~/ssh/ for windows users: - rsa_key = os.path.expanduser("~/ssh/id_rsa") - dsa_key = os.path.expanduser("~/ssh/id_dsa") - ecdsa_key = os.path.expanduser("~/ssh/id_ecdsa") - if os.path.isfile(rsa_key): - keyfiles.append((RSAKey, rsa_key)) - if os.path.isfile(dsa_key): - keyfiles.append((DSSKey, dsa_key)) - if os.path.isfile(ecdsa_key): - keyfiles.append((ECDSAKey, ecdsa_key)) + + for keytype, name in [ + (RSAKey, "rsa"), + (DSSKey, "dsa"), + (ECDSAKey, "ecdsa"), + (Ed25519Key, "ed25519"), + ]: + # ~/ssh/ is for windows + for directory in [".ssh", "ssh"]: + full_path = os.path.expanduser( + "~/%s/id_%s" % (directory, name) + ) + if os.path.isfile(full_path): + keyfiles.append((keytype, full_path)) if not look_for_keys: keyfiles = [] diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 7139daf5..471e2851 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -91,13 +91,7 @@ class DSSKey(PKey): return self.asbytes() def __hash__(self): - h = hash(self.get_name()) - h = h * 37 + hash(self.p) - h = h * 37 + hash(self.q) - h = h * 37 + hash(self.g) - h = h * 37 + hash(self.y) - # h might be a long by now... - return hash(h) + return hash((self.get_name(), self.p, self.q, self.g, self.y)) def get_name(self): return "ssh-dss" diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 0f8c8994..c2ccaba3 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -179,10 +179,13 @@ class ECDSAKey(PKey): return self.asbytes() def __hash__(self): - h = hash(self.get_name()) - h = h * 37 + hash(self.verifying_key.public_numbers().x) - h = h * 37 + hash(self.verifying_key.public_numbers().y) - return hash(h) + return hash( + ( + self.get_name(), + self.verifying_key.public_numbers().x, + self.verifying_key.public_numbers().y, + ) + ) def get_name(self): return self.ecdsa_curve.key_format_identifier diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py new file mode 100644 index 00000000..77d4d37d --- /dev/null +++ b/paramiko/ed25519key.py @@ -0,0 +1,205 @@ +# 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., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +import bcrypt + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher + +import nacl.signing + +import six + +from paramiko.message import Message +from paramiko.pkey import PKey +from paramiko.py3compat import b +from paramiko.ssh_exception import SSHException, PasswordRequiredException + + +OPENSSH_AUTH_MAGIC = b"openssh-key-v1\x00" + + +def unpad(data): + # At the moment, this is only used for unpadding private keys on disk. This + # really ought to be made constant time (possibly by upstreaming this logic + # into pyca/cryptography). + padding_length = six.indexbytes(data, -1) + if padding_length > 16: + raise SSHException("Invalid key") + for i in range(1, padding_length + 1): + if six.indexbytes(data, -i) != (padding_length - i + 1): + raise SSHException("Invalid key") + return data[:-padding_length] + + +class Ed25519Key(PKey): + def __init__(self, msg=None, data=None, filename=None, password=None): + verifying_key = signing_key = None + if msg is None and data is not None: + msg = Message(data) + if msg is not None: + if msg.get_text() != "ssh-ed25519": + raise SSHException("Invalid key") + verifying_key = nacl.signing.VerifyKey(msg.get_binary()) + elif filename is not None: + with open(filename, "r") as f: + data = self._read_private_key("OPENSSH", f) + signing_key = self._parse_signing_key_data(data, password) + + if signing_key is None and verifying_key is None: + raise ValueError("need a key") + + self._signing_key = signing_key + self._verifying_key = verifying_key + + def _parse_signing_key_data(self, data, password): + from paramiko.transport import Transport + + # We may eventually want this to be usable for other key types, as + # OpenSSH moves to it, but for now this is just for Ed25519 keys. + # This format is described here: + # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key + # The description isn't totally complete, and I had to refer to the + # source for a full implementation. + message = Message(data) + if message.get_bytes(len(OPENSSH_AUTH_MAGIC)) != OPENSSH_AUTH_MAGIC: + raise SSHException("Invalid key") + + ciphername = message.get_text() + kdfname = message.get_text() + kdfoptions = message.get_binary() + num_keys = message.get_int() + + if kdfname == "none": + # kdfname of "none" must have an empty kdfoptions, the ciphername + # must be "none" + if kdfoptions or ciphername != "none": + raise SSHException("Invalid key") + elif kdfname == "bcrypt": + if not password: + raise PasswordRequiredException( + "Private key file is encrypted" + ) + kdf = Message(kdfoptions) + bcrypt_salt = kdf.get_binary() + bcrypt_rounds = kdf.get_int() + else: + raise SSHException("Invalid key") + + if ciphername != "none" and ciphername not in Transport._cipher_info: + raise SSHException("Invalid key") + + public_keys = [] + for _ in range(num_keys): + pubkey = Message(message.get_binary()) + if pubkey.get_text() != "ssh-ed25519": + raise SSHException("Invalid key") + public_keys.append(pubkey.get_binary()) + + private_ciphertext = message.get_binary() + if ciphername == "none": + private_data = private_ciphertext + else: + cipher = Transport._cipher_info[ciphername] + key = bcrypt.kdf( + password=b(password), + salt=bcrypt_salt, + desired_key_bytes=cipher["key-size"] + cipher["block-size"], + rounds=bcrypt_rounds, + # We can't control how many rounds are on disk, so no sense + # warning about it. + ignore_few_rounds=True, + ) + decryptor = Cipher( + cipher["class"](key[: cipher["key-size"]]), + cipher["mode"](key[cipher["key-size"] :]), + backend=default_backend(), + ).decryptor() + private_data = ( + decryptor.update(private_ciphertext) + decryptor.finalize() + ) + + message = Message(unpad(private_data)) + if message.get_int() != message.get_int(): + raise SSHException("Invalid key") + + signing_keys = [] + for i in range(num_keys): + if message.get_text() != "ssh-ed25519": + raise SSHException("Invalid key") + # A copy of the public key, again, ignore. + public = message.get_binary() + key_data = message.get_binary() + # The second half of the key data is yet another copy of the public + # key... + signing_key = nacl.signing.SigningKey(key_data[:32]) + # Verify that all the public keys are the same... + assert ( + signing_key.verify_key.encode() + == public + == public_keys[i] + == key_data[32:] + ) + signing_keys.append(signing_key) + # Comment, ignore. + message.get_binary() + + if len(signing_keys) != 1: + raise SSHException("Invalid key") + return signing_keys[0] + + def asbytes(self): + if self.can_sign(): + v = self._signing_key.verify_key + else: + v = self._verifying_key + m = Message() + m.add_string("ssh-ed25519") + m.add_string(v.encode()) + return m.asbytes() + + def __hash__(self): + if self.can_sign(): + v = self._signing_key.verify_key + else: + v = self._verifying_key + return hash((self.get_name(), v)) + + def get_name(self): + return "ssh-ed25519" + + def get_bits(self): + return 256 + + def can_sign(self): + return self._signing_key is not None + + def sign_ssh_data(self, data): + m = Message() + m.add_string("ssh-ed25519") + m.add_string(self._signing_key.sign(data).signature) + return m + + def verify_ssh_sig(self, data, msg): + if msg.get_text() != "ssh-ed25519": + return False + + try: + self._verifying_key.verify(data, msg.get_binary()) + except nacl.exceptions.BadSignatureError: + return False + else: + return True diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 7faf32d8..1cdddb62 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -30,6 +30,7 @@ from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey from paramiko.util import get_logger, constant_time_bytes_eq from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key from paramiko.ssh_exception import SSHException @@ -359,6 +360,8 @@ class HostKeyEntry: key = DSSKey(data=decodebytes(key)) elif keytype in ECDSAKey.supported_key_format_identifiers(): key = ECDSAKey(data=decodebytes(key), validate_point=False) + elif keytype == "ssh-ed25519": + key = Ed25519Key(data=decodebytes(key)) else: log.info("Unable to handle key of type %s" % (keytype,)) return None diff --git a/paramiko/kex_ecdh_nist.py b/paramiko/kex_ecdh_nist.py new file mode 100644 index 00000000..496805ab --- /dev/null +++ b/paramiko/kex_ecdh_nist.py @@ -0,0 +1,126 @@ +""" +Ephemeral Elliptic Curve Diffie-Hellman (ECDH) key exchange +RFC 5656, Section 4 +""" + +from hashlib import sha256, sha384, sha512 +from paramiko.message import Message +from paramiko.py3compat import byte_chr, long +from paramiko.ssh_exception import SSHException +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from binascii import hexlify + +_MSG_KEXECDH_INIT, _MSG_KEXECDH_REPLY = range(30, 32) +c_MSG_KEXECDH_INIT, c_MSG_KEXECDH_REPLY = [byte_chr(c) for c in range(30, 32)] + + +class KexNistp256: + + name = "ecdh-sha2-nistp256" + hash_algo = sha256 + curve = ec.SECP256R1() + + def __init__(self, transport): + self.transport = transport + # private key, client public and server public keys + self.P = long(0) + self.Q_C = None + self.Q_S = None + + def start_kex(self): + self._generate_key_pair() + if self.transport.server_mode: + self.transport._expect_packet(_MSG_KEXECDH_INIT) + return + m = Message() + m.add_byte(c_MSG_KEXECDH_INIT) + # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion + m.add_string(self.Q_C.public_numbers().encode_point()) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXECDH_REPLY) + + def parse_next(self, ptype, m): + if self.transport.server_mode and (ptype == _MSG_KEXECDH_INIT): + return self._parse_kexecdh_init(m) + elif not self.transport.server_mode and (ptype == _MSG_KEXECDH_REPLY): + return self._parse_kexecdh_reply(m) + raise SSHException("KexECDH asked to handle packet type %d" % ptype) + + def _generate_key_pair(self): + self.P = ec.generate_private_key(self.curve, default_backend()) + if self.transport.server_mode: + self.Q_S = self.P.public_key() + return + self.Q_C = self.P.public_key() + + def _parse_kexecdh_init(self, m): + Q_C_bytes = m.get_string() + self.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point( + self.curve, Q_C_bytes + ) + K_S = self.transport.get_server_key().asbytes() + K = self.P.exchange(ec.ECDH(), self.Q_C.public_key(default_backend())) + K = long(hexlify(K), 16) + # compute exchange hash + hm = Message() + hm.add( + self.transport.remote_version, + self.transport.local_version, + self.transport.remote_kex_init, + self.transport.local_kex_init, + ) + hm.add_string(K_S) + hm.add_string(Q_C_bytes) + # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion + hm.add_string(self.Q_S.public_numbers().encode_point()) + hm.add_mpint(long(K)) + H = self.hash_algo(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + sig = self.transport.get_server_key().sign_ssh_data(H) + # construct reply + m = Message() + m.add_byte(c_MSG_KEXECDH_REPLY) + m.add_string(K_S) + m.add_string(self.Q_S.public_numbers().encode_point()) + m.add_string(sig) + self.transport._send_message(m) + self.transport._activate_outbound() + + def _parse_kexecdh_reply(self, m): + K_S = m.get_string() + Q_S_bytes = m.get_string() + self.Q_S = ec.EllipticCurvePublicNumbers.from_encoded_point( + self.curve, Q_S_bytes + ) + sig = m.get_binary() + K = self.P.exchange(ec.ECDH(), self.Q_S.public_key(default_backend())) + K = long(hexlify(K), 16) + # compute exchange hash and verify signature + hm = Message() + hm.add( + self.transport.local_version, + self.transport.remote_version, + self.transport.local_kex_init, + self.transport.remote_kex_init, + ) + hm.add_string(K_S) + # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion + hm.add_string(self.Q_C.public_numbers().encode_point()) + hm.add_string(Q_S_bytes) + hm.add_mpint(K) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) + self.transport._verify_key(K_S, sig) + self.transport._activate_outbound() + + +class KexNistp384(KexNistp256): + name = "ecdh-sha2-nistp384" + hash_algo = sha384 + curve = ec.SECP384R1() + + +class KexNistp521(KexNistp256): + name = "ecdh-sha2-nistp521" + hash_algo = sha512 + curve = ec.SECP521R1() diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 8126e36a..d76bb2dd 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -92,7 +92,6 @@ class KexGSSGroup1(object): """ Start the GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange. """ - self.transport.gss_kex_used = True self._generate_x() if self.transport.server_mode: # compute f = g^x mod p, but don't send it yet @@ -223,14 +222,16 @@ class KexGSSGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, sha1(str(hm)).digest()) + H = sha1(str(hm)).digest() + self.transport._set_K_H(K, H) if srv_token is not None: self.kexgss.ssh_init_sec_context( target=self.gss_host, recv_token=srv_token ) - self.kexgss.ssh_check_mic(mic_token, self.transport.session_id) + self.kexgss.ssh_check_mic(mic_token, H) else: - self.kexgss.ssh_check_mic(mic_token, self.transport.session_id) + self.kexgss.ssh_check_mic(mic_token, H) + self.transport.gss_kex_used = True self.transport._activate_outbound() def _parse_kexgss_init(self, m): @@ -279,6 +280,7 @@ class KexGSSGroup1(object): else: m.add_boolean(False) self.transport._send_message(m) + self.transport.gss_kex_used = True self.transport._activate_outbound() else: m.add_byte(c_MSG_KEXGSS_CONTINUE) @@ -348,7 +350,6 @@ class KexGSSGex(object): """ Start the GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange """ - self.transport.gss_kex_used = True if self.transport.server_mode: self.transport._expect_packet(MSG_KEXGSS_GROUPREQ) return @@ -533,6 +534,7 @@ class KexGSSGex(object): else: m.add_boolean(False) self.transport._send_message(m) + self.transport.gss_kex_used = True self.transport._activate_outbound() else: m.add_byte(c_MSG_KEXGSS_CONTINUE) @@ -621,9 +623,10 @@ class KexGSSGex(object): self.kexgss.ssh_init_sec_context( target=self.gss_host, recv_token=srv_token ) - self.kexgss.ssh_check_mic(mic_token, self.transport.session_id) + self.kexgss.ssh_check_mic(mic_token, H) else: - self.kexgss.ssh_check_mic(mic_token, self.transport.session_id) + self.kexgss.ssh_check_mic(mic_token, H) + self.transport.gss_kex_used = True self.transport._activate_outbound() def _parse_kexgss_error(self, m): diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 31bb4716..a45d1020 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -97,10 +97,9 @@ class RSAKey(PKey): return self.asbytes().decode("utf8", errors="ignore") def __hash__(self): - h = hash(self.get_name()) - h = h * 37 + hash(self.public_numbers.e) - h = h * 37 + hash(self.public_numbers.n) - return hash(h) + return hash( + (self.get_name(), self.public_numbers.e, self.public_numbers.n) + ) def get_name(self): return "ssh-rsa" diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index f9652a34..425aa87d 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -56,6 +56,7 @@ from paramiko.sftp import ( CMD_READLINK, CMD_REALPATH, CMD_STATUS, + CMD_EXTENDED, SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, @@ -394,8 +395,15 @@ class SFTPClient(BaseSFTP, ClosingContextManager): """ Rename a file or folder from ``oldpath`` to ``newpath``. - :param str oldpath: existing name of the file or folder - :param str newpath: new name for the file or folder + .. note:: + This method implements 'standard' SFTP ``RENAME`` behavior; those + seeking the OpenSSH "POSIX rename" extension behavior should use + `posix_rename`. + + :param str oldpath: + existing name of the file or folder + :param str newpath: + new name for the file or folder, must not exist already :raises: ``IOError`` -- if ``newpath`` is a folder, or something else goes @@ -406,6 +414,28 @@ class SFTPClient(BaseSFTP, ClosingContextManager): self._log(DEBUG, "rename(%r, %r)" % (oldpath, newpath)) self._request(CMD_RENAME, oldpath, newpath) + def posix_rename(self, oldpath, newpath): + """ + Rename a file or folder from ``oldpath`` to ``newpath``, following + posix conventions. + + :param str oldpath: existing name of the file or folder + :param str newpath: new name for the file or folder, will be + overwritten if it already exists + + :raises: + ``IOError`` -- if ``newpath`` is a folder, posix-rename is not + supported by the server or something else goes wrong + + :versionadded: 2.2 + """ + oldpath = self._adjust_cwd(oldpath) + newpath = self._adjust_cwd(newpath) + self._log(DEBUG, "posix_rename(%r, %r)" % (oldpath, newpath)) + self._request( + CMD_EXTENDED, "posix-rename@openssh.com", oldpath, newpath + ) + def mkdir(self, path, mode=o777): """ Create a folder (directory) named ``path`` with numeric mode ``mode``. @@ -477,8 +507,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager): def symlink(self, source, dest): """ - Create a symbolic link (shortcut) of the ``source`` path at - ``destination``. + Create a symbolic link to the ``source`` path at ``destination``. :param str source: path of the original file :param str dest: path of the newly created symlink diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index d9a005d6..5c23ea2b 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -526,6 +526,12 @@ class SFTPServer(BaseSFTP, SubsystemHandler): tag = msg.get_text() if tag == "check-file": self._check_file(request_number, msg) + elif tag == "posix-rename@openssh.com": + oldpath = msg.get_text() + newpath = msg.get_text() + self._send_status( + request_number, self.server.posix_rename(oldpath, newpath) + ) else: self._send_status(request_number, SFTP_OP_UNSUPPORTED) else: diff --git a/paramiko/sftp_si.py b/paramiko/sftp_si.py index 98d13f50..40dc561c 100644 --- a/paramiko/sftp_si.py +++ b/paramiko/sftp_si.py @@ -195,10 +195,29 @@ class SFTPServerInterface(object): ``newpath`` already exists. (The rename operation should be non-desctructive.) + .. note:: + This method implements 'standard' SFTP ``RENAME`` behavior; those + seeking the OpenSSH "POSIX rename" extension behavior should use + `posix_rename`. + + :param str oldpath: + the requested path (relative or absolute) of the existing file. + :param str newpath: the requested new path of the file. + :return: an SFTP error code `int` like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED + + def posix_rename(self, oldpath, newpath): + """ + Rename (or move) a file, following posix conventions. If newpath + already exists, it will be overwritten. + :param str oldpath: the requested path (relative or absolute) of the existing file. :param str newpath: the requested new path of the file. :return: an SFTP error code `int` like ``SFTP_OK``. + + :versionadded: 2.2 """ return SFTP_OP_UNSUPPORTED diff --git a/paramiko/transport.py b/paramiko/transport.py index 4a29670d..9517d4a9 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -87,9 +87,11 @@ from paramiko.common import ( ) from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey +from paramiko.ed25519key import Ed25519Key from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko.kex_group1 import KexGroup1 from paramiko.kex_group14 import KexGroup14 +from paramiko.kex_ecdh_nist import KexNistp256, KexNistp384, KexNistp521 from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14 from paramiko.message import Message from paramiko.packet import Packetizer, NeedRekeyException @@ -160,6 +162,7 @@ class Transport(threading.Thread, ClosingContextManager): "hmac-md5-96", ) _preferred_keys = ( + "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", @@ -167,10 +170,13 @@ class Transport(threading.Thread, ClosingContextManager): "ssh-dss", ) _preferred_kex = ( - "diffie-hellman-group1-sha1", - "diffie-hellman-group14-sha1", - "diffie-hellman-group-exchange-sha1", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group14-sha1", + "diffie-hellman-group1-sha1", ) _preferred_gsskex = ( "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==", @@ -245,6 +251,7 @@ class Transport(threading.Thread, ClosingContextManager): "ecdsa-sha2-nistp256": ECDSAKey, "ecdsa-sha2-nistp384": ECDSAKey, "ecdsa-sha2-nistp521": ECDSAKey, + "ssh-ed25519": Ed25519Key, } _kex_info = { @@ -255,6 +262,9 @@ class Transport(threading.Thread, ClosingContextManager): "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGroup1, "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGroup14, "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGex, + "ecdh-sha2-nistp256": KexNistp256, + "ecdh-sha2-nistp384": KexNistp384, + "ecdh-sha2-nistp521": KexNistp521, } _compression_info = { @@ -422,6 +432,8 @@ class Transport(threading.Thread, ClosingContextManager): # how long (seconds) to wait for the handshake to finish after SSH # banner sent. self.handshake_timeout = 15 + # how long (seconds) to wait for the auth response. + self.auth_timeout = 30 # server mode: self.server_mode = False @@ -2108,6 +2120,7 @@ class Transport(threading.Thread, ClosingContextManager): self.clear_to_send.clear() finally: self.clear_to_send_lock.release() + self.gss_kex_used = False self.in_kex = True if self.server_mode: mp_required_prefix = "diffie-hellman-group-exchange-sha" @@ -74,5 +74,10 @@ setup( "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", ], - install_requires=["cryptography>=1.1", "pyasn1>=0.1.7"], + install_requires=[ + "bcrypt>=3.1.3", + "cryptography>=1.1", + "pynacl>=1.0.1", + "pyasn1>=0.1.7", + ], ) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 0a9978c1..b01e596d 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.2.4 <2018-09-18>` - :release:`2.1.6 <2018-09-18>` - :release:`2.0.9 <2018-09-18>` - :bug:`-` Modify protocol message handling such that ``Transport`` does not @@ -38,6 +39,7 @@ Changelog ``black`` code formatter (both of which previously only existed in the 2.4 branch and above) to everything 2.0 and newer. This makes back/forward porting bugfixes significantly easier. +- :release:`2.2.3 <2018-03-12>` - :release:`2.1.5 <2018-03-12>` - :release:`2.0.8 <2018-03-12>` - :release:`1.18.5 <2018-03-12>` @@ -47,13 +49,22 @@ Changelog where authentication status was not checked before processing channel-open and other requests typically only sent after authenticating. Big thanks to Matthijs Kooijman for the report. +- :bug:`1039` Ed25519 auth key decryption raised an unexpected exception when + given a unicode password string (typical in python 3). Report by Theodor van + Nahl and fix by Pierce Lopez. - :bug:`1108 (1.17+)` Rename a private method keyword argument (which was named ``async``) so that we're compatible with the upcoming Python 3.7 release (where ``async`` is a new keyword.) Thanks to ``@vEpiphyte`` for the report. - :support:`- backported` Include LICENSE file in wheel archives. +- :release:`2.2.2 <2017-09-18>` - :release:`2.1.4 <2017-09-18>` - :release:`2.0.7 <2017-09-18>` - :release:`1.18.4 <2017-09-18>` +- :bug:`1065` Add rekeying support to GSSAPI connections, which was erroneously + missing. Without this fix, any attempt to renegotiate the transport keys for + a ``gss-kex``-authed `~paramiko.transport.Transport` would cause a MIC + failure and terminate the connection. Thanks to Sebastian Deiß and Anselm + Kruis for the patch. - :bug:`1061` Clean up GSSAPI authentication procedures so they do not prevent normal fallback to other authentication methods on failure. (In other words, presence of GSSAPI functionality on a target server precluded use of _any_ @@ -71,6 +82,18 @@ Changelog consider a different type to be a "Missing" host key. This fixes a common case where an ECDSA key is in known_hosts and the server also has an RSA host key. Thanks to Pierce Lopez. +- :support:`1012` (via :issue:`1016`) Enhance documentation around the new + `SFTP.posix_rename <paramiko.sftp_client.SFTPClient.posix_rename>` method so + it's referenced in the 'standard' ``rename`` method for increased visibility. + Thanks to Marius Flage for the report. +- :release:`2.2.1 <2017-06-13>` +- :bug:`993` Ed25519 host keys were not comparable/hashable, causing an + exception if such a key existed in a ``known_hosts`` file. Thanks to Oleh + Prypin for the report and Pierce Lopez for the fix. +- :bug:`990` The (added in 2.2.0) ``bcrypt`` dependency should have been on + version 3.1.3 or greater (was initially set to 3.0.0 or greater.) Thanks to + Paul Howarth for the report. +- :release:`2.2.0 <2017-06-09>` - :release:`2.1.3 <2017-06-09>` - :release:`2.0.6 <2017-06-09>` - :release:`1.18.3 <2017-06-09>` @@ -99,9 +122,30 @@ Changelog Thanks to ``@virlos`` for the original report, Chris Harris and ``@ibuler`` for initial draft PRs, and ``@jhgorrell`` for the final patch. -- :support:`956 (1.17+)` Switch code coverage service from coveralls.io to - codecov.io (& then disable the latter's auto-comments.) Thanks to Nikolai - Røed Kristiansen for the patch. +- :feature:`65` (via :issue:`471`) Add support for OpenSSH's SFTP + ``posix-rename`` protocol extension (section 3.3 of `OpenSSH's protocol + extension document + <http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=1.31>`_), + via a new ``posix_rename`` method in `SFTPClient + <paramiko.sftp_client.SFTPClient.posix_rename>` and `SFTPServerInterface + <paramiko.sftp_si.SFTPServerInterface.posix_rename>`. Thanks to Wren Turkal + for the initial patch & Mika Pflüger for the enhanced, merged PR. +- :feature:`869` Add an ``auth_timeout`` kwarg to `SSHClient.connect + <paramiko.client.SSHClient.connect>` (default: 30s) to avoid hangs when the + remote end becomes unresponsive during the authentication step. Credit to + ``@timsavage``. + + .. note:: + This technically changes behavior, insofar as very slow auth steps >30s + will now cause timeout exceptions instead of completing. We doubt most + users will notice; those affected can simply give a higher value to + ``auth_timeout``. + +- :support:`921` Tighten up the ``__hash__`` implementation for various key + classes; less code is good code. Thanks to Francisco Couzo for the patch. +- :support:`956 backported (1.17+)` Switch code coverage service from + coveralls.io to codecov.io (& then disable the latter's auto-comments.) + Thanks to Nikolai Røed Kristiansen for the patch. - :bug:`983` Move ``sha1`` above the now-arguably-broken ``md5`` in the list of preferred MAC algorithms, as an incremental security improvement for users whose target systems offer both. Credit: Pierce Lopez. @@ -109,6 +153,15 @@ Changelog 2.0; but since the algorithm is now known to be completely insecure, we are opting to remove support outright instead of fixing it. Thanks to Alex Gaynor for catch & patch. +- :feature:`857` Allow `SSHClient.set_missing_host_key_policy + <paramiko.client.SSHClient.set_missing_host_key_policy>` to accept policy + classes _or_ instances, instead of only instances, thus fixing a + long-standing gotcha for unaware users. +- :feature:`951` Add support for ECDH key exchange (kex), specifically the + algorithms ``ecdh-sha2-nistp256``, ``ecdh-sha2-nistp384``, and + ``ecdh-sha2-nistp521``. They now come before the older ``diffie-hellman-*`` + family of kex algorithms in the preferred-kex list. Thanks to Shashank + Veerapaneni for the patch & Pierce Lopez for a follow-up. - :support:`- backported` A big formatting pass to clean up an enormous number of invalid Sphinx reference links, discovered by switching to a modern, rigorous nitpicking doc-building mode. @@ -122,6 +175,14 @@ Changelog to hosts which only offer these key types and no others. This is now fixed. Thanks to ``@ncoult`` and ``@kasdoe`` for reports and Pierce Lopez for the patch. +- :feature:`325` (via :issue:`972`) Add Ed25519 support, for both host keys + and user authentication. Big thanks to Alex Gaynor for the patch. + + .. note:: + This change adds the ``bcrypt`` and ``pynacl`` Python libraries as + dependencies. No C-level dependencies beyond those previously required (for + Cryptography) have been added. + - :support:`974 backported` Overhaul the codebase to be PEP-8, etc, compliant (i.e. passes the maintainer's preferred `flake8 <http://flake8.pycqa.org/>`_ configuration) and add a ``flake8`` step to the Travis config. Big thanks to diff --git a/sites/www/index.rst b/sites/www/index.rst index b09ab589..f0a5db8a 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -20,6 +20,7 @@ Please see the sidebar to the left to begin. changelog FAQs <faq> installing + installing-1.x contributing contact diff --git a/sites/www/installing-1.x.rst b/sites/www/installing-1.x.rst index 356fac49..8ede40d5 100644 --- a/sites/www/installing-1.x.rst +++ b/sites/www/installing-1.x.rst @@ -1,3 +1,4 @@ +================ Installing (1.x) ================ diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 6537b850..f335a9e7 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -110,9 +110,3 @@ 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 diff --git a/tests/stub_sftp.py b/tests/stub_sftp.py index 06ceb419..100076d6 100644 --- a/tests/stub_sftp.py +++ b/tests/stub_sftp.py @@ -30,6 +30,7 @@ from paramiko import ( SFTPAttributes, SFTPHandle, SFTP_OK, + SFTP_FAILURE, AUTH_SUCCESSFUL, OPEN_SUCCEEDED, ) @@ -150,6 +151,17 @@ class StubSFTPServer(SFTPServerInterface): def rename(self, oldpath, newpath): oldpath = self._realpath(oldpath) newpath = self._realpath(newpath) + if os.path.exists(newpath): + return SFTP_FAILURE + try: + os.rename(oldpath, newpath) + except OSError as e: + return SFTPServer.convert_errno(e.errno) + return SFTP_OK + + def posix_rename(self, oldpath, newpath): + oldpath = self._realpath(oldpath) + newpath = self._realpath(newpath) try: os.rename(oldpath, newpath) except OSError as e: diff --git a/tests/test_auth.py b/tests/test_auth.py index 45dcb3a4..6358a053 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -23,6 +23,7 @@ Some unit tests for authenticating over a Transport. import sys import threading import unittest +from time import sleep from paramiko import ( Transport, @@ -84,6 +85,9 @@ class NullServer(ServerInterface): return AUTH_SUCCESSFUL if username == "bad-server": raise Exception("Ack!") + if username == "unresponsive-server": + sleep(5) + return AUTH_SUCCESSFUL return AUTH_FAILED def check_auth_publickey(self, username, key): @@ -250,3 +254,18 @@ class AuthTest(unittest.TestCase): except: etype, evalue, etb = sys.exc_info() self.assertTrue(issubclass(etype, AuthenticationException)) + + def test_9_auth_non_responsive(self): + """ + verify that authentication times out if server takes to long to + respond (or never responds). + """ + self.tc.auth_timeout = 1 # 1 second, to speed up test + self.start_server() + self.tc.connect() + try: + remain = self.tc.auth_password("unresponsive-server", "hello") + except: + etype, evalue, etb = sys.exc_info() + self.assertTrue(issubclass(etype, AuthenticationException)) + self.assertTrue("Authentication timeout" in str(evalue)) diff --git a/tests/test_client.py b/tests/test_client.py index fed38791..87f7bcb2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -34,8 +34,8 @@ import weakref from tempfile import mkstemp import paramiko -from paramiko.py3compat import PY2, b -from paramiko.ssh_exception import SSHException +from paramiko.common import PY2 +from paramiko.ssh_exception import SSHException, AuthenticationException from .util import _support, slow @@ -44,6 +44,7 @@ FINGERPRINTS = { "ssh-dss": b"\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c", "ssh-rsa": b"\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5", "ecdsa-sha2-nistp256": b"\x25\x19\xeb\x55\xe6\xa1\x47\xff\x4f\x38\xd2\x75\x6f\xa5\xd5\x60", + "ssh-ed25519": b'\xb3\xd5"\xaa\xf9u^\xe8\xcd\x0e\xea\x02\xb9)\xa2\x80', } @@ -61,6 +62,9 @@ class NullServer(paramiko.ServerInterface): def check_auth_password(self, username, password): if (username == "slowdive") and (password == "pygmalion"): return paramiko.AUTH_SUCCESSFUL + if (username == "slowdive") and (password == "unresponsive-server"): + time.sleep(5) + return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): @@ -203,6 +207,9 @@ class SSHClientTest(unittest.TestCase): """ self._test_connection(key_filename=_support("test_ecdsa_256.key")) + def test_client_ed25519(self): + self._test_connection(key_filename=_support("test_ed25519.key")) + def test_3_multiple_key_files(self): """ verify that SSHClient accepts and tries multiple key files. @@ -393,7 +400,19 @@ class SSHClientTest(unittest.TestCase): ) self._test_connection(**kwargs) - def test_9_auth_trickledown_gsskex(self): + def test_9_auth_timeout(self): + """ + verify that the SSHClient has a configurable auth timeout + """ + # Connect with a half second auth timeout + self.assertRaises( + AuthenticationException, + self._test_connection, + password="unresponsive-server", + auth_timeout=0.5, + ) + + def test_10_auth_trickledown_gsskex(self): """ Failed gssapi-keyex auth doesn't prevent subsequent key auth from succeeding """ @@ -402,7 +421,7 @@ class SSHClientTest(unittest.TestCase): kwargs = dict(gss_kex=True, key_filename=[_support("test_rsa.key")]) self._test_connection(**kwargs) - def test_10_auth_trickledown_gssauth(self): + def test_11_auth_trickledown_gssauth(self): """ Failed gssapi-with-mic auth doesn't prevent subsequent key auth from succeeding """ @@ -411,7 +430,7 @@ class SSHClientTest(unittest.TestCase): kwargs = dict(gss_auth=True, key_filename=[_support("test_rsa.key")]) self._test_connection(**kwargs) - def test_11_reject_policy(self): + def test_12_reject_policy(self): """ verify that SSHClient's RejectPolicy works. """ @@ -427,7 +446,7 @@ class SSHClientTest(unittest.TestCase): **self.connect_kwargs ) - def test_12_reject_policy_gsskex(self): + def test_13_reject_policy_gsskex(self): """ verify that SSHClient's RejectPolicy works, even if gssapi-keyex was enabled but not used. @@ -534,3 +553,19 @@ class SSHClientTest(unittest.TestCase): ) else: self.assertFalse(False, "SSHException was not thrown.") + + def test_missing_key_policy_accepts_classes_or_instances(self): + """ + Client.missing_host_key_policy() can take classes or instances. + """ + # AN ACTUAL UNIT TEST?! GOOD LORD + # (But then we have to test a private API...meh.) + client = paramiko.SSHClient() + # Default + assert isinstance(client._policy, paramiko.RejectPolicy) + # Hand in an instance (classic behavior) + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + assert isinstance(client._policy, paramiko.AutoAddPolicy) + # Hand in just the class (new behavior) + client.set_missing_host_key_policy(paramiko.AutoAddPolicy) + assert isinstance(client._policy, paramiko.AutoAddPolicy) diff --git a/tests/test_ed25519.key b/tests/test_ed25519.key new file mode 100644 index 00000000..eb9f94c2 --- /dev/null +++ b/tests/test_ed25519.key @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXwAAAKhjwAdrY8AH +awAAAAtzc2gtZWQyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXw +AAAEA9tGQi2IrprbOSbDCF+RmAHd6meNSXBUQ2ekKXm4/8xnr1K9komH/1WBIvQbbtvnFV +hryd62EfcgRFuLRiokNfAAAAI2FsZXhfZ2F5bm9yQEFsZXhzLU1hY0Jvb2stQWlyLmxvY2 +FsAQI= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_ed25519_password.key b/tests/test_ed25519_password.key new file mode 100644 index 00000000..d178aaae --- /dev/null +++ b/tests/test_ed25519_password.key @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDaKD4ac7 +kieb+UfXaLaw68AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIOQn7fjND5ozMSV3 +CvbEtIdT73hWCMRjzS/lRdUDw50xAAAAsE8kLGyYBnl9ihJNqv378y6mO3SkzrDbWXOnK6 +ij0vnuTAvcqvWHAnyu6qBbplu/W2m55ZFeAItgaEcV2/V76sh/sAKlERqrLFyXylN0xoOW +NU5+zU08aTlbSKGmeNUU2xE/xfJq12U9XClIRuVUkUpYANxNPbmTRpVrbD3fgXMhK97Jrb +DEn8ca1IqMPiYmd/hpe5+tq3OxyRljXjCUFWTnqkp9VvUdzSTdSGZHsW9i +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_kex.py b/tests/test_kex.py index 5b749b4d..65eb9a17 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -20,7 +20,7 @@ Some unit tests for the key exchange protocols. """ -from binascii import hexlify +from binascii import hexlify, unhexlify import os import unittest @@ -32,12 +32,34 @@ from paramiko.kex_group1 import KexGroup1 from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko import Message from paramiko.common import byte_chr +from paramiko.kex_ecdh_nist import KexNistp256 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec def dummy_urandom(n): return byte_chr(0xcc) * n +def dummy_generate_key_pair(obj): + private_key_value = 94761803665136558137557783047955027733968423115106677159790289642479432803037 + public_key_numbers = "042bdab212fa8ba1b7c843301682a4db424d307246c7e1e6083c41d9ca7b098bf30b3d63e2ec6278488c135360456cc054b3444ecc45998c08894cbc1370f5f989" + public_key_numbers_obj = ec.EllipticCurvePublicNumbers.from_encoded_point( + ec.SECP256R1(), unhexlify(public_key_numbers) + ) + obj.P = ec.EllipticCurvePrivateNumbers( + private_value=private_key_value, public_numbers=public_key_numbers_obj + ).private_key(default_backend()) + if obj.transport.server_mode: + obj.Q_S = ec.EllipticCurvePublicNumbers.from_encoded_point( + ec.SECP256R1(), unhexlify(public_key_numbers) + ).public_key(default_backend()) + return + obj.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point( + ec.SECP256R1(), unhexlify(public_key_numbers) + ).public_key(default_backend()) + + class FakeKey(object): def __str__(self): return "fake-key" @@ -96,9 +118,12 @@ class KexTest(unittest.TestCase): def setUp(self): self._original_urandom = os.urandom os.urandom = dummy_urandom + self._original_generate_key_pair = KexNistp256._generate_key_pair + KexNistp256._generate_key_pair = dummy_generate_key_pair def tearDown(self): os.urandom = self._original_urandom + KexNistp256._generate_key_pair = self._original_generate_key_pair def test_1_group1_client(self): transport = FakeTransport() @@ -423,3 +448,52 @@ class KexTest(unittest.TestCase): self.assertEqual(H, hexlify(transport._H).upper()) self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertTrue(transport._activated) + + def test_11_kex_nistp256_client(self): + K = 91610929826364598472338906427792435253694642563583721654249504912114314269754 + transport = FakeTransport() + transport.server_mode = False + kex = KexNistp256(transport) + kex.start_kex() + self.assertEqual( + (paramiko.kex_ecdh_nist._MSG_KEXECDH_REPLY,), transport._expect + ) + + # fake reply + msg = Message() + msg.add_string("fake-host-key") + Q_S = unhexlify( + "043ae159594ba062efa121480e9ef136203fa9ec6b6e1f8723a321c16e62b945f573f3b822258cbcd094b9fa1c125cbfe5f043280893e66863cc0cb4dccbe70210" + ) + msg.add_string(Q_S) + msg.add_string("fake-sig") + msg.rewind() + kex.parse_next(paramiko.kex_ecdh_nist._MSG_KEXECDH_REPLY, msg) + H = b"BAF7CE243A836037EB5D2221420F35C02B9AB6C957FE3BDE3369307B9612570A" + self.assertEqual(K, kex.transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual((b"fake-host-key", b"fake-sig"), transport._verify) + self.assertTrue(transport._activated) + + def test_12_kex_nistp256_server(self): + K = 91610929826364598472338906427792435253694642563583721654249504912114314269754 + transport = FakeTransport() + transport.server_mode = True + kex = KexNistp256(transport) + kex.start_kex() + self.assertEqual( + (paramiko.kex_ecdh_nist._MSG_KEXECDH_INIT,), transport._expect + ) + + # fake init + msg = Message() + Q_C = unhexlify( + "043ae159594ba062efa121480e9ef136203fa9ec6b6e1f8723a321c16e62b945f573f3b822258cbcd094b9fa1c125cbfe5f043280893e66863cc0cb4dccbe70210" + ) + H = b"2EF4957AFD530DD3F05DBEABF68D724FACC060974DA9704F2AEE4C3DE861E7CA" + msg.add_string(Q_C) + msg.rewind() + kex.parse_next(paramiko.kex_ecdh_nist._MSG_KEXECDH_INIT, msg) + self.assertEqual(K, transport._K) + self.assertTrue(transport._activated) + self.assertEqual(H, hexlify(transport._H).upper()) diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py index d5f624ce..c71ff91c 100644 --- a/tests/test_kex_gss.py +++ b/tests/test_kex_gss.py @@ -95,7 +95,7 @@ class GSSKexTest(unittest.TestCase): server = NullServer() self.ts.start_server(self.event, server) - def test_1_gsskex_and_auth(self): + def _test_gsskex_and_auth(self, gss_host, rekey=False): """ Verify that Paramiko can handle SSHv2 GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication with the GSS-API @@ -114,6 +114,7 @@ class GSSKexTest(unittest.TestCase): username=self.username, gss_auth=True, gss_kex=True, + gss_host=gss_host, ) self.event.wait(1.0) @@ -121,9 +122,12 @@ class GSSKexTest(unittest.TestCase): self.assert_(self.ts.is_active()) self.assertEquals(self.username, self.ts.get_username()) self.assertEquals(True, self.ts.is_authenticated()) + self.assertEquals(True, self.tc.get_transport().gss_kex_used) stdin, stdout, stderr = self.tc.exec_command("yes") schan = self.ts.accept(1.0) + if rekey: + self.tc.get_transport().renegotiate_keys() schan.send("Hello there.\n") schan.send_stderr("This is on stderr.\n") @@ -137,3 +141,17 @@ class GSSKexTest(unittest.TestCase): stdin.close() stdout.close() stderr.close() + + def test_1_gsskex_and_auth(self): + """ + Verify that Paramiko can handle SSHv2 GSS-API / SSPI authenticated + Diffie-Hellman Key Exchange and user authentication with the GSS-API + context created during key exchange. + """ + self._test_gsskex_and_auth(gss_host=None) + + def test_2_gsskex_and_auth_rekey(self): + """ + Verify that Paramiko can rekey. + """ + self._test_gsskex_and_auth(gss_host=None, rekey=True) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 4e9653a0..3a1279b6 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -27,7 +27,7 @@ from binascii import hexlify from hashlib import md5 import base64 -from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util +from paramiko import RSAKey, DSSKey, ECDSAKey, Ed25519Key, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes, PY2 from .util import _support @@ -461,6 +461,32 @@ class KeyTest(unittest.TestCase): comparable = TEST_KEY_BYTESTR_2 if PY2 else TEST_KEY_BYTESTR_3 self.assertEqual(str(key), comparable) + def test_ed25519(self): + key1 = Ed25519Key.from_private_key_file(_support("test_ed25519.key")) + key2 = Ed25519Key.from_private_key_file( + _support("test_ed25519_password.key"), b"abc123" + ) + self.assertNotEqual(key1.asbytes(), key2.asbytes()) + + def test_ed25519_compare(self): + # verify that the private & public keys compare equal + key = Ed25519Key.from_private_key_file(_support("test_ed25519.key")) + self.assertEqual(key, key) + pub = Ed25519Key(data=key.asbytes()) + self.assertTrue(key.can_sign()) + self.assertTrue(not pub.can_sign()) + self.assertEqual(key, pub) + + def test_ed25519_nonbytes_password(self): + # https://github.com/paramiko/paramiko/issues/1039 + key = Ed25519Key.from_private_key_file( + _support("test_ed25519_password.key"), + # NOTE: not a bytes. Amusingly, the test above for same key DOES + # explicitly cast to bytes...code smell! + "abc123", + ) + # No exception -> it's good. Meh. + def test_keyfile_is_actually_encrypted(self): # Read an existing encrypted private key file_ = _support("test_rsa_password.key") diff --git a/tests/test_sftp.py b/tests/test_sftp.py index ccfdf7b0..87c57340 100644 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -185,6 +185,40 @@ class TestSFTP(object): except: pass + def test_5a_posix_rename(self, sftp): + """Test posix-rename@openssh.com protocol extension.""" + try: + # first check that the normal rename works as specified + with sftp.open(sftp.FOLDER + "/a", "w") as f: + f.write("one") + sftp.rename(sftp.FOLDER + "/a", sftp.FOLDER + "/b") + with sftp.open(sftp.FOLDER + "/a", "w") as f: + f.write("two") + try: + sftp.rename(sftp.FOLDER + "/a", sftp.FOLDER + "/b") + self.assertTrue( + False, "no exception when rename-ing onto existing file" + ) + except (OSError, IOError): + pass + + # now check with the posix_rename + sftp.posix_rename(sftp.FOLDER + "/a", sftp.FOLDER + "/b") + with sftp.open(sftp.FOLDER + "/b", "r") as f: + data = u(f.read()) + err = "Contents of renamed file not the same as original file" + assert data == "two", err + + finally: + try: + sftp.remove(sftp.FOLDER + "/a") + except: + pass + try: + sftp.remove(sftp.FOLDER + "/b") + except: + pass + def test_6_folder(self, sftp): """ create a temporary folder, verify that we can create a file in it, then |