diff options
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | README.rst | 2 | ||||
-rw-r--r-- | paramiko/__init__.py | 2 | ||||
-rw-r--r-- | paramiko/_version.py | 2 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 4 | ||||
-rw-r--r-- | paramiko/client.py | 6 | ||||
-rw-r--r-- | paramiko/common.py | 31 | ||||
-rw-r--r-- | paramiko/config.py | 83 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 4 | ||||
-rw-r--r-- | paramiko/kex_curve25519.py | 129 | ||||
-rw-r--r-- | paramiko/kex_ecdh_nist.py | 37 | ||||
-rw-r--r-- | paramiko/kex_group1.py | 2 | ||||
-rw-r--r-- | paramiko/kex_group14.py | 7 | ||||
-rw-r--r-- | paramiko/kex_group16.py | 35 | ||||
-rw-r--r-- | paramiko/packet.py | 87 | ||||
-rw-r--r-- | paramiko/py3compat.py | 41 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 4 | ||||
-rw-r--r-- | paramiko/ssh_gss.py | 13 | ||||
-rw-r--r-- | paramiko/transport.py | 22 | ||||
-rw-r--r-- | setup.cfg | 3 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | sites/www/changelog.rst | 61 | ||||
-rw-r--r-- | sites/www/contact.rst | 2 | ||||
-rw-r--r-- | sites/www/installing-1.x.rst | 2 | ||||
-rw-r--r-- | sites/www/installing.rst | 10 | ||||
-rw-r--r-- | tests/test_config.py | 74 | ||||
-rw-r--r-- | tests/test_kex.py | 140 |
27 files changed, 702 insertions, 112 deletions
diff --git a/.travis.yml b/.travis.yml index 324c5f3f..41c074bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,9 +20,9 @@ matrix: # to whatever the latest default is. include: - python: 2.7 - env: "OLDEST_CRYPTO=1.5" + env: "OLDEST_CRYPTO=2.5" - python: 3.7 - env: "OLDEST_CRYPTO=1.5" + env: "OLDEST_CRYPTO=2.5" install: # Ensure modern pip/etc to avoid some issues w/ older worker environs - pip install pip==9.0.1 setuptools==36.6.0 @@ -11,7 +11,7 @@ Paramiko :Paramiko: Python SSH module :Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com> -:Copyright: Copyright (c) 2013-2017 Jeff Forcier <jeff@bitprophet.org> +:Copyright: Copyright (c) 2013-2019 Jeff Forcier <jeff@bitprophet.org> :License: `LGPL <https://www.gnu.org/copyleft/lesser.html>`_ :Homepage: http://www.paramiko.org/ :API docs: http://docs.paramiko.org diff --git a/paramiko/__init__.py b/paramiko/__init__.py index b5b577d3..6afc7a61 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -100,6 +100,8 @@ __all__ = [ "Channel", "ChannelException", "DSSKey", + "ECDSAKey", + "Ed25519Key", "HostKeys", "Message", "MissingHostKeyPolicy", diff --git a/paramiko/_version.py b/paramiko/_version.py index 2e797d40..2916a60a 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 4, 2) +__version_info__ = (2, 5, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 26605a16..5c7d6be6 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -61,7 +61,7 @@ from paramiko.common import ( cMSG_USERAUTH_BANNER, ) from paramiko.message import Message -from paramiko.py3compat import bytestring +from paramiko.py3compat import b from paramiko.ssh_exception import ( SSHException, AuthenticationException, @@ -280,7 +280,7 @@ class AuthHandler(object): m.add_string(self.auth_method) if self.auth_method == "password": m.add_boolean(False) - password = bytestring(self.password) + password = b(self.password) m.add_string(password) elif self.auth_method == "publickey": m.add_boolean(True) diff --git a/paramiko/client.py b/paramiko/client.py index 2538d582..6bf479d4 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -476,6 +476,9 @@ class SSHClient(ClosingContextManager): Python :param int timeout: set command's channel timeout. See `.Channel.settimeout` + :param bool get_pty: + Request a pseudo-terminal from the server (default ``False``). + See `.Channel.get_pty` :param dict environment: a dict of shell environment variables, to be merged into the default environment that the remote command executes within. @@ -489,6 +492,9 @@ class SSHClient(ClosingContextManager): 3-tuple :raises: `.SSHException` -- if the server fails to execute the command + + .. versionchanged:: 1.10 + Added the ``get_pty`` kwarg. """ chan = self._transport.open_session(timeout=timeout) if get_pty: diff --git a/paramiko/common.py b/paramiko/common.py index 87d3dcf6..7bd0cb10 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -20,7 +20,7 @@ Common constants and global variables. """ import logging -from paramiko.py3compat import byte_chr, PY2, bytes_types, text_type, long +from paramiko.py3compat import byte_chr, PY2, long, b ( MSG_DISCONNECT, @@ -191,17 +191,24 @@ else: def asbytes(s): - """Coerce to bytes if possible or return unchanged.""" - if isinstance(s, bytes_types): - return s - if isinstance(s, text_type): - # Accept text and encode as utf-8 for compatibility only. - return s.encode("utf-8") - asbytes = getattr(s, "asbytes", None) - if asbytes is not None: - return asbytes() - # May be an object that implements the buffer api, let callers handle. - return s + """ + Coerce to bytes if possible or return unchanged. + """ + try: + # Attempt to run through our version of b(), which does the Right Thing + # for string/unicode/buffer (Py2) or bytes/str (Py3), and raises + # TypeError if it's not one of those types. + return b(s) + except TypeError: + try: + # If it wasn't a string/byte/buffer type object, try calling an + # asbytes() method, which many of our internal classes implement. + return s.asbytes() + except AttributeError: + # Finally, just do nothing & assume this object is sufficiently + # byte-y or buffer-y that everything will work out (or that callers + # are capable of handling whatever it is.) + return s xffffffff = long(0xffffffff) diff --git a/paramiko/config.py b/paramiko/config.py index 21c9dab8..aeb59593 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -95,7 +95,7 @@ class SSHConfig(object): def lookup(self, hostname): """ - Return a dict of config options for a given hostname. + Return a dict (`SSHConfigDict`) of config options for a given hostname. The host-matching rules of OpenSSH's ``ssh_config`` man page are used: For each parameter, the first obtained value will be used. The @@ -111,7 +111,17 @@ class SSHConfig(object): ``"port"``, not ``"Port"``. The values are processed according to the rules for substitution variable expansion in ``ssh_config``. + Finally, please see the docs for `SSHConfigDict` for deeper info on + features such as optional type conversion methods, e.g.:: + + conf = my_config.lookup('myhost') + assert conf['passwordauthentication'] == 'yes' + assert conf.as_bool('passwordauthentication') is True + :param str hostname: the hostname to lookup + + .. versionchanged:: 2.5 + Returns `SSHConfigDict` objects instead of dict literals. """ matches = [ config @@ -119,7 +129,7 @@ class SSHConfig(object): if self._allowed(config["host"], hostname) ] - ret = {} + ret = SSHConfigDict() for match in matches: for key, value in match["config"].items(): if key not in ret: @@ -291,3 +301,72 @@ class LazyFqdn(object): # Cache self.fqdn = fqdn return self.fqdn + + +class SSHConfigDict(dict): + """ + A dictionary wrapper/subclass for per-host configuration structures. + + This class introduces some usage niceties for consumers of `SSHConfig`, + specifically around the issue of variable type conversions: normal value + access yields strings, but there are now methods such as `as_bool` and + `as_int` that yield casted values instead. + + For example, given the following ``ssh_config`` file snippet:: + + Host foo.example.com + PasswordAuthentication no + Compression yes + ServerAliveInterval 60 + + the following code highlights how you can access the raw strings as well as + usefully Python type-casted versions (recalling that keys are all + normalized to lowercase first):: + + my_config = SSHConfig() + my_config.parse(open('~/.ssh/config')) + conf = my_config.lookup('foo.example.com') + + assert conf['passwordauthentication'] == 'no' + assert conf.as_bool('passwordauthentication') is False + assert conf['compression'] == 'yes' + assert conf.as_bool('compression') is True + assert conf['serveraliveinterval'] == '60' + assert conf.as_int('serveraliveinterval') == 60 + + .. versionadded:: 2.5 + """ + + def __init__(self, *args, **kwargs): + # Hey, guess what? Python 2's userdict is an old-style class! + super(SSHConfigDict, self).__init__(*args, **kwargs) + + def as_bool(self, key): + """ + Express given key's value as a boolean type. + + Typically, this is used for ``ssh_config``'s pseudo-boolean values + which are either ``"yes"`` or ``"no"``. In such cases, ``"yes"`` yields + ``True`` and any other value becomes ``False``. + + .. note:: + If (for whatever reason) the stored value is already boolean in + nature, it's simply returned. + + .. versionadded:: 2.5 + """ + val = self[key] + if isinstance(val, bool): + return val + return val.lower() == "yes" + + def as_int(self, key): + """ + Express given key's value as an integer, if possible. + + This method will raise ``ValueError`` or similar if the value is not + int-appropriate, same as the builtin `int` type. + + .. versionadded:: 2.5 + """ + return int(self[key]) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index b73a969e..353c5f9e 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -160,12 +160,12 @@ class ECDSAKey(PKey): pointinfo = msg.get_binary() try: - numbers = ec.EllipticCurvePublicNumbers.from_encoded_point( + key = ec.EllipticCurvePublicKey.from_encoded_point( self.ecdsa_curve.curve_class(), pointinfo ) + self.verifying_key = key except ValueError: raise SSHException("Invalid public key") - self.verifying_key = numbers.public_key(backend=default_backend()) @classmethod def supported_key_format_identifiers(cls): diff --git a/paramiko/kex_curve25519.py b/paramiko/kex_curve25519.py new file mode 100644 index 00000000..59710c1a --- /dev/null +++ b/paramiko/kex_curve25519.py @@ -0,0 +1,129 @@ +import binascii +import hashlib + +from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.hazmat.primitives import constant_time, serialization +from cryptography.hazmat.primitives.asymmetric.x25519 import ( + X25519PrivateKey, + X25519PublicKey, +) + +from paramiko.message import Message +from paramiko.py3compat import byte_chr, long +from paramiko.ssh_exception import SSHException + + +_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 KexCurve25519(object): + hash_algo = hashlib.sha256 + + def __init__(self, transport): + self.transport = transport + self.key = None + + @classmethod + def is_available(cls): + try: + X25519PrivateKey.generate() + except UnsupportedAlgorithm: + return False + else: + return True + + def _perform_exchange(self, peer_key): + secret = self.key.exchange(peer_key) + if constant_time.bytes_eq(secret, b"\x00" * 32): + raise SSHException( + "peer's curve25519 public value has wrong order" + ) + return secret + + def start_kex(self): + self.key = X25519PrivateKey.generate() + if self.transport.server_mode: + self.transport._expect_packet(_MSG_KEXECDH_INIT) + return + + m = Message() + m.add_byte(c_MSG_KEXECDH_INIT) + m.add_string( + self.key.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) + ) + 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( + "KexCurve25519 asked to handle packet type {:d}".format(ptype) + ) + + def _parse_kexecdh_init(self, m): + peer_key_bytes = m.get_string() + peer_key = X25519PublicKey.from_public_bytes(peer_key_bytes) + K = self._perform_exchange(peer_key) + K = long(binascii.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, + ) + server_key_bytes = self.transport.get_server_key().asbytes() + exchange_key_bytes = self.key.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) + hm.add_string(server_key_bytes) + hm.add_string(peer_key_bytes) + hm.add_string(exchange_key_bytes) + hm.add_mpint(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(server_key_bytes) + m.add_string(exchange_key_bytes) + m.add_string(sig) + self.transport._send_message(m) + self.transport._activate_outbound() + + def _parse_kexecdh_reply(self, m): + peer_host_key_bytes = m.get_string() + peer_key_bytes = m.get_string() + sig = m.get_binary() + + peer_key = X25519PublicKey.from_public_bytes(peer_key_bytes) + + K = self._perform_exchange(peer_key) + K = long(binascii.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(peer_host_key_bytes) + hm.add_string( + self.key.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) + ) + hm.add_string(peer_key_bytes) + hm.add_mpint(K) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) + self.transport._verify_key(peer_host_key_bytes, sig) + self.transport._activate_outbound() diff --git a/paramiko/kex_ecdh_nist.py b/paramiko/kex_ecdh_nist.py index 1d87442a..ad5c9c79 100644 --- a/paramiko/kex_ecdh_nist.py +++ b/paramiko/kex_ecdh_nist.py @@ -9,6 +9,7 @@ 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 cryptography.hazmat.primitives import serialization from binascii import hexlify _MSG_KEXECDH_INIT, _MSG_KEXECDH_REPLY = range(30, 32) @@ -36,7 +37,12 @@ class KexNistp256: 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()) + m.add_string( + self.Q_C.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) self.transport._send_message(m) self.transport._expect_packet(_MSG_KEXECDH_REPLY) @@ -58,11 +64,11 @@ class KexNistp256: def _parse_kexecdh_init(self, m): Q_C_bytes = m.get_string() - self.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point( + self.Q_C = ec.EllipticCurvePublicKey.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 = self.P.exchange(ec.ECDH(), self.Q_C) K = long(hexlify(K), 16) # compute exchange hash hm = Message() @@ -75,7 +81,12 @@ class KexNistp256: 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_string( + self.Q_S.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) hm.add_mpint(long(K)) H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) @@ -84,7 +95,12 @@ class KexNistp256: 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( + self.Q_S.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) m.add_string(sig) self.transport._send_message(m) self.transport._activate_outbound() @@ -92,11 +108,11 @@ class KexNistp256: 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.Q_S = ec.EllipticCurvePublicKey.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 = self.P.exchange(ec.ECDH(), self.Q_S) K = long(hexlify(K), 16) # compute exchange hash and verify signature hm = Message() @@ -108,7 +124,12 @@ class KexNistp256: ) 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( + self.Q_C.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) hm.add_string(Q_S_bytes) hm.add_mpint(K) self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 904835d7..5131e895 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -116,7 +116,7 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py index 0df302e3..a620c1a3 100644 --- a/paramiko/kex_group14.py +++ b/paramiko/kex_group14.py @@ -22,7 +22,7 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of """ from paramiko.kex_group1 import KexGroup1 -from hashlib import sha1 +from hashlib import sha1, sha256 class KexGroup14(KexGroup1): @@ -33,3 +33,8 @@ class KexGroup14(KexGroup1): name = "diffie-hellman-group14-sha1" hash_algo = sha1 + + +class KexGroup14SHA256(KexGroup14): + name = "diffie-hellman-group14-sha256" + hash_algo = sha256 diff --git a/paramiko/kex_group16.py b/paramiko/kex_group16.py new file mode 100644 index 00000000..15b0acfe --- /dev/null +++ b/paramiko/kex_group16.py @@ -0,0 +1,35 @@ +# Copyright (C) 2019 Edgar Sousa <https://github.com/edgsousa> +# +# 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. + +""" +Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of +4096 bit key halves, using a known "p" prime and "g" generator. +""" + +from paramiko.kex_group1 import KexGroup1 +from hashlib import sha512 + + +class KexGroup16SHA512(KexGroup1): + name = "diffie-hellman-group16-sha512" + # http://tools.ietf.org/html/rfc3526#section-5 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF # noqa + G = 2 + + name = "diffie-hellman-group16-sha512" + hash_algo = sha512 diff --git a/paramiko/packet.py b/paramiko/packet.py index cb46835e..12663168 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -111,6 +111,8 @@ class Packetizer(object): self.__compress_engine_in = None self.__sequence_number_out = 0 self.__sequence_number_in = 0 + self.__etm_out = False + self.__etm_in = False # lock around outbound writes (packet computation) self.__write_lock = threading.RLock() @@ -142,9 +144,11 @@ class Packetizer(object): mac_size, mac_key, sdctr=False, + etm=False, ): """ Switch outbound data cipher. + :param etm: Set encrypt-then-mac from OpenSSH """ self.__block_engine_out = block_engine self.__sdctr_out = sdctr @@ -154,6 +158,7 @@ class Packetizer(object): self.__mac_key_out = mac_key self.__sent_bytes = 0 self.__sent_packets = 0 + self.__etm_out = etm # wait until the reset happens in both directions before clearing # rekey flag self.__init_count |= 1 @@ -162,10 +167,17 @@ class Packetizer(object): self.__need_rekey = False def set_inbound_cipher( - self, block_engine, block_size, mac_engine, mac_size, mac_key + self, + block_engine, + block_size, + mac_engine, + mac_size, + mac_key, + etm=False, ): """ Switch inbound data cipher. + :param etm: Set encrypt-then-mac from OpenSSH """ self.__block_engine_in = block_engine self.__block_size_in = block_size @@ -176,6 +188,7 @@ class Packetizer(object): self.__received_packets = 0 self.__received_bytes_overflow = 0 self.__received_packets_overflow = 0 + self.__etm_in = etm # wait until the reset happens in both directions before clearing # rekey flag self.__init_count |= 2 @@ -396,14 +409,19 @@ class Packetizer(object): ) self._log(DEBUG, util.format_binary(packet, "OUT: ")) if self.__block_engine_out is not None: - out = self.__block_engine_out.update(packet) + if self.__etm_out: + # packet length is not encrypted in EtM + out = packet[0:4] + self.__block_engine_out.update( + packet[4:] + ) + else: + out = self.__block_engine_out.update(packet) else: out = packet # + mac if self.__block_engine_out is not None: - payload = ( - struct.pack(">I", self.__sequence_number_out) + packet - ) + packed = struct.pack(">I", self.__sequence_number_out) + payload = packed + (out if self.__etm_out else packet) out += compute_hmac( self.__mac_key_out, payload, self.__mac_engine_out )[: self.__mac_size_out] @@ -439,26 +457,54 @@ class Packetizer(object): :raises: `.NeedRekeyException` -- if the transport should rekey """ header = self.read_all(self.__block_size_in, check_rekey=True) + if self.__etm_in: + packet_size = struct.unpack(">I", header[:4])[0] + remaining = packet_size - self.__block_size_in + 4 + packet = header[4:] + self.read_all(remaining, check_rekey=False) + mac = self.read_all(self.__mac_size_in, check_rekey=False) + mac_payload = ( + struct.pack(">II", self.__sequence_number_in, packet_size) + + packet + ) + my_mac = compute_hmac( + self.__mac_key_in, mac_payload, self.__mac_engine_in + )[: self.__mac_size_in] + if not util.constant_time_bytes_eq(my_mac, mac): + raise SSHException("Mismatched MAC") + header = packet + if self.__block_engine_in is not None: 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] - # leftover contains decrypted bytes from the first block (after the - # length field) - leftover = header[4:] - if (packet_size - len(leftover)) % self.__block_size_in != 0: - raise SSHException("Invalid packet blocking") - buf = self.read_all(packet_size + self.__mac_size_in - len(leftover)) - 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.update(packet) + + # When ETM is in play, we've already read the packet size & decrypted + # everything, so just set the packet back to the header we obtained. + if self.__etm_in: + packet = header + # Otherwise, use the older non-ETM logic + else: + packet_size = struct.unpack(">I", header[:4])[0] + + # leftover contains decrypted bytes from the first block (after the + # length field) + leftover = header[4:] + if (packet_size - len(leftover)) % self.__block_size_in != 0: + raise SSHException("Invalid packet blocking") + buf = self.read_all( + packet_size + self.__mac_size_in - len(leftover) + ) + 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.update(packet) + packet = leftover + packet + if self.__dump_packets: self._log(DEBUG, util.format_binary(packet, "IN: ")) - packet = leftover + packet - if self.__mac_size_in > 0: + if self.__mac_size_in > 0 and not self.__etm_in: mac = post_packet[: self.__mac_size_in] mac_payload = ( struct.pack(">II", self.__sequence_number_in, packet_size) @@ -579,7 +625,10 @@ class Packetizer(object): def _build_packet(self, payload): # pad up at least 4 bytes, to nearest block-size (usually 8) bsize = self.__block_size_out - padding = 3 + bsize - ((len(payload) + 8) % bsize) + # do not include payload length in computations for padding in EtM mode + # (payload length won't be encrypted) + addlen = 4 if self.__etm_out else 8 + padding = 3 + bsize - ((len(payload) + addlen) % bsize) packet = struct.pack(">IB", len(payload) + padding + 1, padding) packet += payload if self.__sdctr_out or self.__block_engine_out is None: diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py index cbe20ca6..0f80e19f 100644 --- a/paramiko/py3compat.py +++ b/paramiko/py3compat.py @@ -2,29 +2,28 @@ import sys import base64 __all__ = [ + "BytesIO", + "MAXSIZE", "PY2", - "string_types", - "integer_types", - "text_type", - "bytes_types", + "StringIO", + "b", + "b2s", + "builtins", + "byte_chr", + "byte_mask", + "byte_ord", "bytes", - "long", - "input", + "bytes_types", "decodebytes", "encodebytes", - "bytestring", - "byte_ord", - "byte_chr", - "byte_mask", - "b", - "u", - "b2s", - "StringIO", - "BytesIO", + "input", + "integer_types", "is_callable", - "MAXSIZE", + "long", "next", - "builtins", + "string_types", + "text_type", + "u", ] PY2 = sys.version_info[0] < 3 @@ -42,11 +41,6 @@ if PY2: import __builtin__ as builtins - def bytestring(s): # NOQA - if isinstance(s, unicode): # NOQA - return s.encode("utf-8") - return s - byte_ord = ord # NOQA byte_chr = chr # NOQA @@ -124,9 +118,6 @@ else: decodebytes = base64.decodebytes encodebytes = base64.encodebytes - def bytestring(s): - return s - def byte_ord(c): # In case we're handed a string instead of an int. if not isinstance(c, int): diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 1a9147fc..93190d85 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -28,7 +28,7 @@ from paramiko import util from paramiko.channel import Channel from paramiko.message import Message from paramiko.common import INFO, DEBUG, o777 -from paramiko.py3compat import bytestring, b, u, long +from paramiko.py3compat import b, u, long from paramiko.sftp import ( BaseSFTP, CMD_OPENDIR, @@ -522,7 +522,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager): """ dest = self._adjust_cwd(dest) self._log(DEBUG, "symlink({!r}, {!r})".format(source, dest)) - source = bytestring(source) + source = b(source) self._request(CMD_SYMLINK, source, dest) def chmod(self, path, mode): diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index eb8826e0..3f299aee 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -42,10 +42,6 @@ GSS_AUTH_AVAILABLE = True GSS_EXCEPTIONS = () -from pyasn1.type.univ import ObjectIdentifier -from pyasn1.codec.der import encoder, decoder - - #: :var str _API: Constraint for the used API _API = "MIT" @@ -163,6 +159,9 @@ class _SSH_GSSAuth(object): :note: In server mode we just return the OID length and the DER encoded OID. """ + from pyasn1.type.univ import ObjectIdentifier + from pyasn1.codec.der import encoder + OIDs = self._make_uint32(1) krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech)) OID_len = self._make_uint32(len(krb5_OID)) @@ -177,6 +176,8 @@ class _SSH_GSSAuth(object): :param str desired_mech: The desired GSS-API mechanism of the client :return: ``True`` if the given OID is supported, otherwise C{False} """ + from pyasn1.codec.der import decoder + mech, __ = decoder.decode(desired_mech) if mech.__str__() != self._krb5_mech: return False @@ -269,6 +270,8 @@ class _SSH_GSSAPI(_SSH_GSSAuth): :return: A ``String`` if the GSS-API has returned a token or ``None`` if no token was returned """ + from pyasn1.codec.der import decoder + self._username = username self._gss_host = target targ_name = gssapi.Name( @@ -443,6 +446,8 @@ class _SSH_SSPI(_SSH_GSSAuth): :return: A ``String`` if the SSPI has returned a token or ``None`` if no token was returned """ + from pyasn1.codec.der import decoder + self._username = username self._gss_host = target error = 0 diff --git a/paramiko/transport.py b/paramiko/transport.py index f72eebaf..25213b4a 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -88,9 +88,11 @@ from paramiko.common import ( from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey from paramiko.ed25519key import Ed25519Key +from paramiko.kex_curve25519 import KexCurve25519 from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko.kex_group1 import KexGroup1 -from paramiko.kex_group14 import KexGroup14 +from paramiko.kex_group14 import KexGroup14, KexGroup14SHA256 +from paramiko.kex_group16 import KexGroup16SHA512 from paramiko.kex_ecdh_nist import KexNistp256, KexNistp384, KexNistp521 from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14 from paramiko.message import Message @@ -156,6 +158,8 @@ class Transport(threading.Thread, ClosingContextManager): _preferred_macs = ( "hmac-sha2-256", "hmac-sha2-512", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512-etm@openssh.com", "hmac-sha1", "hmac-md5", "hmac-sha1-96", @@ -173,11 +177,15 @@ class Transport(threading.Thread, ClosingContextManager): "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", + "diffie-hellman-group16-sha512", "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group14-sha256", "diffie-hellman-group-exchange-sha1", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1", ) + if KexCurve25519.is_available(): + _preferred_kex = ("curve25519-sha256@libssh.org",) + _preferred_kex _preferred_gsskex = ( "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==", "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==", @@ -240,7 +248,9 @@ class Transport(threading.Thread, ClosingContextManager): "hmac-sha1": {"class": sha1, "size": 20}, "hmac-sha1-96": {"class": sha1, "size": 12}, "hmac-sha2-256": {"class": sha256, "size": 32}, + "hmac-sha2-256-etm@openssh.com": {"class": sha256, "size": 32}, "hmac-sha2-512": {"class": sha512, "size": 64}, + "hmac-sha2-512-etm@openssh.com": {"class": sha512, "size": 64}, "hmac-md5": {"class": md5, "size": 16}, "hmac-md5-96": {"class": md5, "size": 12}, } @@ -265,6 +275,8 @@ class Transport(threading.Thread, ClosingContextManager): "diffie-hellman-group14-sha1": KexGroup14, "diffie-hellman-group-exchange-sha1": KexGex, "diffie-hellman-group-exchange-sha256": KexGexSHA256, + "diffie-hellman-group14-sha256": KexGroup14SHA256, + "diffie-hellman-group16-sha512": KexGroup16SHA512, "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGroup1, "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGroup14, "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGex, @@ -272,6 +284,8 @@ class Transport(threading.Thread, ClosingContextManager): "ecdh-sha2-nistp384": KexNistp384, "ecdh-sha2-nistp521": KexNistp521, } + if KexCurve25519.is_available(): + _kex_info["curve25519-sha256@libssh.org"] = KexCurve25519 _compression_info = { # zlib@openssh.com is just zlib, but only turned on after a successful @@ -2440,6 +2454,7 @@ class Transport(threading.Thread, ClosingContextManager): engine = self._get_cipher( self.remote_cipher, key_in, IV_in, self._DECRYPT ) + etm = "etm@openssh.com" in self.remote_mac 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 @@ -2449,7 +2464,7 @@ class Transport(threading.Thread, ClosingContextManager): else: mac_key = self._compute_key("F", mac_engine().digest_size) self.packetizer.set_inbound_cipher( - engine, block_size, mac_engine, mac_size, mac_key + engine, block_size, mac_engine, mac_size, mac_key, etm=etm ) compress_in = self._compression_info[self.remote_compression][1] if compress_in is not None and ( @@ -2478,6 +2493,7 @@ class Transport(threading.Thread, ClosingContextManager): engine = self._get_cipher( self.local_cipher, key_out, IV_out, self._ENCRYPT ) + etm = "etm@openssh.com" in self.local_mac 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 @@ -2488,7 +2504,7 @@ class Transport(threading.Thread, ClosingContextManager): mac_key = self._compute_key("E", mac_engine().digest_size) sdctr = self.local_cipher.endswith("-ctr") self.packetizer.set_outbound_cipher( - engine, block_size, mac_engine, mac_size, mac_key, sdctr + engine, block_size, mac_engine, mac_size, mac_key, sdctr, etm=etm ) compress_out = self._compression_info[self.local_compression][0] if compress_out is not None and ( @@ -23,8 +23,5 @@ addopts = -p no:relaxed # Loop on failure looponfailroots = tests paramiko # Ignore some warnings we cannot easily handle. -# (NOTE: the CryptographyDeprecationWarning one should ONLY be in our 2.4 -# branch) filterwarnings = ignore::DeprecationWarning:pkg_resources - ignore::cryptography.utils.CryptographyDeprecationWarning @@ -73,10 +73,5 @@ setup( "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - install_requires=[ - "bcrypt>=3.1.3", - "cryptography>=1.5", - "pynacl>=1.0.1", - "pyasn1>=0.1.7", - ], + install_requires=["bcrypt>=3.1.3", "cryptography>=2.5", "pynacl>=1.0.1"], ) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 8a1eb76b..41c50ce5 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,34 @@ Changelog ========= +- :release:`2.5.0 <2019-06-09>` +- :feature:`1233` (also :issue:`1229`, :issue:`1332`) Add support for + encrypt-then-MAC (ETM) schemes (``hmac-sha2-256-etm@openssh.com``, + ``hmac-sha2-512-etm@openssh.com``) and two newer Diffie-Hellman group key + exchange algorithms (``group14``, using SHA256; and ``group16``, using + SHA512). Patch courtesy of Edgar Sousa. +- :feature:`532` (via :issue:`1384` and :issue:`1258`) Add support for + Curve25519 key exchange (aka ``curve25519-sha256@libssh.org``). Thanks to + Alex Gaynor and Dan Fuhry for supplying patches. +- :support:`1379` (also :issue:`1369`) Raise Cryptography dependency + requirement to version 2.5 (from 1.5) and update some deprecated uses of its + API. + + This removes a bunch of warnings of the style + ``CryptographyDeprecationWarning: encode_point has been deprecated on + EllipticCurvePublicNumbers and will be removed in a future version. Please + use EllipticCurvePublicKey.public_bytes to obtain both compressed and + uncompressed point encoding`` and similar, which users who had eventually + upgraded to Cryptography 2.x would encounter. + + .. warning:: + This change is backwards incompatible **if** you are unable to upgrade your + version of Cryptography. Please see `Cryptography's own changelog + <https://cryptography.io/en/latest/changelog/>`_ for details on what may + change when you upgrade; for the most part the only changes involved + dropping older Python versions (such as 2.6, 3.3, or some PyPy editions) + which Paramiko itself has already dropped. + - :support:`1378 backported` Add support for the modern (as of Python 3.3) import location of ``MutableMapping`` (used in host key management) to avoid the old location becoming deprecated in Python 3.8. Thanks to Josh Karpel for @@ -27,8 +55,9 @@ Changelog for this particular channel). Thanks to Daniel Hoffman for the detailed report. -- :support:`1292 backported` Backport changes from :issue:`979` (added in - Paramiko 2.3) to Paramiko 2.0-2.2, using duck-typing to preserve backwards +- :support:`1292 backported (<2.4)` Backport changes from :issue:`979` (added + in Paramiko + 2.3) to Paramiko 2.0-2.2, using duck-typing to preserve backwards compatibility. This allows these older versions to use newer Cryptography sign/verify APIs when available, without requiring them (as is the case with Paramiko 2.3+). @@ -41,13 +70,29 @@ Changelog This is a no-op for Paramiko 2.3+, which have required newer Cryptography releases since they were released. -- :support:`1291 backported` Backport pytest support and application of the - ``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 +- :support:`1291 backported (<2.4)` Backport pytest support and application of + the ``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. - :support:`1262 backported` Add ``*.pub`` files to the MANIFEST so distributed source packages contain some necessary test assets. Credit: Alexander Kapshuna. +- :feature:`1212` Updated `SSHConfig.lookup <paramiko.config.SSHConfig.lookup>` + so it returns a new, type-casting-friendly dict subclass + (`~paramiko.config.SSHConfigDict`) in lieu of dict literals. This ought to be + backwards compatible, and allows an easier way to check boolean or int type + ``ssh_config`` values. Thanks to Chris Rose for the patch. +- :support:`1191` Update our install docs with (somewhat) recently added + additional dependencies; we previously only required Cryptography, but the + docs never got updated after we incurred ``bcrypt`` and ``pynacl`` + requirements for Ed25519 key support. + + Additionally, ``pyasn1`` was never actually hard-required; it was necessary + during a development branch, and is used by the optional GSSAPI support, but + is not required for regular installation. Thus, it has been removed from our + ``setup.py`` and its imports in the GSSAPI code made optional. + + Credit to ``@stevenwinfield`` for highlighting the outdated install docs. - :release:`2.4.1 <2018-03-12>` - :release:`2.3.2 <2018-03-12>` - :release:`2.2.3 <2018-03-12>` @@ -60,6 +105,10 @@ 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:`1168` Add newer key classes for Ed25519 and ECDSA to + ``paramiko.__all__`` so that code introspecting that attribute, or using + ``from paramiko import *`` (such as some IDEs) sees them. Thanks to + ``@patriksevallius`` for the patch. - :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. @@ -79,7 +128,7 @@ Changelog - :support:`1100` Updated the test suite & related docs/metadata/config to be compatible with pytest instead of using the old, custom, crufty unittest-based ``test.py``. - + This includes marking known-slow tests (mostly the SFTP ones) so they can be filtered out by ``inv test``'s default behavior; as well as other minor tweaks to test collection and/or display (for example, GSSAPI tests are diff --git a/sites/www/contact.rst b/sites/www/contact.rst index 7e6c947e..dafc1bd4 100644 --- a/sites/www/contact.rst +++ b/sites/www/contact.rst @@ -6,7 +6,5 @@ You can get in touch with the developer & user community in any of the following ways: * IRC: ``#paramiko`` on Freenode -* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage - <http://librelist.com>`_ for usage details). * This website - a blog section is forthcoming. * Submit contributions on Github - see the :doc:`contributing` page. diff --git a/sites/www/installing-1.x.rst b/sites/www/installing-1.x.rst index 8ede40d5..7421a6c2 100644 --- a/sites/www/installing-1.x.rst +++ b/sites/www/installing-1.x.rst @@ -118,4 +118,4 @@ 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. +`pyasn1 <https://pypi.org/project/pyasn1/>`_ ``0.1.7`` or better. diff --git a/sites/www/installing.rst b/sites/www/installing.rst index e6db2dca..3631eb0d 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -22,8 +22,12 @@ via `pip <http://pip-installer.org>`_:: We currently support **Python 2.7, 3.4+, and PyPy**. Users on Python 2.6 or older (or 3.3 or older) are urged to upgrade. -Paramiko has only one direct hard dependency: the Cryptography library. See -:ref:`cryptography`. +Paramiko has only a few direct dependencies: + +- The big one, with its own sub-dependencies, is Cryptography; see :ref:`its + specific note below <cryptography>` for more details. +- `bcrypt <https://pypi.org/project/bcrypt/>`_, for Ed25519 key support; +- `pynacl <https://pypi.org/project/PyNaCl/>`_, also for Ed25519 key support. If you need GSS-API / SSPI support, see :ref:`the below subsection on it <gssapi>` for details on its optional dependencies. @@ -97,7 +101,7 @@ 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. -* **Unix** needs `python-gssapi <https://pypi.python.org/pypi/python-gssapi/>`_ +* **Unix** needs `python-gssapi <https://pypi.org/project/python-gssapi/>`_ ``0.6.1`` or better. .. note:: This library appears to only function on Python 2.7 and up. diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..cbd3f623 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,74 @@ +# This file is part of Paramiko and subject to the license in /LICENSE in this +# repository + +import pytest + +from paramiko import config +from paramiko.util import parse_ssh_config +from paramiko.py3compat import StringIO + + +def test_SSHConfigDict_construct_empty(): + assert not config.SSHConfigDict() + + +def test_SSHConfigDict_construct_from_list(): + assert config.SSHConfigDict([(1, 2)])[1] == 2 + + +def test_SSHConfigDict_construct_from_dict(): + assert config.SSHConfigDict({1: 2})[1] == 2 + + +@pytest.mark.parametrize("true_ish", ("yes", "YES", "Yes", True)) +def test_SSHConfigDict_as_bool_true_ish(true_ish): + assert config.SSHConfigDict({"key": true_ish}).as_bool("key") is True + + +@pytest.mark.parametrize("false_ish", ("no", "NO", "No", False)) +def test_SSHConfigDict_as_bool(false_ish): + assert config.SSHConfigDict({"key": false_ish}).as_bool("key") is False + + +@pytest.mark.parametrize("int_val", ("42", 42)) +def test_SSHConfigDict_as_int(int_val): + assert config.SSHConfigDict({"key": int_val}).as_int("key") == 42 + + +@pytest.mark.parametrize("non_int", ("not an int", None, object())) +def test_SSHConfigDict_as_int_failures(non_int): + conf = config.SSHConfigDict({"key": non_int}) + + try: + int(non_int) + except Exception as e: + exception_type = type(e) + + with pytest.raises(exception_type): + conf.as_int("key") + + +def test_SSHConfig_host_dicts_are_SSHConfigDict_instances(): + test_config_file = """ +Host *.example.com + Port 2222 + +Host * + Port 3333 + """ + f = StringIO(test_config_file) + config = parse_ssh_config(f) + assert config.lookup("foo.example.com").as_int("port") == 2222 + + +def test_SSHConfig_wildcard_host_dicts_are_SSHConfigDict_instances(): + test_config_file = """\ +Host *.example.com + Port 2222 + +Host * + Port 3333 + """ + f = StringIO(test_config_file) + config = parse_ssh_config(f) + assert config.lookup("anything-else").as_int("port") == 3333 diff --git a/tests/test_kex.py b/tests/test_kex.py index 69492ee2..0244ae84 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -24,15 +24,26 @@ from binascii import hexlify, unhexlify import os import unittest +from mock import Mock, patch +import pytest + from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec +try: + from cryptography.hazmat.primitives.asymmetric import x25519 +except ImportError: + x25519 = None + import paramiko.util from paramiko.kex_group1 import KexGroup1 +from paramiko.kex_group14 import KexGroup14SHA256 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 paramiko.kex_group16 import KexGroup16SHA512 +from paramiko.kex_curve25519 import KexCurve25519 def dummy_urandom(n): @@ -42,20 +53,20 @@ def dummy_urandom(n): def dummy_generate_key_pair(obj): private_key_value = 94761803665136558137557783047955027733968423115106677159790289642479432803037 # noqa public_key_numbers = "042bdab212fa8ba1b7c843301682a4db424d307246c7e1e6083c41d9ca7b098bf30b3d63e2ec6278488c135360456cc054b3444ecc45998c08894cbc1370f5f989" # noqa - public_key_numbers_obj = ec.EllipticCurvePublicNumbers.from_encoded_point( + public_key_numbers_obj = ec.EllipticCurvePublicKey.from_encoded_point( ec.SECP256R1(), unhexlify(public_key_numbers) - ) + ).public_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( + obj.Q_S = ec.EllipticCurvePublicKey.from_encoded_point( ec.SECP256R1(), unhexlify(public_key_numbers) - ).public_key(default_backend()) + ) return - obj.Q_C = ec.EllipticCurvePublicNumbers.from_encoded_point( + obj.Q_C = ec.EllipticCurvePublicKey.from_encoded_point( ec.SECP256R1(), unhexlify(public_key_numbers) - ).public_key(default_backend()) + ) class FakeKey(object): @@ -119,9 +130,25 @@ class KexTest(unittest.TestCase): self._original_generate_key_pair = KexNistp256._generate_key_pair KexNistp256._generate_key_pair = dummy_generate_key_pair + if KexCurve25519.is_available(): + static_x25519_key = x25519.X25519PrivateKey.from_private_bytes( + unhexlify( + b"2184abc7eb3e656d2349d2470ee695b570c227340c2b2863b6c9ff427af1f040" # noqa + ) + ) + mock_x25519 = Mock() + mock_x25519.generate.return_value = static_x25519_key + patcher = patch( + "paramiko.kex_curve25519.X25519PrivateKey", mock_x25519 + ) + patcher.start() + self.x25519_patcher = patcher + def tearDown(self): os.urandom = self._original_urandom KexNistp256._generate_key_pair = self._original_generate_key_pair + if hasattr(self, "x25519_patcher"): + self.x25519_patcher.stop() def test_group1_client(self): transport = FakeTransport() @@ -495,3 +522,104 @@ class KexTest(unittest.TestCase): self.assertEqual(K, transport._K) self.assertTrue(transport._activated) self.assertEqual(H, hexlify(transport._H).upper()) + + def test_kex_group14_sha256_client(self): + transport = FakeTransport() + transport.server_mode = False + kex = KexGroup14SHA256(transport) + kex.start_kex() + x = b"1E00000101009850B3A8DE3ECCD3F19644139137C93D9C11BC28ED8BE850908EE294E1D43B88B9295311EFAEF5B736A1B652EBE184CCF36CFB0681C1ED66430088FA448B83619F928E7B9592ED6160EC11D639D51C303603F930F743C646B1B67DA38A1D44598DCE6C3F3019422B898044141420E9A10C29B9C58668F7F20A40F154B2C4768FCF7A9AA7179FB6366A7167EE26DD58963E8B880A0572F641DE0A73DC74C930F7C3A0C9388553F3F8403E40CF8B95FEDB1D366596FCF3FDDEB21A0005ADA650EF1733628D807BE5ACB83925462765D9076570056E39994FB328E3108FE406275758D6BF5F32790EF15D8416BF5548164859E785DB45E7787BB0E727ADE08641ED" # noqa + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual( + (paramiko.kex_group1._MSG_KEXDH_REPLY,), transport._expect + ) + + # fake "reply" + msg = Message() + msg.add_string("fake-host-key") + msg.add_mpint(69) + msg.add_string("fake-sig") + msg.rewind() + kex.parse_next(paramiko.kex_group1._MSG_KEXDH_REPLY, msg) + K = 21526936926159575624241589599003964979640840086252478029709904308461709651400109485351462666820496096345766733042945918306284902585618061272525323382142547359684512114160415969631877620660064043178086464811345023251493620331559440565662862858765724251890489795332144543057725932216208403143759943169004775947331771556537814494448612329251887435553890674764339328444948425882382475260315505741818518926349729970262019325118040559191290279100613049085709127598666890434114956464502529053036826173452792849566280474995114751780998069614898221773345705289637708545219204637224261997310181473787577166103031529148842107599 # noqa + H = b"D007C23686BE8A7737F828DC9E899F8EB5AF423F495F138437BE2529C1B8455F" + self.assertEqual(K, 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_kex_group16_sha512_client(self): + transport = FakeTransport() + transport.server_mode = False + kex = KexGroup16SHA512(transport) + kex.start_kex() + x = b"1E0000020100859FF55A23E0F66463561DD8BFC4764C69C05F85665B06EC9E29EF5003A53A8FA890B6A6EB624DEB55A4FB279DE7010A53580A126817E3D235B05A1081662B1500961D0625F0AAD287F1B597CBA9DB9550D9CC26355C4C59F92E613B5C21AC191F152C09A5DB46DCBA5EA58E3CA6A8B0EB7183E27FAC10106022E8521FA91240FB389060F1E1E4A355049D29DCC82921CE6588791743E4B1DEEE0166F7CC5180C3C75F3773342DF95C8C10AAA5D12975257027936B99B3DED6E6E98CF27EADEAEAE04E7F0A28071F578646B985FCE28A59CEB36287CB65759BE0544D4C4018CDF03C9078FE9CA79ECA611CB6966899E6FD29BE0781491C659FE2380E0D99D50D9CFAAB94E61BE311779719C4C43C6D223AD3799C3915A9E55076A21152DBBF911D6594296D6ECDC1B6FA71997CD29DF987B80FCA7F36BB7F19863C72BBBF839746AFBF9A5B407D468C976AA3E36FA118D3EAAD2E08BF6AE219F81F2CE2BE946337F06CC09BBFABE938A4087E413921CBEC1965ED905999B83396ECA226110CDF6EFB80F815F6489AF87561DA3857F13A7705921306D94176231FBB336B17C3724BC17A28BECB910093AB040873D5D760E8C182B88ECCE3E38DDA68CE35BD152DF7550BD908791FCCEDD1FFDF5ED2A57FFAE79599E487A7726D8A3D950B1729A08FBB60EE462A6BBE8BF0F5F0E1358129A37840FE5B3EEB8BF26E99FA222EAE83" # noqa + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual( + (paramiko.kex_group1._MSG_KEXDH_REPLY,), transport._expect + ) + + # fake "reply" + msg = Message() + msg.add_string("fake-host-key") + msg.add_mpint(69) + msg.add_string("fake-sig") + msg.rewind() + kex.parse_next(paramiko.kex_group1._MSG_KEXDH_REPLY, msg) + K = 933242830095376162107925500057692534838883186615567574891154103836907630698358649443101764908667358576734565553213003142941996368306996312915844839972197961603283544950658467545799914435739152351344917376359963584614213874232577733869049670230112638724993540996854599166318001059065780674008011575015459772051180901213815080343343801745386220342919837913506966863570473712948197760657442974564354432738520446202131551650771882909329069340612274196233658123593466135642819578182367229641847749149740891990379052266213711500434128970973602206842980669193719602075489724202241641553472106310932258574377789863734311328542715212248147206865762697424822447603031087553480483833829498375309975229907460562402877655519980113688369262871485777790149373908739910846630414678346163764464587129010141922982925829457954376352735653834300282864445132624993186496129911208133529828461690634463092007726349795944930302881758403402084584307180896465875803621285362317770276493727205689466142632599776710824902573926951951209239626732358074877997756011804454926541386215567756538832824717436605031489511654178384081883801272314328403020205577714999460724519735573055540814037716770051316113795603990199374791348798218428912977728347485489266146775472 # noqa + H = b"F6E2BCC846B9B62591EFB86663D55D4769CA06B2EDABE469DF831639B2DDD5A271985011900A724CB2C87F19F347B3632A7C1536AF3D12EE463E6EA75281AF0C" # noqa + self.assertEqual(K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual((b"fake-host-key", b"fake-sig"), transport._verify) + self.assertTrue(transport._activated) + + @pytest.mark.skipif("not KexCurve25519.is_available()") + def test_kex_c25519_client(self): + K = 71294722834835117201316639182051104803802881348227506835068888449366462300724 # noqa + transport = FakeTransport() + transport.server_mode = False + kex = KexCurve25519(transport) + kex.start_kex() + self.assertEqual( + (paramiko.kex_curve25519._MSG_KEXECDH_REPLY,), transport._expect + ) + + # fake reply + msg = Message() + msg.add_string("fake-host-key") + Q_S = unhexlify( + "8d13a119452382a1ada8eea4c979f3e63ad3f0c7366786d6c5b54b87219bae49" + ) + msg.add_string(Q_S) + msg.add_string("fake-sig") + msg.rewind() + kex.parse_next(paramiko.kex_curve25519._MSG_KEXECDH_REPLY, msg) + H = b"05B6F6437C0CF38D1A6C5A6F6E2558DEB54E7FC62447EBFB1E5D7407326A5475" + 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) + + @pytest.mark.skipif("not KexCurve25519.is_available()") + def test_kex_c25519_server(self): + K = 71294722834835117201316639182051104803802881348227506835068888449366462300724 # noqa + transport = FakeTransport() + transport.server_mode = True + kex = KexCurve25519(transport) + kex.start_kex() + self.assertEqual( + (paramiko.kex_curve25519._MSG_KEXECDH_INIT,), transport._expect + ) + + # fake init + msg = Message() + Q_C = unhexlify( + "8d13a119452382a1ada8eea4c979f3e63ad3f0c7366786d6c5b54b87219bae49" + ) + H = b"DF08FCFCF31560FEE639D9B6D56D760BC3455B5ADA148E4514181023E7A9B042" + msg.add_string(Q_C) + msg.rewind() + kex.parse_next(paramiko.kex_curve25519._MSG_KEXECDH_INIT, msg) + self.assertEqual(K, transport._K) + self.assertTrue(transport._activated) + self.assertEqual(H, hexlify(transport._H).upper()) |