summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--README.rst2
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/auth_handler.py4
-rw-r--r--paramiko/client.py6
-rw-r--r--paramiko/common.py31
-rw-r--r--paramiko/config.py83
-rw-r--r--paramiko/ecdsakey.py4
-rw-r--r--paramiko/kex_curve25519.py129
-rw-r--r--paramiko/kex_ecdh_nist.py37
-rw-r--r--paramiko/py3compat.py41
-rw-r--r--paramiko/sftp_client.py4
-rw-r--r--paramiko/ssh_gss.py13
-rw-r--r--paramiko/transport.py5
-rw-r--r--setup.py7
-rw-r--r--sites/www/changelog.rst44
-rw-r--r--sites/www/contact.rst2
-rw-r--r--sites/www/installing-1.x.rst2
-rw-r--r--sites/www/installing.rst10
-rw-r--r--tests/test_config.py74
-rw-r--r--tests/test_kex.py86
21 files changed, 511 insertions, 79 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
diff --git a/README.rst b/README.rst
index 6e49bd68..c918652f 100644
--- a/README.rst
+++ b/README.rst
@@ -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 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/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/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..f25ef95d 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -88,6 +88,7 @@ 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
@@ -178,6 +179,8 @@ class Transport(threading.Thread, ClosingContextManager):
"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==",
@@ -272,6 +275,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
diff --git a/setup.py b/setup.py
index 4c0bac55..e6c4a077 100644
--- a/setup.py
+++ b/setup.py
@@ -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..dd00254d 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,28 @@
Changelog
=========
+- :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
@@ -48,6 +70,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>`
@@ -60,6 +98,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 +121,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..456a213f 100644
--- a/tests/test_kex.py
+++ b/tests/test_kex.py
@@ -24,15 +24,24 @@ 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_gex import KexGex, KexGexSHA256
from paramiko import Message
from paramiko.common import byte_chr
from paramiko.kex_ecdh_nist import KexNistp256
+from paramiko.kex_curve25519 import KexCurve25519
def dummy_urandom(n):
@@ -42,20 +51,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 +128,23 @@ class KexTest(unittest.TestCase):
self._original_generate_key_pair = KexNistp256._generate_key_pair
KexNistp256._generate_key_pair = dummy_generate_key_pair
+ 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
+ self.x25519_patcher.stop()
def test_group1_client(self):
transport = FakeTransport()
@@ -495,3 +518,54 @@ class KexTest(unittest.TestCase):
self.assertEqual(K, transport._K)
self.assertTrue(transport._activated)
self.assertEqual(H, hexlify(transport._H).upper())
+
+ @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())