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