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/_version.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/kex_group1.py2
-rw-r--r--paramiko/kex_group14.py7
-rw-r--r--paramiko/kex_group16.py35
-rw-r--r--paramiko/packet.py87
-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.py22
-rw-r--r--setup.cfg3
-rw-r--r--setup.py7
-rw-r--r--sites/www/changelog.rst61
-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.py140
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
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 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 (
diff --git a/setup.cfg b/setup.cfg
index 1b589871..b6b2eea3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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
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..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())