diff options
-rw-r--r-- | .travis.yml | 21 | ||||
-rw-r--r-- | README.rst | 2 | ||||
-rw-r--r-- | dev-requirements.txt | 6 | ||||
-rw-r--r-- | paramiko/__init__.py | 2 | ||||
-rw-r--r-- | paramiko/agent.py | 4 | ||||
-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/hostkeys.py | 6 | ||||
-rw-r--r-- | paramiko/kex_ecdh_nist.py | 37 | ||||
-rw-r--r-- | paramiko/pkey.py | 6 | ||||
-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/util.py | 18 | ||||
-rw-r--r-- | setup.cfg | 4 | ||||
-rw-r--r-- | setup.py | 9 | ||||
-rw-r--r-- | sites/shared_conf.py | 1 | ||||
-rw-r--r-- | sites/www/changelog.rst | 47 | ||||
-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_auth.py | 18 | ||||
-rw-r--r-- | tests/test_config.py | 74 | ||||
-rw-r--r-- | tests/test_kex.py | 12 |
27 files changed, 352 insertions, 115 deletions
diff --git a/.travis.yml b/.travis.yml index 11de689c..2a496e55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial language: python sudo: false cache: @@ -8,21 +9,23 @@ python: - "3.4" - "3.5" - "3.6" - - "3.7-dev" - - "pypy-5.6.0" + - "3.7" + - "3.8-dev" + - "pypy" + - "pypy3" matrix: allow_failures: - - python: "3.7-dev" + - python: "3.8-dev" # Explicitly test against our oldest supported cryptography.io, in addition # to whatever the latest default is. include: - python: 2.7 - env: "CRYPTO_BEFORE=1.6" - - python: 3.6 - env: "CRYPTO_BEFORE=1.6" + env: "OLDEST_CRYPTO=2.5" + - python: 3.7 + env: "OLDEST_CRYPTO=2.5" - python: 2.7 env: "USE_K5TEST=yes" - - python: 3.6 + - python: 3.7 env: "USE_K5TEST=yes" install: # Ensure modern pip/etc to avoid some issues w/ older worker environs @@ -30,8 +33,8 @@ install: # Grab a specific version of Cryptography if desired. Doing this before other # installations ensures we don't have to do any downgrading/overriding. - | - if [[ -n "$CRYPTO_BEFORE" ]]; then - pip install "cryptography<${CRYPTO_BEFORE}" + if [[ -n "$OLDEST_CRYPTO" ]]; then + pip install "cryptography==${OLDEST_CRYPTO}" fi # Self-install for setup.py-driven deps - pip install -e . @@ -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/dev-requirements.txt b/dev-requirements.txt index 8814627f..4c118991 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -5,16 +5,16 @@ invocations>=1.2.0,<2.0 pytest>=3.2,<3.3 pytest-relaxed==1.1.4 # pytest-xdist for test dir watching and the inv guard task -pytest-xdist>=1.22,<2.0 +pytest-xdist>=1.22,<1.25.0 mock==2.0.0 # Linting! -flake8==2.4.0 +flake8==3.6.0 # Coverage! coverage==3.7.1 codecov==1.6.3 # Documentation tools sphinx>=1.4,<1.7 -alabaster>=0.7,<2.0 +alabaster==0.7.12 releases>=1.5,<2.0 # Release tools semantic_version>=2.4,<2.5 diff --git a/paramiko/__init__.py b/paramiko/__init__.py index f77a2bcc..ebfa72a8 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/agent.py b/paramiko/agent.py index 0630ebf3..622b95e4 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -80,8 +80,8 @@ class AgentSSH(object): def _send_message(self, msg): msg = asbytes(msg) self._conn.send(struct.pack(">I", len(msg)) + msg) - l = self._read_all(4) - msg = Message(self._read_all(struct.unpack(">I", l)[0])) + data = self._read_all(4) + msg = Message(self._read_all(struct.unpack(">I", data)[0])) return ord(msg.get_byte()), msg def _read_all(self, wanted): 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/hostkeys.py b/paramiko/hostkeys.py index f31b8819..d0660cc8 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -19,8 +19,12 @@ import binascii import os +import sys -from collections import MutableMapping +if sys.version_info[:2] >= (3, 3): + from collections.abc import MutableMapping +else: + from collections import MutableMapping from hashlib import sha1 from hmac import HMAC 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/pkey.py b/paramiko/pkey.py index fa014800..e37f7751 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -291,10 +291,10 @@ class PKey(object): headers = {} start += 1 while start < len(lines): - l = lines[start].split(": ") - if len(l) == 1: + line = lines[start].split(": ") + if len(line) == 1: break - headers[l[0].lower()] = l[1].strip() + headers[line[0].lower()] = line[1].strip() start += 1 # find end end = start 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 06aac761..a8424036 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 = None @@ -176,6 +172,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)) @@ -190,6 +189,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 @@ -283,6 +284,8 @@ class _SSH_GSSAPI_OLD(_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( @@ -635,6 +638,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/util.py b/paramiko/util.py index de4a5647..29c52bfb 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -245,16 +245,16 @@ def get_thread_id(): def log_to_file(filename, level=DEBUG): """send paramiko logs to a logfile, if they're not already going somewhere""" - l = logging.getLogger("paramiko") - if len(l.handlers) > 0: + logger = logging.getLogger("paramiko") + if len(logger.handlers) > 0: return - l.setLevel(level) + logger.setLevel(level) f = open(filename, "a") - lh = logging.StreamHandler(f) + handler = logging.StreamHandler(f) frm = "%(levelname)-.3s [%(asctime)s.%(msecs)03d] thr=%(_threadid)-3d" frm += " %(name)s: %(message)s" - lh.setFormatter(logging.Formatter(frm, "%Y%m%d-%H:%M:%S")) - l.addHandler(lh) + handler.setFormatter(logging.Formatter(frm, "%Y%m%d-%H:%M:%S")) + logger.addHandler(handler) # make only one filter object, so it doesn't get applied more than once @@ -268,9 +268,9 @@ _pfilter = PFilter() def get_logger(name): - l = logging.getLogger(name) - l.addFilter(_pfilter) - return l + logger = logging.getLogger(name) + logger.addFilter(_pfilter) + return logger def retry_on_signal(function): @@ -11,7 +11,9 @@ omit = paramiko/_winapi.py exclude = sites,.git,build,dist,demos,tests # NOTE: W503, E203 are concessions to black 18.0b5 and could be reinstated # later if fixed on that end. -ignore = E124,E125,E128,E261,E301,E302,E303,E402,E721,W503,E203 +# NOTE: E722 seems to only have started popping up on move to flake8 3.6.0 from +# 2.4.0. Not sure why, bare excepts have been a (regrettable) thing forever... +ignore = E124,E125,E128,E261,E301,E302,E303,E402,E721,W503,E203,E722 max-line-length = 79 [tool:pytest] @@ -70,11 +70,8 @@ setup( "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "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/shared_conf.py b/sites/shared_conf.py index f4806cf1..7bb503ce 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -14,6 +14,7 @@ html_theme_options = { "github_repo": "paramiko", "analytics_id": "UA-18486793-2", "travis_button": True, + "tidelift_url": "https://tidelift.com/subscription/pkg/pypi-paramiko?utm_source=pypi-paramiko&utm_medium=referral&utm_campaign=docs", } html_sidebars = { "**": ["about.html", "navigation.html", "searchbox.html", "donate.html"] diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5c9843e8..032edb44 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,29 @@ Changelog ========= +- :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 + catch & patch. - :release:`2.4.2 <2018-09-18>` - :release:`2.3.3 <2018-09-18>` - :release:`2.2.4 <2018-09-18>` @@ -12,7 +35,7 @@ Changelog behavior probably didn't cause any outright errors, but it doesn't seem to conform to the RFCs and could cause (non-infinite) feedback loops in some scenarios (usually those involving Paramiko on both ends). -- :bug:`1283` Fix exploit (CVE pending) in Paramiko's server mode (**not** +- :bug:`1283` Fix exploit (CVE-2018-1000805) in Paramiko's server mode (**not** client mode) where hostile clients could trick the server into thinking they were authenticated without actually submitting valid authentication. @@ -44,6 +67,22 @@ Changelog - :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>` @@ -56,6 +95,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. @@ -75,7 +118,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_auth.py b/tests/test_auth.py index fe1a32a1..d98a00c4 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -142,7 +142,7 @@ class AuthTest(unittest.TestCase): self.assertTrue(self.event.is_set()) self.assertTrue(self.ts.is_active()) - def test_1_bad_auth_type(self): + def test_bad_auth_type(self): """ verify that we get the right exception when an unsupported auth type is requested. @@ -160,7 +160,7 @@ class AuthTest(unittest.TestCase): self.assertEqual(BadAuthenticationType, etype) self.assertEqual(["publickey"], evalue.allowed_types) - def test_2_bad_password(self): + def test_bad_password(self): """ verify that a bad password gets the right exception, and that a retry with the right password works. @@ -176,7 +176,7 @@ class AuthTest(unittest.TestCase): self.tc.auth_password(username="slowdive", password="pygmalion") self.verify_finished() - def test_3_multipart_auth(self): + def test_multipart_auth(self): """ verify that multipart auth works. """ @@ -191,7 +191,7 @@ class AuthTest(unittest.TestCase): self.assertEqual([], remain) self.verify_finished() - def test_4_interactive_auth(self): + def test_interactive_auth(self): """ verify keyboard-interactive auth works. """ @@ -210,7 +210,7 @@ class AuthTest(unittest.TestCase): self.assertEqual([], remain) self.verify_finished() - def test_5_interactive_auth_fallback(self): + def test_interactive_auth_fallback(self): """ verify that a password auth attempt will fallback to "interactive" if password auth isn't supported but interactive is. @@ -221,7 +221,7 @@ class AuthTest(unittest.TestCase): self.assertEqual([], remain) self.verify_finished() - def test_6_auth_utf8(self): + def test_auth_utf8(self): """ verify that utf-8 encoding happens in authentication. """ @@ -231,7 +231,7 @@ class AuthTest(unittest.TestCase): self.assertEqual([], remain) self.verify_finished() - def test_7_auth_non_utf8(self): + def test_auth_non_utf8(self): """ verify that non-utf-8 encoded passwords can be used for broken servers. @@ -242,7 +242,7 @@ class AuthTest(unittest.TestCase): self.assertEqual([], remain) self.verify_finished() - def test_8_auth_gets_disconnected(self): + def test_auth_gets_disconnected(self): """ verify that we catch a server disconnecting during auth, and report it as an auth failure. @@ -256,7 +256,7 @@ class AuthTest(unittest.TestCase): self.assertTrue(issubclass(etype, AuthenticationException)) @slow - def test_9_auth_non_responsive(self): + def test_auth_non_responsive(self): """ verify that authentication times out if server takes to long to respond (or never responds). 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 62512beb..d42355a1 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -42,20 +42,20 @@ def dummy_urandom(n): def dummy_generate_key_pair(obj): private_key_value = 94761803665136558137557783047955027733968423115106677159790289642479432803037 public_key_numbers = "042bdab212fa8ba1b7c843301682a4db424d307246c7e1e6083c41d9ca7b098bf30b3d63e2ec6278488c135360456cc054b3444ecc45998c08894cbc1370f5f989" - 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): |