summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 17:47:36 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 17:47:36 +0000
commit3c7813683b1845959aca706eaa23f062a006356b (patch)
treeecba42f14f0c919d94332e2633d9b0e6834c9cec /tests
parentInitial commit. (diff)
downloadparamiko-3c7813683b1845959aca706eaa23f062a006356b.tar.xz
paramiko-3c7813683b1845959aca706eaa23f062a006356b.zip
Adding upstream version 3.4.0.upstream/3.4.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests')
-rw-r--r--tests/__init__.py56
-rw-r--r--tests/_loop.py98
-rw-r--r--tests/_stub_sftp.py232
-rw-r--r--tests/_support/dss.key12
-rw-r--r--tests/_support/dss.key-cert.pub1
-rw-r--r--tests/_support/ecdsa-256.key5
-rw-r--r--tests/_support/ecdsa-256.key-cert.pub1
-rw-r--r--tests/_support/ed25519.key8
-rw-r--r--tests/_support/ed25519.key-cert.pub1
-rw-r--r--tests/_support/ed448.key4
-rw-r--r--tests/_support/rsa-lonely.key15
-rw-r--r--tests/_support/rsa-missing.key-cert.pub1
-rw-r--r--tests/_support/rsa.key15
-rw-r--r--tests/_support/rsa.key-cert.pub1
-rw-r--r--tests/_util.py468
-rw-r--r--tests/agent.py151
-rw-r--r--tests/auth.py580
-rw-r--r--tests/badhash_key1.ed25519.key7
-rw-r--r--tests/badhash_key2.ed25519.key7
-rw-r--r--tests/blank_rsa.key0
-rw-r--r--tests/configs/basic4
-rw-r--r--tests/configs/canon8
-rw-r--r--tests/configs/canon-always5
-rw-r--r--tests/configs/canon-ipv46
-rw-r--r--tests/configs/canon-local6
-rw-r--r--tests/configs/canon-local-always6
-rw-r--r--tests/configs/deep-canon11
-rw-r--r--tests/configs/deep-canon-maxdots12
-rw-r--r--tests/configs/empty-canon6
-rw-r--r--tests/configs/fallback-no6
-rw-r--r--tests/configs/fallback-yes6
-rw-r--r--tests/configs/hostname-exec-tokenized2
-rw-r--r--tests/configs/hostname-tokenized1
-rw-r--r--tests/configs/invalid1
-rw-r--r--tests/configs/match-all2
-rw-r--r--tests/configs/match-all-after-canonical5
-rw-r--r--tests/configs/match-all-and-more2
-rw-r--r--tests/configs/match-all-and-more-before2
-rw-r--r--tests/configs/match-all-before-canonical5
-rw-r--r--tests/configs/match-canonical-no7
-rw-r--r--tests/configs/match-canonical-yes5
-rw-r--r--tests/configs/match-complex17
-rw-r--r--tests/configs/match-exec16
-rw-r--r--tests/configs/match-exec-canonical10
-rw-r--r--tests/configs/match-exec-negation5
-rw-r--r--tests/configs/match-exec-no-arg2
-rw-r--r--tests/configs/match-final14
-rw-r--r--tests/configs/match-host2
-rw-r--r--tests/configs/match-host-canonicalized8
-rw-r--r--tests/configs/match-host-from-match5
-rw-r--r--tests/configs/match-host-glob2
-rw-r--r--tests/configs/match-host-glob-list8
-rw-r--r--tests/configs/match-host-name4
-rw-r--r--tests/configs/match-host-negated2
-rw-r--r--tests/configs/match-host-no-arg2
-rw-r--r--tests/configs/match-localuser14
-rw-r--r--tests/configs/match-localuser-no-arg2
-rw-r--r--tests/configs/match-orighost16
-rw-r--r--tests/configs/match-orighost-canonical5
-rw-r--r--tests/configs/match-orighost-no-arg2
-rw-r--r--tests/configs/match-user14
-rw-r--r--tests/configs/match-user-explicit4
-rw-r--r--tests/configs/match-user-no-arg2
-rw-r--r--tests/configs/multi-canon-domains5
-rw-r--r--tests/configs/no-canon5
-rw-r--r--tests/configs/robey17
-rw-r--r--tests/configs/zero-maxdots9
-rw-r--r--tests/conftest.py170
-rw-r--r--tests/pkey.py229
-rw-r--r--tests/test_buffered_pipe.py91
-rw-r--r--tests/test_channelfile.py60
-rw-r--r--tests/test_client.py837
-rw-r--r--tests/test_config.py1048
-rw-r--r--tests/test_dss_openssh.key22
-rw-r--r--tests/test_dss_password.key15
-rw-r--r--tests/test_ecdsa_384.key6
-rw-r--r--tests/test_ecdsa_384_openssh.key11
-rw-r--r--tests/test_ecdsa_521.key7
-rw-r--r--tests/test_ecdsa_password_256.key8
-rw-r--r--tests/test_ecdsa_password_384.key9
-rw-r--r--tests/test_ecdsa_password_521.key10
-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_password.key8
-rw-r--r--tests/test_file.py226
-rw-r--r--tests/test_gssapi.py225
-rw-r--r--tests/test_hostkeys.py172
-rw-r--r--tests/test_kex.py668
-rw-r--r--tests/test_kex_gss.py154
-rw-r--r--tests/test_message.py113
-rw-r--r--tests/test_packetizer.py148
-rw-r--r--tests/test_pkey.py696
-rw-r--r--tests/test_proxy.py150
-rw-r--r--tests/test_rsa.key.pub1
-rw-r--r--tests/test_rsa_openssh.key28
-rw-r--r--tests/test_rsa_openssh_nopad.key27
-rw-r--r--tests/test_rsa_password.key18
-rw-r--r--tests/test_sftp.py832
-rw-r--r--tests/test_sftp_big.py416
-rw-r--r--tests/test_ssh_exception.py75
-rw-r--r--tests/test_ssh_gss.py160
-rw-r--r--tests/test_transport.py1446
-rw-r--r--tests/test_util.py136
103 files changed, 10200 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..f43975d
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,56 @@
+"""Base classes and helpers for testing paramiko."""
+
+import functools
+import locale
+import os
+
+from pytest import skip
+
+
+# List of locales which have non-ascii characters in all categories.
+# Omits most European languages which for instance may have only some months
+# with names that include accented characters.
+_non_ascii_locales = [
+ # East Asian locales
+ "ja_JP",
+ "ko_KR",
+ "zh_CN",
+ "zh_TW",
+ # European locales with non-latin alphabets
+ "el_GR",
+ "ru_RU",
+ "uk_UA",
+]
+# Also include UTF-8 versions of these locales
+_non_ascii_locales.extend([name + ".utf8" for name in _non_ascii_locales])
+
+
+def requireNonAsciiLocale(category_name="LC_ALL"):
+ """Run decorated test under a non-ascii locale or skip if not possible."""
+ if os.name != "posix":
+ return skip("Non-posix OSes don't really use C locales")
+ cat = getattr(locale, category_name)
+ return functools.partial(_decorate_with_locale, cat, _non_ascii_locales)
+
+
+def _decorate_with_locale(category, try_locales, test_method):
+ """Decorate test_method to run after switching to a different locale."""
+
+ def _test_under_locale(testself, *args, **kwargs):
+ original = locale.setlocale(category)
+ while try_locales:
+ try:
+ locale.setlocale(category, try_locales[0])
+ except locale.Error:
+ # Mutating original list is ok, setlocale would keep failing
+ try_locales.pop(0)
+ else:
+ try:
+ return test_method(testself, *args, **kwargs)
+ finally:
+ locale.setlocale(category, original)
+ # No locales could be used? Just skip the decorated test :(
+ skip("No usable locales installed")
+
+ functools.update_wrapper(_test_under_locale, test_method)
+ return _test_under_locale
diff --git a/tests/_loop.py b/tests/_loop.py
new file mode 100644
index 0000000..a374001
--- /dev/null
+++ b/tests/_loop.py
@@ -0,0 +1,98 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import socket
+import threading
+
+from paramiko.util import asbytes
+
+
+class LoopSocket:
+ """
+ A LoopSocket looks like a normal socket, but all data written to it is
+ delivered on the read-end of another LoopSocket, and vice versa. It's
+ like a software "socketpair".
+ """
+
+ def __init__(self):
+ self.__in_buffer = bytes()
+ self.__lock = threading.Lock()
+ self.__cv = threading.Condition(self.__lock)
+ self.__timeout = None
+ self.__mate = None
+ self._closed = False
+
+ def close(self):
+ self.__unlink()
+ self._closed = True
+ try:
+ self.__lock.acquire()
+ self.__in_buffer = bytes()
+ finally:
+ self.__lock.release()
+
+ def send(self, data):
+ data = asbytes(data)
+ if self.__mate is None:
+ # EOF
+ raise EOFError()
+ self.__mate.__feed(data)
+ return len(data)
+
+ def recv(self, n):
+ self.__lock.acquire()
+ try:
+ if self.__mate is None:
+ # EOF
+ return bytes()
+ if len(self.__in_buffer) == 0:
+ self.__cv.wait(self.__timeout)
+ if len(self.__in_buffer) == 0:
+ raise socket.timeout
+ out = self.__in_buffer[:n]
+ self.__in_buffer = self.__in_buffer[n:]
+ return out
+ finally:
+ self.__lock.release()
+
+ def settimeout(self, n):
+ self.__timeout = n
+
+ def link(self, other):
+ self.__mate = other
+ self.__mate.__mate = self
+
+ def __feed(self, data):
+ self.__lock.acquire()
+ try:
+ self.__in_buffer += data
+ self.__cv.notify_all()
+ finally:
+ self.__lock.release()
+
+ def __unlink(self):
+ m = None
+ self.__lock.acquire()
+ try:
+ if self.__mate is not None:
+ m = self.__mate
+ self.__mate = None
+ finally:
+ self.__lock.release()
+ if m is not None:
+ m.__unlink()
diff --git a/tests/_stub_sftp.py b/tests/_stub_sftp.py
new file mode 100644
index 0000000..0c0372e
--- /dev/null
+++ b/tests/_stub_sftp.py
@@ -0,0 +1,232 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+A stub SFTP server for loopback SFTP testing.
+"""
+
+import os
+
+from paramiko import (
+ AUTH_SUCCESSFUL,
+ OPEN_SUCCEEDED,
+ SFTPAttributes,
+ SFTPHandle,
+ SFTPServer,
+ SFTPServerInterface,
+ SFTP_FAILURE,
+ SFTP_OK,
+ ServerInterface,
+)
+from paramiko.common import o666
+
+
+class StubServer(ServerInterface):
+ def check_auth_password(self, username, password):
+ # all are allowed
+ return AUTH_SUCCESSFUL
+
+ def check_channel_request(self, kind, chanid):
+ return OPEN_SUCCEEDED
+
+
+class StubSFTPHandle(SFTPHandle):
+ def stat(self):
+ try:
+ return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+
+ def chattr(self, attr):
+ # python doesn't have equivalents to fchown or fchmod, so we have to
+ # use the stored filename
+ try:
+ SFTPServer.set_file_attr(self.filename, attr)
+ return SFTP_OK
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+
+
+class StubSFTPServer(SFTPServerInterface):
+ # assume current folder is a fine root
+ # (the tests always create and eventually delete a subfolder, so there
+ # shouldn't be any mess)
+ ROOT = os.getcwd()
+
+ def _realpath(self, path):
+ return self.ROOT + self.canonicalize(path)
+
+ def list_folder(self, path):
+ path = self._realpath(path)
+ try:
+ out = []
+ flist = os.listdir(path)
+ for fname in flist:
+ attr = SFTPAttributes.from_stat(
+ os.stat(os.path.join(path, fname))
+ )
+ attr.filename = fname
+ out.append(attr)
+ return out
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+
+ def stat(self, path):
+ path = self._realpath(path)
+ try:
+ return SFTPAttributes.from_stat(os.stat(path))
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+
+ def lstat(self, path):
+ path = self._realpath(path)
+ try:
+ return SFTPAttributes.from_stat(os.lstat(path))
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+
+ def open(self, path, flags, attr):
+ path = self._realpath(path)
+ try:
+ binary_flag = getattr(os, "O_BINARY", 0)
+ flags |= binary_flag
+ mode = getattr(attr, "st_mode", None)
+ if mode is not None:
+ fd = os.open(path, flags, mode)
+ else:
+ # os.open() defaults to 0777 which is
+ # an odd default mode for files
+ fd = os.open(path, flags, o666)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ if (flags & os.O_CREAT) and (attr is not None):
+ attr._flags &= ~attr.FLAG_PERMISSIONS
+ SFTPServer.set_file_attr(path, attr)
+ if flags & os.O_WRONLY:
+ if flags & os.O_APPEND:
+ fstr = "ab"
+ else:
+ fstr = "wb"
+ elif flags & os.O_RDWR:
+ if flags & os.O_APPEND:
+ fstr = "a+b"
+ else:
+ fstr = "r+b"
+ else:
+ # O_RDONLY (== 0)
+ fstr = "rb"
+ try:
+ f = os.fdopen(fd, fstr)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ fobj = StubSFTPHandle(flags)
+ fobj.filename = path
+ fobj.readfile = f
+ fobj.writefile = f
+ return fobj
+
+ def remove(self, path):
+ path = self._realpath(path)
+ try:
+ os.remove(path)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ return SFTP_OK
+
+ 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:
+ return SFTPServer.convert_errno(e.errno)
+ return SFTP_OK
+
+ def mkdir(self, path, attr):
+ path = self._realpath(path)
+ try:
+ os.mkdir(path)
+ if attr is not None:
+ SFTPServer.set_file_attr(path, attr)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ return SFTP_OK
+
+ def rmdir(self, path):
+ path = self._realpath(path)
+ try:
+ os.rmdir(path)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ return SFTP_OK
+
+ def chattr(self, path, attr):
+ path = self._realpath(path)
+ try:
+ SFTPServer.set_file_attr(path, attr)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ return SFTP_OK
+
+ def symlink(self, target_path, path):
+ path = self._realpath(path)
+ if (len(target_path) > 0) and (target_path[0] == "/"):
+ # absolute symlink
+ target_path = os.path.join(self.ROOT, target_path[1:])
+ if target_path[:2] == "//":
+ # bug in os.path.join
+ target_path = target_path[1:]
+ else:
+ # compute relative to path
+ abspath = os.path.join(os.path.dirname(path), target_path)
+ if abspath[: len(self.ROOT)] != self.ROOT:
+ # this symlink isn't going to work anyway -- just break it
+ # immediately
+ target_path = "<error>"
+ try:
+ os.symlink(target_path, path)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ return SFTP_OK
+
+ def readlink(self, path):
+ path = self._realpath(path)
+ try:
+ symlink = os.readlink(path)
+ except OSError as e:
+ return SFTPServer.convert_errno(e.errno)
+ # if it's absolute, remove the root
+ if os.path.isabs(symlink):
+ if symlink[: len(self.ROOT)] == self.ROOT:
+ symlink = symlink[len(self.ROOT) :]
+ if (len(symlink) == 0) or (symlink[0] != "/"):
+ symlink = "/" + symlink
+ else:
+ symlink = "<error>"
+ return symlink
diff --git a/tests/_support/dss.key b/tests/_support/dss.key
new file mode 100644
index 0000000..e10807f
--- /dev/null
+++ b/tests/_support/dss.key
@@ -0,0 +1,12 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQDngaYDZ30c6/7cJgEEbtl8FgKdwhba1Z7oOrOn4MI/6C42G1bY
+wMuqZf4dBCglsdq39SHrcjbE8Vq54gPSOh3g4+uV9Rcg5IOoPLbwp2jQfF6f1FIb
+sx7hrDCIqUcQccPSxetPBKmXI9RN8rZLaFuQeTnI65BKM98Ruwvq6SI2LwIVAPDP
+hSeawaJI27mKqOfe5PPBSmyHAoGBAJMXxXmPD9sGaQ419DIpmZecJKBUAy9uXD8x
+gbgeDpwfDaFJP8owByCKREocPFfi86LjCuQkyUKOfjYMN6iHIf1oEZjB8uJAatUr
+FzI0ArXtUqOhwTLwTyFuUojE5own2WYsOAGByvgfyWjsGhvckYNhI4ODpNdPlxQ8
+ZamaPGPsAoGARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmn
+jO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacI
+BlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgECFGI9QPSc
+h9pT9XHqn+1rZ4bK+QGA
+-----END DSA PRIVATE KEY-----
diff --git a/tests/_support/dss.key-cert.pub b/tests/_support/dss.key-cert.pub
new file mode 100644
index 0000000..07fd557
--- /dev/null
+++ b/tests/_support/dss.key-cert.pub
@@ -0,0 +1 @@
+ssh-dss-cert-v01@openssh.com AAAAHHNzaC1kc3MtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgJA3GjLmg6JbIWxokW/c827lmPOSvSfPDIY586yICFqIAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgEAAAAAAAAAAAAAAAEAAAAJdXNlcl90ZXN0AAAACAAAAAR0ZXN0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDskr46Umjxh3wo7PoPQsSVS3xt6+5PhwmXrnVtBBnkOo+zHRwQo8G8sY+Lc6oOOzA5GCSawKOwqE305GIDfB8/L9EKOkAjdN18imDjw/YuJFA4bl9yFhsXrCb1GZPJw0pJ0H0Eid9EldyMQAhGE49MWvnFMQl1TgO6YWq/g71xAFimge0LvVWijlbMy7O+nsGxSpinIprV5S9Viv8XC/ku89tadZfca1uxq751aGfAWGeYrVytpUl8UO0ggqH6BaUvkDU7rWh2n5RHUTvgzceKWnz5wqd8BngK37WmJjAgCtHCJS5ZRf6oJGj2QVcqc6cjvEFWsCuOKB4KAjktauWxAAABDwAAAAdzc2gtcnNhAAABAK6jweL231fRhFoybEGTOXJfj0lx55KpDsw9Q1rBvZhrSgwUr2dFr9HVcKe44mTC7CMtdW5VcyB67l1fnMil/D/e4zYxI0PvbW6RxLFNqvvtxBu5sGt5B7uzV4aAV31TpWR0l5RwwpZqc0NUlTx7oMutN1BDrPqW70QZ/iTEwalkn5fo1JWej0cf4BdC9VgYDLnprx0KN3IToukbszRQySnuR6MQUfj0m7lUloJfF3rq8G0kNxWqDGoJilMhO5Lqu9wAhlZWdouypI6bViO6+ToCVixLNUYs3EfS1zCxvXpiyMvh6rZofJ6WqzUuSd4Mzb2Ka4ocTKi7kynF+OG0Ivo= tests/test_dss.key.pub
diff --git a/tests/_support/ecdsa-256.key b/tests/_support/ecdsa-256.key
new file mode 100644
index 0000000..42d4473
--- /dev/null
+++ b/tests/_support/ecdsa-256.key
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIKB6ty3yVyKEnfF/zprx0qwC76MsMlHY4HXCnqho2eKioAoGCCqGSM49
+AwEHoUQDQgAElI9mbdlaS+T9nHxY/59lFnn80EEecZDBHq4gLpccY8Mge5ZTMiMD
+ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g==
+-----END EC PRIVATE KEY-----
diff --git a/tests/_support/ecdsa-256.key-cert.pub b/tests/_support/ecdsa-256.key-cert.pub
new file mode 100644
index 0000000..f2c93cc
--- /dev/null
+++ b/tests/_support/ecdsa-256.key-cert.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgJ+ZkRXedIWPl9y6fvel60p47ys5WgwMSjiwzJ2Ho+4MAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eoAAAAAAAAAAAAAAAEAAAAJdXNlcl90ZXN0AAAACAAAAAR0ZXN0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDskr46Umjxh3wo7PoPQsSVS3xt6+5PhwmXrnVtBBnkOo+zHRwQo8G8sY+Lc6oOOzA5GCSawKOwqE305GIDfB8/L9EKOkAjdN18imDjw/YuJFA4bl9yFhsXrCb1GZPJw0pJ0H0Eid9EldyMQAhGE49MWvnFMQl1TgO6YWq/g71xAFimge0LvVWijlbMy7O+nsGxSpinIprV5S9Viv8XC/ku89tadZfca1uxq751aGfAWGeYrVytpUl8UO0ggqH6BaUvkDU7rWh2n5RHUTvgzceKWnz5wqd8BngK37WmJjAgCtHCJS5ZRf6oJGj2QVcqc6cjvEFWsCuOKB4KAjktauWxAAABDwAAAAdzc2gtcnNhAAABALdnEil8XIFkcgLZgYwS2cIQPHetUzMNxYCqzk7mSfVpCaIYNTr27RG+f+sD0cerdAIUUvhCT7iA82/Y7wzwkO2RUBi61ATfw9DDPPRQTDfix1SSRwbmPB/nVI1HlPMCEs6y48PFaBZqXwJPS3qycgSxoTBhaLCLzT+r6HRaibY7kiRLDeL3/WHyasK2PRdcYJ6KrLd0ctQcUHZCLK3fJfMfuQRg8MZLVrmK3fHStCXHpRFueRxUhZjaiS9evA/NtzEQhf46JDClQ2rLYpSqSg7QUR/rKwqWWyMuQkOHmlJw797VVa+ZzpUFXP7ekWel3FaBj8IHiimIA7Jm6dOCLm4= tests/test_ecdsa_256.key.pub
diff --git a/tests/_support/ed25519.key b/tests/_support/ed25519.key
new file mode 100644
index 0000000..eb9f94c
--- /dev/null
+++ b/tests/_support/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/_support/ed25519.key-cert.pub b/tests/_support/ed25519.key-cert.pub
new file mode 100644
index 0000000..4e01415
--- /dev/null
+++ b/tests/_support/ed25519.key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIjBkc8l1X887CLBHraU+d6/74Hxr9oa+3HC0iioecZ6AAAAIHr1K9komH/1WBIvQbbtvnFVhryd62EfcgRFuLRiokNfAAAAAAAAAAAAAAABAAAACXVzZXJfdGVzdAAAAAgAAAAEdGVzdAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA7JK+OlJo8Yd8KOz6D0LElUt8bevuT4cJl651bQQZ5DqPsx0cEKPBvLGPi3OqDjswORgkmsCjsKhN9ORiA3wfPy/RCjpAI3TdfIpg48P2LiRQOG5fchYbF6wm9RmTycNKSdB9BInfRJXcjEAIRhOPTFr5xTEJdU4DumFqv4O9cQBYpoHtC71Voo5WzMuzvp7BsUqYpyKa1eUvVYr/Fwv5LvPbWnWX3Gtbsau+dWhnwFhnmK1craVJfFDtIIKh+gWlL5A1O61odp+UR1E74M3Hilp8+cKnfAZ4Ct+1piYwIArRwiUuWUX+qCRo9kFXKnOnI7xBVrArjigeCgI5LWrlsQAAAQ8AAAAHc3NoLXJzYQAAAQCNfYITv/GCW42fLI89x0pKpXIET/xHIBVan5S3fy5SZq9gLG1Db9g/FITDfOVA7OX8mU/91rucHGtuEi3isILdNFrCcoLEml289tyyluUbbFD5fjvBchMWBkYPwrOPfEzSs299Yk8ZgfV1pjWlndfV54s4c9pinkGu8c0Vzc6stEbWkdmoOHE8su3ogUPg/hOygDzJ+ZOgP5HIUJ6YgkgVpWgZm7zofwdZfa2HEb+WhZaKfMK1UCw1UiSBVk9dx6qzF9m243tHwSHraXvb9oJ1wT1S/MypTbP4RT4fHN8koYNrv2szEBN+lkRgk1D7xaxS/Md2TJsau9ho/UCXSR8L tests/test_ed25519.key.pub
diff --git a/tests/_support/ed448.key b/tests/_support/ed448.key
new file mode 100644
index 0000000..887b51c
--- /dev/null
+++ b/tests/_support/ed448.key
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MEcCAQAwBQYDK2VxBDsEOcvcl9IoD0ktR5RWtW84NM7O2e4LmD2cWfRg7Wht/OA9
+POkmRW12VNvlP6BsXKir5yygumIjD91SQQ==
+-----END PRIVATE KEY-----
diff --git a/tests/_support/rsa-lonely.key b/tests/_support/rsa-lonely.key
new file mode 100644
index 0000000..f50e9c5
--- /dev/null
+++ b/tests/_support/rsa-lonely.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
+oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
+d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
+gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
+EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
+soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
+tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
+avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
+4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
+H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
+qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
+HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
+nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
+-----END RSA PRIVATE KEY-----
diff --git a/tests/_support/rsa-missing.key-cert.pub b/tests/_support/rsa-missing.key-cert.pub
new file mode 100644
index 0000000..7487ab6
--- /dev/null
+++ b/tests/_support/rsa-missing.key-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgsZlXTd5NE4uzGAn6TyAqQj+IPbsTEFGap2x5pTRwQR8AAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAAAAAAAE0gAAAAEAAAAmU2FtcGxlIHNlbGYtc2lnbmVkIE9wZW5TU0ggY2VydGlmaWNhdGUAAAASAAAABXVzZXIxAAAABXVzZXIyAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAACVAAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAACPAAAAB3NzaC1yc2EAAACATFHFsARDgQevc6YLxNnDNjsFtZ08KPMyYVx0w5xm95IVZHVWSOc5w+ccjqN9HRwxV3kP7IvL91qx0Uc3MJdB9g/O6HkAP+rpxTVoTb2EAMekwp5+i8nQJW4CN2BSsbQY1M6r7OBZ5nmF4hOW/5Pu4l22lXe2ydy8kEXOEuRpUeQ= test_rsa.key.pub
diff --git a/tests/_support/rsa.key b/tests/_support/rsa.key
new file mode 100644
index 0000000..f50e9c5
--- /dev/null
+++ b/tests/_support/rsa.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
+oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
+d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
+gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
+EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
+soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
+tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
+avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
+4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
+H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
+qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
+HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
+nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
+-----END RSA PRIVATE KEY-----
diff --git a/tests/_support/rsa.key-cert.pub b/tests/_support/rsa.key-cert.pub
new file mode 100644
index 0000000..7487ab6
--- /dev/null
+++ b/tests/_support/rsa.key-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgsZlXTd5NE4uzGAn6TyAqQj+IPbsTEFGap2x5pTRwQR8AAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAAAAAAAE0gAAAAEAAAAmU2FtcGxlIHNlbGYtc2lnbmVkIE9wZW5TU0ggY2VydGlmaWNhdGUAAAASAAAABXVzZXIxAAAABXVzZXIyAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAACVAAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAACPAAAAB3NzaC1yc2EAAACATFHFsARDgQevc6YLxNnDNjsFtZ08KPMyYVx0w5xm95IVZHVWSOc5w+ccjqN9HRwxV3kP7IvL91qx0Uc3MJdB9g/O6HkAP+rpxTVoTb2EAMekwp5+i8nQJW4CN2BSsbQY1M6r7OBZ5nmF4hOW/5Pu4l22lXe2ydy8kEXOEuRpUeQ= test_rsa.key.pub
diff --git a/tests/_util.py b/tests/_util.py
new file mode 100644
index 0000000..f0ae1d4
--- /dev/null
+++ b/tests/_util.py
@@ -0,0 +1,468 @@
+from contextlib import contextmanager
+from os.path import dirname, realpath, join
+import builtins
+import os
+from pathlib import Path
+import socket
+import struct
+import sys
+import unittest
+import time
+import threading
+
+import pytest
+
+from paramiko import (
+ ServerInterface,
+ RSAKey,
+ DSSKey,
+ AUTH_FAILED,
+ AUTH_PARTIALLY_SUCCESSFUL,
+ AUTH_SUCCESSFUL,
+ OPEN_SUCCEEDED,
+ OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED,
+ InteractiveQuery,
+ Transport,
+)
+from paramiko.ssh_gss import GSS_AUTH_AVAILABLE
+
+from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import padding, rsa
+
+tests_dir = dirname(realpath(__file__))
+
+from ._loop import LoopSocket
+
+
+def _support(filename):
+ base = Path(tests_dir)
+ top = base / filename
+ deeper = base / "_support" / filename
+ return str(deeper if deeper.exists() else top)
+
+
+def _config(name):
+ return join(tests_dir, "configs", name)
+
+
+needs_gssapi = pytest.mark.skipif(
+ not GSS_AUTH_AVAILABLE, reason="No GSSAPI to test"
+)
+
+
+def needs_builtin(name):
+ """
+ Skip decorated test if builtin name does not exist.
+ """
+ reason = "Test requires a builtin '{}'".format(name)
+ return pytest.mark.skipif(not hasattr(builtins, name), reason=reason)
+
+
+slow = pytest.mark.slow
+
+# GSSAPI / Kerberos related tests need a working Kerberos environment.
+# The class `KerberosTestCase` provides such an environment or skips all tests.
+# There are 3 distinct cases:
+#
+# - A Kerberos environment has already been created and the environment
+# contains the required information.
+#
+# - We can use the package 'k5test' to setup an working kerberos environment on
+# the fly.
+#
+# - We skip all tests.
+#
+# ToDo: add a Windows specific implementation?
+
+if (
+ os.environ.get("K5TEST_USER_PRINC", None)
+ and os.environ.get("K5TEST_HOSTNAME", None)
+ and os.environ.get("KRB5_KTNAME", None)
+): # add other vars as needed
+
+ # The environment provides the required information
+ class DummyK5Realm:
+ def __init__(self):
+ for k in os.environ:
+ if not k.startswith("K5TEST_"):
+ continue
+ setattr(self, k[7:].lower(), os.environ[k])
+ self.env = {}
+
+ class KerberosTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.realm = DummyK5Realm()
+
+ @classmethod
+ def tearDownClass(cls):
+ del cls.realm
+
+else:
+ try:
+ # Try to setup a kerberos environment
+ from k5test import KerberosTestCase
+ except Exception:
+ # Use a dummy, that skips all tests
+ class KerberosTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ raise unittest.SkipTest(
+ "Missing extension package k5test. "
+ 'Please run "pip install k5test" '
+ "to install it."
+ )
+
+
+def update_env(testcase, mapping, env=os.environ):
+ """Modify os.environ during a test case and restore during cleanup."""
+ saved_env = env.copy()
+
+ def replace(target, source):
+ target.update(source)
+ for k in list(target):
+ if k not in source:
+ target.pop(k, None)
+
+ testcase.addCleanup(replace, env, saved_env)
+ env.update(mapping)
+ return testcase
+
+
+def k5shell(args=None):
+ """Create a shell with an kerberos environment
+
+ This can be used to debug paramiko or to test the old GSSAPI.
+ To test a different GSSAPI, simply activate a suitable venv
+ within the shell.
+ """
+ import k5test
+ import atexit
+ import subprocess
+
+ k5 = k5test.K5Realm()
+ atexit.register(k5.stop)
+ os.environ.update(k5.env)
+ for n in ("realm", "user_princ", "hostname"):
+ os.environ["K5TEST_" + n.upper()] = getattr(k5, n)
+
+ if not args:
+ args = sys.argv[1:]
+ if not args:
+ args = [os.environ.get("SHELL", "bash")]
+ sys.exit(subprocess.call(args))
+
+
+def is_low_entropy():
+ """
+ Attempts to detect whether running interpreter is low-entropy.
+
+ "low-entropy" is defined as being in 32-bit mode and with the hash seed set
+ to zero.
+ """
+ is_32bit = struct.calcsize("P") == 32 / 8
+ # I don't see a way to tell internally if the hash seed was set this
+ # way, but env should be plenty sufficient, this is only for testing.
+ return is_32bit and os.environ.get("PYTHONHASHSEED", None) == "0"
+
+
+def sha1_signing_unsupported():
+ """
+ This is used to skip tests in environments where SHA-1 signing is
+ not supported by the backend.
+ """
+ private_key = rsa.generate_private_key(
+ public_exponent=65537, key_size=2048, backend=default_backend()
+ )
+ message = b"Some dummy text"
+ try:
+ private_key.sign(
+ message,
+ padding.PSS(
+ mgf=padding.MGF1(hashes.SHA1()),
+ salt_length=padding.PSS.MAX_LENGTH,
+ ),
+ hashes.SHA1(),
+ )
+ return False
+ except UnsupportedAlgorithm as e:
+ return e._reason is _Reasons.UNSUPPORTED_HASH
+
+
+requires_sha1_signing = unittest.skipIf(
+ sha1_signing_unsupported(), "SHA-1 signing not supported"
+)
+
+_disable_sha2 = dict(
+ disabled_algorithms=dict(keys=["rsa-sha2-256", "rsa-sha2-512"])
+)
+_disable_sha1 = dict(disabled_algorithms=dict(keys=["ssh-rsa"]))
+_disable_sha2_pubkey = dict(
+ disabled_algorithms=dict(pubkeys=["rsa-sha2-256", "rsa-sha2-512"])
+)
+_disable_sha1_pubkey = dict(disabled_algorithms=dict(pubkeys=["ssh-rsa"]))
+
+
+unicodey = "\u2022"
+
+
+class TestServer(ServerInterface):
+ paranoid_did_password = False
+ paranoid_did_public_key = False
+ # TODO: make this ed25519 or something else modern? (_is_ this used??)
+ paranoid_key = DSSKey.from_private_key_file(_support("dss.key"))
+
+ def __init__(self, allowed_keys=None):
+ self.allowed_keys = allowed_keys if allowed_keys is not None else []
+
+ def check_channel_request(self, kind, chanid):
+ if kind == "bogus":
+ return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
+ return OPEN_SUCCEEDED
+
+ def check_channel_exec_request(self, channel, command):
+ if command != b"yes":
+ return False
+ return True
+
+ def check_channel_shell_request(self, channel):
+ return True
+
+ def check_global_request(self, kind, msg):
+ self._global_request = kind
+ # NOTE: for w/e reason, older impl of this returned False always, even
+ # tho that's only supposed to occur if the request cannot be served.
+ # For now, leaving that the default unless test supplies specific
+ # 'acceptable' request kind
+ return kind == "acceptable"
+
+ def check_channel_x11_request(
+ self,
+ channel,
+ single_connection,
+ auth_protocol,
+ auth_cookie,
+ screen_number,
+ ):
+ self._x11_single_connection = single_connection
+ self._x11_auth_protocol = auth_protocol
+ self._x11_auth_cookie = auth_cookie
+ self._x11_screen_number = screen_number
+ return True
+
+ def check_port_forward_request(self, addr, port):
+ self._listen = socket.socket()
+ self._listen.bind(("127.0.0.1", 0))
+ self._listen.listen(1)
+ return self._listen.getsockname()[1]
+
+ def cancel_port_forward_request(self, addr, port):
+ self._listen.close()
+ self._listen = None
+
+ def check_channel_direct_tcpip_request(self, chanid, origin, destination):
+ self._tcpip_dest = destination
+ return OPEN_SUCCEEDED
+
+ def get_allowed_auths(self, username):
+ if username == "slowdive":
+ return "publickey,password"
+ if username == "paranoid":
+ if (
+ not self.paranoid_did_password
+ and not self.paranoid_did_public_key
+ ):
+ return "publickey,password"
+ elif self.paranoid_did_password:
+ return "publickey"
+ else:
+ return "password"
+ if username == "commie":
+ return "keyboard-interactive"
+ if username == "utf8":
+ return "password"
+ if username == "non-utf8":
+ return "password"
+ return "publickey"
+
+ def check_auth_password(self, username, password):
+ if (username == "slowdive") and (password == "pygmalion"):
+ return AUTH_SUCCESSFUL
+ if (username == "paranoid") and (password == "paranoid"):
+ # 2-part auth (even openssh doesn't support this)
+ self.paranoid_did_password = True
+ if self.paranoid_did_public_key:
+ return AUTH_SUCCESSFUL
+ return AUTH_PARTIALLY_SUCCESSFUL
+ if (username == "utf8") and (password == unicodey):
+ return AUTH_SUCCESSFUL
+ if (username == "non-utf8") and (password == "\xff"):
+ return AUTH_SUCCESSFUL
+ if username == "bad-server":
+ raise Exception("Ack!")
+ if username == "unresponsive-server":
+ time.sleep(5)
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def check_auth_publickey(self, username, key):
+ if (username == "paranoid") and (key == self.paranoid_key):
+ # 2-part auth
+ self.paranoid_did_public_key = True
+ if self.paranoid_did_password:
+ return AUTH_SUCCESSFUL
+ return AUTH_PARTIALLY_SUCCESSFUL
+ # TODO: make sure all tests incidentally using this to pass, _without
+ # sending a username oops_, get updated somehow - probably via server()
+ # default always injecting a username
+ elif key in self.allowed_keys:
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def check_auth_interactive(self, username, submethods):
+ if username == "commie":
+ self.username = username
+ return InteractiveQuery(
+ "password", "Please enter a password.", ("Password", False)
+ )
+ return AUTH_FAILED
+
+ def check_auth_interactive_response(self, responses):
+ if self.username == "commie":
+ if (len(responses) == 1) and (responses[0] == "cat"):
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+
+@contextmanager
+def server(
+ hostkey=None,
+ init=None,
+ server_init=None,
+ client_init=None,
+ connect=None,
+ pubkeys=None,
+ catch_error=False,
+ transport_factory=None,
+ server_transport_factory=None,
+ defer=False,
+ skip_verify=False,
+):
+ """
+ SSH server contextmanager for testing.
+
+ Yields a tuple of ``(tc, ts)`` (client- and server-side `Transport`
+ objects), or ``(tc, ts, err)`` when ``catch_error==True``.
+
+ :param hostkey:
+ Host key to use for the server; if None, loads
+ ``rsa.key``.
+ :param init:
+ Default `Transport` constructor kwargs to use for both sides.
+ :param server_init:
+ Extends and/or overrides ``init`` for server transport only.
+ :param client_init:
+ Extends and/or overrides ``init`` for client transport only.
+ :param connect:
+ Kwargs to use for ``connect()`` on the client.
+ :param pubkeys:
+ List of public keys for auth.
+ :param catch_error:
+ Whether to capture connection errors & yield from contextmanager.
+ Necessary for connection_time exception testing.
+ :param transport_factory:
+ Like the same-named param in SSHClient: which Transport class to use.
+ :param server_transport_factory:
+ Like ``transport_factory``, but only impacts the server transport.
+ :param bool defer:
+ Whether to defer authentication during connecting.
+
+ This is really just shorthand for ``connect={}`` which would do roughly
+ the same thing. Also: this implies skip_verify=True automatically!
+ :param bool skip_verify:
+ Whether NOT to do the default "make sure auth passed" check.
+ """
+ if init is None:
+ init = {}
+ if server_init is None:
+ server_init = {}
+ if client_init is None:
+ client_init = {}
+ if connect is None:
+ # No auth at all please
+ if defer:
+ connect = dict()
+ # Default username based auth
+ else:
+ connect = dict(username="slowdive", password="pygmalion")
+ socks = LoopSocket()
+ sockc = LoopSocket()
+ sockc.link(socks)
+ if transport_factory is None:
+ transport_factory = Transport
+ if server_transport_factory is None:
+ server_transport_factory = transport_factory
+ tc = transport_factory(sockc, **dict(init, **client_init))
+ ts = server_transport_factory(socks, **dict(init, **server_init))
+
+ if hostkey is None:
+ hostkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ ts.add_server_key(hostkey)
+ event = threading.Event()
+ server = TestServer(allowed_keys=pubkeys)
+ assert not event.is_set()
+ assert not ts.is_active()
+ assert tc.get_username() is None
+ assert ts.get_username() is None
+ assert not tc.is_authenticated()
+ assert not ts.is_authenticated()
+
+ err = None
+ # Trap errors and yield instead of raising right away; otherwise callers
+ # cannot usefully deal with problems at connect time which stem from errors
+ # in the server side.
+ try:
+ ts.start_server(event, server)
+ tc.connect(**connect)
+
+ event.wait(1.0)
+ assert event.is_set()
+ assert ts.is_active()
+ assert tc.is_active()
+
+ except Exception as e:
+ if not catch_error:
+ raise
+ err = e
+
+ yield (tc, ts, err) if catch_error else (tc, ts)
+
+ if not (catch_error or skip_verify or defer):
+ assert ts.is_authenticated()
+ assert tc.is_authenticated()
+
+ tc.close()
+ ts.close()
+ socks.close()
+ sockc.close()
+
+
+def wait_until(condition, *, timeout=2):
+ """
+ Wait until `condition()` no longer raises an `AssertionError` or until
+ `timeout` seconds have passed, which causes a `TimeoutError` to be raised.
+ """
+ deadline = time.time() + timeout
+
+ while True:
+ try:
+ condition()
+ except AssertionError as e:
+ if time.time() > deadline:
+ timeout_message = f"Condition not reached after {timeout}s"
+ raise TimeoutError(timeout_message) from e
+ else:
+ return
+ time.sleep(0.01)
diff --git a/tests/agent.py b/tests/agent.py
new file mode 100644
index 0000000..bcbfb21
--- /dev/null
+++ b/tests/agent.py
@@ -0,0 +1,151 @@
+from unittest.mock import Mock
+
+from pytest import mark, raises
+
+from paramiko import AgentKey, Message, RSAKey
+from paramiko.agent import (
+ SSH2_AGENT_SIGN_RESPONSE,
+ SSH_AGENT_RSA_SHA2_256,
+ SSH_AGENT_RSA_SHA2_512,
+ cSSH2_AGENTC_SIGN_REQUEST,
+)
+
+from ._util import _support
+
+
+# AgentKey with no inner_key
+class _BareAgentKey(AgentKey):
+ def __init__(self, name, blob):
+ self.name = name
+ self.blob = blob
+ self.inner_key = None
+
+
+class AgentKey_:
+ def str_is_repr(self):
+ # Tests for a missed spot in Python 3 upgrades: AgentKey.__str__ was
+ # returning bytes, as if under Python 2. When bug present, this
+ # explodes with "__str__ returned non-string".
+ key = AgentKey(None, b"secret!!!")
+ assert str(key) == repr(key)
+
+ class init:
+ def needs_at_least_two_arguments(self):
+ with raises(TypeError):
+ AgentKey()
+ with raises(TypeError):
+ AgentKey(None)
+
+ def sets_attributes_and_parses_blob(self):
+ agent = Mock()
+ blob = Message()
+ blob.add_string("bad-type")
+ key = AgentKey(agent=agent, blob=bytes(blob))
+ assert key.agent is agent
+ assert key.name == "bad-type"
+ assert key.blob == bytes(blob)
+ assert key.comment == "" # default
+ # TODO: logger testing
+ assert key.inner_key is None # no 'bad-type' algorithm
+
+ def comment_optional(self):
+ blob = Message()
+ blob.add_string("bad-type")
+ key = AgentKey(agent=Mock(), blob=bytes(blob), comment="hi!")
+ assert key.comment == "hi!"
+
+ def sets_inner_key_when_known_type(self, keys):
+ key = AgentKey(agent=Mock(), blob=bytes(keys.pkey))
+ assert key.inner_key == keys.pkey
+
+ class fields:
+ def defaults_to_get_name_and_blob(self):
+ key = _BareAgentKey(name="lol", blob=b"lmao")
+ assert key._fields == ["lol", b"lmao"]
+
+ # TODO: pytest-relaxed is buggy (now?), this shows up under get_bits?
+ def defers_to_inner_key_when_present(self, keys):
+ key = AgentKey(agent=None, blob=keys.pkey.asbytes())
+ assert key._fields == keys.pkey._fields
+ assert key == keys.pkey
+
+ class get_bits:
+ def defaults_to_superclass_implementation(self):
+ # TODO 4.0: assert raises NotImplementedError like changed parent?
+ assert _BareAgentKey(None, None).get_bits() == 0
+
+ def defers_to_inner_key_when_present(self, keys):
+ key = AgentKey(agent=None, blob=keys.pkey.asbytes())
+ assert key.get_bits() == keys.pkey.get_bits()
+
+ class asbytes:
+ def defaults_to_owned_blob(self):
+ blob = Mock()
+ assert _BareAgentKey(name=None, blob=blob).asbytes() is blob
+
+ def defers_to_inner_key_when_present(self, keys):
+ key = AgentKey(agent=None, blob=keys.pkey_with_cert.asbytes())
+ # Artificially make outer key blob != inner key blob; comment in
+ # AgentKey.asbytes implies this can sometimes really happen but I
+ # no longer recall when that could be?
+ key.blob = b"nope"
+ assert key.asbytes() == key.inner_key.asbytes()
+
+ @mark.parametrize(
+ "sign_kwargs,expected_flag",
+ [
+ # No algorithm kwarg: no flags (bitfield -> 0 int)
+ (dict(), 0),
+ (dict(algorithm="rsa-sha2-256"), SSH_AGENT_RSA_SHA2_256),
+ (dict(algorithm="rsa-sha2-512"), SSH_AGENT_RSA_SHA2_512),
+ # TODO: ideally we only send these when key is a cert,
+ # but it doesn't actually break when not; meh. Really just wants
+ # all the parameterization of this test rethought.
+ (
+ dict(algorithm="rsa-sha2-256-cert-v01@openssh.com"),
+ SSH_AGENT_RSA_SHA2_256,
+ ),
+ (
+ dict(algorithm="rsa-sha2-512-cert-v01@openssh.com"),
+ SSH_AGENT_RSA_SHA2_512,
+ ),
+ ],
+ )
+ def signing_data(self, sign_kwargs, expected_flag):
+ class FakeAgent:
+ def _send_message(self, msg):
+ # The thing we actually care most about, we're not testing
+ # ssh-agent itself here
+ self._sent_message = msg
+ sig = Message()
+ sig.add_string("lol")
+ sig.rewind()
+ return SSH2_AGENT_SIGN_RESPONSE, sig
+
+ for do_cert in (False, True):
+ agent = FakeAgent()
+ # Get key kinda like how a real agent would give it to us - if
+ # cert, it'd be the entire public blob, not just the pubkey. This
+ # ensures the code under test sends _just the pubkey part_ back to
+ # the agent during signature requests (bug was us sending _the
+ # entire cert blob_, which somehow "worked ok" but always got us
+ # SHA1)
+ # NOTE: using lower level loader to avoid auto-cert-load when
+ # testing regular key (agents expose them separately)
+ inner_key = RSAKey.from_private_key_file(_support("rsa.key"))
+ blobby = inner_key.asbytes()
+ # NOTE: expected key blob always wants to be the real key, even
+ # when the "key" is a certificate.
+ expected_request_key_blob = blobby
+ if do_cert:
+ inner_key.load_certificate(_support("rsa.key-cert.pub"))
+ blobby = inner_key.public_blob.key_blob
+ key = AgentKey(agent, blobby)
+ result = key.sign_ssh_data(b"data-to-sign", **sign_kwargs)
+ assert result == b"lol"
+ msg = agent._sent_message
+ msg.rewind()
+ assert msg.get_byte() == cSSH2_AGENTC_SIGN_REQUEST
+ assert msg.get_string() == expected_request_key_blob
+ assert msg.get_string() == b"data-to-sign"
+ assert msg.get_int() == expected_flag
diff --git a/tests/auth.py b/tests/auth.py
new file mode 100644
index 0000000..c0afe88
--- /dev/null
+++ b/tests/auth.py
@@ -0,0 +1,580 @@
+"""
+Tests focusing primarily on the authentication step.
+
+Thus, they concern AuthHandler and AuthStrategy, with a side of Transport.
+"""
+
+from logging import Logger
+from unittest.mock import Mock
+
+from pytest import raises
+
+from paramiko import (
+ AgentKey,
+ AuthenticationException,
+ AuthFailure,
+ AuthResult,
+ AuthSource,
+ AuthStrategy,
+ BadAuthenticationType,
+ DSSKey,
+ InMemoryPrivateKey,
+ NoneAuth,
+ OnDiskPrivateKey,
+ Password,
+ PrivateKey,
+ PKey,
+ RSAKey,
+ SSHException,
+ ServiceRequestingTransport,
+ SourceResult,
+)
+
+from ._util import (
+ _disable_sha1_pubkey,
+ _disable_sha2,
+ _disable_sha2_pubkey,
+ _support,
+ requires_sha1_signing,
+ server,
+ unicodey,
+)
+
+
+class AuthHandler_:
+ """
+ Most of these tests are explicit about the auth method they call.
+
+ This is because not too many other tests do so (they rely on the implicit
+ auth trigger of various connect() kwargs).
+ """
+
+ def bad_auth_type(self):
+ """
+ verify that we get the right exception when an unsupported auth
+ type is requested.
+ """
+ # Server won't allow password auth for this user, so should fail
+ # and return just publickey allowed types
+ with server(
+ connect=dict(username="unknown", password="error"),
+ catch_error=True,
+ ) as (_, _, err):
+ assert isinstance(err, BadAuthenticationType)
+ assert err.allowed_types == ["publickey"]
+
+ def bad_password(self):
+ """
+ verify that a bad password gets the right exception, and that a retry
+ with the right password works.
+ """
+ # NOTE: Transport.connect doesn't do any auth upfront if no userauth
+ # related kwargs given.
+ with server(defer=True) as (tc, ts):
+ # Auth once, badly
+ with raises(AuthenticationException):
+ tc.auth_password(username="slowdive", password="error")
+ # And again, correctly
+ tc.auth_password(username="slowdive", password="pygmalion")
+
+ def multipart_auth(self):
+ """
+ verify that multipart auth works.
+ """
+ with server(defer=True) as (tc, ts):
+ assert tc.auth_password(
+ username="paranoid", password="paranoid"
+ ) == ["publickey"]
+ key = DSSKey.from_private_key_file(_support("dss.key"))
+ assert tc.auth_publickey(username="paranoid", key=key) == []
+
+ def interactive_auth(self):
+ """
+ verify keyboard-interactive auth works.
+ """
+
+ def handler(title, instructions, prompts):
+ self.got_title = title
+ self.got_instructions = instructions
+ self.got_prompts = prompts
+ return ["cat"]
+
+ with server(defer=True) as (tc, ts):
+ assert tc.auth_interactive("commie", handler) == []
+ assert self.got_title == "password"
+ assert self.got_prompts == [("Password", False)]
+
+ def interactive_fallback(self):
+ """
+ verify that a password auth attempt will fallback to "interactive"
+ if password auth isn't supported but interactive is.
+ """
+ with server(defer=True) as (tc, ts):
+ # This username results in an allowed_auth of just kbd-int,
+ # and has a configured interactive->response on the server.
+ assert tc.auth_password("commie", "cat") == []
+
+ def utf8(self):
+ """
+ verify that utf-8 encoding happens in authentication.
+ """
+ with server(defer=True) as (tc, ts):
+ assert tc.auth_password("utf8", unicodey) == []
+
+ def non_utf8(self):
+ """
+ verify that non-utf-8 encoded passwords can be used for broken
+ servers.
+ """
+ with server(defer=True) as (tc, ts):
+ assert tc.auth_password("non-utf8", "\xff") == []
+
+ def auth_exception_when_disconnected(self):
+ """
+ verify that we catch a server disconnecting during auth, and report
+ it as an auth failure.
+ """
+ with server(defer=True, skip_verify=True) as (tc, ts), raises(
+ AuthenticationException
+ ):
+ tc.auth_password("bad-server", "hello")
+
+ def non_responsive_triggers_auth_exception(self):
+ """
+ verify that authentication times out if server takes to long to
+ respond (or never responds).
+ """
+ with server(defer=True, skip_verify=True) as (tc, ts), raises(
+ AuthenticationException
+ ) as info:
+ tc.auth_timeout = 1 # 1 second, to speed up test
+ tc.auth_password("unresponsive-server", "hello")
+ assert "Authentication timeout" in str(info.value)
+
+
+class AuthOnlyHandler_:
+ def _server(self, *args, **kwargs):
+ kwargs.setdefault("transport_factory", ServiceRequestingTransport)
+ return server(*args, **kwargs)
+
+ class fallback_pubkey_algorithm:
+ @requires_sha1_signing
+ def key_type_algo_selected_when_no_server_sig_algs(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ # Server pretending to be an apparently common setup:
+ # - doesn't support (or have enabled) sha2
+ # - also doesn't support (or have enabled) server-sig-algs/ext-info
+ # This is the scenario in which Paramiko has to guess-the-algo, and
+ # where servers that don't support sha2 or server-sig-algs can give
+ # us trouble.
+ server_init = dict(_disable_sha2_pubkey, server_sig_algs=False)
+ with self._server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ server_init=server_init,
+ catch_error=True,
+ ) as (tc, ts, err):
+ # Auth did work
+ assert tc.is_authenticated()
+ # Selected ssh-rsa, instead of first-in-the-list (rsa-sha2-512)
+ assert tc._agreed_pubkey_algorithm == "ssh-rsa"
+
+ @requires_sha1_signing
+ def key_type_algo_selection_is_cert_suffix_aware(self):
+ # This key has a cert next to it, which should trigger cert-aware
+ # loading within key classes.
+ privkey = PKey.from_path(_support("rsa.key"))
+ server_init = dict(_disable_sha2_pubkey, server_sig_algs=False)
+ with self._server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ server_init=server_init,
+ catch_error=True,
+ ) as (tc, ts, err):
+ assert not err
+ # Auth did work
+ assert tc.is_authenticated()
+ # Selected expected cert type
+ assert (
+ tc._agreed_pubkey_algorithm
+ == "ssh-rsa-cert-v01@openssh.com"
+ )
+
+ @requires_sha1_signing
+ def uses_first_preferred_algo_if_key_type_not_in_list(self):
+ # This is functionally the same as legacy AuthHandler, just
+ # arriving at the same place in a different manner.
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ server_init = dict(_disable_sha2_pubkey, server_sig_algs=False)
+ with self._server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ server_init=server_init,
+ client_init=_disable_sha1_pubkey, # no ssh-rsa
+ catch_error=True,
+ ) as (tc, ts, err):
+ assert not tc.is_authenticated()
+ assert isinstance(err, AuthenticationException)
+ assert tc._agreed_pubkey_algorithm == "rsa-sha2-512"
+
+
+class SHA2SignaturePubkeys:
+ def pubkey_auth_honors_disabled_algorithms(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ with server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ init=dict(
+ disabled_algorithms=dict(
+ pubkeys=["ssh-rsa", "rsa-sha2-256", "rsa-sha2-512"]
+ )
+ ),
+ catch_error=True,
+ ) as (_, _, err):
+ assert isinstance(err, SSHException)
+ assert "no RSA pubkey algorithms" in str(err)
+
+ def client_sha2_disabled_server_sha1_disabled_no_match(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ with server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ client_init=_disable_sha2_pubkey,
+ server_init=_disable_sha1_pubkey,
+ catch_error=True,
+ ) as (tc, ts, err):
+ assert isinstance(err, AuthenticationException)
+
+ def client_sha1_disabled_server_sha2_disabled_no_match(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ with server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ client_init=_disable_sha1_pubkey,
+ server_init=_disable_sha2_pubkey,
+ catch_error=True,
+ ) as (tc, ts, err):
+ assert isinstance(err, AuthenticationException)
+
+ @requires_sha1_signing
+ def ssh_rsa_still_used_when_sha2_disabled(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ # NOTE: this works because key obj comparison uses public bytes
+ # TODO: would be nice for PKey to grow a legit "give me another obj of
+ # same class but just the public bits" using asbytes()
+ with server(
+ pubkeys=[privkey], connect=dict(pkey=privkey), init=_disable_sha2
+ ) as (tc, _):
+ assert tc.is_authenticated()
+
+ @requires_sha1_signing
+ def first_client_preferred_algo_used_when_no_server_sig_algs(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ # Server pretending to be an apparently common setup:
+ # - doesn't support (or have enabled) sha2
+ # - also doesn't support (or have enabled) server-sig-algs/ext-info
+ # This is the scenario in which Paramiko has to guess-the-algo, and
+ # where servers that don't support sha2 or server-sig-algs give us
+ # trouble.
+ server_init = dict(_disable_sha2_pubkey, server_sig_algs=False)
+ with server(
+ pubkeys=[privkey],
+ connect=dict(username="slowdive", pkey=privkey),
+ server_init=server_init,
+ catch_error=True,
+ ) as (tc, ts, err):
+ assert not tc.is_authenticated()
+ assert isinstance(err, AuthenticationException)
+ # Oh no! this isn't ssh-rsa, and our server doesn't support sha2!
+ assert tc._agreed_pubkey_algorithm == "rsa-sha2-512"
+
+ def sha2_512(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ with server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ init=dict(
+ disabled_algorithms=dict(pubkeys=["ssh-rsa", "rsa-sha2-256"])
+ ),
+ ) as (tc, ts):
+ assert tc.is_authenticated()
+ assert tc._agreed_pubkey_algorithm == "rsa-sha2-512"
+
+ def sha2_256(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ with server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ init=dict(
+ disabled_algorithms=dict(pubkeys=["ssh-rsa", "rsa-sha2-512"])
+ ),
+ ) as (tc, ts):
+ assert tc.is_authenticated()
+ assert tc._agreed_pubkey_algorithm == "rsa-sha2-256"
+
+ def sha2_256_when_client_only_enables_256(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ with server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ # Client-side only; server still accepts all 3.
+ client_init=dict(
+ disabled_algorithms=dict(pubkeys=["ssh-rsa", "rsa-sha2-512"])
+ ),
+ ) as (tc, ts):
+ assert tc.is_authenticated()
+ assert tc._agreed_pubkey_algorithm == "rsa-sha2-256"
+
+
+class AuthSource_:
+ class base_class:
+ def init_requires_and_saves_username(self):
+ with raises(TypeError):
+ AuthSource()
+ assert AuthSource(username="foo").username == "foo"
+
+ def dunder_repr_delegates_to_helper(self):
+ source = AuthSource("foo")
+ source._repr = Mock(wraps=lambda: "whatever")
+ repr(source)
+ source._repr.assert_called_once_with()
+
+ def repr_helper_prints_basic_kv_pairs(self):
+ assert repr(AuthSource("foo")) == "AuthSource()"
+ assert (
+ AuthSource("foo")._repr(bar="open") == "AuthSource(bar='open')"
+ )
+
+ def authenticate_takes_transport_and_is_abstract(self):
+ # TODO: this test kinda just goes away once we're typed?
+ with raises(TypeError):
+ AuthSource("foo").authenticate()
+ with raises(NotImplementedError):
+ AuthSource("foo").authenticate(None)
+
+ class NoneAuth_:
+ def authenticate_auths_none(self):
+ trans = Mock()
+ result = NoneAuth("foo").authenticate(trans)
+ trans.auth_none.assert_called_once_with("foo")
+ assert result is trans.auth_none.return_value
+
+ def repr_shows_class(self):
+ assert repr(NoneAuth("foo")) == "NoneAuth()"
+
+ class Password_:
+ def init_takes_and_stores_password_getter(self):
+ with raises(TypeError):
+ Password("foo")
+ getter = Mock()
+ pw = Password("foo", password_getter=getter)
+ assert pw.password_getter is getter
+
+ def repr_adds_username(self):
+ pw = Password("foo", password_getter=Mock())
+ assert repr(pw) == "Password(user='foo')"
+
+ def authenticate_gets_and_supplies_password(self):
+ getter = Mock(return_value="bar")
+ trans = Mock()
+ pw = Password("foo", password_getter=getter)
+ result = pw.authenticate(trans)
+ trans.auth_password.assert_called_once_with("foo", "bar")
+ assert result is trans.auth_password.return_value
+
+ class PrivateKey_:
+ def authenticate_calls_publickey_with_pkey(self):
+ source = PrivateKey(username="foo")
+ source.pkey = Mock() # set by subclasses
+ trans = Mock()
+ result = source.authenticate(trans)
+ trans.auth_publickey.assert_called_once_with("foo", source.pkey)
+ assert result is trans.auth_publickey.return_value
+
+ class InMemoryPrivateKey_:
+ def init_takes_pkey_object(self):
+ with raises(TypeError):
+ InMemoryPrivateKey("foo")
+ pkey = Mock()
+ source = InMemoryPrivateKey(username="foo", pkey=pkey)
+ assert source.pkey is pkey
+
+ def repr_shows_pkey_repr(self):
+ pkey = PKey.from_path(_support("ed25519.key"))
+ source = InMemoryPrivateKey("foo", pkey)
+ assert (
+ repr(source)
+ == "InMemoryPrivateKey(pkey=PKey(alg=ED25519, bits=256, fp=SHA256:J6VESFdD3xSChn8y9PzWzeF+1tl892mOy2TqkMLO4ow))" # noqa
+ )
+
+ def repr_appends_agent_flag_when_AgentKey(self):
+ real_key = PKey.from_path(_support("ed25519.key"))
+ pkey = AgentKey(agent=None, blob=bytes(real_key))
+ source = InMemoryPrivateKey("foo", pkey)
+ assert (
+ repr(source)
+ == "InMemoryPrivateKey(pkey=PKey(alg=ED25519, bits=256, fp=SHA256:J6VESFdD3xSChn8y9PzWzeF+1tl892mOy2TqkMLO4ow)) [agent]" # noqa
+ )
+
+ class OnDiskPrivateKey_:
+ def init_takes_source_path_and_pkey(self):
+ with raises(TypeError):
+ OnDiskPrivateKey("foo")
+ with raises(TypeError):
+ OnDiskPrivateKey("foo", "bar")
+ with raises(TypeError):
+ OnDiskPrivateKey("foo", "bar", "biz")
+ source = OnDiskPrivateKey(
+ username="foo",
+ source="ssh-config",
+ path="of-exile",
+ pkey="notreally",
+ )
+ assert source.username == "foo"
+ assert source.source == "ssh-config"
+ assert source.path == "of-exile"
+ assert source.pkey == "notreally"
+
+ def init_requires_specific_value_for_source(self):
+ with raises(
+ ValueError,
+ match=r"source argument must be one of: \('ssh-config', 'python-config', 'implicit-home'\)", # noqa
+ ):
+ OnDiskPrivateKey("foo", source="what?", path="meh", pkey="no")
+
+ def repr_reflects_source_path_and_pkey(self):
+ source = OnDiskPrivateKey(
+ username="foo",
+ source="ssh-config",
+ path="of-exile",
+ pkey="notreally",
+ )
+ assert (
+ repr(source)
+ == "OnDiskPrivateKey(key='notreally', source='ssh-config', path='of-exile')" # noqa
+ )
+
+
+class AuthResult_:
+ def setup_method(self):
+ self.strat = AuthStrategy(None)
+
+ def acts_like_list_with_strategy_attribute(self):
+ with raises(TypeError):
+ AuthResult()
+ # kwarg works by itself
+ AuthResult(strategy=self.strat)
+ # or can be given as posarg w/ regular list() args after
+ result = AuthResult(self.strat, [1, 2, 3])
+ assert result.strategy is self.strat
+ assert result == [1, 2, 3]
+ assert isinstance(result, list)
+
+ def repr_is_list_repr_untouched(self):
+ result = AuthResult(self.strat, [1, 2, 3])
+ assert repr(result) == "[1, 2, 3]"
+
+ class dunder_str:
+ def is_multiline_display_of_sourceresult_tuples(self):
+ result = AuthResult(self.strat)
+ result.append(SourceResult("foo", "bar"))
+ result.append(SourceResult("biz", "baz"))
+ assert str(result) == "foo -> bar\nbiz -> baz"
+
+ def shows_str_not_repr_of_auth_source_and_result(self):
+ result = AuthResult(self.strat)
+ result.append(
+ SourceResult(NoneAuth("foo"), ["password", "pubkey"])
+ )
+ assert str(result) == "NoneAuth() -> ['password', 'pubkey']"
+
+ def empty_list_result_values_show_success_string(self):
+ result = AuthResult(self.strat)
+ result.append(SourceResult(NoneAuth("foo"), []))
+ assert str(result) == "NoneAuth() -> success"
+
+
+class AuthFailure_:
+ def is_an_AuthenticationException(self):
+ assert isinstance(AuthFailure(None), AuthenticationException)
+
+ def init_requires_result(self):
+ with raises(TypeError):
+ AuthFailure()
+ result = AuthResult(None)
+ fail = AuthFailure(result=result)
+ assert fail.result is result
+
+ def str_is_newline_plus_result_str(self):
+ result = AuthResult(None)
+ result.append(SourceResult(NoneAuth("foo"), Exception("onoz")))
+ fail = AuthFailure(result)
+ assert str(fail) == "\nNoneAuth() -> onoz"
+
+
+class AuthStrategy_:
+ def init_requires_ssh_config_param_and_sets_up_a_logger(self):
+ with raises(TypeError):
+ AuthStrategy()
+ conf = object()
+ strat = AuthStrategy(ssh_config=conf)
+ assert strat.ssh_config is conf
+ assert isinstance(strat.log, Logger)
+ assert strat.log.name == "paramiko.auth_strategy"
+
+ def get_sources_is_abstract(self):
+ with raises(NotImplementedError):
+ AuthStrategy(None).get_sources()
+
+ class authenticate:
+ def setup_method(self):
+ self.strat = AuthStrategy(None) # ssh_config not used directly
+ self.source, self.transport = NoneAuth(None), Mock()
+ self.source.authenticate = Mock()
+ self.strat.get_sources = Mock(return_value=[self.source])
+
+ def requires_and_uses_transport_with_methods_returning_result(self):
+ with raises(TypeError):
+ self.strat.authenticate()
+ result = self.strat.authenticate(self.transport)
+ self.strat.get_sources.assert_called_once_with()
+ self.source.authenticate.assert_called_once_with(self.transport)
+ assert isinstance(result, AuthResult)
+ assert result.strategy is self.strat
+ assert len(result) == 1
+ source_res = result[0]
+ assert isinstance(source_res, SourceResult)
+ assert source_res.source is self.source
+ assert source_res.result is self.source.authenticate.return_value
+
+ def logs_sources_attempted(self):
+ self.strat.log = Mock()
+ self.strat.authenticate(self.transport)
+ self.strat.log.debug.assert_called_once_with("Trying NoneAuth()")
+
+ def raises_AuthFailure_if_no_successes(self):
+ self.strat.log = Mock()
+ oops = Exception("onoz")
+ self.source.authenticate.side_effect = oops
+ with raises(AuthFailure) as info:
+ self.strat.authenticate(self.transport)
+ result = info.value.result
+ assert isinstance(result, AuthResult)
+ assert len(result) == 1
+ source_res = result[0]
+ assert isinstance(source_res, SourceResult)
+ assert source_res.source is self.source
+ assert source_res.result is oops
+ self.strat.log.info.assert_called_once_with(
+ "Authentication via NoneAuth() failed with Exception"
+ )
+
+ def short_circuits_on_successful_auth(self):
+ kaboom = Mock(authenticate=Mock(side_effect=Exception("onoz")))
+ self.strat.get_sources.return_value = [self.source, kaboom]
+ result = self.strat.authenticate(self.transport)
+ # No exception, and it's just a regular ol Result
+ assert isinstance(result, AuthResult)
+ # And it did not capture any attempt to execute the 2nd source
+ assert len(result) == 1
+ assert result[0].source is self.source
diff --git a/tests/badhash_key1.ed25519.key b/tests/badhash_key1.ed25519.key
new file mode 100644
index 0000000..3e33781
--- /dev/null
+++ b/tests/badhash_key1.ed25519.key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUx
+OQAAACCULQjdmVfwpbDAFYz4mhKo6aCiAfkbaC+dEdq5eP1R9QAAAIjXZhzv12Yc7wAAAAtzc2gt
+ZWQyNTUxOQAAACCULQjdmVfwpbDAFYz4mhKo6aCiAfkbaC+dEdq5eP1R9QAAAEByeJbhZUBL2aJ6
+wP85amzQuqDJRrNyAGMtDBJ43SURxpQtCN2ZV/ClsMAVjPiaEqjpoKIB+RtoL50R2rl4/VH1AAAA
+AAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/badhash_key2.ed25519.key b/tests/badhash_key2.ed25519.key
new file mode 100644
index 0000000..bf48eda
--- /dev/null
+++ b/tests/badhash_key2.ed25519.key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUx
+OQAAACACJbNmFu2Bk34HArxhiRYajoIN03Vr0umfNvsc9atE0AAAAIi5+po1ufqaNQAAAAtzc2gt
+ZWQyNTUxOQAAACACJbNmFu2Bk34HArxhiRYajoIN03Vr0umfNvsc9atE0AAAAECh/ZzZJDOZGnil
+BxJMm+nOhBpc07IVBjU1ii+S8zqFaAIls2YW7YGTfgcCvGGJFhqOgg3TdWvS6Z82+xz1q0TQAAAA
+AAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/blank_rsa.key b/tests/blank_rsa.key
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/blank_rsa.key
diff --git a/tests/configs/basic b/tests/configs/basic
new file mode 100644
index 0000000..93fe3be
--- /dev/null
+++ b/tests/configs/basic
@@ -0,0 +1,4 @@
+CanonicalDomains paramiko.org
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/canon b/tests/configs/canon
new file mode 100644
index 0000000..7b97940
--- /dev/null
+++ b/tests/configs/canon
@@ -0,0 +1,8 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+IdentityFile base.key
+
+Host www.paramiko.org
+ User rando
+ IdentityFile canonicalized.key
diff --git a/tests/configs/canon-always b/tests/configs/canon-always
new file mode 100644
index 0000000..f3f56b7
--- /dev/null
+++ b/tests/configs/canon-always
@@ -0,0 +1,5 @@
+CanonicalDomains paramiko.org
+CanonicalizeHostname always
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/canon-ipv4 b/tests/configs/canon-ipv4
new file mode 100644
index 0000000..92c3875
--- /dev/null
+++ b/tests/configs/canon-ipv4
@@ -0,0 +1,6 @@
+CanonicalDomains paramiko.org
+CanonicalizeHostname yes
+AddressFamily inet
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/canon-local b/tests/configs/canon-local
new file mode 100644
index 0000000..dde9f77
--- /dev/null
+++ b/tests/configs/canon-local
@@ -0,0 +1,6 @@
+Host www.paramiko.org
+ User rando
+
+Host www
+ CanonicalDomains paramiko.org
+ CanonicalizeHostname yes
diff --git a/tests/configs/canon-local-always b/tests/configs/canon-local-always
new file mode 100644
index 0000000..0ad0535
--- /dev/null
+++ b/tests/configs/canon-local-always
@@ -0,0 +1,6 @@
+Host www.paramiko.org
+ User rando
+
+Host www
+ CanonicalDomains paramiko.org
+ CanonicalizeHostname always
diff --git a/tests/configs/deep-canon b/tests/configs/deep-canon
new file mode 100644
index 0000000..483823d
--- /dev/null
+++ b/tests/configs/deep-canon
@@ -0,0 +1,11 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Host www.paramiko.org
+ User rando
+
+Host sub.www.paramiko.org
+ User deep
+
+Host subber.sub.www.paramiko.org
+ User deeper
diff --git a/tests/configs/deep-canon-maxdots b/tests/configs/deep-canon-maxdots
new file mode 100644
index 0000000..7785f66
--- /dev/null
+++ b/tests/configs/deep-canon-maxdots
@@ -0,0 +1,12 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeMaxDots 2
+
+Host www.paramiko.org
+ User rando
+
+Host sub.www.paramiko.org
+ User deep
+
+Host subber.sub.www.paramiko.org
+ User deeper
diff --git a/tests/configs/empty-canon b/tests/configs/empty-canon
new file mode 100644
index 0000000..19743ad
--- /dev/null
+++ b/tests/configs/empty-canon
@@ -0,0 +1,6 @@
+CanonicalizeHostname yes
+CanonicalDomains
+AddressFamily inet
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/fallback-no b/tests/configs/fallback-no
new file mode 100644
index 0000000..ec8d13e
--- /dev/null
+++ b/tests/configs/fallback-no
@@ -0,0 +1,6 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeFallbackLocal no
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/fallback-yes b/tests/configs/fallback-yes
new file mode 100644
index 0000000..bc4f4ee
--- /dev/null
+++ b/tests/configs/fallback-yes
@@ -0,0 +1,6 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeFallbackLocal yes
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/hostname-exec-tokenized b/tests/configs/hostname-exec-tokenized
new file mode 100644
index 0000000..1cae2c0
--- /dev/null
+++ b/tests/configs/hostname-exec-tokenized
@@ -0,0 +1,2 @@
+Match exec "ping %h"
+ HostName pingable.%h
diff --git a/tests/configs/hostname-tokenized b/tests/configs/hostname-tokenized
new file mode 100644
index 0000000..1905c0c
--- /dev/null
+++ b/tests/configs/hostname-tokenized
@@ -0,0 +1 @@
+HostName prefix.%h
diff --git a/tests/configs/invalid b/tests/configs/invalid
new file mode 100644
index 0000000..81332fe
--- /dev/null
+++ b/tests/configs/invalid
@@ -0,0 +1 @@
+lolwut
diff --git a/tests/configs/match-all b/tests/configs/match-all
new file mode 100644
index 0000000..7673e0a
--- /dev/null
+++ b/tests/configs/match-all
@@ -0,0 +1,2 @@
+Match all
+ User awesome
diff --git a/tests/configs/match-all-after-canonical b/tests/configs/match-all-after-canonical
new file mode 100644
index 0000000..531112c
--- /dev/null
+++ b/tests/configs/match-all-after-canonical
@@ -0,0 +1,5 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Match canonical all
+ User awesome
diff --git a/tests/configs/match-all-and-more b/tests/configs/match-all-and-more
new file mode 100644
index 0000000..bb50696
--- /dev/null
+++ b/tests/configs/match-all-and-more
@@ -0,0 +1,2 @@
+Match all exec "lol nope"
+ HostName whatever
diff --git a/tests/configs/match-all-and-more-before b/tests/configs/match-all-and-more-before
new file mode 100644
index 0000000..4d5b2e3
--- /dev/null
+++ b/tests/configs/match-all-and-more-before
@@ -0,0 +1,2 @@
+Match exec "lol nope" all
+ HostName whatever
diff --git a/tests/configs/match-all-before-canonical b/tests/configs/match-all-before-canonical
new file mode 100644
index 0000000..35e3b0e
--- /dev/null
+++ b/tests/configs/match-all-before-canonical
@@ -0,0 +1,5 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Match all canonical
+ User oops
diff --git a/tests/configs/match-canonical-no b/tests/configs/match-canonical-no
new file mode 100644
index 0000000..e528dc6
--- /dev/null
+++ b/tests/configs/match-canonical-no
@@ -0,0 +1,7 @@
+CanonicalizeHostname no
+
+Match canonical all
+ User awesome
+
+Match !canonical host specific
+ User overload
diff --git a/tests/configs/match-canonical-yes b/tests/configs/match-canonical-yes
new file mode 100644
index 0000000..d6c2092
--- /dev/null
+++ b/tests/configs/match-canonical-yes
@@ -0,0 +1,5 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Match !canonical host www*
+ User hidden
diff --git a/tests/configs/match-complex b/tests/configs/match-complex
new file mode 100644
index 0000000..6363403
--- /dev/null
+++ b/tests/configs/match-complex
@@ -0,0 +1,17 @@
+HostName bogus
+
+Match originalhost target host bogus
+ User rand
+
+Match originalhost remote localuser rando
+ User calrissian
+
+# Just to set user for subsequent match
+Match originalhost www
+ User calrissian
+
+Match !canonical originalhost www host bogus localuser rando user calrissian
+ Port 7777
+
+Match !canonical !originalhost www host bogus localuser rando !user calrissian
+ Port 1234
diff --git a/tests/configs/match-exec b/tests/configs/match-exec
new file mode 100644
index 0000000..62a147a
--- /dev/null
+++ b/tests/configs/match-exec
@@ -0,0 +1,16 @@
+Match exec "quoted"
+ User benjamin
+
+Match exec unquoted
+ User rando
+
+Match exec "quoted spaced"
+ User neil
+
+# Just to prepopulate values for tokenizing subsequent exec
+Host target
+ User intermediate
+ HostName configured
+
+Match exec "%C %d %h %L %l %n %p %r %u"
+ Port 1337
diff --git a/tests/configs/match-exec-canonical b/tests/configs/match-exec-canonical
new file mode 100644
index 0000000..794ee9d
--- /dev/null
+++ b/tests/configs/match-exec-canonical
@@ -0,0 +1,10 @@
+CanonicalDomains paramiko.org
+CanonicalizeHostname always
+
+# This will match in the first, uncanonicalized pass
+Match !canonical exec uncanonicalized
+ User defenseless
+
+# And this will match the second time
+Match canonical exec canonicalized
+ Port 8007
diff --git a/tests/configs/match-exec-negation b/tests/configs/match-exec-negation
new file mode 100644
index 0000000..937c910
--- /dev/null
+++ b/tests/configs/match-exec-negation
@@ -0,0 +1,5 @@
+Match !exec "this succeeds"
+ User nope
+
+Match !exec "this fails"
+ User yup
diff --git a/tests/configs/match-exec-no-arg b/tests/configs/match-exec-no-arg
new file mode 100644
index 0000000..20c16d1
--- /dev/null
+++ b/tests/configs/match-exec-no-arg
@@ -0,0 +1,2 @@
+Match exec
+ User uh-oh
diff --git a/tests/configs/match-final b/tests/configs/match-final
new file mode 100644
index 0000000..21e927f
--- /dev/null
+++ b/tests/configs/match-final
@@ -0,0 +1,14 @@
+Host jump
+ HostName jump.example.org
+ Port 1003
+
+Host finally
+ HostName finally.example.org
+ Port 1001
+
+Host default-port
+ HostName default-port.example.org
+
+Match final host "*.example.org" !host jump.example.org
+ ProxyJump jump
+ Port 1002
diff --git a/tests/configs/match-host b/tests/configs/match-host
new file mode 100644
index 0000000..86cbff5
--- /dev/null
+++ b/tests/configs/match-host
@@ -0,0 +1,2 @@
+Match host target
+ User rand
diff --git a/tests/configs/match-host-canonicalized b/tests/configs/match-host-canonicalized
new file mode 100644
index 0000000..52dadea
--- /dev/null
+++ b/tests/configs/match-host-canonicalized
@@ -0,0 +1,8 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Match host www.paramiko.org
+ User rand
+
+Match canonical host docs.paramiko.org
+ User eric
diff --git a/tests/configs/match-host-from-match b/tests/configs/match-host-from-match
new file mode 100644
index 0000000..172ee11
--- /dev/null
+++ b/tests/configs/match-host-from-match
@@ -0,0 +1,5 @@
+Match host original-host
+ HostName substituted-host
+
+Match host substituted-host
+ User inner
diff --git a/tests/configs/match-host-glob b/tests/configs/match-host-glob
new file mode 100644
index 0000000..3d53cf4
--- /dev/null
+++ b/tests/configs/match-host-glob
@@ -0,0 +1,2 @@
+Match host *ever
+ User matrim
diff --git a/tests/configs/match-host-glob-list b/tests/configs/match-host-glob-list
new file mode 100644
index 0000000..3617d13
--- /dev/null
+++ b/tests/configs/match-host-glob-list
@@ -0,0 +1,8 @@
+Match host *ever
+ User matrim
+
+Match host somehost,someotherhost
+ User thom
+
+Match host goo*,!goof
+ User perrin
diff --git a/tests/configs/match-host-name b/tests/configs/match-host-name
new file mode 100644
index 0000000..783d939
--- /dev/null
+++ b/tests/configs/match-host-name
@@ -0,0 +1,4 @@
+HostName default-host
+
+Match host default-host
+ User silly
diff --git a/tests/configs/match-host-negated b/tests/configs/match-host-negated
new file mode 100644
index 0000000..7c5d3f3
--- /dev/null
+++ b/tests/configs/match-host-negated
@@ -0,0 +1,2 @@
+Match !host www
+ User jeff
diff --git a/tests/configs/match-host-no-arg b/tests/configs/match-host-no-arg
new file mode 100644
index 0000000..191cebb
--- /dev/null
+++ b/tests/configs/match-host-no-arg
@@ -0,0 +1,2 @@
+Match host
+ User oops
diff --git a/tests/configs/match-localuser b/tests/configs/match-localuser
new file mode 100644
index 0000000..fe4a276
--- /dev/null
+++ b/tests/configs/match-localuser
@@ -0,0 +1,14 @@
+Match localuser gandalf
+ HostName gondor
+
+Match localuser b*
+ HostName shire
+
+Match localuser aragorn,frodo
+ HostName moria
+
+Match localuser gimli,!legolas
+ Port 7373
+
+Match !localuser sauron
+ HostName mordor
diff --git a/tests/configs/match-localuser-no-arg b/tests/configs/match-localuser-no-arg
new file mode 100644
index 0000000..6623553
--- /dev/null
+++ b/tests/configs/match-localuser-no-arg
@@ -0,0 +1,2 @@
+Match localuser
+ User oops
diff --git a/tests/configs/match-orighost b/tests/configs/match-orighost
new file mode 100644
index 0000000..1054199
--- /dev/null
+++ b/tests/configs/match-orighost
@@ -0,0 +1,16 @@
+HostName bogus
+
+Match originalhost target
+ User tuon
+
+Match originalhost what*
+ User matrim
+
+Match originalhost comma,sep*
+ User chameleon
+
+Match originalhost yep,!nope
+ User skipped
+
+Match !originalhost www !originalhost nope
+ User thom
diff --git a/tests/configs/match-orighost-canonical b/tests/configs/match-orighost-canonical
new file mode 100644
index 0000000..737345e
--- /dev/null
+++ b/tests/configs/match-orighost-canonical
@@ -0,0 +1,5 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+
+Match originalhost www
+ User tuon
diff --git a/tests/configs/match-orighost-no-arg b/tests/configs/match-orighost-no-arg
new file mode 100644
index 0000000..427382b
--- /dev/null
+++ b/tests/configs/match-orighost-no-arg
@@ -0,0 +1,2 @@
+Match originalhost
+ User oops
diff --git a/tests/configs/match-user b/tests/configs/match-user
new file mode 100644
index 0000000..14d6ac1
--- /dev/null
+++ b/tests/configs/match-user
@@ -0,0 +1,14 @@
+Match user gandalf
+ HostName gondor
+
+Match user b*
+ HostName shire
+
+Match user aragorn,frodo
+ HostName moria
+
+Match user gimli,!legolas
+ Port 7373
+
+Match !user sauron
+ HostName mordor
diff --git a/tests/configs/match-user-explicit b/tests/configs/match-user-explicit
new file mode 100644
index 0000000..9a2b1d8
--- /dev/null
+++ b/tests/configs/match-user-explicit
@@ -0,0 +1,4 @@
+User explicit
+
+Match user explicit
+ HostName dumb
diff --git a/tests/configs/match-user-no-arg b/tests/configs/match-user-no-arg
new file mode 100644
index 0000000..65a11ab
--- /dev/null
+++ b/tests/configs/match-user-no-arg
@@ -0,0 +1,2 @@
+Match user
+ User oops
diff --git a/tests/configs/multi-canon-domains b/tests/configs/multi-canon-domains
new file mode 100644
index 0000000..5674b44
--- /dev/null
+++ b/tests/configs/multi-canon-domains
@@ -0,0 +1,5 @@
+CanonicalizeHostname yes
+CanonicalDomains not-a-real-tld paramiko.org
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/no-canon b/tests/configs/no-canon
new file mode 100644
index 0000000..033f8c5
--- /dev/null
+++ b/tests/configs/no-canon
@@ -0,0 +1,5 @@
+CanonicalizeHostname no
+CanonicalDomains paramiko.org
+
+Host www.paramiko.org
+ User rando
diff --git a/tests/configs/robey b/tests/configs/robey
new file mode 100644
index 0000000..b202622
--- /dev/null
+++ b/tests/configs/robey
@@ -0,0 +1,17 @@
+# A timeless classic?
+# NOTE: some lines in here have 'extra' whitespace (incl trailing, and mixed
+# tabs/spaces!) on purpose.
+
+Host *
+ User robey
+ IdentityFile =~/.ssh/id_rsa
+
+# comment
+Host *.example.com
+ User bjork
+Port=3333
+Host *
+ Crazy something dumb
+Host spoo.example.com
+Crazy something else
+
diff --git a/tests/configs/zero-maxdots b/tests/configs/zero-maxdots
new file mode 100644
index 0000000..dc00054
--- /dev/null
+++ b/tests/configs/zero-maxdots
@@ -0,0 +1,9 @@
+CanonicalizeHostname yes
+CanonicalDomains paramiko.org
+CanonicalizeMaxDots 0
+
+Host www.paramiko.org
+ User rando
+
+Host sub.www.paramiko.org
+ User deep
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..12b9728
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,170 @@
+import logging
+import os
+import shutil
+import threading
+from pathlib import Path
+
+from invoke.vendor.lexicon import Lexicon
+
+import pytest
+from paramiko import (
+ SFTPServer,
+ SFTP,
+ Transport,
+ DSSKey,
+ RSAKey,
+ Ed25519Key,
+ ECDSAKey,
+ PKey,
+)
+
+from ._loop import LoopSocket
+from ._stub_sftp import StubServer, StubSFTPServer
+from ._util import _support
+
+from icecream import ic, install as install_ic
+
+
+# Better print() for debugging - use ic()!
+install_ic()
+ic.configureOutput(includeContext=True)
+
+
+# Perform logging by default; pytest will capture and thus hide it normally,
+# presenting it on error/failure. (But also allow turning it off when doing
+# very pinpoint debugging - e.g. using breakpoints, so you don't want output
+# hiding enabled, but also don't want all the logging to gum up the terminal.)
+if not os.environ.get("DISABLE_LOGGING", False):
+ logging.basicConfig(
+ level=logging.DEBUG,
+ # Also make sure to set up timestamping for more sanity when debugging.
+ format="[%(relativeCreated)s]\t%(levelname)s:%(name)s:%(message)s",
+ datefmt="%H:%M:%S",
+ )
+
+
+def make_sftp_folder():
+ """
+ Ensure expected target temp folder exists on the remote end.
+
+ Will clean it out if it already exists.
+ """
+ # TODO: go back to using the sftp functionality itself for folder setup so
+ # we can test against live SFTP servers again someday. (Not clear if anyone
+ # is/was using the old capability for such, though...)
+ # TODO: something that would play nicer with concurrent testing (but
+ # probably e.g. using thread ID or UUIDs or something; not the "count up
+ # until you find one not used!" crap from before...)
+ # TODO: if we want to lock ourselves even harder into localhost-only
+ # testing (probably not?) could use tempdir modules for this for improved
+ # safety. Then again...why would someone have such a folder???
+ path = os.environ.get("TEST_FOLDER", "paramiko-test-target")
+ # Forcibly nuke this directory locally, since at the moment, the below
+ # fixtures only ever run with a locally scoped stub test server.
+ shutil.rmtree(path, ignore_errors=True)
+ # Then create it anew, again locally, for the same reason.
+ os.mkdir(path)
+ return path
+
+
+@pytest.fixture # (scope='session')
+def sftp_server():
+ """
+ Set up an in-memory SFTP server thread. Yields the client Transport/socket.
+
+ The resulting client Transport (along with all the server components) will
+ be the same object throughout the test session; the `sftp` fixture then
+ creates new higher level client objects wrapped around the client
+ Transport, as necessary.
+ """
+ # Sockets & transports
+ socks = LoopSocket()
+ sockc = LoopSocket()
+ sockc.link(socks)
+ # TODO: reuse with new server fixture if possible
+ tc = Transport(sockc)
+ ts = Transport(socks)
+ # Auth
+ host_key = RSAKey.from_private_key_file(_support("rsa.key"))
+ ts.add_server_key(host_key)
+ # Server setup
+ event = threading.Event()
+ server = StubServer()
+ ts.set_subsystem_handler("sftp", SFTPServer, StubSFTPServer)
+ ts.start_server(event, server)
+ # Wait (so client has time to connect? Not sure. Old.)
+ event.wait(1.0)
+ # Make & yield connection.
+ tc.connect(username="slowdive", password="pygmalion")
+ yield tc
+ # TODO: any need for shutdown? Why didn't old suite do so? Or was that the
+ # point of the "join all threads from threading module" crap in test.py?
+
+
+@pytest.fixture
+def sftp(sftp_server):
+ """
+ Yield an SFTP client connected to the global in-session SFTP server thread.
+ """
+ # Client setup
+ client = SFTP.from_transport(sftp_server)
+ # Work in 'remote' folder setup (as it wants to use the client)
+ # TODO: how cleanest to make this available to tests? Doing it this way is
+ # marginally less bad than the previous 'global'-using setup, but not by
+ # much?
+ client.FOLDER = make_sftp_folder()
+ # Yield client to caller
+ yield client
+ # Clean up - as in make_sftp_folder, we assume local-only exec for now.
+ shutil.rmtree(client.FOLDER, ignore_errors=True)
+
+
+key_data = [
+ ["ssh-rsa", RSAKey, "SHA256:OhNL391d/beeFnxxg18AwWVYTAHww+D4djEE7Co0Yng"],
+ ["ssh-dss", DSSKey, "SHA256:uHwwykG099f4M4kfzvFpKCTino0/P03DRbAidpAmPm0"],
+ [
+ "ssh-ed25519",
+ Ed25519Key,
+ "SHA256:J6VESFdD3xSChn8y9PzWzeF+1tl892mOy2TqkMLO4ow",
+ ],
+ [
+ "ecdsa-sha2-nistp256",
+ ECDSAKey,
+ "SHA256:BrQG04oNKUETjKCeL4ifkARASg3yxS/pUHl3wWM26Yg",
+ ],
+]
+for datum in key_data:
+ # Add true first member with human-facing short algo name
+ short = datum[0].replace("ssh-", "").replace("sha2-nistp", "")
+ datum.insert(0, short)
+
+
+@pytest.fixture(scope="session", params=key_data, ids=lambda x: x[0])
+def keys(request):
+ """
+ Yield an object for each known type of key, with attributes:
+
+ - ``short_type``: short identifier, eg ``rsa`` or ``ecdsa-256``
+ - ``full_type``: the "message style" key identifier, eg ``ssh-rsa``, or
+ ``ecdsa-sha2-nistp256``.
+ - ``path``: a pathlib Path object to the fixture key file
+ - ``pkey``: PKey object, which may or may not also have a cert loaded
+ - ``expected_fp``: the expected fingerprint of said key
+ """
+ short_type, key_type, key_class, fingerprint = request.param
+ bag = Lexicon()
+ bag.short_type = short_type
+ bag.full_type = key_type
+ bag.path = Path(_support(f"{short_type}.key"))
+ with bag.path.open() as fd:
+ bag.pkey = key_class.from_private_key(fd)
+ # Second copy for things like equality-but-not-identity testing
+ with bag.path.open() as fd:
+ bag.pkey2 = key_class.from_private_key(fd)
+ bag.expected_fp = fingerprint
+ # Also tack on the cert-bearing variant for some tests
+ cert = bag.path.with_suffix(".key-cert.pub")
+ bag.pkey_with_cert = PKey.from_path(cert) if cert.exists() else None
+ # Safety checks
+ assert bag.pkey.fingerprint == fingerprint
+ yield bag
diff --git a/tests/pkey.py b/tests/pkey.py
new file mode 100644
index 0000000..691fda0
--- /dev/null
+++ b/tests/pkey.py
@@ -0,0 +1,229 @@
+from pathlib import Path
+from unittest.mock import patch, call
+
+from pytest import raises
+
+from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey
+from paramiko import (
+ DSSKey,
+ ECDSAKey,
+ Ed25519Key,
+ Message,
+ PKey,
+ PublicBlob,
+ RSAKey,
+ UnknownKeyType,
+)
+
+from ._util import _support
+
+
+class PKey_:
+ # NOTE: this is incidentally tested by a number of other tests, such as the
+ # agent.py test suite
+ class from_type_string:
+ def loads_from_type_and_bytes(self, keys):
+ obj = PKey.from_type_string(keys.full_type, keys.pkey.asbytes())
+ assert obj == keys.pkey
+
+ # TODO: exceptions
+ #
+ # TODO: passphrase? OTOH since this is aimed at the agent...irrelephant
+
+ class from_path:
+ def loads_from_Path(self, keys):
+ obj = PKey.from_path(keys.path)
+ assert obj == keys.pkey
+
+ def loads_from_str(self):
+ key = PKey.from_path(str(_support("rsa.key")))
+ assert isinstance(key, RSAKey)
+
+ @patch("paramiko.pkey.Path")
+ def expands_user(self, mPath):
+ # real key for guts that want a real key format
+ mykey = Path(_support("rsa.key"))
+ pathy = mPath.return_value.expanduser.return_value
+ # read_bytes for cryptography.io's loaders
+ pathy.read_bytes.return_value = mykey.read_bytes()
+ # open() for our own class loader
+ pathy.open.return_value = mykey.open()
+ # fake out exists() to avoid attempts to load cert
+ pathy.exists.return_value = False
+ PKey.from_path("whatever") # we're not testing expanduser itself
+ # Both key and cert paths
+ mPath.return_value.expanduser.assert_has_calls([call(), call()])
+
+ def raises_UnknownKeyType_for_unknown_types(self):
+ # I.e. a real, becomes a useful object via cryptography.io, key
+ # class that we do NOT support. Chose Ed448 randomly as OpenSSH
+ # doesn't seem to support it either, going by ssh-keygen...
+ keypath = _support("ed448.key")
+ with raises(UnknownKeyType) as exc:
+ PKey.from_path(keypath)
+ assert issubclass(exc.value.key_type, Ed448PrivateKey)
+ with open(keypath, "rb") as fd:
+ assert exc.value.key_bytes == fd.read()
+
+ def leaves_cryptography_exceptions_untouched(self):
+ # a Python file is not a private key!
+ with raises(ValueError):
+ PKey.from_path(__file__)
+
+ # TODO: passphrase support tested
+
+ class automatically_loads_certificates:
+ def existing_cert_loaded_when_given_key_path(self):
+ key = PKey.from_path(_support("rsa.key"))
+ # Public blob exists despite no .load_certificate call
+ assert key.public_blob is not None
+ assert (
+ key.public_blob.key_type == "ssh-rsa-cert-v01@openssh.com"
+ )
+ # And it's definitely the one we expected
+ assert key.public_blob == PublicBlob.from_file(
+ _support("rsa.key-cert.pub")
+ )
+
+ def can_be_given_cert_path_instead(self):
+ key = PKey.from_path(_support("rsa.key-cert.pub"))
+ # It's still a key, not a PublicBlob
+ assert isinstance(key, RSAKey)
+ # Public blob exists despite no .load_certificate call
+ assert key.public_blob is not None
+ assert (
+ key.public_blob.key_type == "ssh-rsa-cert-v01@openssh.com"
+ )
+ # And it's definitely the one we expected
+ assert key.public_blob == PublicBlob.from_file(
+ _support("rsa.key-cert.pub")
+ )
+
+ def no_cert_load_if_no_cert(self):
+ # This key exists (it's a copy of the regular one) but has no
+ # matching -cert.pub
+ key = PKey.from_path(_support("rsa-lonely.key"))
+ assert key.public_blob is None
+
+ def excepts_usefully_if_no_key_only_cert(self):
+ # TODO: is that truly an error condition? the cert is ~the
+ # pubkey and we still require the privkey for signing, yea?
+ # This cert exists (it's a copy of the regular one) but there's
+ # no rsa-missing.key to load.
+ with raises(FileNotFoundError) as info:
+ PKey.from_path(_support("rsa-missing.key-cert.pub"))
+ assert info.value.filename.endswith("rsa-missing.key")
+
+ class load_certificate:
+ def rsa_public_cert_blobs(self):
+ # Data to test signing with (arbitrary)
+ data = b"ice weasels"
+ # Load key w/o cert at first (so avoiding .from_path)
+ key = RSAKey.from_private_key_file(_support("rsa.key"))
+ assert key.public_blob is None
+ # Sign regular-style (using, arbitrarily, SHA2)
+ msg = key.sign_ssh_data(data, "rsa-sha2-256")
+ msg.rewind()
+ assert "rsa-sha2-256" == msg.get_text()
+ signed = msg.get_binary() # for comparison later
+
+ # Load cert and inspect its internals
+ key.load_certificate(_support("rsa.key-cert.pub"))
+ assert key.public_blob is not None
+ assert key.public_blob.key_type == "ssh-rsa-cert-v01@openssh.com"
+ assert key.public_blob.comment == "test_rsa.key.pub"
+ msg = Message(key.public_blob.key_blob)
+ # cert type
+ assert msg.get_text() == "ssh-rsa-cert-v01@openssh.com"
+ # nonce
+ msg.get_string()
+ # public numbers
+ assert msg.get_mpint() == key.public_numbers.e
+ assert msg.get_mpint() == key.public_numbers.n
+ # serial number
+ assert msg.get_int64() == 1234
+ # TODO: whoever wrote the OG tests didn't care about the remaining
+ # fields from
+ # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys
+ # so neither do I, for now...
+
+ # Sign cert-style (still SHA256 - so this actually does almost
+ # exactly the same thing under the hood as the previous sign)
+ msg = key.sign_ssh_data(data, "rsa-sha2-256-cert-v01@openssh.com")
+ msg.rewind()
+ assert "rsa-sha2-256" == msg.get_text()
+ assert signed == msg.get_binary() # same signature as above
+ msg.rewind()
+ assert key.verify_ssh_sig(b"ice weasels", msg) # our data verified
+
+ def loading_cert_of_different_type_from_key_raises_ValueError(self):
+ edkey = Ed25519Key.from_private_key_file(_support("ed25519.key"))
+ err = "PublicBlob type ssh-rsa-cert-v01@openssh.com incompatible with key type ssh-ed25519" # noqa
+ with raises(ValueError, match=err):
+ edkey.load_certificate(_support("rsa.key-cert.pub"))
+
+ def fingerprint(self, keys):
+ # NOTE: Hardcoded fingerprint expectation stored in fixture.
+ assert keys.pkey.fingerprint == keys.expected_fp
+
+ def algorithm_name(self, keys):
+ key = keys.pkey
+ if isinstance(key, RSAKey):
+ assert key.algorithm_name == "RSA"
+ elif isinstance(key, DSSKey):
+ assert key.algorithm_name == "DSS"
+ elif isinstance(key, ECDSAKey):
+ assert key.algorithm_name == "ECDSA"
+ elif isinstance(key, Ed25519Key):
+ assert key.algorithm_name == "ED25519"
+ # TODO: corner case: AgentKey, whose .name can be cert-y (due to the
+ # value of the name field passed via agent protocol) and thus
+ # algorithm_name is eg "RSA-CERT" - keys loaded directly from disk will
+ # never look this way, even if they have a .public_blob attached.
+
+ class equality_and_hashing:
+ def same_key_is_equal_to_itself(self, keys):
+ assert keys.pkey == keys.pkey2
+
+ def same_key_same_hash(self, keys):
+ # NOTE: this isn't a great test due to hashseed randomization under
+ # Python 3 preventing use of static values, but it does still prove
+ # that __hash__ is implemented/doesn't explode & works across
+ # instances
+ assert hash(keys.pkey) == hash(keys.pkey2)
+
+ def keys_are_not_equal_to_other_types(self, keys):
+ for value in [None, True, ""]:
+ assert keys.pkey != value
+
+ class identifiers_classmethods:
+ def default_is_class_name_attribute(self):
+ # NOTE: not all classes _have_ this, only the ones that don't
+ # customize identifiers().
+ class MyKey(PKey):
+ name = "it me"
+
+ assert MyKey.identifiers() == ["it me"]
+
+ def rsa_is_all_combos_of_cert_and_sha_type(self):
+ assert RSAKey.identifiers() == [
+ "ssh-rsa",
+ "ssh-rsa-cert-v01@openssh.com",
+ "rsa-sha2-256",
+ "rsa-sha2-256-cert-v01@openssh.com",
+ "rsa-sha2-512",
+ "rsa-sha2-512-cert-v01@openssh.com",
+ ]
+
+ def dss_is_protocol_name(self):
+ assert DSSKey.identifiers() == ["ssh-dss"]
+
+ def ed25519_is_protocol_name(self):
+ assert Ed25519Key.identifiers() == ["ssh-ed25519"]
+
+ def ecdsa_is_all_curve_names(self):
+ assert ECDSAKey.identifiers() == [
+ "ecdsa-sha2-nistp256",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp521",
+ ]
diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py
new file mode 100644
index 0000000..35e2cde
--- /dev/null
+++ b/tests/test_buffered_pipe.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for BufferedPipe.
+"""
+
+import threading
+import time
+import unittest
+
+from paramiko.buffered_pipe import BufferedPipe, PipeTimeout
+from paramiko import pipe
+
+
+def delay_thread(p):
+ p.feed("a")
+ time.sleep(0.5)
+ p.feed("b")
+ p.close()
+
+
+def close_thread(p):
+ time.sleep(0.2)
+ p.close()
+
+
+class BufferedPipeTest(unittest.TestCase):
+ def test_buffered_pipe(self):
+ p = BufferedPipe()
+ self.assertTrue(not p.read_ready())
+ p.feed("hello.")
+ self.assertTrue(p.read_ready())
+ data = p.read(6)
+ self.assertEqual(b"hello.", data)
+
+ p.feed("plus/minus")
+ self.assertEqual(b"plu", p.read(3))
+ self.assertEqual(b"s/m", p.read(3))
+ self.assertEqual(b"inus", p.read(4))
+
+ p.close()
+ self.assertTrue(not p.read_ready())
+ self.assertEqual(b"", p.read(1))
+
+ def test_delay(self):
+ p = BufferedPipe()
+ self.assertTrue(not p.read_ready())
+ threading.Thread(target=delay_thread, args=(p,)).start()
+ self.assertEqual(b"a", p.read(1, 0.1))
+ try:
+ p.read(1, 0.1)
+ self.assertTrue(False)
+ except PipeTimeout:
+ pass
+ self.assertEqual(b"b", p.read(1, 1.0))
+ self.assertEqual(b"", p.read(1))
+
+ def test_close_while_reading(self):
+ p = BufferedPipe()
+ threading.Thread(target=close_thread, args=(p,)).start()
+ data = p.read(1, 1.0)
+ self.assertEqual(b"", data)
+
+ def test_or_pipe(self):
+ p = pipe.make_pipe()
+ p1, p2 = pipe.make_or_pipe(p)
+ self.assertFalse(p._set)
+ p1.set()
+ self.assertTrue(p._set)
+ p2.set()
+ self.assertTrue(p._set)
+ p1.clear()
+ self.assertTrue(p._set)
+ p2.clear()
+ self.assertFalse(p._set)
diff --git a/tests/test_channelfile.py b/tests/test_channelfile.py
new file mode 100644
index 0000000..e2b6306
--- /dev/null
+++ b/tests/test_channelfile.py
@@ -0,0 +1,60 @@
+from unittest.mock import patch, MagicMock
+
+from paramiko import Channel, ChannelFile, ChannelStderrFile, ChannelStdinFile
+
+
+class ChannelFileBase:
+ @patch("paramiko.channel.ChannelFile._set_mode")
+ def test_defaults_to_unbuffered_reading(self, setmode):
+ self.klass(Channel(None))
+ setmode.assert_called_once_with("r", -1)
+
+ @patch("paramiko.channel.ChannelFile._set_mode")
+ def test_can_override_mode_and_bufsize(self, setmode):
+ self.klass(Channel(None), mode="w", bufsize=25)
+ setmode.assert_called_once_with("w", 25)
+
+ def test_read_recvs_from_channel(self):
+ chan = MagicMock()
+ cf = self.klass(chan)
+ cf.read(100)
+ chan.recv.assert_called_once_with(100)
+
+ def test_write_calls_channel_sendall(self):
+ chan = MagicMock()
+ cf = self.klass(chan, mode="w")
+ cf.write("ohai")
+ chan.sendall.assert_called_once_with(b"ohai")
+
+
+class TestChannelFile(ChannelFileBase):
+ klass = ChannelFile
+
+
+class TestChannelStderrFile:
+ def test_read_calls_channel_recv_stderr(self):
+ chan = MagicMock()
+ cf = ChannelStderrFile(chan)
+ cf.read(100)
+ chan.recv_stderr.assert_called_once_with(100)
+
+ def test_write_calls_channel_sendall(self):
+ chan = MagicMock()
+ cf = ChannelStderrFile(chan, mode="w")
+ cf.write("ohai")
+ chan.sendall_stderr.assert_called_once_with(b"ohai")
+
+
+class TestChannelStdinFile(ChannelFileBase):
+ klass = ChannelStdinFile
+
+ def test_close_calls_channel_shutdown_write(self):
+ chan = MagicMock()
+ cf = ChannelStdinFile(chan, mode="wb")
+ cf.flush = MagicMock()
+ cf.close()
+ # Sanity check that we still call BufferedFile.close()
+ cf.flush.assert_called_once_with()
+ assert cf._closed is True
+ # Actual point of test
+ chan.shutdown_write.assert_called_once_with()
diff --git a/tests/test_client.py b/tests/test_client.py
new file mode 100644
index 0000000..1c0c6c8
--- /dev/null
+++ b/tests/test_client.py
@@ -0,0 +1,837 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for SSHClient.
+"""
+
+
+import gc
+import os
+import platform
+import socket
+import threading
+import time
+import unittest
+import warnings
+import weakref
+from tempfile import mkstemp
+
+import pytest
+from pytest_relaxed import raises
+from unittest.mock import patch, Mock
+
+import paramiko
+from paramiko import SSHClient
+from paramiko.pkey import PublicBlob
+from paramiko.ssh_exception import SSHException, AuthenticationException
+
+from ._util import _support, requires_sha1_signing, slow
+
+
+requires_gss_auth = unittest.skipUnless(
+ paramiko.GSS_AUTH_AVAILABLE, "GSS auth not available"
+)
+
+FINGERPRINTS = {
+ "ssh-dss": b"\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c", # noqa
+ "ssh-rsa": b"\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5", # noqa
+ "ecdsa-sha2-nistp256": b"\x25\x19\xeb\x55\xe6\xa1\x47\xff\x4f\x38\xd2\x75\x6f\xa5\xd5\x60", # noqa
+ "ssh-ed25519": b'\xb3\xd5"\xaa\xf9u^\xe8\xcd\x0e\xea\x02\xb9)\xa2\x80',
+}
+
+
+class NullServer(paramiko.ServerInterface):
+ def __init__(self, *args, **kwargs):
+ # Allow tests to enable/disable specific key types
+ self.__allowed_keys = kwargs.pop("allowed_keys", [])
+ # And allow them to set a (single...meh) expected public blob (cert)
+ self.__expected_public_blob = kwargs.pop("public_blob", None)
+ super().__init__(*args, **kwargs)
+
+ def get_allowed_auths(self, username):
+ if username == "slowdive":
+ return "publickey,password"
+ return "publickey"
+
+ 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):
+ try:
+ expected = FINGERPRINTS[key.get_name()]
+ except KeyError:
+ return paramiko.AUTH_FAILED
+ # Base check: allowed auth type & fingerprint matches
+ happy = (
+ key.get_name() in self.__allowed_keys
+ and key.get_fingerprint() == expected
+ )
+ # Secondary check: if test wants assertions about cert data
+ if (
+ self.__expected_public_blob is not None
+ and key.public_blob != self.__expected_public_blob
+ ):
+ happy = False
+ return paramiko.AUTH_SUCCESSFUL if happy else paramiko.AUTH_FAILED
+
+ def check_channel_request(self, kind, chanid):
+ return paramiko.OPEN_SUCCEEDED
+
+ def check_channel_exec_request(self, channel, command):
+ if command != b"yes":
+ return False
+ return True
+
+ def check_channel_env_request(self, channel, name, value):
+ if name == "INVALID_ENV":
+ return False
+
+ if not hasattr(channel, "env"):
+ setattr(channel, "env", {})
+
+ channel.env[name] = value
+ return True
+
+
+class ClientTest(unittest.TestCase):
+ def setUp(self):
+ self.sockl = socket.socket()
+ self.sockl.bind(("localhost", 0))
+ self.sockl.listen(1)
+ self.addr, self.port = self.sockl.getsockname()
+ self.connect_kwargs = dict(
+ hostname=self.addr,
+ port=self.port,
+ username="slowdive",
+ look_for_keys=False,
+ )
+ self.event = threading.Event()
+ self.kill_event = threading.Event()
+
+ def tearDown(self):
+ # Shut down client Transport
+ if hasattr(self, "tc"):
+ self.tc.close()
+ # Shut down shared socket
+ if hasattr(self, "sockl"):
+ # Signal to server thread that it should shut down early; it checks
+ # this immediately after accept(). (In scenarios where connection
+ # actually succeeded during the test, this becomes a no-op.)
+ self.kill_event.set()
+ # Forcibly connect to server sock in case the server thread is
+ # hanging out in its accept() (e.g. if the client side of the test
+ # fails before it even gets to connecting); there's no other good
+ # way to force an accept() to exit.
+ put_a_sock_in_it = socket.socket()
+ put_a_sock_in_it.connect((self.addr, self.port))
+ put_a_sock_in_it.close()
+ # Then close "our" end of the socket (which _should_ cause the
+ # accept() to bail out, but does not, for some reason. I blame
+ # threading.)
+ self.sockl.close()
+
+ def _run(
+ self,
+ allowed_keys=None,
+ delay=0,
+ public_blob=None,
+ kill_event=None,
+ server_name=None,
+ ):
+ if allowed_keys is None:
+ allowed_keys = FINGERPRINTS.keys()
+ self.socks, addr = self.sockl.accept()
+ # If the kill event was set at this point, it indicates an early
+ # shutdown, so bail out now and don't even try setting up a Transport
+ # (which will just verbosely die.)
+ if kill_event and kill_event.is_set():
+ self.socks.close()
+ return
+ self.ts = paramiko.Transport(self.socks)
+ if server_name is not None:
+ self.ts.local_version = server_name
+ keypath = _support("rsa.key")
+ host_key = paramiko.RSAKey.from_private_key_file(keypath)
+ self.ts.add_server_key(host_key)
+ keypath = _support("ecdsa-256.key")
+ host_key = paramiko.ECDSAKey.from_private_key_file(keypath)
+ self.ts.add_server_key(host_key)
+ server = NullServer(allowed_keys=allowed_keys, public_blob=public_blob)
+ if delay:
+ time.sleep(delay)
+ self.ts.start_server(self.event, server)
+
+ def _test_connection(self, **kwargs):
+ """
+ (Most) kwargs get passed directly into SSHClient.connect().
+
+ The exceptions are ``allowed_keys``/``public_blob``/``server_name``
+ which are stripped and handed to the ``NullServer`` used for testing.
+ """
+ run_kwargs = {"kill_event": self.kill_event}
+ for key in ("allowed_keys", "public_blob", "server_name"):
+ run_kwargs[key] = kwargs.pop(key, None)
+ # Server setup
+ threading.Thread(target=self._run, kwargs=run_kwargs).start()
+ host_key = paramiko.RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ # Client setup
+ self.tc = SSHClient()
+ self.tc.get_host_keys().add(
+ f"[{self.addr}]:{self.port}", "ssh-rsa", public_host_key
+ )
+
+ # Actual connection
+ self.tc.connect(**dict(self.connect_kwargs, **kwargs))
+
+ # Authentication successful?
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+ self.assertEqual(
+ self.connect_kwargs["username"], self.ts.get_username()
+ )
+ self.assertEqual(True, self.ts.is_authenticated())
+ self.assertEqual(False, self.tc.get_transport().gss_kex_used)
+
+ # Command execution functions?
+ stdin, stdout, stderr = self.tc.exec_command("yes")
+ schan = self.ts.accept(1.0)
+
+ # Nobody else tests the API of exec_command so let's do it here for
+ # now. :weary:
+ assert isinstance(stdin, paramiko.ChannelStdinFile)
+ assert isinstance(stdout, paramiko.ChannelFile)
+ assert isinstance(stderr, paramiko.ChannelStderrFile)
+
+ schan.send("Hello there.\n")
+ schan.send_stderr("This is on stderr.\n")
+ schan.close()
+
+ self.assertEqual("Hello there.\n", stdout.readline())
+ self.assertEqual("", stdout.readline())
+ self.assertEqual("This is on stderr.\n", stderr.readline())
+ self.assertEqual("", stderr.readline())
+
+ # Cleanup
+ stdin.close()
+ stdout.close()
+ stderr.close()
+
+
+class SSHClientTest(ClientTest):
+ @requires_sha1_signing
+ def test_client(self):
+ """
+ verify that the SSHClient stuff works too.
+ """
+ self._test_connection(password="pygmalion")
+
+ @requires_sha1_signing
+ def test_client_dsa(self):
+ """
+ verify that SSHClient works with a DSA key.
+ """
+ self._test_connection(key_filename=_support("dss.key"))
+
+ @requires_sha1_signing
+ def test_client_rsa(self):
+ """
+ verify that SSHClient works with an RSA key.
+ """
+ self._test_connection(key_filename=_support("rsa.key"))
+
+ @requires_sha1_signing
+ def test_client_ecdsa(self):
+ """
+ verify that SSHClient works with an ECDSA key.
+ """
+ self._test_connection(key_filename=_support("ecdsa-256.key"))
+
+ @requires_sha1_signing
+ def test_client_ed25519(self):
+ self._test_connection(key_filename=_support("ed25519.key"))
+
+ @requires_sha1_signing
+ def test_multiple_key_files(self):
+ """
+ verify that SSHClient accepts and tries multiple key files.
+ """
+ # This is dumb :(
+ types_ = {
+ "rsa": "ssh-rsa",
+ "dss": "ssh-dss",
+ "ecdsa": "ecdsa-sha2-nistp256",
+ }
+ # Various combos of attempted & valid keys
+ # TODO: try every possible combo using itertools functions
+ # TODO: use new key(s) fixture(s)
+ for attempt, accept in (
+ (["rsa", "dss"], ["dss"]), # Original test #3
+ (["dss", "rsa"], ["dss"]), # Ordering matters sometimes, sadly
+ (["dss", "rsa", "ecdsa-256"], ["dss"]), # Try ECDSA but fail
+ (["rsa", "ecdsa-256"], ["ecdsa"]), # ECDSA success
+ ):
+ try:
+ self._test_connection(
+ key_filename=[
+ _support("{}.key".format(x)) for x in attempt
+ ],
+ allowed_keys=[types_[x] for x in accept],
+ )
+ finally:
+ # Clean up to avoid occasional gc-related deadlocks.
+ # TODO: use nose test generators after nose port
+ self.tearDown()
+ self.setUp()
+
+ @requires_sha1_signing
+ def test_multiple_key_files_failure(self):
+ """
+ Expect failure when multiple keys in play and none are accepted
+ """
+ # Until #387 is fixed we have to catch a high-up exception since
+ # various platforms trigger different errors here >_<
+ self.assertRaises(
+ SSHException,
+ self._test_connection,
+ key_filename=[_support("rsa.key")],
+ allowed_keys=["ecdsa-sha2-nistp256"],
+ )
+
+ @requires_sha1_signing
+ def test_certs_allowed_as_key_filename_values(self):
+ # NOTE: giving cert path here, not key path. (Key path test is below.
+ # They're similar except for which path is given; the expected auth and
+ # server-side behavior is 100% identical.)
+ # NOTE: only bothered whipping up one cert per overall class/family.
+ for type_ in ("rsa", "dss", "ecdsa-256", "ed25519"):
+ key_path = _support(f"{type_}.key")
+ self._test_connection(
+ key_filename=key_path,
+ public_blob=PublicBlob.from_file(f"{key_path}-cert.pub"),
+ )
+
+ @requires_sha1_signing
+ def test_certs_implicitly_loaded_alongside_key_filename_keys(self):
+ # NOTE: a regular test_connection() w/ rsa.key would incidentally
+ # test this (because test_xxx.key-cert.pub exists) but incidental tests
+ # stink, so NullServer and friends were updated to allow assertions
+ # about the server-side key object's public blob. Thus, we can prove
+ # that a specific cert was found, along with regular authorization
+ # succeeding proving that the overall flow works.
+ for type_ in ("rsa", "dss", "ecdsa-256", "ed25519"):
+ key_path = _support(f"{type_}.key")
+ self._test_connection(
+ key_filename=key_path,
+ public_blob=PublicBlob.from_file(f"{key_path}-cert.pub"),
+ )
+
+ def _cert_algo_test(self, ver, alg):
+ # Issue #2017; see auth_handler.py
+ self.connect_kwargs["username"] = "somecertuser" # neuter pw auth
+ self._test_connection(
+ # NOTE: SSHClient is able to take either the key or the cert & will
+ # set up its internals as needed
+ key_filename=_support("rsa.key-cert.pub"),
+ server_name="SSH-2.0-OpenSSH_{}".format(ver),
+ )
+ assert (
+ self.tc._transport._agreed_pubkey_algorithm
+ == "{}-cert-v01@openssh.com".format(alg)
+ )
+
+ @requires_sha1_signing
+ def test_old_openssh_needs_ssh_rsa_for_certs_not_rsa_sha2(self):
+ self._cert_algo_test(ver="7.7", alg="ssh-rsa")
+
+ @requires_sha1_signing
+ def test_newer_openssh_uses_rsa_sha2_for_certs_not_ssh_rsa(self):
+ # NOTE: 512 happens to be first in our list and is thus chosen
+ self._cert_algo_test(ver="7.8", alg="rsa-sha2-512")
+
+ def test_default_key_locations_trigger_cert_loads_if_found(self):
+ # TODO: what it says on the tin: ~/.ssh/id_rsa tries to load
+ # ~/.ssh/id_rsa-cert.pub. Right now no other tests actually test that
+ # code path (!) so we're punting too, sob.
+ pass
+
+ def test_auto_add_policy(self):
+ """
+ verify that SSHClient's AutoAddPolicy works.
+ """
+ threading.Thread(target=self._run).start()
+ hostname = f"[{self.addr}]:{self.port}"
+ key_file = _support("ecdsa-256.key")
+ public_host_key = paramiko.ECDSAKey.from_private_key_file(key_file)
+
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self.assertEqual(0, len(self.tc.get_host_keys()))
+ self.tc.connect(password="pygmalion", **self.connect_kwargs)
+
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+ self.assertEqual("slowdive", self.ts.get_username())
+ self.assertEqual(True, self.ts.is_authenticated())
+ self.assertEqual(1, len(self.tc.get_host_keys()))
+ new_host_key = list(self.tc.get_host_keys()[hostname].values())[0]
+ self.assertEqual(public_host_key, new_host_key)
+
+ def test_save_host_keys(self):
+ """
+ verify that SSHClient correctly saves a known_hosts file.
+ """
+ warnings.filterwarnings("ignore", "tempnam.*")
+
+ host_key = paramiko.RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+ fd, localname = mkstemp()
+ os.close(fd)
+
+ client = SSHClient()
+ assert len(client.get_host_keys()) == 0
+
+ host_id = f"[{self.addr}]:{self.port}"
+
+ client.get_host_keys().add(host_id, "ssh-rsa", public_host_key)
+ assert len(client.get_host_keys()) == 1
+ assert public_host_key == client.get_host_keys()[host_id]["ssh-rsa"]
+
+ client.save_host_keys(localname)
+
+ with open(localname) as fd:
+ assert host_id in fd.read()
+
+ os.unlink(localname)
+
+ def test_cleanup(self):
+ """
+ verify that when an SSHClient is collected, its transport (and the
+ transport's packetizer) is closed.
+ """
+ # Skipped on PyPy because it fails on CI for unknown reasons
+ if platform.python_implementation() == "PyPy":
+ return
+
+ threading.Thread(target=self._run).start()
+
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ assert len(self.tc.get_host_keys()) == 0
+ self.tc.connect(**dict(self.connect_kwargs, password="pygmalion"))
+
+ self.event.wait(1.0)
+ assert self.event.is_set()
+ assert self.ts.is_active()
+
+ p = weakref.ref(self.tc._transport.packetizer)
+ assert p() is not None
+ self.tc.close()
+ del self.tc
+
+ # force a collection to see whether the SSHClient object is deallocated
+ # 2 GCs are needed on PyPy, time is needed for Python 3
+ # TODO 4.0: this still fails randomly under CircleCI under Python 3.7,
+ # 3.8 at the very least. bumped sleep 0.3->1.0s but the underlying
+ # functionality should get reevaluated now we've dropped Python 2.
+ time.sleep(1)
+ gc.collect()
+ gc.collect()
+
+ assert p() is None
+
+ @patch("paramiko.client.socket.socket")
+ @patch("paramiko.client.socket.getaddrinfo")
+ def test_closes_socket_on_socket_errors(self, getaddrinfo, mocket):
+ getaddrinfo.return_value = (
+ ("irrelevant", None, None, None, "whatever"),
+ )
+
+ class SocksToBeYou(socket.error):
+ pass
+
+ my_socket = mocket.return_value
+ my_socket.connect.side_effect = SocksToBeYou
+ client = SSHClient()
+ with pytest.raises(SocksToBeYou):
+ client.connect(hostname="nope")
+ my_socket.close.assert_called_once_with()
+
+ def test_client_can_be_used_as_context_manager(self):
+ """
+ verify that an SSHClient can be used a context manager
+ """
+ threading.Thread(target=self._run).start()
+
+ with SSHClient() as tc:
+ self.tc = tc
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ assert len(self.tc.get_host_keys()) == 0
+ self.tc.connect(**dict(self.connect_kwargs, password="pygmalion"))
+
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+
+ self.assertTrue(self.tc._transport is not None)
+
+ self.assertTrue(self.tc._transport is None)
+
+ def test_banner_timeout(self):
+ """
+ verify that the SSHClient has a configurable banner timeout.
+ """
+ # Start the thread with a 1 second wait.
+ threading.Thread(target=self._run, kwargs={"delay": 1}).start()
+ host_key = paramiko.RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ self.tc = SSHClient()
+ self.tc.get_host_keys().add(
+ f"[{self.addr}]:{self.port}", "ssh-rsa", public_host_key
+ )
+ # Connect with a half second banner timeout.
+ kwargs = dict(self.connect_kwargs, banner_timeout=0.5)
+ self.assertRaises(paramiko.SSHException, self.tc.connect, **kwargs)
+
+ @requires_sha1_signing
+ def test_auth_trickledown(self):
+ """
+ Failed key auth doesn't prevent subsequent pw auth from succeeding
+ """
+ # NOTE: re #387, re #394
+ # If pkey module used within Client._auth isn't correctly handling auth
+ # errors (e.g. if it allows things like ValueError to bubble up as per
+ # midway through #394) client.connect() will fail (at key load step)
+ # instead of succeeding (at password step)
+ kwargs = dict(
+ # Password-protected key whose passphrase is not 'pygmalion' (it's
+ # 'television' as per tests/test_pkey.py). NOTE: must use
+ # key_filename, loading the actual key here with PKey will except
+ # immediately; we're testing the try/except crap within Client.
+ key_filename=[_support("test_rsa_password.key")],
+ # Actual password for default 'slowdive' user
+ password="pygmalion",
+ )
+ self._test_connection(**kwargs)
+
+ @requires_sha1_signing
+ @slow
+ def test_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,
+ )
+
+ @patch.object(
+ paramiko.Channel,
+ "_set_remote_channel",
+ lambda *args, **kwargs: time.sleep(100),
+ )
+ def test_channel_timeout(self):
+ """
+ verify that the SSHClient has a configurable channel timeout
+ """
+ threading.Thread(target=self._run).start()
+ # Client setup
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+ # Actual connection
+ self.tc.connect(
+ **dict(
+ self.connect_kwargs, password="pygmalion", channel_timeout=0.5
+ )
+ )
+ self.event.wait(1.0)
+
+ self.assertRaises(paramiko.SSHException, self.tc.open_sftp)
+
+ @requires_gss_auth
+ def test_auth_trickledown_gsskex(self):
+ """
+ Failed gssapi-keyex doesn't prevent subsequent key from succeeding
+ """
+ kwargs = dict(gss_kex=True, key_filename=[_support("rsa.key")])
+ self._test_connection(**kwargs)
+
+ @requires_gss_auth
+ def test_auth_trickledown_gssauth(self):
+ """
+ Failed gssapi-with-mic doesn't prevent subsequent key from succeeding
+ """
+ kwargs = dict(gss_auth=True, key_filename=[_support("rsa.key")])
+ self._test_connection(**kwargs)
+
+ def test_reject_policy(self):
+ """
+ verify that SSHClient's RejectPolicy works.
+ """
+ threading.Thread(target=self._run).start()
+
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ self.assertEqual(0, len(self.tc.get_host_keys()))
+ self.assertRaises(
+ paramiko.SSHException,
+ self.tc.connect,
+ password="pygmalion",
+ **self.connect_kwargs,
+ )
+
+ @requires_gss_auth
+ def test_reject_policy_gsskex(self):
+ """
+ verify that SSHClient's RejectPolicy works,
+ even if gssapi-keyex was enabled but not used.
+ """
+ # Test for a bug present in paramiko versions released before
+ # 2017-08-01
+ threading.Thread(target=self._run).start()
+
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ self.assertEqual(0, len(self.tc.get_host_keys()))
+ self.assertRaises(
+ paramiko.SSHException,
+ self.tc.connect,
+ password="pygmalion",
+ gss_kex=True,
+ **self.connect_kwargs,
+ )
+
+ def _client_host_key_bad(self, host_key):
+ threading.Thread(target=self._run).start()
+ hostname = f"[{self.addr}]:{self.port}"
+
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.WarningPolicy())
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.assertRaises(
+ paramiko.BadHostKeyException,
+ self.tc.connect,
+ password="pygmalion",
+ **self.connect_kwargs,
+ )
+
+ def _client_host_key_good(self, ktype, kfile):
+ threading.Thread(target=self._run).start()
+ hostname = f"[{self.addr}]:{self.port}"
+
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ host_key = ktype.from_private_key_file(_support(kfile))
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.tc.connect(password="pygmalion", **self.connect_kwargs)
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+ self.assertEqual(True, self.ts.is_authenticated())
+
+ def test_host_key_negotiation_1(self):
+ host_key = paramiko.ECDSAKey.generate()
+ self._client_host_key_bad(host_key)
+
+ @requires_sha1_signing
+ def test_host_key_negotiation_2(self):
+ host_key = paramiko.RSAKey.generate(2048)
+ self._client_host_key_bad(host_key)
+
+ def test_host_key_negotiation_3(self):
+ self._client_host_key_good(paramiko.ECDSAKey, "ecdsa-256.key")
+
+ @requires_sha1_signing
+ def test_host_key_negotiation_4(self):
+ self._client_host_key_good(paramiko.RSAKey, "rsa.key")
+
+ def _setup_for_env(self):
+ threading.Thread(target=self._run).start()
+
+ self.tc = SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self.assertEqual(0, len(self.tc.get_host_keys()))
+ self.tc.connect(
+ self.addr, self.port, username="slowdive", password="pygmalion"
+ )
+
+ self.event.wait(1.0)
+ self.assertTrue(self.event.isSet())
+ self.assertTrue(self.ts.is_active())
+
+ def test_update_environment(self):
+ """
+ Verify that environment variables can be set by the client.
+ """
+ self._setup_for_env()
+ target_env = {b"A": b"B", b"C": b"d"}
+
+ self.tc.exec_command("yes", environment=target_env)
+ schan = self.ts.accept(1.0)
+ self.assertEqual(target_env, getattr(schan, "env", {}))
+ schan.close()
+
+ @unittest.skip("Clients normally fail silently, thus so do we, for now")
+ def test_env_update_failures(self):
+ self._setup_for_env()
+ with self.assertRaises(SSHException) as manager:
+ # Verify that a rejection by the server can be detected
+ self.tc.exec_command("yes", environment={b"INVALID_ENV": b""})
+ self.assertTrue(
+ "INVALID_ENV" in str(manager.exception),
+ "Expected variable name in error message",
+ )
+ self.assertTrue(
+ isinstance(manager.exception.args[1], SSHException),
+ "Expected original SSHException in exception",
+ )
+
+ 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 = 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)
+
+ @patch("paramiko.client.Transport")
+ def test_disabled_algorithms_defaults_to_None(self, Transport):
+ SSHClient().connect("host", sock=Mock(), password="no")
+ assert Transport.call_args[1]["disabled_algorithms"] is None
+
+ @patch("paramiko.client.Transport")
+ def test_disabled_algorithms_passed_directly_if_given(self, Transport):
+ SSHClient().connect(
+ "host",
+ sock=Mock(),
+ password="no",
+ disabled_algorithms={"keys": ["ssh-dss"]},
+ )
+ call_arg = Transport.call_args[1]["disabled_algorithms"]
+ assert call_arg == {"keys": ["ssh-dss"]}
+
+ @patch("paramiko.client.Transport")
+ def test_transport_factory_defaults_to_Transport(self, Transport):
+ sock, kex, creds, algos = Mock(), Mock(), Mock(), Mock()
+ SSHClient().connect(
+ "host",
+ sock=sock,
+ password="no",
+ gss_kex=kex,
+ gss_deleg_creds=creds,
+ disabled_algorithms=algos,
+ )
+ Transport.assert_called_once_with(
+ sock, gss_kex=kex, gss_deleg_creds=creds, disabled_algorithms=algos
+ )
+
+ @patch("paramiko.client.Transport")
+ def test_transport_factory_may_be_specified(self, Transport):
+ factory = Mock()
+ sock, kex, creds, algos = Mock(), Mock(), Mock(), Mock()
+ SSHClient().connect(
+ "host",
+ sock=sock,
+ password="no",
+ gss_kex=kex,
+ gss_deleg_creds=creds,
+ disabled_algorithms=algos,
+ transport_factory=factory,
+ )
+ factory.assert_called_once_with(
+ sock, gss_kex=kex, gss_deleg_creds=creds, disabled_algorithms=algos
+ )
+ # Safety check
+ assert not Transport.called
+
+
+class PasswordPassphraseTests(ClientTest):
+ # TODO: most of these could reasonably be set up to use mocks/assertions
+ # (e.g. "gave passphrase -> expect PKey was given it as the passphrase")
+ # instead of suffering a real connection cycle.
+ # TODO: in that case, move the below to be part of an integration suite?
+
+ @requires_sha1_signing
+ def test_password_kwarg_works_for_password_auth(self):
+ # Straightforward / duplicate of earlier basic password test.
+ self._test_connection(password="pygmalion")
+
+ # TODO: more granular exception pending #387; should be signaling "no auth
+ # methods available" because no key and no password
+ @raises(SSHException)
+ @requires_sha1_signing
+ def test_passphrase_kwarg_not_used_for_password_auth(self):
+ # Using the "right" password in the "wrong" field shouldn't work.
+ self._test_connection(passphrase="pygmalion")
+
+ @requires_sha1_signing
+ def test_passphrase_kwarg_used_for_key_passphrase(self):
+ # Straightforward again, with new passphrase kwarg.
+ self._test_connection(
+ key_filename=_support("test_rsa_password.key"),
+ passphrase="television",
+ )
+
+ @requires_sha1_signing
+ def test_password_kwarg_used_for_passphrase_when_no_passphrase_kwarg_given(
+ self,
+ ): # noqa
+ # Backwards compatibility: passphrase in the password field.
+ self._test_connection(
+ key_filename=_support("test_rsa_password.key"),
+ password="television",
+ )
+
+ @raises(AuthenticationException) # TODO: more granular
+ @requires_sha1_signing
+ def test_password_kwarg_not_used_for_passphrase_when_passphrase_kwarg_given( # noqa
+ self,
+ ):
+ # Sanity: if we're given both fields, the password field is NOT used as
+ # a passphrase.
+ self._test_connection(
+ key_filename=_support("test_rsa_password.key"),
+ password="television",
+ passphrase="wat? lol no",
+ )
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..2e49aa3
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,1048 @@
+# This file is part of Paramiko and subject to the license in /LICENSE in this
+# repository
+
+from os.path import expanduser
+from socket import gaierror
+
+try:
+ from invoke import Result
+except ImportError:
+ Result = None
+
+from unittest.mock import patch
+from pytest import raises, mark, fixture
+
+from paramiko import (
+ SSHConfig,
+ SSHConfigDict,
+ CouldNotCanonicalize,
+ ConfigParseError,
+)
+
+from ._util import _config
+
+
+@fixture
+def socket():
+ """
+ Patch all of socket.* in our config module to prevent eg real DNS lookups.
+
+ Also forces getaddrinfo (used in our addressfamily lookup stuff) to always
+ fail by default to mimic usual lack of AddressFamily related crap.
+
+ Callers who want to mock DNS lookups can then safely assume gethostbyname()
+ will be in use.
+ """
+ with patch("paramiko.config.socket") as mocket:
+ # Reinstate gaierror as an actual exception and not a sub-mock.
+ # (Presumably this would work with any exception, but why not use the
+ # real one?)
+ mocket.gaierror = gaierror
+ # Patch out getaddrinfo, used to detect family-specific IP lookup -
+ # only useful for a few specific tests.
+ mocket.getaddrinfo.side_effect = mocket.gaierror
+ # Patch out getfqdn to return some real string for when it gets called;
+ # some code (eg tokenization) gets mad w/ MagicMocks
+ mocket.getfqdn.return_value = "some.fake.fqdn"
+ mocket.gethostname.return_value = "local.fake.fqdn"
+ yield mocket
+
+
+def load_config(name):
+ return SSHConfig.from_path(_config(name))
+
+
+class TestSSHConfig:
+ def setup(self):
+ self.config = load_config("robey")
+
+ def test_init(self):
+ # No args!
+ with raises(TypeError):
+ SSHConfig("uh oh!")
+ # No args.
+ assert not SSHConfig()._config
+
+ def test_from_text(self):
+ config = SSHConfig.from_text("User foo")
+ assert config.lookup("foo.example.com")["user"] == "foo"
+
+ def test_from_file(self):
+ with open(_config("robey")) as flo:
+ config = SSHConfig.from_file(flo)
+ assert config.lookup("whatever")["user"] == "robey"
+
+ def test_from_path(self):
+ # NOTE: DO NOT replace with use of load_config() :D
+ config = SSHConfig.from_path(_config("robey"))
+ assert config.lookup("meh.example.com")["port"] == "3333"
+
+ def test_parse_config(self):
+ expected = [
+ {"host": ["*"], "config": {}},
+ {
+ "host": ["*"],
+ "config": {"identityfile": ["~/.ssh/id_rsa"], "user": "robey"},
+ },
+ {
+ "host": ["*.example.com"],
+ "config": {"user": "bjork", "port": "3333"},
+ },
+ {"host": ["*"], "config": {"crazy": "something dumb"}},
+ {
+ "host": ["spoo.example.com"],
+ "config": {"crazy": "something else"},
+ },
+ ]
+ assert self.config._config == expected
+
+ @mark.parametrize(
+ "host,values",
+ (
+ (
+ "irc.danger.com",
+ {
+ "crazy": "something dumb",
+ "hostname": "irc.danger.com",
+ "user": "robey",
+ },
+ ),
+ (
+ "irc.example.com",
+ {
+ "crazy": "something dumb",
+ "hostname": "irc.example.com",
+ "user": "robey",
+ "port": "3333",
+ },
+ ),
+ (
+ "spoo.example.com",
+ {
+ "crazy": "something dumb",
+ "hostname": "spoo.example.com",
+ "user": "robey",
+ "port": "3333",
+ },
+ ),
+ ),
+ )
+ def test_host_config(self, host, values):
+ expected = dict(
+ values, hostname=host, identityfile=[expanduser("~/.ssh/id_rsa")]
+ )
+ assert self.config.lookup(host) == expected
+
+ def test_fabric_issue_33(self):
+ config = SSHConfig.from_text(
+ """
+Host www13.*
+ Port 22
+
+Host *.example.com
+ Port 2222
+
+Host *
+ Port 3333
+"""
+ )
+ host = "www13.example.com"
+ expected = {"hostname": host, "port": "22"}
+ assert config.lookup(host) == expected
+
+ def test_proxycommand_config_equals_parsing(self):
+ """
+ ProxyCommand should not split on equals signs within the value.
+ """
+ config = SSHConfig.from_text(
+ """
+Host space-delimited
+ ProxyCommand foo bar=biz baz
+
+Host equals-delimited
+ ProxyCommand=foo bar=biz baz
+"""
+ )
+ for host in ("space-delimited", "equals-delimited"):
+ value = config.lookup(host)["proxycommand"]
+ assert value == "foo bar=biz baz"
+
+ def test_proxycommand_interpolation(self):
+ """
+ ProxyCommand should perform interpolation on the value
+ """
+ config = SSHConfig.from_text(
+ """
+Host specific
+ Port 37
+ ProxyCommand host %h port %p lol
+
+Host portonly
+ Port 155
+
+Host *
+ Port 25
+ ProxyCommand host %h port %p
+"""
+ )
+ for host, val in (
+ ("foo.com", "host foo.com port 25"),
+ ("specific", "host specific port 37 lol"),
+ ("portonly", "host portonly port 155"),
+ ):
+ assert config.lookup(host)["proxycommand"] == val
+
+ def test_proxycommand_tilde_expansion(self):
+ """
+ Tilde (~) should be expanded inside ProxyCommand
+ """
+ config = SSHConfig.from_text(
+ """
+Host test
+ ProxyCommand ssh -F ~/.ssh/test_config bastion nc %h %p
+"""
+ )
+ expected = "ssh -F {}/.ssh/test_config bastion nc test 22".format(
+ expanduser("~")
+ )
+ got = config.lookup("test")["proxycommand"]
+ assert got == expected
+
+ @patch("paramiko.config.getpass")
+ def test_proxyjump_token_expansion(self, getpass):
+ getpass.getuser.return_value = "gandalf"
+ config = SSHConfig.from_text(
+ """
+Host justhost
+ ProxyJump jumpuser@%h
+Host userhost
+ ProxyJump %r@%h:222
+Host allcustom
+ ProxyJump %r@%h:%p
+"""
+ )
+ assert config.lookup("justhost")["proxyjump"] == "jumpuser@justhost"
+ assert config.lookup("userhost")["proxyjump"] == "gandalf@userhost:222"
+ assert (
+ config.lookup("allcustom")["proxyjump"] == "gandalf@allcustom:22"
+ )
+
+ @patch("paramiko.config.getpass")
+ def test_controlpath_token_expansion(self, getpass, socket):
+ getpass.getuser.return_value = "gandalf"
+ config = SSHConfig.from_text(
+ """
+Host explicit_user
+ User root
+ ControlPath user %u remoteuser %r
+
+Host explicit_host
+ HostName ohai
+ ControlPath remoteuser %r host %h orighost %n
+
+Host hashbrowns
+ ControlPath %C
+ """
+ )
+ result = config.lookup("explicit_user")["controlpath"]
+ # Remote user is User val, local user is User val
+ assert result == "user gandalf remoteuser root"
+ result = config.lookup("explicit_host")["controlpath"]
+ # Remote user falls back to local user; host and orighost may differ
+ assert result == "remoteuser gandalf host ohai orighost explicit_host"
+ # Supports %C
+ result = config.lookup("hashbrowns")["controlpath"]
+ assert result == "a438e7dbf5308b923aba9db8fe2ca63447ac8688"
+
+ def test_negation(self):
+ config = SSHConfig.from_text(
+ """
+Host www13.* !*.example.com
+ Port 22
+
+Host *.example.com !www13.*
+ Port 2222
+
+Host www13.*
+ Port 8080
+
+Host *
+ Port 3333
+"""
+ )
+ host = "www13.example.com"
+ expected = {"hostname": host, "port": "8080"}
+ assert config.lookup(host) == expected
+
+ def test_proxycommand(self):
+ config = SSHConfig.from_text(
+ """
+Host proxy-with-equal-divisor-and-space
+ProxyCommand = foo=bar
+
+Host proxy-with-equal-divisor-and-no-space
+ProxyCommand=foo=bar
+
+Host proxy-without-equal-divisor
+ProxyCommand foo=bar:%h-%p
+"""
+ )
+ for host, values in {
+ "proxy-with-equal-divisor-and-space": {
+ "hostname": "proxy-with-equal-divisor-and-space",
+ "proxycommand": "foo=bar",
+ },
+ "proxy-with-equal-divisor-and-no-space": {
+ "hostname": "proxy-with-equal-divisor-and-no-space",
+ "proxycommand": "foo=bar",
+ },
+ "proxy-without-equal-divisor": {
+ "hostname": "proxy-without-equal-divisor",
+ "proxycommand": "foo=bar:proxy-without-equal-divisor-22",
+ },
+ }.items():
+
+ assert config.lookup(host) == values
+
+ @patch("paramiko.config.getpass")
+ def test_identityfile(self, getpass, socket):
+ getpass.getuser.return_value = "gandalf"
+ config = SSHConfig.from_text(
+ """
+IdentityFile id_dsa0
+
+Host *
+IdentityFile id_dsa1
+
+Host dsa2
+IdentityFile id_dsa2
+
+Host dsa2*
+IdentityFile id_dsa22
+
+Host hashbrowns
+IdentityFile %C
+"""
+ )
+ for host, values in {
+ "foo": {"hostname": "foo", "identityfile": ["id_dsa0", "id_dsa1"]},
+ "dsa2": {
+ "hostname": "dsa2",
+ "identityfile": ["id_dsa0", "id_dsa1", "id_dsa2", "id_dsa22"],
+ },
+ "dsa22": {
+ "hostname": "dsa22",
+ "identityfile": ["id_dsa0", "id_dsa1", "id_dsa22"],
+ },
+ "hashbrowns": {
+ "hostname": "hashbrowns",
+ "identityfile": [
+ "id_dsa0",
+ "id_dsa1",
+ "a438e7dbf5308b923aba9db8fe2ca63447ac8688",
+ ],
+ },
+ }.items():
+ assert config.lookup(host) == values
+
+ def test_config_addressfamily_and_lazy_fqdn(self):
+ """
+ Ensure the code path honoring non-'all' AddressFamily doesn't asplode
+ """
+ config = SSHConfig.from_text(
+ """
+AddressFamily inet
+IdentityFile something_%l_using_fqdn
+"""
+ )
+ assert config.lookup(
+ "meh"
+ ) # will die during lookup() if bug regresses
+
+ def test_config_dos_crlf_succeeds(self):
+ config = SSHConfig.from_text(
+ """
+Host abcqwerty\r\nHostName 127.0.0.1\r\n
+"""
+ )
+ assert config.lookup("abcqwerty")["hostname"] == "127.0.0.1"
+
+ def test_get_hostnames(self):
+ expected = {"*", "*.example.com", "spoo.example.com"}
+ assert self.config.get_hostnames() == expected
+
+ def test_quoted_host_names(self):
+ config = SSHConfig.from_text(
+ """
+Host "param pam" param "pam"
+ Port 1111
+
+Host "param2"
+ Port 2222
+
+Host param3 parara
+ Port 3333
+
+Host param4 "p a r" "p" "par" para
+ Port 4444
+"""
+ )
+ res = {
+ "param pam": {"hostname": "param pam", "port": "1111"},
+ "param": {"hostname": "param", "port": "1111"},
+ "pam": {"hostname": "pam", "port": "1111"},
+ "param2": {"hostname": "param2", "port": "2222"},
+ "param3": {"hostname": "param3", "port": "3333"},
+ "parara": {"hostname": "parara", "port": "3333"},
+ "param4": {"hostname": "param4", "port": "4444"},
+ "p a r": {"hostname": "p a r", "port": "4444"},
+ "p": {"hostname": "p", "port": "4444"},
+ "par": {"hostname": "par", "port": "4444"},
+ "para": {"hostname": "para", "port": "4444"},
+ }
+ for host, values in res.items():
+ assert config.lookup(host) == values
+
+ def test_quoted_params_in_config(self):
+ config = SSHConfig.from_text(
+ """
+Host "param pam" param "pam"
+ IdentityFile id_rsa
+
+Host "param2"
+ IdentityFile "test rsa key"
+
+Host param3 parara
+ IdentityFile id_rsa
+ IdentityFile "test rsa key"
+"""
+ )
+ res = {
+ "param pam": {"hostname": "param pam", "identityfile": ["id_rsa"]},
+ "param": {"hostname": "param", "identityfile": ["id_rsa"]},
+ "pam": {"hostname": "pam", "identityfile": ["id_rsa"]},
+ "param2": {"hostname": "param2", "identityfile": ["test rsa key"]},
+ "param3": {
+ "hostname": "param3",
+ "identityfile": ["id_rsa", "test rsa key"],
+ },
+ "parara": {
+ "hostname": "parara",
+ "identityfile": ["id_rsa", "test rsa key"],
+ },
+ }
+ for host, values in res.items():
+ assert config.lookup(host) == values
+
+ def test_quoted_host_in_config(self):
+ conf = SSHConfig()
+ correct_data = {
+ "param": ["param"],
+ '"param"': ["param"],
+ "param pam": ["param", "pam"],
+ '"param" "pam"': ["param", "pam"],
+ '"param" pam': ["param", "pam"],
+ 'param "pam"': ["param", "pam"],
+ 'param "pam" p': ["param", "pam", "p"],
+ '"param" pam "p"': ["param", "pam", "p"],
+ '"pa ram"': ["pa ram"],
+ '"pa ram" pam': ["pa ram", "pam"],
+ 'param "p a m"': ["param", "p a m"],
+ }
+ incorrect_data = ['param"', '"param', 'param "pam', 'param "pam" "p a']
+ for host, values in correct_data.items():
+ assert conf._get_hosts(host) == values
+ for host in incorrect_data:
+ with raises(ConfigParseError):
+ conf._get_hosts(host)
+
+ def test_invalid_line_format_excepts(self):
+ with raises(ConfigParseError):
+ load_config("invalid")
+
+ def test_proxycommand_none_issue_415(self):
+ config = SSHConfig.from_text(
+ """
+Host proxycommand-standard-none
+ ProxyCommand None
+
+Host proxycommand-with-equals-none
+ ProxyCommand=None
+"""
+ )
+ for host, values in {
+ "proxycommand-standard-none": {
+ "hostname": "proxycommand-standard-none",
+ "proxycommand": None,
+ },
+ "proxycommand-with-equals-none": {
+ "hostname": "proxycommand-with-equals-none",
+ "proxycommand": None,
+ },
+ }.items():
+
+ assert config.lookup(host) == values
+
+ def test_proxycommand_none_masking(self):
+ # Re: https://github.com/paramiko/paramiko/issues/670
+ config = SSHConfig.from_text(
+ """
+Host specific-host
+ ProxyCommand none
+
+Host other-host
+ ProxyCommand other-proxy
+
+Host *
+ ProxyCommand default-proxy
+"""
+ )
+ # In versions <3.0, 'None' ProxyCommands got deleted, and this itself
+ # caused bugs. In 3.0, we more cleanly map "none" to None. This test
+ # has been altered accordingly but left around to ensure no
+ # regressions.
+ assert config.lookup("specific-host")["proxycommand"] is None
+ assert config.lookup("other-host")["proxycommand"] == "other-proxy"
+ cmd = config.lookup("some-random-host")["proxycommand"]
+ assert cmd == "default-proxy"
+
+ def test_hostname_tokenization(self):
+ result = load_config("hostname-tokenized").lookup("whatever")
+ assert result["hostname"] == "prefix.whatever"
+
+
+class TestSSHConfigDict:
+ def test_SSHConfigDict_construct_empty(self):
+ assert not SSHConfigDict()
+
+ def test_SSHConfigDict_construct_from_list(self):
+ assert SSHConfigDict([(1, 2)])[1] == 2
+
+ def test_SSHConfigDict_construct_from_dict(self):
+ assert SSHConfigDict({1: 2})[1] == 2
+
+ @mark.parametrize("true_ish", ("yes", "YES", "Yes", True))
+ def test_SSHConfigDict_as_bool_true_ish(self, true_ish):
+ assert SSHConfigDict({"key": true_ish}).as_bool("key") is True
+
+ @mark.parametrize("false_ish", ("no", "NO", "No", False))
+ def test_SSHConfigDict_as_bool(self, false_ish):
+ assert SSHConfigDict({"key": false_ish}).as_bool("key") is False
+
+ @mark.parametrize("int_val", ("42", 42))
+ def test_SSHConfigDict_as_int(self, int_val):
+ assert SSHConfigDict({"key": int_val}).as_int("key") == 42
+
+ @mark.parametrize("non_int", ("not an int", None, object()))
+ def test_SSHConfigDict_as_int_failures(self, non_int):
+ conf = SSHConfigDict({"key": non_int})
+
+ try:
+ int(non_int)
+ except Exception as e:
+ exception_type = type(e)
+
+ with raises(exception_type):
+ conf.as_int("key")
+
+ def test_SSHConfig_host_dicts_are_SSHConfigDict_instances(self):
+ config = SSHConfig.from_text(
+ """
+Host *.example.com
+ Port 2222
+
+Host *
+ Port 3333
+"""
+ )
+ assert config.lookup("foo.example.com").as_int("port") == 2222
+
+ def test_SSHConfig_wildcard_host_dicts_are_SSHConfigDict_instances(self):
+ config = SSHConfig.from_text(
+ """
+Host *.example.com
+ Port 2222
+
+Host *
+ Port 3333
+"""
+ )
+ assert config.lookup("anything-else").as_int("port") == 3333
+
+
+class TestHostnameCanonicalization:
+ # NOTE: this class uses on-disk configs, and ones with real (at time of
+ # writing) DNS names, so that one can easily test OpenSSH's behavior using
+ # "ssh -F path/to/file.config -G <target>".
+
+ def test_off_by_default(self, socket):
+ result = load_config("basic").lookup("www")
+ assert result["hostname"] == "www"
+ assert "user" not in result
+ assert not socket.gethostbyname.called
+
+ def test_explicit_no_same_as_default(self, socket):
+ result = load_config("no-canon").lookup("www")
+ assert result["hostname"] == "www"
+ assert "user" not in result
+ assert not socket.gethostbyname.called
+
+ @mark.parametrize(
+ "config_name",
+ ("canon", "canon-always", "canon-local", "canon-local-always"),
+ )
+ def test_canonicalization_base_cases(self, socket, config_name):
+ result = load_config(config_name).lookup("www")
+ assert result["hostname"] == "www.paramiko.org"
+ assert result["user"] == "rando"
+ socket.gethostbyname.assert_called_once_with("www.paramiko.org")
+
+ def test_uses_getaddrinfo_when_AddressFamily_given(self, socket):
+ # Undo default 'always fails' mock
+ socket.getaddrinfo.side_effect = None
+ socket.getaddrinfo.return_value = [True] # just need 1st value truthy
+ result = load_config("canon-ipv4").lookup("www")
+ assert result["hostname"] == "www.paramiko.org"
+ assert result["user"] == "rando"
+ assert not socket.gethostbyname.called
+ gai_args = socket.getaddrinfo.call_args[0]
+ assert gai_args[0] == "www.paramiko.org"
+ assert gai_args[2] is socket.AF_INET # Mocked, but, still useful
+
+ @mark.skip
+ def test_empty_CanonicalDomains_canonicalizes_despite_noop(self, socket):
+ # Confirmed this is how OpenSSH behaves as well. Bit silly, but.
+ # TODO: this requires modifying SETTINGS_REGEX, which is a mite scary
+ # (honestly I'd prefer to move to a real parser lib anyhow) and since
+ # this is a very dumb corner case, it's marked skip for now.
+ result = load_config("empty-canon").lookup("www")
+ assert result["hostname"] == "www" # no paramiko.org
+ assert "user" not in result # did not discover canonicalized block
+
+ def test_CanonicalDomains_may_be_set_to_space_separated_list(self, socket):
+ # Test config has a bogus domain, followed by paramiko.org
+ socket.gethostbyname.side_effect = [socket.gaierror, True]
+ result = load_config("multi-canon-domains").lookup("www")
+ assert result["hostname"] == "www.paramiko.org"
+ assert result["user"] == "rando"
+ assert [x[0][0] for x in socket.gethostbyname.call_args_list] == [
+ "www.not-a-real-tld",
+ "www.paramiko.org",
+ ]
+
+ def test_canonicalization_applies_to_single_dot_by_default(self, socket):
+ result = load_config("deep-canon").lookup("sub.www")
+ assert result["hostname"] == "sub.www.paramiko.org"
+ assert result["user"] == "deep"
+
+ def test_canonicalization_not_applied_to_two_dots_by_default(self, socket):
+ result = load_config("deep-canon").lookup("subber.sub.www")
+ assert result["hostname"] == "subber.sub.www"
+ assert "user" not in result
+
+ def test_hostname_depth_controllable_with_max_dots_directive(self, socket):
+ # This config sets MaxDots of 2, so now canonicalization occurs
+ result = load_config("deep-canon-maxdots").lookup("subber.sub.www")
+ assert result["hostname"] == "subber.sub.www.paramiko.org"
+ assert result["user"] == "deeper"
+
+ def test_max_dots_may_be_zero(self, socket):
+ result = load_config("zero-maxdots").lookup("sub.www")
+ assert result["hostname"] == "sub.www"
+ assert "user" not in result
+
+ def test_fallback_yes_does_not_canonicalize_or_error(self, socket):
+ socket.gethostbyname.side_effect = socket.gaierror
+ result = load_config("fallback-yes").lookup("www")
+ assert result["hostname"] == "www"
+ assert "user" not in result
+
+ def test_fallback_no_causes_errors_for_unresolvable_names(self, socket):
+ socket.gethostbyname.side_effect = socket.gaierror
+ with raises(CouldNotCanonicalize) as info:
+ load_config("fallback-no").lookup("doesnotexist")
+ assert str(info.value) == "doesnotexist"
+
+ def test_identityfile_continues_being_appended_to(self, socket):
+ result = load_config("canon").lookup("www")
+ assert result["identityfile"] == ["base.key", "canonicalized.key"]
+
+
+@mark.skip
+class TestCanonicalizationOfCNAMEs:
+ def test_permitted_cnames_may_be_one_to_one_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com:*.bar.com
+ pass
+
+ def test_permitted_cnames_may_be_one_to_many_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com:*.bar.com,*.biz.com
+ pass
+
+ def test_permitted_cnames_may_be_many_to_one_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com,*.bar.com:*.biz.com
+ pass
+
+ def test_permitted_cnames_may_be_many_to_many_mapping(self):
+ # CanonicalizePermittedCNAMEs *.foo.com,*.bar.com:*.biz.com,*.baz.com
+ pass
+
+ def test_permitted_cnames_may_be_multiple_mappings(self):
+ # CanonicalizePermittedCNAMEs *.foo.com,*.bar.com *.biz.com:*.baz.com
+ pass
+
+ def test_permitted_cnames_may_be_multiple_complex_mappings(self):
+ # Same as prev but with multiple patterns on both ends in both args
+ pass
+
+
+class TestMatchAll:
+ def test_always_matches(self):
+ result = load_config("match-all").lookup("general")
+ assert result["user"] == "awesome"
+
+ def test_may_not_mix_with_non_canonical_keywords(self):
+ for config in ("match-all-and-more", "match-all-and-more-before"):
+ with raises(ConfigParseError):
+ load_config(config).lookup("whatever")
+
+ def test_may_come_after_canonical(self, socket):
+ result = load_config("match-all-after-canonical").lookup("www")
+ assert result["user"] == "awesome"
+
+ def test_may_not_come_before_canonical(self, socket):
+ with raises(ConfigParseError):
+ load_config("match-all-before-canonical")
+
+ def test_after_canonical_not_loaded_when_non_canonicalized(self, socket):
+ result = load_config("match-canonical-no").lookup("a-host")
+ assert "user" not in result
+
+
+def _expect(success_on):
+ """
+ Returns a side_effect-friendly Invoke success result for given command(s).
+
+ Ensures that any other commands fail; this is useful for testing 'Match
+ exec' because it means all other such clauses under test act like no-ops.
+
+ :param success_on:
+ Single string or list of strings, noting commands that should appear to
+ succeed.
+ """
+ if isinstance(success_on, str):
+ success_on = [success_on]
+
+ def inner(command, *args, **kwargs):
+ # Sanity checking - we always expect that invoke.run is called with
+ # these.
+ assert kwargs.get("hide", None) == "stdout"
+ assert kwargs.get("warn", None) is True
+ # Fake exit
+ exit = 0 if command in success_on else 1
+ return Result(exited=exit)
+
+ return inner
+
+
+@mark.skipif(Result is None, reason="requires invoke package")
+class TestMatchExec:
+ @patch("paramiko.config.invoke", new=None)
+ @patch("paramiko.config.invoke_import_error", new=ImportError("meh"))
+ def test_raises_invoke_ImportErrors_at_runtime(self):
+ # Not an ideal test, but I don't know of a non-bad way to fake out
+ # module-time ImportErrors. So we mock the symptoms. Meh!
+ with raises(ImportError) as info:
+ load_config("match-exec").lookup("oh-noes")
+ assert str(info.value) == "meh"
+
+ @patch("paramiko.config.invoke.run")
+ @mark.parametrize(
+ "cmd,user",
+ [
+ ("unquoted", "rando"),
+ ("quoted", "benjamin"),
+ ("quoted spaced", "neil"),
+ ],
+ )
+ def test_accepts_single_possibly_quoted_argument(self, run, cmd, user):
+ run.side_effect = _expect(cmd)
+ result = load_config("match-exec").lookup("whatever")
+ assert result["user"] == user
+
+ @patch("paramiko.config.invoke.run")
+ def test_does_not_match_nonzero_exit_codes(self, run):
+ # Nothing will succeed -> no User ever gets loaded
+ run.return_value = Result(exited=1)
+ result = load_config("match-exec").lookup("whatever")
+ assert "user" not in result
+
+ @patch("paramiko.config.getpass")
+ @patch("paramiko.config.invoke.run")
+ def test_tokenizes_argument(self, run, getpass, socket):
+ getpass.getuser.return_value = "gandalf"
+ # Actual exec value is "%C %d %h %L %l %n %p %r %u"
+ parts = (
+ "bf5ba06778434a9384ee4217e462f64888bd0cd2",
+ expanduser("~"),
+ "configured",
+ "local",
+ "some.fake.fqdn",
+ "target",
+ "22",
+ "intermediate",
+ "gandalf",
+ )
+ run.side_effect = _expect(" ".join(parts))
+ result = load_config("match-exec").lookup("target")
+ assert result["port"] == "1337"
+
+ @patch("paramiko.config.invoke.run")
+ def test_works_with_canonical(self, run, socket):
+ # Ensure both stanzas' exec components appear to match
+ run.side_effect = _expect(["uncanonicalized", "canonicalized"])
+ result = load_config("match-exec-canonical").lookup("who-cares")
+ # Prove both config values got loaded up, across the two passes
+ assert result["user"] == "defenseless"
+ assert result["port"] == "8007"
+
+ @patch("paramiko.config.invoke.run")
+ def test_may_be_negated(self, run):
+ run.side_effect = _expect("this succeeds")
+ result = load_config("match-exec-negation").lookup("so-confusing")
+ # If negation did not work, the first of the two Match exec directives
+ # would have set User to 'nope' (and/or the second would have NOT set
+ # User to 'yup')
+ assert result["user"] == "yup"
+
+ def test_requires_an_argument(self):
+ with raises(ConfigParseError):
+ load_config("match-exec-no-arg")
+
+ @patch("paramiko.config.invoke.run")
+ def test_works_with_tokenized_hostname(self, run):
+ run.side_effect = _expect("ping target")
+ result = load_config("hostname-exec-tokenized").lookup("target")
+ assert result["hostname"] == "pingable.target"
+
+
+class TestMatchHost:
+ def test_matches_target_name_when_no_hostname(self):
+ result = load_config("match-host").lookup("target")
+ assert result["user"] == "rand"
+
+ def test_matches_hostname_from_global_setting(self):
+ # Also works for ones set in regular Host stanzas
+ result = load_config("match-host-name").lookup("anything")
+ assert result["user"] == "silly"
+
+ def test_matches_hostname_from_earlier_match(self):
+ # Corner case: one Match matches original host, sets HostName,
+ # subsequent Match matches the latter.
+ result = load_config("match-host-from-match").lookup("original-host")
+ assert result["user"] == "inner"
+
+ def test_may_be_globbed(self):
+ result = load_config("match-host-glob-list").lookup("whatever")
+ assert result["user"] == "matrim"
+
+ def test_may_be_comma_separated_list(self):
+ for target in ("somehost", "someotherhost"):
+ result = load_config("match-host-glob-list").lookup(target)
+ assert result["user"] == "thom"
+
+ def test_comma_separated_list_may_have_internal_negation(self):
+ conf = load_config("match-host-glob-list")
+ assert conf.lookup("good")["user"] == "perrin"
+ assert "user" not in conf.lookup("goof")
+
+ def test_matches_canonicalized_name(self, socket):
+ # Without 'canonical' explicitly declared, mind.
+ result = load_config("match-host-canonicalized").lookup("www")
+ assert result["user"] == "rand"
+
+ def test_works_with_canonical_keyword(self, socket):
+ # NOTE: distinct from 'happens to be canonicalized' above
+ result = load_config("match-host-canonicalized").lookup("docs")
+ assert result["user"] == "eric"
+
+ def test_may_be_negated(self):
+ conf = load_config("match-host-negated")
+ assert conf.lookup("docs")["user"] == "jeff"
+ assert "user" not in conf.lookup("www")
+
+ def test_requires_an_argument(self):
+ with raises(ConfigParseError):
+ load_config("match-host-no-arg")
+
+
+class TestMatchOriginalHost:
+ def test_matches_target_host_not_hostname(self):
+ result = load_config("match-orighost").lookup("target")
+ assert result["hostname"] == "bogus"
+ assert result["user"] == "tuon"
+
+ def test_matches_target_host_not_canonicalized_name(self, socket):
+ result = load_config("match-orighost-canonical").lookup("www")
+ assert result["hostname"] == "www.paramiko.org"
+ assert result["user"] == "tuon"
+
+ def test_may_be_globbed(self):
+ result = load_config("match-orighost").lookup("whatever")
+ assert result["user"] == "matrim"
+
+ def test_may_be_comma_separated_list(self):
+ for target in ("comma", "separated"):
+ result = load_config("match-orighost").lookup(target)
+ assert result["user"] == "chameleon"
+
+ def test_comma_separated_list_may_have_internal_negation(self):
+ result = load_config("match-orighost").lookup("nope")
+ assert "user" not in result
+
+ def test_may_be_negated(self):
+ result = load_config("match-orighost").lookup("docs")
+ assert result["user"] == "thom"
+
+ def test_requires_an_argument(self):
+ with raises(ConfigParseError):
+ load_config("match-orighost-no-arg")
+
+
+class TestMatchUser:
+ def test_matches_configured_username(self):
+ result = load_config("match-user-explicit").lookup("anything")
+ assert result["hostname"] == "dumb"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_matches_local_username_by_default(self, getuser):
+ getuser.return_value = "gandalf"
+ result = load_config("match-user").lookup("anything")
+ assert result["hostname"] == "gondor"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_may_be_globbed(self, getuser):
+ for user in ("bilbo", "bombadil"):
+ getuser.return_value = user
+ result = load_config("match-user").lookup("anything")
+ assert result["hostname"] == "shire"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_may_be_comma_separated_list(self, getuser):
+ for user in ("aragorn", "frodo"):
+ getuser.return_value = user
+ result = load_config("match-user").lookup("anything")
+ assert result["hostname"] == "moria"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_comma_separated_list_may_have_internal_negation(self, getuser):
+ getuser.return_value = "legolas"
+ result = load_config("match-user").lookup("anything")
+ assert "port" not in result
+ getuser.return_value = "gimli"
+ result = load_config("match-user").lookup("anything")
+ assert result["port"] == "7373"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_may_be_negated(self, getuser):
+ getuser.return_value = "saruman"
+ result = load_config("match-user").lookup("anything")
+ assert result["hostname"] == "mordor"
+
+ def test_requires_an_argument(self):
+ with raises(ConfigParseError):
+ load_config("match-user-no-arg")
+
+
+# NOTE: highly derivative of previous suite due to the former's use of
+# localuser fallback. Doesn't seem worth conflating/refactoring right now.
+class TestMatchLocalUser:
+ @patch("paramiko.config.getpass.getuser")
+ def test_matches_local_username(self, getuser):
+ getuser.return_value = "gandalf"
+ result = load_config("match-localuser").lookup("anything")
+ assert result["hostname"] == "gondor"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_may_be_globbed(self, getuser):
+ for user in ("bilbo", "bombadil"):
+ getuser.return_value = user
+ result = load_config("match-localuser").lookup("anything")
+ assert result["hostname"] == "shire"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_may_be_comma_separated_list(self, getuser):
+ for user in ("aragorn", "frodo"):
+ getuser.return_value = user
+ result = load_config("match-localuser").lookup("anything")
+ assert result["hostname"] == "moria"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_comma_separated_list_may_have_internal_negation(self, getuser):
+ getuser.return_value = "legolas"
+ result = load_config("match-localuser").lookup("anything")
+ assert "port" not in result
+ getuser.return_value = "gimli"
+ result = load_config("match-localuser").lookup("anything")
+ assert result["port"] == "7373"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_may_be_negated(self, getuser):
+ getuser.return_value = "saruman"
+ result = load_config("match-localuser").lookup("anything")
+ assert result["hostname"] == "mordor"
+
+ def test_requires_an_argument(self):
+ with raises(ConfigParseError):
+ load_config("match-localuser-no-arg")
+
+
+class TestComplexMatching:
+ # NOTE: this is still a cherry-pick of a few levels of complexity, there's
+ # no point testing literally all possible combinations.
+
+ def test_originalhost_host(self):
+ result = load_config("match-complex").lookup("target")
+ assert result["hostname"] == "bogus"
+ assert result["user"] == "rand"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_originalhost_localuser(self, getuser):
+ getuser.return_value = "rando"
+ result = load_config("match-complex").lookup("remote")
+ assert result["user"] == "calrissian"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_everything_but_all(self, getuser):
+ getuser.return_value = "rando"
+ result = load_config("match-complex").lookup("www")
+ assert result["port"] == "7777"
+
+ @patch("paramiko.config.getpass.getuser")
+ def test_everything_but_all_with_some_negated(self, getuser):
+ getuser.return_value = "rando"
+ result = load_config("match-complex").lookup("docs")
+ assert result["port"] == "1234"
+
+ def test_negated_canonical(self, socket):
+ # !canonical in a config that is not canonicalized - does match
+ result = load_config("match-canonical-no").lookup("specific")
+ assert result["user"] == "overload"
+ # !canonical in a config that is canonicalized - does NOT match
+ result = load_config("match-canonical-yes").lookup("www")
+ assert result["user"] == "hidden"
+
+
+class TestFinalMatching(object):
+ def test_finally(self):
+ result = load_config("match-final").lookup("finally")
+ assert result["proxyjump"] == "jump"
+ assert result["port"] == "1001"
+
+ def test_default_port(self):
+ result = load_config("match-final").lookup("default-port")
+ assert result["proxyjump"] == "jump"
+ assert result["port"] == "1002"
+
+ def test_negated(self):
+ result = load_config("match-final").lookup("jump")
+ assert result["port"] == "1003"
diff --git a/tests/test_dss_openssh.key b/tests/test_dss_openssh.key
new file mode 100644
index 0000000..2a9f892
--- /dev/null
+++ b/tests/test_dss_openssh.key
@@ -0,0 +1,22 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAsyq4pxL
+R5sOprPDHGpvzxAAAAEAAAAAEAAAGxAAAAB3NzaC1kc3MAAACBAL8XEx7F9xuwBNles+vW
+pNF+YcofrBhjX1r5QhpBe0eoYWLHRcroN6lxwCdGYRfgOoRjTncBiixQX/uUxAY96zDh3i
+r492s2BcJt4ihvNn/AY0I0OTuX/2IwGk9CGzafjaeZNVYxMa8lcVt0hSOTjkPQ7gVuk6bJ
+zMInvie+VWKLAAAAFQDUgYdY+rhR0SkKbC09BS/SIHcB+wAAAIB44+4zpCNcd0CGvZlowH
+99zyPX8uxQtmTLQFuR2O8O0FgVVuCdDgD0D9W8CLOp32oatpM0jyyN89EdvSWzjHzZJ+L6
+H1FtZps7uhpDFWHdva1R25vyGecLMUuXjo5t/D7oCDih+HwHoSAxoi0QvsPd8/qqHQVznN
+JKtR6thUpXEwAAAIAG4DCBjbgTTgpBw0egRkJwBSz0oTt+1IcapNU2jA6N8urMSk9YXHEQ
+HKN68BAF3YJ59q2Ujv3LOXmBqGd1T+kzwUszfMlgzq8MMu19Yfzse6AIK1Agn1Vj6F7YXL
+sXDN+T4KszX5+FJa7t/Zsp3nALWy6l0f4WKivEF5Y2QpEFcQAAAgCH6XUl1hYWB6kgCSHV
+a4C+vQHrgFNgNwEQnE074LXHXlAhxC+Dm8XTGqVPX1KRPWzadq9/+v6pqLFqiRueB86uRb
+J5WtAbUs3WwxAaC5Mi+mn42MBfL9PIwWPWCvstrAq9Nyj3EBMeX3XFLxN3RuGXIQnY/5rF
+f5hriUVxhWDQGIVbBKhkpn7Geqg6nLpn7iqQhzFmFGjPmAdrllgdVGJRLyIN6BRsaltDdy
+vxufkvGzKudvQ85QvsaoFJQ6K1d0S7907pexvxmWpcO7zchXb6i09BITWOAKIcHpVkbNQw
++8pzSdpggsAwCRbfk/Jkezz8sXVUCfmmJ23NFUw04/0ZbilCADRsUaPfafgVPeDznBnuCm
+tfXa4JSrVUvPdwoex3SKZmYsFXwsuOEQnFkhUGHfWwTbmOmxzy6dtC24KYhnWG5OGFVJXh
+3B8jQJGGs2ANfusI/Z0o15tAnQy5fqsLf9TT3RX7RG2ujIiDBsU+A1g//IXmSxxkUOQMZs
+v+cMI8KfODAXmQtB30+yAgoV03Zb/bdptv+HqPT4eeecstJUxzEGYADt1mDq3uV7fQbNmo
+80bppU52JjztrJb7hBmXsXHPRRK6spQ1FCatqvu1ggZeXZpEifNsHeqCljt87ueXsQsORY
+pvhLzjTbTKZmjLDPuB+GxUNLEKh1ZNyAqKng==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/test_dss_password.key b/tests/test_dss_password.key
new file mode 100644
index 0000000..e2a9bc5
--- /dev/null
+++ b/tests/test_dss_password.key
@@ -0,0 +1,15 @@
+-----BEGIN DSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,78DAEB836ED0A646
+
+ldWkq9OMlXqWmjIqppNnmNPIUj5uVT12LkBosTApTbibTme3kIJb1uDeG2BShVfY
++vDOTUE9koGPDLsxW1t5At+EVyIDK8aIO0uHteXM5AbBX20LLUWRbRVqZhsMxqQh
+3H3XlHiN+QhaWcb4fFuu18a8SkimTFpDnZuffoCDl/zh/B7XieARTLA805K/ZgVB
+BBwflkR2BE053XHrJAIx9BEUlLP76Fo18rvjLZOSeu3s+VnnhqUb5FCt5h50a46u
+YXQBbo2r9Zo1ilGMNEXJO0gk5hwGVmTySz53NkPA5HmWt8NIzv5jQHMDy7N+ZykF
+uwpP1R5M/ZIFY4Y5h/lvn6IJjQ7VySRPIbpN8o2YJv2OD1Ja80n3tU8Mg77o3o4d
+NwKm7cCjlq+FuIBdOsSgsB8FPQRUhW+jpFDxmWN64DM2cEg6RUdptby7WmMp0HwK
+1qyEfxHjLMuDVlD7lASIDBrRlUjPtXEH1DzIYQuYaRZaixFoZ7EY+X73TwmrKFEU
+US9ZnQZtRtroRqGwR4fz4wQQsjTl/AmOijlBmi29taJccJsT/THrLQ5plOEd8OMv
+9FsaPJXBU85gaRKo3JZtrw==
+-----END DSA PRIVATE KEY-----
diff --git a/tests/test_ecdsa_384.key b/tests/test_ecdsa_384.key
new file mode 100644
index 0000000..796bf41
--- /dev/null
+++ b/tests/test_ecdsa_384.key
@@ -0,0 +1,6 @@
+-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDBDdO8IXvlLJgM7+sNtPl7tI7FM5kzuEUEEPRjXIPQM7mISciwJPBt+
+y43EuG8nL4mgBwYFK4EEACKhZANiAAQWxom0C1vQAGYhjdoREMVmGKBWlisDdzyk
+mgyUjKpiJ9WfbIEVLsPGP8OdNjhr1y/8BZNIts+dJd6VmYw+4HzB+4F+U1Igs8K0
+JEvh59VNkvWheViadDXCM2MV8Nq+DNg=
+-----END EC PRIVATE KEY-----
diff --git a/tests/test_ecdsa_384_openssh.key b/tests/test_ecdsa_384_openssh.key
new file mode 100644
index 0000000..8a160ce
--- /dev/null
+++ b/tests/test_ecdsa_384_openssh.key
@@ -0,0 +1,11 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDwIHkBEZ
+75XuqQS6/7daAIAAAAEAAAAAEAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlz
+dHAzODQAAABhBIch5LXTq/L/TWsTGG6dIktxD8DIMh7EfvoRmWsks6CuNDTvFvbQNtY4QO
+1mn5OXegHbS0M5DPIS++wpKGFP3suDEH08O35vZQasLNrL0tO2jyyEnzB2ZEx3PPYci811
+ygAAAOBKGxFl+JcMHjldOdTA9iwv88gxoelCwln/NATglUuyzHMLJwx53n8NLqrnHALvbz
+RHjyTmjU4dbSM9o9Vjhcvq+1aipjAQg2qx825f7T4BMoKyhLBS/qTg7RfyW/h0Sbequ1wl
+PhBfwhv0LUphRFsGdnOgrXWfZqWqxOP1WhJWIh1p+ja5va/Ii/+hD6RORQjvzbHTPJA53c
+OguISImkx0vdqPuFTLyclaC3eO4Px68Ki0b8cdyivExbAWLkNOtBdIAgeO7Egbruu4O5Sn
+I6bn1Kc+kZlWtO02IkwSA5DaKw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/test_ecdsa_521.key b/tests/test_ecdsa_521.key
new file mode 100644
index 0000000..b87dc90
--- /dev/null
+++ b/tests/test_ecdsa_521.key
@@ -0,0 +1,7 @@
+-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIAprQtAS3OF6iVUkT8IowTHWicHzShGgk86EtuEXvfQnhZFKsWm6Jo
+iqAr1yEaiuI9LfB3Xs8cjuhgEEfbduYr/f6gBwYFK4EEACOhgYkDgYYABACaOaFL
+ZGuxa5AW16qj6VLypFbLrEWrt9AZUloCMefxO8bNLjK/O5g0rAVasar1TnyHE9qj
+4NwzANZASWjQNbc4MAG8vzqezFwLIn/kNyNTsXNfqEko9OgHZknlj2Z79dwTJcRA
+L4QLcT5aND0EHZLB2fAUDXiWIb2j4rg1mwPlBMiBXA==
+-----END EC PRIVATE KEY-----
diff --git a/tests/test_ecdsa_password_256.key b/tests/test_ecdsa_password_256.key
new file mode 100644
index 0000000..eb7910e
--- /dev/null
+++ b/tests/test_ecdsa_password_256.key
@@ -0,0 +1,8 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,EEB56BC745EDB2DE04FC3FE1F8DA387E
+
+wdt7QTCa6ahTJLaEPH7NhHyBcxhzrzf93d4UwQOuAhkM6//jKD4lF9fErHBW0f3B
+ExberCU3UxfEF3xX2thXiLw47JgeOCeQUlqRFx92p36k6YmfNGX6W8CsZ3d+XodF
+Z+pb6m285CiSX+W95NenFMexXFsIpntiCvTifTKJ8os=
+-----END EC PRIVATE KEY-----
diff --git a/tests/test_ecdsa_password_384.key b/tests/test_ecdsa_password_384.key
new file mode 100644
index 0000000..eba33c1
--- /dev/null
+++ b/tests/test_ecdsa_password_384.key
@@ -0,0 +1,9 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,7F7B5DBE4CE040D822441AFE7A023A1D
+
+y/d6tGonAXYgJniQoFCdto+CuT1y1s41qzwNLN9YdNq/+R/dtQvZAaOuGtHJRFE6
+wWabhY1bSjavVPT2z1Zw1jhDJX5HGrf9LDoyORKtUWtUJoUvGdYLHbcg8Q+//WRf
+R0A01YuSw1SJX0a225S1aRcsDAk1k5F8EMb8QzSSDgjAOI8ldQF35JI+ofNSGjgS
+BPOlorQXTJxDOGmokw/Wql6MbhajXKPO39H2Z53W88U=
+-----END EC PRIVATE KEY-----
diff --git a/tests/test_ecdsa_password_521.key b/tests/test_ecdsa_password_521.key
new file mode 100644
index 0000000..5986b93
--- /dev/null
+++ b/tests/test_ecdsa_password_521.key
@@ -0,0 +1,10 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,AEB2DE62C65D1A88C4940A3476B2F10A
+
+5kNk/FFPbHa0402QTrgpIT28uirJ4Amvb2/ryOEyOCe0NPbTLCqlQekj2RFYH2Un
+pgCLUDkelKQv4pyuK8qWS7R+cFjE/gHHCPUWkK3djZUC8DKuA9lUKeQIE+V1vBHc
+L5G+MpoYrPgaydcGx/Uqnc/kVuZx1DXLwrGGtgwNROVBtmjXC9EdfeXHLL1y0wvH
+paNgacJpUtgqJEmiehf7eL/eiReegG553rZK3jjfboGkREUaKR5XOgamiKUtgKoc
+sMpImVYCsRKd/9RI+VOqErZaEvy/9j0Ye3iH32wGOaA=
+-----END EC PRIVATE KEY-----
diff --git a/tests/test_ed25519-funky-padding.key b/tests/test_ed25519-funky-padding.key
new file mode 100644
index 0000000..f178ca4
--- /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 0000000..1b135d6
--- /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_password.key b/tests/test_ed25519_password.key
new file mode 100644
index 0000000..d178aaa
--- /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_file.py b/tests/test_file.py
new file mode 100644
index 0000000..9344495
--- /dev/null
+++ b/tests/test_file.py
@@ -0,0 +1,226 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for the BufferedFile abstraction.
+"""
+
+import unittest
+from io import BytesIO
+
+from paramiko.common import linefeed_byte, crlf, cr_byte
+from paramiko.file import BufferedFile
+
+from ._util import needs_builtin
+
+
+class LoopbackFile(BufferedFile):
+ """
+ BufferedFile object that you can write data into, and then read it back.
+ """
+
+ def __init__(self, mode="r", bufsize=-1):
+ BufferedFile.__init__(self)
+ self._set_mode(mode, bufsize)
+ self.buffer = BytesIO()
+ self.offset = 0
+
+ def _read(self, size):
+ data = self.buffer.getvalue()[self.offset : self.offset + size]
+ self.offset += len(data)
+ return data
+
+ def _write(self, data):
+ self.buffer.write(data)
+ return len(data)
+
+
+class BufferedFileTest(unittest.TestCase):
+ def test_simple(self):
+ f = LoopbackFile("r")
+ try:
+ f.write(b"hi")
+ self.assertTrue(False, "no exception on write to read-only file")
+ except:
+ pass
+ f.close()
+
+ f = LoopbackFile("w")
+ try:
+ f.read(1)
+ self.assertTrue(False, "no exception to read from write-only file")
+ except:
+ pass
+ f.close()
+
+ def test_readline(self):
+ f = LoopbackFile("r+U")
+ f.write(
+ b"First line.\nSecond line.\r\nThird line.\n"
+ + b"Fourth line.\nFinal line non-terminated."
+ )
+
+ self.assertEqual(f.readline(), "First line.\n")
+ # universal newline mode should convert this linefeed:
+ self.assertEqual(f.readline(), "Second line.\n")
+ # truncated line:
+ self.assertEqual(f.readline(7), "Third l")
+ self.assertEqual(f.readline(), "ine.\n")
+ # newline should be detected and only the fourth line returned
+ self.assertEqual(f.readline(39), "Fourth line.\n")
+ self.assertEqual(f.readline(), "Final line non-terminated.")
+ self.assertEqual(f.readline(), "")
+ f.close()
+ try:
+ f.readline()
+ self.assertTrue(False, "no exception on readline of closed file")
+ except IOError:
+ pass
+ self.assertTrue(linefeed_byte in f.newlines)
+ self.assertTrue(crlf in f.newlines)
+ self.assertTrue(cr_byte not in f.newlines)
+
+ def test_lf(self):
+ """
+ try to trick the linefeed detector.
+ """
+ f = LoopbackFile("r+U")
+ f.write(b"First line.\r")
+ self.assertEqual(f.readline(), "First line.\n")
+ f.write(b"\nSecond.\r\n")
+ self.assertEqual(f.readline(), "Second.\n")
+ f.close()
+ self.assertEqual(f.newlines, crlf)
+
+ def test_write(self):
+ """
+ verify that write buffering is on.
+ """
+ f = LoopbackFile("r+", 1)
+ f.write(b"Complete line.\nIncomplete line.")
+ self.assertEqual(f.readline(), "Complete line.\n")
+ self.assertEqual(f.readline(), "")
+ f.write("..\n")
+ self.assertEqual(f.readline(), "Incomplete line...\n")
+ f.close()
+
+ def test_flush(self):
+ """
+ verify that flush will force a write.
+ """
+ f = LoopbackFile("r+", 512)
+ f.write("Not\nquite\n512 bytes.\n")
+ self.assertEqual(f.read(1), b"")
+ f.flush()
+ self.assertEqual(f.read(6), b"Not\nqu")
+ self.assertEqual(f.read(4), b"ite\n")
+ self.assertEqual(f.read(5), b"512 b")
+ self.assertEqual(f.read(9), b"ytes.\n")
+ self.assertEqual(f.read(3), b"")
+ f.close()
+
+ def test_buffering_flushes(self):
+ """
+ verify that flushing happens automatically on buffer crossing.
+ """
+ f = LoopbackFile("r+", 16)
+ f.write(b"Too small.")
+ self.assertEqual(f.read(4), b"")
+ f.write(b" ")
+ self.assertEqual(f.read(4), b"")
+ f.write(b"Enough.")
+ self.assertEqual(f.read(20), b"Too small. Enough.")
+ f.close()
+
+ def test_read_all(self):
+ """
+ verify that read(-1) returns everything left in the file.
+ """
+ f = LoopbackFile("r+", 16)
+ f.write(b"The first thing you need to do is open your eyes. ")
+ f.write(b"Then, you need to close them again.\n")
+ s = f.read(-1)
+ self.assertEqual(
+ s,
+ b"The first thing you need to do is open your eyes. Then, you "
+ + b"need to close them again.\n",
+ )
+ f.close()
+
+ def test_readable(self):
+ f = LoopbackFile("r")
+ self.assertTrue(f.readable())
+ self.assertFalse(f.writable())
+ self.assertFalse(f.seekable())
+ f.close()
+
+ def test_writable(self):
+ f = LoopbackFile("w")
+ self.assertTrue(f.writable())
+ self.assertFalse(f.readable())
+ self.assertFalse(f.seekable())
+ f.close()
+
+ def test_readinto(self):
+ data = bytearray(5)
+ f = LoopbackFile("r+")
+ f._write(b"hello")
+ f.readinto(data)
+ self.assertEqual(data, b"hello")
+ f.close()
+
+ def test_write_bad_type(self):
+ with LoopbackFile("wb") as f:
+ self.assertRaises(TypeError, f.write, object())
+
+ def test_write_unicode_as_binary(self):
+ text = "\xa7 why is writing text to a binary file allowed?\n"
+ with LoopbackFile("rb+") as f:
+ f.write(text)
+ self.assertEqual(f.read(), text.encode("utf-8"))
+
+ @needs_builtin("memoryview")
+ def test_write_bytearray(self):
+ with LoopbackFile("rb+") as f:
+ f.write(bytearray(12))
+ self.assertEqual(f.read(), 12 * b"\0")
+
+ @needs_builtin("buffer")
+ def test_write_buffer(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile("rb+") as f:
+ for offset in offsets:
+ f.write(buffer(data, offset, 8)) # noqa
+ self.assertEqual(f.read(), data)
+
+ @needs_builtin("memoryview")
+ def test_write_memoryview(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile("rb+") as f:
+ view = memoryview(data)
+ for offset in offsets:
+ f.write(view[offset : offset + 8])
+ self.assertEqual(f.read(), data)
+
+
+if __name__ == "__main__":
+ from unittest import main
+
+ main()
diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py
new file mode 100644
index 0000000..da62fd9
--- /dev/null
+++ b/tests/test_gssapi.py
@@ -0,0 +1,225 @@
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.de>
+#
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Test the used APIs for GSS-API / SSPI authentication
+"""
+
+import socket
+
+from ._util import needs_gssapi, KerberosTestCase, update_env
+
+#
+# NOTE: KerberosTestCase skips all tests if it was unable to import k5test
+# third-party library. That's the primary trigger for whether this module
+# effectively gets run or not. See tests/util.py for other triggers (a set of
+# env vars a human might have defined).
+#
+
+
+@needs_gssapi
+class GSSAPITest(KerberosTestCase):
+ def setUp(self):
+ super().setUp()
+ # TODO: these vars should all come from os.environ or whatever the
+ # approved pytest method is for runtime-configuring test data.
+ self.krb5_mech = "1.2.840.113554.1.2.2"
+ self.targ_name = self.realm.hostname
+ self.server_mode = False
+ update_env(self, self.realm.env)
+
+ def test_pyasn1(self):
+ """
+ Test the used methods of pyasn1.
+ """
+ from pyasn1.type.univ import ObjectIdentifier
+ from pyasn1.codec.der import encoder, decoder
+
+ oid = encoder.encode(ObjectIdentifier(self.krb5_mech))
+ mech, __ = decoder.decode(oid)
+ self.assertEquals(self.krb5_mech, mech.__str__())
+
+ def _gssapi_sspi_test(self):
+ """
+ Test the used methods of python-gssapi or sspi, sspicon from pywin32.
+ """
+ try:
+ import gssapi
+
+ if (
+ hasattr(gssapi, "__title__")
+ and gssapi.__title__ == "python-gssapi"
+ ):
+ _API = "PYTHON-GSSAPI-OLD"
+ else:
+ _API = "PYTHON-GSSAPI-NEW"
+ except ImportError:
+ import sspicon
+ import sspi
+
+ _API = "SSPI"
+
+ c_token = None
+ gss_ctxt_status = False
+ mic_msg = b"G'day Mate!"
+
+ if _API == "PYTHON-GSSAPI-OLD":
+ if self.server_mode:
+ gss_flags = (
+ gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_MUTUAL_FLAG,
+ gssapi.C_DELEG_FLAG,
+ )
+ else:
+ gss_flags = (
+ gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_DELEG_FLAG,
+ )
+ # Initialize a GSS-API context.
+ ctx = gssapi.Context()
+ ctx.flags = gss_flags
+ krb5_oid = gssapi.OID.mech_from_string(self.krb5_mech)
+ target_name = gssapi.Name(
+ "host@" + self.targ_name, gssapi.C_NT_HOSTBASED_SERVICE
+ )
+ gss_ctxt = gssapi.InitContext(
+ peer_name=target_name, mech_type=krb5_oid, req_flags=ctx.flags
+ )
+ if self.server_mode:
+ c_token = gss_ctxt.step(c_token)
+ gss_ctxt_status = gss_ctxt.established
+ self.assertEquals(False, gss_ctxt_status)
+ # Accept a GSS-API context.
+ gss_srv_ctxt = gssapi.AcceptContext()
+ s_token = gss_srv_ctxt.step(c_token)
+ gss_ctxt_status = gss_srv_ctxt.established
+ self.assertNotEquals(None, s_token)
+ self.assertEquals(True, gss_ctxt_status)
+ # Establish the client context
+ c_token = gss_ctxt.step(s_token)
+ self.assertEquals(None, c_token)
+ else:
+ while not gss_ctxt.established:
+ c_token = gss_ctxt.step(c_token)
+ self.assertNotEquals(None, c_token)
+ # Build MIC
+ mic_token = gss_ctxt.get_mic(mic_msg)
+
+ if self.server_mode:
+ # Check MIC
+ status = gss_srv_ctxt.verify_mic(mic_msg, mic_token)
+ self.assertEquals(0, status)
+ elif _API == "PYTHON-GSSAPI-NEW":
+ if self.server_mode:
+ gss_flags = (
+ gssapi.RequirementFlag.protection_ready,
+ gssapi.RequirementFlag.integrity,
+ gssapi.RequirementFlag.mutual_authentication,
+ gssapi.RequirementFlag.delegate_to_peer,
+ )
+ else:
+ gss_flags = (
+ gssapi.RequirementFlag.protection_ready,
+ gssapi.RequirementFlag.integrity,
+ gssapi.RequirementFlag.delegate_to_peer,
+ )
+ # Initialize a GSS-API context.
+ krb5_oid = gssapi.MechType.kerberos
+ target_name = gssapi.Name(
+ "host@" + self.targ_name,
+ name_type=gssapi.NameType.hostbased_service,
+ )
+ gss_ctxt = gssapi.SecurityContext(
+ name=target_name,
+ flags=gss_flags,
+ mech=krb5_oid,
+ usage="initiate",
+ )
+ if self.server_mode:
+ c_token = gss_ctxt.step(c_token)
+ gss_ctxt_status = gss_ctxt.complete
+ self.assertEquals(False, gss_ctxt_status)
+ # Accept a GSS-API context.
+ gss_srv_ctxt = gssapi.SecurityContext(usage="accept")
+ s_token = gss_srv_ctxt.step(c_token)
+ gss_ctxt_status = gss_srv_ctxt.complete
+ self.assertNotEquals(None, s_token)
+ self.assertEquals(True, gss_ctxt_status)
+ # Establish the client context
+ c_token = gss_ctxt.step(s_token)
+ self.assertEquals(None, c_token)
+ else:
+ while not gss_ctxt.complete:
+ c_token = gss_ctxt.step(c_token)
+ self.assertNotEquals(None, c_token)
+ # Build MIC
+ mic_token = gss_ctxt.get_signature(mic_msg)
+
+ if self.server_mode:
+ # Check MIC
+ status = gss_srv_ctxt.verify_signature(mic_msg, mic_token)
+ self.assertEquals(0, status)
+ else:
+ gss_flags = (
+ sspicon.ISC_REQ_INTEGRITY
+ | sspicon.ISC_REQ_MUTUAL_AUTH
+ | sspicon.ISC_REQ_DELEGATE
+ )
+ # Initialize a GSS-API context.
+ target_name = "host/" + socket.getfqdn(self.targ_name)
+ gss_ctxt = sspi.ClientAuth(
+ "Kerberos", scflags=gss_flags, targetspn=target_name
+ )
+ if self.server_mode:
+ error, token = gss_ctxt.authorize(c_token)
+ c_token = token[0].Buffer
+ self.assertEquals(0, error)
+ # Accept a GSS-API context.
+ gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=target_name)
+ error, token = gss_srv_ctxt.authorize(c_token)
+ s_token = token[0].Buffer
+ # Establish the context.
+ error, token = gss_ctxt.authorize(s_token)
+ c_token = token[0].Buffer
+ self.assertEquals(None, c_token)
+ self.assertEquals(0, error)
+ # Build MIC
+ mic_token = gss_ctxt.sign(mic_msg)
+ # Check MIC
+ gss_srv_ctxt.verify(mic_msg, mic_token)
+ else:
+ error, token = gss_ctxt.authorize(c_token)
+ c_token = token[0].Buffer
+ self.assertNotEquals(0, error)
+
+ def test_gssapi_sspi_client(self):
+ """
+ Test the used methods of python-gssapi or sspi, sspicon from pywin32.
+ """
+ self._gssapi_sspi_test()
+
+ def test_gssapi_sspi_server(self):
+ """
+ Test the used methods of python-gssapi or sspi, sspicon from pywin32.
+ """
+ self.server_mode = True
+ self._gssapi_sspi_test()
diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py
new file mode 100644
index 0000000..a028411
--- /dev/null
+++ b/tests/test_hostkeys.py
@@ -0,0 +1,172 @@
+# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for HostKeys.
+"""
+
+from base64 import decodebytes
+from binascii import hexlify
+import os
+import unittest
+
+import paramiko
+
+
+test_hosts_file = """\
+secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\
+9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\
+D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc=
+broken.example.com ssh-rsa AAAA
+happy.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\
+BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
+5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
+modern.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKHEChAIxsh2hr8Q\
++Ea1AAHZyfEB2elEc2YgduVzBtp+
+curvy.example.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz\
+dHAyNTYAAABBBAa+pY7djSpbg5viAcZhPt56AO3U3Sd7h7dnlUp0EjfDgyYHYQxl2QZ4JGgfwR5iv9\
+T9iRZjQzvJd5s+kBAZtpk=
+"""
+
+test_hosts_file_tabs = """\
+secure.example.com\tssh-rsa\tAAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\
+9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\
+D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc=
+happy.example.com\tssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\
+BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
+5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
+modern.example.com\tssh-ed25519\tAAAAC3NzaC1lZDI1NTE5AAAAIKHEChAIxsh2hr8Q\
++Ea1AAHZyfEB2elEc2YgduVzBtp+
+curvy.example.com\tecdsa-sha2-nistp256\tAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbml\
+zdHAyNTYAAABBBAa+pY7djSpbg5viAcZhPt56AO3U3Sd7h7dnlUp0EjfDgyYHYQxl2QZ4JGgfwR5iv\
+9T9iRZjQzvJd5s+kBAZtpk=
+"""
+
+keyblob = b"""\
+AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31MBGQ3GQ/Fc7SX6gkpXkwcZryoi4k\
+NFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW5ymME3bQ4J/k1IKxCtz/bAlAqFgK\
+oc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M="""
+
+keyblob_dss = b"""\
+AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/\
+h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF60\
+8EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIE\
+AkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+\
+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg\
+4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO\
+1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o\
+0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgE="""
+
+
+class HostKeysTest(unittest.TestCase):
+ def setUp(self):
+ with open("hostfile.temp", "w") as f:
+ f.write(test_hosts_file)
+
+ def tearDown(self):
+ os.unlink("hostfile.temp")
+
+ def test_load(self):
+ hostdict = paramiko.HostKeys("hostfile.temp")
+ assert len(hostdict) == 4
+ self.assertEqual(1, len(list(hostdict.values())[0]))
+ self.assertEqual(1, len(list(hostdict.values())[1]))
+ fp = hexlify(
+ hostdict["secure.example.com"]["ssh-rsa"].get_fingerprint()
+ ).upper()
+ self.assertEqual(b"E6684DB30E109B67B70FF1DC5C7F1363", fp)
+
+ def test_add(self):
+ hostdict = paramiko.HostKeys("hostfile.temp")
+ hh = "|1|BMsIC6cUIP2zBuXR3t2LRcJYjzM=|hpkJMysjTk/+zzUUzxQEa2ieq6c="
+ key = paramiko.RSAKey(data=decodebytes(keyblob))
+ hostdict.add(hh, "ssh-rsa", key)
+ assert len(hostdict) == 5
+ x = hostdict["foo.example.com"]
+ fp = hexlify(x["ssh-rsa"].get_fingerprint()).upper()
+ self.assertEqual(b"7EC91BB336CB6D810B124B1353C32396", fp)
+ self.assertTrue(hostdict.check("foo.example.com", key))
+
+ def test_dict(self):
+ hostdict = paramiko.HostKeys("hostfile.temp")
+ self.assertTrue("secure.example.com" in hostdict)
+ self.assertTrue("not.example.com" not in hostdict)
+ self.assertTrue("secure.example.com" in hostdict)
+ self.assertTrue("not.example.com" not in hostdict)
+ x = hostdict.get("secure.example.com", None)
+ self.assertTrue(x is not None)
+ fp = hexlify(x["ssh-rsa"].get_fingerprint()).upper()
+ self.assertEqual(b"E6684DB30E109B67B70FF1DC5C7F1363", fp)
+ assert list(hostdict) == hostdict.keys()
+ assert len(list(hostdict)) == len(hostdict.keys()) == 4
+
+ def test_dict_set(self):
+ hostdict = paramiko.HostKeys("hostfile.temp")
+ key = paramiko.RSAKey(data=decodebytes(keyblob))
+ key_dss = paramiko.DSSKey(data=decodebytes(keyblob_dss))
+ hostdict["secure.example.com"] = {"ssh-rsa": key, "ssh-dss": key_dss}
+ hostdict["fake.example.com"] = {}
+ hostdict["fake.example.com"]["ssh-rsa"] = key
+
+ assert len(hostdict) == 5
+ self.assertEqual(2, len(list(hostdict.values())[0]))
+ self.assertEqual(1, len(list(hostdict.values())[1]))
+ self.assertEqual(1, len(list(hostdict.values())[2]))
+ fp = hexlify(
+ hostdict["secure.example.com"]["ssh-rsa"].get_fingerprint()
+ ).upper()
+ self.assertEqual(b"7EC91BB336CB6D810B124B1353C32396", fp)
+ fp = hexlify(
+ hostdict["secure.example.com"]["ssh-dss"].get_fingerprint()
+ ).upper()
+ self.assertEqual(b"4478F0B9A23CC5182009FF755BC1D26C", fp)
+
+ def test_delitem(self):
+ hostdict = paramiko.HostKeys("hostfile.temp")
+ target = "happy.example.com"
+ hostdict[target] # will KeyError if not present
+ del hostdict[target]
+ try:
+ hostdict[target]
+ except KeyError:
+ pass # Good
+ else:
+ assert False, "Entry was not deleted from HostKeys on delitem!"
+
+ def test_entry_delitem(self):
+ hostdict = paramiko.HostKeys("hostfile.temp")
+ target = "happy.example.com"
+ entry = hostdict[target]
+ key_type_list = [key_type for key_type in entry]
+ for key_type in key_type_list:
+ del entry[key_type]
+
+ # will KeyError if not present
+ for key_type in key_type_list:
+ try:
+ del entry[key_type]
+ except KeyError:
+ pass # Good
+ else:
+ assert False, "Key was not deleted from Entry on delitem!"
+
+
+class HostKeysTabsTest(HostKeysTest):
+ def setUp(self):
+ with open("hostfile.temp", "w") as f:
+ f.write(test_hosts_file_tabs)
diff --git a/tests/test_kex.py b/tests/test_kex.py
new file mode 100644
index 0000000..c3bf2b0
--- /dev/null
+++ b/tests/test_kex.py
@@ -0,0 +1,668 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for the key exchange protocols.
+"""
+
+from binascii import hexlify, unhexlify
+import os
+import unittest
+
+from unittest.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):
+ return byte_chr(0xCC) * n
+
+
+def dummy_generate_key_pair(obj):
+ private_key_value = 94761803665136558137557783047955027733968423115106677159790289642479432803037 # noqa
+ public_key_numbers = "042bdab212fa8ba1b7c843301682a4db424d307246c7e1e6083c41d9ca7b098bf30b3d63e2ec6278488c135360456cc054b3444ecc45998c08894cbc1370f5f989" # noqa
+ 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.EllipticCurvePublicKey.from_encoded_point(
+ ec.SECP256R1(), unhexlify(public_key_numbers)
+ )
+ return
+ obj.Q_C = ec.EllipticCurvePublicKey.from_encoded_point(
+ ec.SECP256R1(), unhexlify(public_key_numbers)
+ )
+
+
+class FakeKey:
+ def __str__(self):
+ return "fake-key"
+
+ def asbytes(self):
+ return b"fake-key"
+
+ def sign_ssh_data(self, H, algorithm):
+ return b"fake-sig"
+
+
+class FakeModulusPack:
+ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF # noqa
+ G = 2
+
+ def get_modulus(self, min, ask, max):
+ return self.G, self.P
+
+
+class FakeTransport:
+ local_version = "SSH-2.0-paramiko_1.0"
+ remote_version = "SSH-2.0-lame"
+ local_kex_init = "local-kex-init"
+ remote_kex_init = "remote-kex-init"
+ host_key_type = "fake-key"
+
+ def _send_message(self, m):
+ self._message = m
+
+ def _expect_packet(self, *t):
+ self._expect = t
+
+ def _set_K_H(self, K, H):
+ self._K = K
+ self._H = H
+
+ def _verify_key(self, host_key, sig):
+ self._verify = (host_key, sig)
+
+ def _activate_outbound(self):
+ self._activated = True
+
+ def _log(self, level, s):
+ pass
+
+ def get_server_key(self):
+ return FakeKey()
+
+ def _get_modulus_pack(self):
+ return FakeModulusPack()
+
+
+class KexTest(unittest.TestCase):
+
+ K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504 # noqa
+
+ 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
+
+ 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()
+ transport.server_mode = False
+ kex = KexGroup1(transport)
+ kex.start_kex()
+ x = b"1E000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4" # 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)
+ H = b"03079780F3D3AD0B3C6DB30C8D21685F367A86D2"
+ self.assertEqual(self.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_group1_server(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGroup1(transport)
+ kex.start_kex()
+ self.assertEqual(
+ (paramiko.kex_group1._MSG_KEXDH_INIT,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(69)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_group1._MSG_KEXDH_INIT, msg)
+ H = b"B16BF34DD10945EDE84E9C1EF24A14BFDC843389"
+ x = b"1F0000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967" # noqa
+ self.assertEqual(self.K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+ def test_gex_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGex(transport)
+ kex.start_kex()
+ x = b"22000004000000080000002000"
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b"20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_string("fake-host-key")
+ msg.add_mpint(69)
+ msg.add_string("fake-sig")
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b"A265563F2FA87F1A89BF007EE90D58BE2E4A4BD0"
+ self.assertEqual(self.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_gex_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGex(transport)
+ kex.start_kex(_test_old_style=True)
+ x = b"1E00000800"
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b"20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_string("fake-host-key")
+ msg.add_mpint(69)
+ msg.add_string("fake-sig")
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b"807F87B269EF7AC5EC7E75676808776A27D5864C"
+ self.assertEqual(self.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_gex_server(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGex(transport)
+ kex.start_kex()
+ self.assertEqual(
+ (
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST,
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD,
+ ),
+ transport._expect,
+ )
+
+ msg = Message()
+ msg.add_int(1024)
+ msg.add_int(2048)
+ msg.add_int(4096)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg)
+ x = b"1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 # noqa
+ H = b"CE754197C21BF3452863B4F44D0B3951F12516EF"
+ x = b"210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967" # noqa
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+ def test_gex_server_with_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGex(transport)
+ kex.start_kex()
+ self.assertEqual(
+ (
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST,
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD,
+ ),
+ transport._expect,
+ )
+
+ msg = Message()
+ msg.add_int(2048)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg)
+ x = b"1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 # noqa
+ H = b"B41A06B2E59043CEFC1AE16EC31F1E2D12EC455B"
+ x = b"210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967" # noqa
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+ def test_gex_sha256_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ x = b"22000004000000080000002000"
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b"20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_string("fake-host-key")
+ msg.add_mpint(69)
+ msg.add_string("fake-sig")
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b"AD1A9365A67B4496F05594AD1BF656E3CDA0851289A4C1AFF549FEAE50896DF4"
+ self.assertEqual(self.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_gex_sha256_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGexSHA256(transport)
+ kex.start_kex(_test_old_style=True)
+ x = b"1E00000800"
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b"20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_string("fake-host-key")
+ msg.add_mpint(69)
+ msg.add_string("fake-sig")
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b"518386608B15891AE5237DEE08DCADDE76A0BCEFCE7F6DB3AD66BC41D256DFE5"
+ self.assertEqual(self.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_gex_sha256_server(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ self.assertEqual(
+ (
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST,
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD,
+ ),
+ transport._expect,
+ )
+
+ msg = Message()
+ msg.add_int(1024)
+ msg.add_int(2048)
+ msg.add_int(4096)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg)
+ x = b"1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 # noqa
+ H = b"CCAC0497CF0ABA1DBF55E1A3995D17F4CC31824B0E8D95CDF8A06F169D050D80"
+ x = b"210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967" # noqa
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+ def test_gex_sha256_server_with_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ self.assertEqual(
+ (
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST,
+ paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD,
+ ),
+ transport._expect,
+ )
+
+ msg = Message()
+ msg.add_int(2048)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg)
+ x = b"1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102" # noqa
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual(
+ (paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 # noqa
+ H = b"3DDD2AD840AD095E397BA4D0573972DC60F6461FD38A187CACA6615A5BC8ADBB"
+ x = b"210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967" # noqa
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+ def test_kex_nistp256_client(self):
+ K = 91610929826364598472338906427792435253694642563583721654249504912114314269754 # noqa
+ 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" # noqa
+ )
+ 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_kex_nistp256_server(self):
+ K = 91610929826364598472338906427792435253694642563583721654249504912114314269754 # noqa
+ 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" # noqa
+ )
+ 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())
+
+ 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_group14_sha256_server(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGroup14SHA256(transport)
+ kex.start_kex()
+ self.assertEqual(
+ (paramiko.kex_group1._MSG_KEXDH_INIT,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(69)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_group1._MSG_KEXDH_INIT, msg)
+ K = 21526936926159575624241589599003964979640840086252478029709904308461709651400109485351462666820496096345766733042945918306284902585618061272525323382142547359684512114160415969631877620660064043178086464811345023251493620331559440565662862858765724251890489795332144543057725932216208403143759943169004775947331771556537814494448612329251887435553890674764339328444948425882382475260315505741818518926349729970262019325118040559191290279100613049085709127598666890434114956464502529053036826173452792849566280474995114751780998069614898221773345705289637708545219204637224261997310181473787577166103031529148842107599 # noqa
+ H = b"15080A19894D489ACD0DA724480E1B08E71293E07EBC25FAD10F263C00B343DC"
+ x = b"1F0000000866616B652D6B657900000101009850B3A8DE3ECCD3F19644139137C93D9C11BC28ED8BE850908EE294E1D43B88B9295311EFAEF5B736A1B652EBE184CCF36CFB0681C1ED66430088FA448B83619F928E7B9592ED6160EC11D639D51C303603F930F743C646B1B67DA38A1D44598DCE6C3F3019422B898044141420E9A10C29B9C58668F7F20A40F154B2C4768FCF7A9AA7179FB6366A7167EE26DD58963E8B880A0572F641DE0A73DC74C930F7C3A0C9388553F3F8403E40CF8B95FEDB1D366596FCF3FDDEB21A0005ADA650EF1733628D807BE5ACB83925462765D9076570056E39994FB328E3108FE406275758D6BF5F32790EF15D8416BF5548164859E785DB45E7787BB0E727ADE08641ED0000000866616B652D736967" # noqa
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ 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)
+
+ def test_kex_group16_sha512_server(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGroup16SHA512(transport)
+ kex.start_kex()
+ self.assertEqual(
+ (paramiko.kex_group1._MSG_KEXDH_INIT,), transport._expect
+ )
+
+ msg = Message()
+ msg.add_mpint(69)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_group1._MSG_KEXDH_INIT, msg)
+ K = 933242830095376162107925500057692534838883186615567574891154103836907630698358649443101764908667358576734565553213003142941996368306996312915844839972197961603283544950658467545799914435739152351344917376359963584614213874232577733869049670230112638724993540996854599166318001059065780674008011575015459772051180901213815080343343801745386220342919837913506966863570473712948197760657442974564354432738520446202131551650771882909329069340612274196233658123593466135642819578182367229641847749149740891990379052266213711500434128970973602206842980669193719602075489724202241641553472106310932258574377789863734311328542715212248147206865762697424822447603031087553480483833829498375309975229907460562402877655519980113688369262871485777790149373908739910846630414678346163764464587129010141922982925829457954376352735653834300282864445132624993186496129911208133529828461690634463092007726349795944930302881758403402084584307180896465875803621285362317770276493727205689466142632599776710824902573926951951209239626732358074877997756011804454926541386215567756538832824717436605031489511654178384081883801272314328403020205577714999460724519735573055540814037716770051316113795603990199374791348798218428912977728347485489266146775472 # noqa
+ H = b"F97BB05A572A663688CA7EA1AA812D3C82EE6C8FA9D4B1D69435783D931157F199909EA38B003E4E4385C8861183CBFF0CF0EF1433A8B3C69AB4DD9420FCC85F" # noqa
+ x = b"1F0000000866616B652D6B65790000020100859FF55A23E0F66463561DD8BFC4764C69C05F85665B06EC9E29EF5003A53A8FA890B6A6EB624DEB55A4FB279DE7010A53580A126817E3D235B05A1081662B1500961D0625F0AAD287F1B597CBA9DB9550D9CC26355C4C59F92E613B5C21AC191F152C09A5DB46DCBA5EA58E3CA6A8B0EB7183E27FAC10106022E8521FA91240FB389060F1E1E4A355049D29DCC82921CE6588791743E4B1DEEE0166F7CC5180C3C75F3773342DF95C8C10AAA5D12975257027936B99B3DED6E6E98CF27EADEAEAE04E7F0A28071F578646B985FCE28A59CEB36287CB65759BE0544D4C4018CDF03C9078FE9CA79ECA611CB6966899E6FD29BE0781491C659FE2380E0D99D50D9CFAAB94E61BE311779719C4C43C6D223AD3799C3915A9E55076A21152DBBF911D6594296D6ECDC1B6FA71997CD29DF987B80FCA7F36BB7F19863C72BBBF839746AFBF9A5B407D468C976AA3E36FA118D3EAAD2E08BF6AE219F81F2CE2BE946337F06CC09BBFABE938A4087E413921CBEC1965ED905999B83396ECA226110CDF6EFB80F815F6489AF87561DA3857F13A7705921306D94176231FBB336B17C3724BC17A28BECB910093AB040873D5D760E8C182B88ECCE3E38DDA68CE35BD152DF7550BD908791FCCEDD1FFDF5ED2A57FFAE79599E487A7726D8A3D950B1729A08FBB60EE462A6BBE8BF0F5F0E1358129A37840FE5B3EEB8BF26E99FA222EAE830000000866616B652D736967" # noqa
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ 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())
diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py
new file mode 100644
index 0000000..a81b195
--- /dev/null
+++ b/tests/test_kex_gss.py
@@ -0,0 +1,154 @@
+# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.de>
+#
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Unit Tests for the GSS-API / SSPI SSHv2 Diffie-Hellman Key Exchange and user
+authentication
+"""
+
+
+import socket
+import threading
+import unittest
+
+import paramiko
+
+from ._util import needs_gssapi, KerberosTestCase, update_env, _support
+
+
+class NullServer(paramiko.ServerInterface):
+ def get_allowed_auths(self, username):
+ return "gssapi-keyex"
+
+ def check_auth_gssapi_keyex(
+ self, username, gss_authenticated=paramiko.AUTH_FAILED, cc_file=None
+ ):
+ if gss_authenticated == paramiko.AUTH_SUCCESSFUL:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def enable_auth_gssapi(self):
+ UseGSSAPI = True
+ return UseGSSAPI
+
+ def check_channel_request(self, kind, chanid):
+ return paramiko.OPEN_SUCCEEDED
+
+ def check_channel_exec_request(self, channel, command):
+ if command != b"yes":
+ return False
+ return True
+
+
+@needs_gssapi
+class GSSKexTest(KerberosTestCase):
+ def setUp(self):
+ self.username = self.realm.user_princ
+ self.hostname = socket.getfqdn(self.realm.hostname)
+ self.sockl = socket.socket()
+ self.sockl.bind((self.realm.hostname, 0))
+ self.sockl.listen(1)
+ self.addr, self.port = self.sockl.getsockname()
+ self.event = threading.Event()
+ update_env(self, self.realm.env)
+ thread = threading.Thread(target=self._run)
+ thread.start()
+
+ def tearDown(self):
+ for attr in "tc ts socks sockl".split():
+ if hasattr(self, attr):
+ getattr(self, attr).close()
+
+ def _run(self):
+ self.socks, addr = self.sockl.accept()
+ self.ts = paramiko.Transport(self.socks, gss_kex=True)
+ host_key = paramiko.RSAKey.from_private_key_file(_support("rsa.key"))
+ self.ts.add_server_key(host_key)
+ self.ts.set_gss_host(self.realm.hostname)
+ try:
+ self.ts.load_server_moduli()
+ except:
+ print("(Failed to load moduli -- gex will be unsupported.)")
+ server = NullServer()
+ self.ts.start_server(self.event, server)
+
+ 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
+ context created during key exchange.
+ """
+ host_key = paramiko.RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ self.tc = paramiko.SSHClient()
+ self.tc.get_host_keys().add(
+ f"[{self.hostname}]:{self.port}", "ssh-rsa", public_host_key
+ )
+ self.tc.connect(
+ self.hostname,
+ self.port,
+ username=self.username,
+ gss_auth=True,
+ gss_kex=True,
+ gss_host=gss_host,
+ )
+
+ self.event.wait(1.0)
+ self.assert_(self.event.is_set())
+ 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")
+ schan.close()
+
+ self.assertEquals("Hello there.\n", stdout.readline())
+ self.assertEquals("", stdout.readline())
+ self.assertEquals("This is on stderr.\n", stderr.readline())
+ self.assertEquals("", stderr.readline())
+
+ stdin.close()
+ stdout.close()
+ stderr.close()
+
+ def test_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)
+
+ # To be investigated, see https://github.com/paramiko/paramiko/issues/1312
+ @unittest.expectedFailure
+ def test_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_message.py b/tests/test_message.py
new file mode 100644
index 0000000..3c5f961
--- /dev/null
+++ b/tests/test_message.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for ssh protocol message blocks.
+"""
+
+import unittest
+
+from paramiko.message import Message
+from paramiko.common import byte_chr, zero_byte
+
+
+class MessageTest(unittest.TestCase):
+
+ __a = (
+ b"\x00\x00\x00\x17\x07\x60\xe0\x90\x00\x00\x00\x01\x71\x00\x00\x00\x05\x68\x65\x6c\x6c\x6f\x00\x00\x03\xe8" # noqa
+ + b"x" * 1000
+ )
+ __b = b"\x01\x00\xf3\x00\x3f\x00\x00\x00\x10\x68\x75\x65\x79\x2c\x64\x65\x77\x65\x79\x2c\x6c\x6f\x75\x69\x65" # noqa
+ __c = b"\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x01\x11\x00\x00\x00\x07\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x06\x9a\x1b\x2c\x3d\x4e\xf7" # noqa
+ __d = b"\x00\x00\x00\x05\xff\x00\x00\x00\x05\x11\x22\x33\x44\x55\xff\x00\x00\x00\x0a\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x63\x61\x74\x00\x00\x00\x03\x61\x2c\x62" # noqa
+
+ def test_encode(self):
+ msg = Message()
+ msg.add_int(23)
+ msg.add_int(123789456)
+ msg.add_string("q")
+ msg.add_string("hello")
+ msg.add_string("x" * 1000)
+ self.assertEqual(msg.asbytes(), self.__a)
+
+ msg = Message()
+ msg.add_boolean(True)
+ msg.add_boolean(False)
+ msg.add_byte(byte_chr(0xF3))
+
+ msg.add_bytes(zero_byte + byte_chr(0x3F))
+ msg.add_list(["huey", "dewey", "louie"])
+ self.assertEqual(msg.asbytes(), self.__b)
+
+ msg = Message()
+ msg.add_int64(5)
+ msg.add_int64(0xF5E4D3C2B109)
+ msg.add_mpint(17)
+ msg.add_mpint(0xF5E4D3C2B109)
+ msg.add_mpint(-0x65E4D3C2B109)
+ self.assertEqual(msg.asbytes(), self.__c)
+
+ def test_decode(self):
+ msg = Message(self.__a)
+ self.assertEqual(msg.get_int(), 23)
+ self.assertEqual(msg.get_int(), 123789456)
+ self.assertEqual(msg.get_text(), "q")
+ self.assertEqual(msg.get_text(), "hello")
+ self.assertEqual(msg.get_text(), "x" * 1000)
+
+ msg = Message(self.__b)
+ self.assertEqual(msg.get_boolean(), True)
+ self.assertEqual(msg.get_boolean(), False)
+ self.assertEqual(msg.get_byte(), byte_chr(0xF3))
+ self.assertEqual(msg.get_bytes(2), zero_byte + byte_chr(0x3F))
+ self.assertEqual(msg.get_list(), ["huey", "dewey", "louie"])
+
+ msg = Message(self.__c)
+ self.assertEqual(msg.get_int64(), 5)
+ self.assertEqual(msg.get_int64(), 0xF5E4D3C2B109)
+ self.assertEqual(msg.get_mpint(), 17)
+ self.assertEqual(msg.get_mpint(), 0xF5E4D3C2B109)
+ self.assertEqual(msg.get_mpint(), -0x65E4D3C2B109)
+
+ def test_add(self):
+ msg = Message()
+ msg.add(5)
+ msg.add(0x1122334455)
+ msg.add(0xF00000000000000000)
+ msg.add(True)
+ msg.add("cat")
+ msg.add(["a", "b"])
+ self.assertEqual(msg.asbytes(), self.__d)
+
+ def test_misc(self):
+ msg = Message(self.__d)
+ self.assertEqual(msg.get_adaptive_int(), 5)
+ self.assertEqual(msg.get_adaptive_int(), 0x1122334455)
+ self.assertEqual(msg.get_adaptive_int(), 0xF00000000000000000)
+ self.assertEqual(msg.get_so_far(), self.__d[:29])
+ self.assertEqual(msg.get_remainder(), self.__d[29:])
+ msg.rewind()
+ self.assertEqual(msg.get_adaptive_int(), 5)
+ self.assertEqual(msg.get_so_far(), self.__d[:4])
+ self.assertEqual(msg.get_remainder(), self.__d[4:])
+
+ def test_bytes_str_and_repr(self):
+ msg = Message(self.__d)
+ assert str(msg) == f"paramiko.Message({self.__d!r})"
+ assert repr(msg) == str(msg)
+ assert bytes(msg) == msg.asbytes() == self.__d
diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py
new file mode 100644
index 0000000..aee21c2
--- /dev/null
+++ b/tests/test_packetizer.py
@@ -0,0 +1,148 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for the ssh2 protocol in Transport.
+"""
+
+import sys
+import unittest
+from hashlib import sha1
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
+
+from paramiko import Message, Packetizer, util
+from paramiko.common import byte_chr, zero_byte
+
+from ._loop import LoopSocket
+
+
+x55 = byte_chr(0x55)
+x1f = byte_chr(0x1F)
+
+
+class PacketizerTest(unittest.TestCase):
+ def test_write(self):
+ rsock = LoopSocket()
+ wsock = LoopSocket()
+ rsock.link(wsock)
+ p = Packetizer(wsock)
+ p.set_log(util.get_logger("paramiko.transport"))
+ p.set_hexdump(True)
+ encryptor = Cipher(
+ algorithms.AES(zero_byte * 16),
+ modes.CBC(x55 * 16),
+ backend=default_backend(),
+ ).encryptor()
+ p.set_outbound_cipher(encryptor, 16, sha1, 12, x1f * 20)
+
+ # message has to be at least 16 bytes long, so we'll have at least one
+ # block of data encrypted that contains zero random padding bytes
+ m = Message()
+ m.add_byte(byte_chr(100))
+ m.add_int(100)
+ m.add_int(1)
+ m.add_int(900)
+ p.send_message(m)
+ data = rsock.recv(100)
+ # 32 + 12 bytes of MAC = 44
+ self.assertEqual(44, len(data))
+ self.assertEqual(
+ b"\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0", # noqa
+ data[:16],
+ )
+
+ def test_read(self):
+ rsock = LoopSocket()
+ wsock = LoopSocket()
+ rsock.link(wsock)
+ p = Packetizer(rsock)
+ p.set_log(util.get_logger("paramiko.transport"))
+ p.set_hexdump(True)
+ decryptor = Cipher(
+ algorithms.AES(zero_byte * 16),
+ modes.CBC(x55 * 16),
+ backend=default_backend(),
+ ).decryptor()
+ p.set_inbound_cipher(decryptor, 16, sha1, 12, x1f * 20)
+ wsock.send(
+ b"\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59" # noqa
+ )
+ cmd, m = p.read_message()
+ self.assertEqual(100, cmd)
+ self.assertEqual(100, m.get_int())
+ self.assertEqual(1, m.get_int())
+ self.assertEqual(900, m.get_int())
+
+ def test_closed(self):
+ if sys.platform.startswith("win"): # no SIGALRM on windows
+ return
+ rsock = LoopSocket()
+ wsock = LoopSocket()
+ rsock.link(wsock)
+ p = Packetizer(wsock)
+ p.set_log(util.get_logger("paramiko.transport"))
+ p.set_hexdump(True)
+ encryptor = Cipher(
+ algorithms.AES(zero_byte * 16),
+ modes.CBC(x55 * 16),
+ backend=default_backend(),
+ ).encryptor()
+ p.set_outbound_cipher(encryptor, 16, sha1, 12, x1f * 20)
+
+ # message has to be at least 16 bytes long, so we'll have at least one
+ # block of data encrypted that contains zero random padding bytes
+ m = Message()
+ m.add_byte(byte_chr(100))
+ m.add_int(100)
+ m.add_int(1)
+ m.add_int(900)
+ wsock.send = lambda x: 0
+ from functools import wraps
+ import errno
+ import os
+ import signal
+
+ class TimeoutError(Exception):
+ def __init__(self, error_message):
+ if hasattr(errno, "ETIME"):
+ self.message = os.sterror(errno.ETIME)
+ else:
+ self.messaage = error_message
+
+ def timeout(seconds=1, error_message="Timer expired"):
+ def decorator(func):
+ def _handle_timeout(signum, frame):
+ raise TimeoutError(error_message)
+
+ def wrapper(*args, **kwargs):
+ signal.signal(signal.SIGALRM, _handle_timeout)
+ signal.alarm(seconds)
+ try:
+ result = func(*args, **kwargs)
+ finally:
+ signal.alarm(0)
+ return result
+
+ return wraps(func)(wrapper)
+
+ return decorator
+
+ send = timeout()(p.send_message)
+ self.assertRaises(EOFError, send, m)
diff --git a/tests/test_pkey.py b/tests/test_pkey.py
new file mode 100644
index 0000000..d4d193b
--- /dev/null
+++ b/tests/test_pkey.py
@@ -0,0 +1,696 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for public/private key objects.
+"""
+
+import unittest
+import os
+import stat
+from binascii import hexlify
+from hashlib import md5
+from io import StringIO
+
+from paramiko import (
+ RSAKey,
+ DSSKey,
+ ECDSAKey,
+ Ed25519Key,
+ Message,
+ util,
+ SSHException,
+)
+from paramiko.util import b
+from paramiko.common import o600, byte_chr
+
+from cryptography.exceptions import UnsupportedAlgorithm
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateNumbers
+from unittest.mock import patch, Mock
+import pytest
+
+from ._util import _support, is_low_entropy, requires_sha1_signing
+
+
+# from openssh's ssh-keygen
+PUB_RSA = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=" # noqa
+PUB_DSS = "ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgE=" # noqa
+PUB_ECDSA_256 = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eo=" # noqa
+PUB_ECDSA_384 = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBbGibQLW9AAZiGN2hEQxWYYoFaWKwN3PKSaDJSMqmIn1Z9sgRUuw8Y/w502OGvXL/wFk0i2z50l3pWZjD7gfMH7gX5TUiCzwrQkS+Hn1U2S9aF5WJp0NcIzYxXw2r4M2A==" # noqa
+PUB_ECDSA_521 = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACaOaFLZGuxa5AW16qj6VLypFbLrEWrt9AZUloCMefxO8bNLjK/O5g0rAVasar1TnyHE9qj4NwzANZASWjQNbc4MAG8vzqezFwLIn/kNyNTsXNfqEko9OgHZknlj2Z79dwTJcRAL4QLcT5aND0EHZLB2fAUDXiWIb2j4rg1mwPlBMiBXA==" # noqa
+PUB_RSA_2K_OPENSSH = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDF+Dpr54DX0WdeTDpNAMdkCWEkl3OXtNgf58qlN1gX572OLBqLf0zT4bHstUEpU3piazph/rSWcUMuBoD46tZ6jiH7H9b9Pem2eYQWaELDDkM+v9BMbEy5rMbFRLol5OtEvPFqneyEAanPOgvd8t3yyhSev9QVusakzJ8j8LGgrA8huYZ+Srnw0shEWLG70KUKCh3rG0QIvA8nfhtUOisr2Gp+F0YxMGb5gwBlQYAYE5l6u1SjZ7hNjyNosjK+wRBFgFFBYVpkZKJgWoK9w4ijFyzMZTucnZMqKOKAjIJvHfKBf2/cEfYxSq1EndqTqjYsd9T7/s2vcn1OH5a0wkER" # noqa
+RSA_2K_OPENSSH_P = 161773687847617758886803946572654778625119997081005961935077336594287351354258259920334554906235187683459069634729972458348855793639393524799865799559575414247668746919721196359908321800753913350455861871582087986355637886875933045224711827701526739934602161222599672381604211130651397331775901258858869418853 # noqa
+RSA_2K_OPENSSH_Q = 154483416325630619558401349033571772244816915504195060221073502923720741119664820208064202825686848103224453777955988437823797692957091438442833606009978046057345917301441832647551208158342812551003395417862260727795454409459089912659057393394458150862012620127030757893820711157099494238156383382454310199869 # noqa
+PUB_DSS_1K_OPENSSH = "ssh-dss AAAAB3NzaC1kc3MAAACBAL8XEx7F9xuwBNles+vWpNF+YcofrBhjX1r5QhpBe0eoYWLHRcroN6lxwCdGYRfgOoRjTncBiixQX/uUxAY96zDh3ir492s2BcJt4ihvNn/AY0I0OTuX/2IwGk9CGzafjaeZNVYxMa8lcVt0hSOTjkPQ7gVuk6bJzMInvie+VWKLAAAAFQDUgYdY+rhR0SkKbC09BS/SIHcB+wAAAIB44+4zpCNcd0CGvZlowH99zyPX8uxQtmTLQFuR2O8O0FgVVuCdDgD0D9W8CLOp32oatpM0jyyN89EdvSWzjHzZJ+L6H1FtZps7uhpDFWHdva1R25vyGecLMUuXjo5t/D7oCDih+HwHoSAxoi0QvsPd8/qqHQVznNJKtR6thUpXEwAAAIAG4DCBjbgTTgpBw0egRkJwBSz0oTt+1IcapNU2jA6N8urMSk9YXHEQHKN68BAF3YJ59q2Ujv3LOXmBqGd1T+kzwUszfMlgzq8MMu19Yfzse6AIK1Agn1Vj6F7YXLsXDN+T4KszX5+FJa7t/Zsp3nALWy6l0f4WKivEF5Y2QpEFcQ==" # noqa
+PUB_EC_384_OPENSSH = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBIch5LXTq/L/TWsTGG6dIktxD8DIMh7EfvoRmWsks6CuNDTvFvbQNtY4QO1mn5OXegHbS0M5DPIS++wpKGFP3suDEH08O35vZQasLNrL0tO2jyyEnzB2ZEx3PPYci811yg==" # noqa
+
+FINGER_RSA = "1024 60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5"
+FINGER_DSS = "1024 44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c"
+FINGER_ECDSA_256 = "256 25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60"
+FINGER_ECDSA_384 = "384 c1:8d:a0:59:09:47:41:8e:a8:a6:07:01:29:23:b4:65"
+FINGER_ECDSA_521 = "521 44:58:22:52:12:33:16:0e:ce:0e:be:2c:7c:7e:cc:1e"
+SIGNED_RSA = "20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97:3d:a2:cd:5f:ca:20:21:73:4c:ad:34:73:8f:20:77:28:e2:94:15:08:d8:91:40:7a:85:83:bf:18:37:95:dc:54:1a:9b:88:29:6c:73:ca:38:b4:04:f1:56:b9:f2:42:9d:52:1b:29:29:b4:4f:fd:c9:2d:af:47:d2:40:76:30:f3:63:45:0c:d9:1d:43:86:0f:1c:70:e2:93:12:34:f3:ac:c5:0a:2f:14:50:66:59:f1:88:ee:c1:4a:e9:d1:9c:4e:46:f0:0e:47:6f:38:74:f1:44:a8" # noqa
+SIGNED_RSA_256 = "cc:6:60:e0:0:2c:ac:9e:26:bc:d5:68:64:3f:9f:a7:e5:aa:41:eb:88:4a:25:5:9c:93:84:66:ef:ef:60:f4:34:fb:f4:c8:3d:55:33:6a:77:bd:b2:ee:83:f:71:27:41:7e:f5:7:5:0:a9:4c:7:80:6f:be:76:67:cb:58:35:b9:2b:f3:c2:d3:3c:ee:e1:3f:59:e0:fa:e4:5c:92:ed:ae:74:de:d:d6:27:16:8f:84:a3:86:68:c:94:90:7d:6e:cc:81:12:d8:b6:ad:aa:31:a8:13:3d:63:81:3e:bb:5:b6:38:4d:2:d:1b:5b:70:de:83:cc:3a:cb:31" # noqa
+SIGNED_RSA_512 = "87:46:8b:75:92:33:78:a0:22:35:32:39:23:c6:ab:e1:6:92:ad:bc:7f:6e:ab:19:32:e4:78:b2:2c:8f:1d:c:65:da:fc:a5:7:ca:b6:55:55:31:83:b1:a0:af:d1:95:c5:2e:af:56:ba:f5:41:64:f:39:9d:af:82:43:22:8f:90:52:9d:89:e7:45:97:df:f3:f2:bc:7b:3a:db:89:e:34:fd:18:62:25:1b:ef:77:aa:c6:6c:99:36:3a:84:d6:9c:2a:34:8c:7f:f4:bb:c9:a5:9a:6c:11:f2:cf:da:51:5e:1e:7f:90:27:34:de:b2:f3:15:4f:db:47:32:6b:a7" # noqa
+FINGER_RSA_2K_OPENSSH = "2048 68:d1:72:01:bf:c0:0c:66:97:78:df:ce:75:74:46:d6"
+FINGER_DSS_1K_OPENSSH = "1024 cf:1d:eb:d7:61:d3:12:94:c6:c0:c6:54:35:35:b0:82"
+FINGER_EC_384_OPENSSH = "384 72:14:df:c1:9a:c3:e6:0e:11:29:d6:32:18:7b:ea:9b"
+
+RSA_PRIVATE_OUT = """\
+-----BEGIN RSA PRIVATE KEY-----
+MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
+oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
+d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
+gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
+EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
+soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
+tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
+avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
+4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
+H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
+qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
+HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
+nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
+-----END RSA PRIVATE KEY-----
+"""
+
+DSS_PRIVATE_OUT = """\
+-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQDngaYDZ30c6/7cJgEEbtl8FgKdwhba1Z7oOrOn4MI/6C42G1bY
+wMuqZf4dBCglsdq39SHrcjbE8Vq54gPSOh3g4+uV9Rcg5IOoPLbwp2jQfF6f1FIb
+sx7hrDCIqUcQccPSxetPBKmXI9RN8rZLaFuQeTnI65BKM98Ruwvq6SI2LwIVAPDP
+hSeawaJI27mKqOfe5PPBSmyHAoGBAJMXxXmPD9sGaQ419DIpmZecJKBUAy9uXD8x
+gbgeDpwfDaFJP8owByCKREocPFfi86LjCuQkyUKOfjYMN6iHIf1oEZjB8uJAatUr
+FzI0ArXtUqOhwTLwTyFuUojE5own2WYsOAGByvgfyWjsGhvckYNhI4ODpNdPlxQ8
+ZamaPGPsAoGARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmn
+jO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacI
+BlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgECFGI9QPSc
+h9pT9XHqn+1rZ4bK+QGA
+-----END DSA PRIVATE KEY-----
+"""
+
+ECDSA_PRIVATE_OUT_256 = """\
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIKB6ty3yVyKEnfF/zprx0qwC76MsMlHY4HXCnqho2eKioAoGCCqGSM49
+AwEHoUQDQgAElI9mbdlaS+T9nHxY/59lFnn80EEecZDBHq4gLpccY8Mge5ZTMiMD
+ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g==
+-----END EC PRIVATE KEY-----
+"""
+
+ECDSA_PRIVATE_OUT_384 = """\
+-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDBDdO8IXvlLJgM7+sNtPl7tI7FM5kzuEUEEPRjXIPQM7mISciwJPBt+
+y43EuG8nL4mgBwYFK4EEACKhZANiAAQWxom0C1vQAGYhjdoREMVmGKBWlisDdzyk
+mgyUjKpiJ9WfbIEVLsPGP8OdNjhr1y/8BZNIts+dJd6VmYw+4HzB+4F+U1Igs8K0
+JEvh59VNkvWheViadDXCM2MV8Nq+DNg=
+-----END EC PRIVATE KEY-----
+"""
+
+ECDSA_PRIVATE_OUT_521 = """\
+-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIAprQtAS3OF6iVUkT8IowTHWicHzShGgk86EtuEXvfQnhZFKsWm6Jo
+iqAr1yEaiuI9LfB3Xs8cjuhgEEfbduYr/f6gBwYFK4EEACOhgYkDgYYABACaOaFL
+ZGuxa5AW16qj6VLypFbLrEWrt9AZUloCMefxO8bNLjK/O5g0rAVasar1TnyHE9qj
+4NwzANZASWjQNbc4MAG8vzqezFwLIn/kNyNTsXNfqEko9OgHZknlj2Z79dwTJcRA
+L4QLcT5aND0EHZLB2fAUDXiWIb2j4rg1mwPlBMiBXA==
+-----END EC PRIVATE KEY-----
+"""
+
+x1234 = b"\x01\x02\x03\x04"
+
+TEST_KEY_BYTESTR = "\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x00ӏV\x07k%<\x1fT$E#>ғfD\x18 \x0cae#̬S#VlE\x1epvo\x17M߉DUXL<\x06\x10דw\u2bd5ٿw˟0)#y{\x10l\tPru\t\x19Π\u070e/f0yFmm\x1f" # noqa
+
+
+class KeyTest(unittest.TestCase):
+ def assert_keyfile_is_encrypted(self, keyfile):
+ """
+ A quick check that filename looks like an encrypted key.
+ """
+ with open(keyfile, "r") as fh:
+ self.assertEqual(
+ fh.readline()[:-1], "-----BEGIN RSA PRIVATE KEY-----"
+ )
+ self.assertEqual(fh.readline()[:-1], "Proc-Type: 4,ENCRYPTED")
+ self.assertEqual(fh.readline()[0:10], "DEK-Info: ")
+
+ def test_generate_key_bytes(self):
+ key = util.generate_key_bytes(md5, x1234, "happy birthday", 30)
+ exp = b"\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64" # noqa
+ self.assertEqual(exp, key)
+
+ def test_load_rsa(self):
+ key = RSAKey.from_private_key_file(_support("rsa.key"))
+ self.assertEqual("ssh-rsa", key.get_name())
+ exp_rsa = b(FINGER_RSA.split()[1].replace(":", ""))
+ my_rsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_rsa, my_rsa)
+ self.assertEqual(PUB_RSA.split()[1], key.get_base64())
+ self.assertEqual(1024, key.get_bits())
+
+ s = StringIO()
+ key.write_private_key(s)
+ self.assertEqual(RSA_PRIVATE_OUT, s.getvalue())
+ s.seek(0)
+ key2 = RSAKey.from_private_key(s)
+ self.assertEqual(key, key2)
+
+ def test_load_rsa_transmutes_crypto_exceptions(self):
+ # TODO: nix unittest for pytest
+ for exception in (TypeError("onoz"), UnsupportedAlgorithm("oops")):
+ with patch(
+ "paramiko.rsakey.serialization.load_der_private_key"
+ ) as loader:
+ loader.side_effect = exception
+ with pytest.raises(SSHException, match=str(exception)):
+ RSAKey.from_private_key_file(_support("rsa.key"))
+
+ def test_loading_empty_keys_errors_usefully(self):
+ # #1599 - raise SSHException instead of IndexError
+ with pytest.raises(SSHException, match="no lines"):
+ RSAKey.from_private_key_file(_support("blank_rsa.key"))
+
+ def test_load_rsa_password(self):
+ key = RSAKey.from_private_key_file(
+ _support("test_rsa_password.key"), "television"
+ )
+ self.assertEqual("ssh-rsa", key.get_name())
+ exp_rsa = b(FINGER_RSA.split()[1].replace(":", ""))
+ my_rsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_rsa, my_rsa)
+ self.assertEqual(PUB_RSA.split()[1], key.get_base64())
+ self.assertEqual(1024, key.get_bits())
+
+ def test_load_dss(self):
+ key = DSSKey.from_private_key_file(_support("dss.key"))
+ self.assertEqual("ssh-dss", key.get_name())
+ exp_dss = b(FINGER_DSS.split()[1].replace(":", ""))
+ my_dss = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_dss, my_dss)
+ self.assertEqual(PUB_DSS.split()[1], key.get_base64())
+ self.assertEqual(1024, key.get_bits())
+
+ s = StringIO()
+ key.write_private_key(s)
+ self.assertEqual(DSS_PRIVATE_OUT, s.getvalue())
+ s.seek(0)
+ key2 = DSSKey.from_private_key(s)
+ self.assertEqual(key, key2)
+
+ def test_load_dss_password(self):
+ key = DSSKey.from_private_key_file(
+ _support("test_dss_password.key"), "television"
+ )
+ self.assertEqual("ssh-dss", key.get_name())
+ exp_dss = b(FINGER_DSS.split()[1].replace(":", ""))
+ my_dss = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_dss, my_dss)
+ self.assertEqual(PUB_DSS.split()[1], key.get_base64())
+ self.assertEqual(1024, key.get_bits())
+
+ def test_compare_rsa(self):
+ # verify that the private & public keys compare equal
+ key = RSAKey.from_private_key_file(_support("rsa.key"))
+ self.assertEqual(key, key)
+ pub = RSAKey(data=key.asbytes())
+ self.assertTrue(key.can_sign())
+ self.assertTrue(not pub.can_sign())
+ self.assertEqual(key, pub)
+
+ def test_compare_dss(self):
+ # verify that the private & public keys compare equal
+ key = DSSKey.from_private_key_file(_support("dss.key"))
+ self.assertEqual(key, key)
+ pub = DSSKey(data=key.asbytes())
+ self.assertTrue(key.can_sign())
+ self.assertTrue(not pub.can_sign())
+ self.assertEqual(key, pub)
+
+ def _sign_and_verify_rsa(self, algorithm, saved_sig):
+ key = RSAKey.from_private_key_file(_support("rsa.key"))
+ msg = key.sign_ssh_data(b"ice weasels", algorithm)
+ assert isinstance(msg, Message)
+ msg.rewind()
+ assert msg.get_text() == algorithm
+ expected = b"".join(
+ [byte_chr(int(x, 16)) for x in saved_sig.split(":")]
+ )
+ assert msg.get_binary() == expected
+ msg.rewind()
+ pub = RSAKey(data=key.asbytes())
+ self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg))
+
+ @requires_sha1_signing
+ def test_sign_and_verify_ssh_rsa(self):
+ self._sign_and_verify_rsa("ssh-rsa", SIGNED_RSA)
+
+ def test_sign_and_verify_rsa_sha2_512(self):
+ self._sign_and_verify_rsa("rsa-sha2-512", SIGNED_RSA_512)
+
+ def test_sign_and_verify_rsa_sha2_256(self):
+ self._sign_and_verify_rsa("rsa-sha2-256", SIGNED_RSA_256)
+
+ def test_sign_dss(self):
+ # verify that the dss private key can sign and verify
+ key = DSSKey.from_private_key_file(_support("dss.key"))
+ msg = key.sign_ssh_data(b"ice weasels")
+ self.assertTrue(type(msg) is Message)
+ msg.rewind()
+ self.assertEqual("ssh-dss", msg.get_text())
+ # can't do the same test as we do for RSA, because DSS signatures
+ # are usually different each time. but we can test verification
+ # anyway so it's ok.
+ self.assertEqual(40, len(msg.get_binary()))
+ msg.rewind()
+ pub = DSSKey(data=key.asbytes())
+ self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg))
+
+ @requires_sha1_signing
+ def test_generate_rsa(self):
+ key = RSAKey.generate(1024)
+ msg = key.sign_ssh_data(b"jerri blank")
+ msg.rewind()
+ self.assertTrue(key.verify_ssh_sig(b"jerri blank", msg))
+
+ def test_generate_dss(self):
+ key = DSSKey.generate(1024)
+ msg = key.sign_ssh_data(b"jerri blank")
+ msg.rewind()
+ self.assertTrue(key.verify_ssh_sig(b"jerri blank", msg))
+
+ def test_generate_ecdsa(self):
+ key = ECDSAKey.generate()
+ msg = key.sign_ssh_data(b"jerri blank")
+ msg.rewind()
+ self.assertTrue(key.verify_ssh_sig(b"jerri blank", msg))
+ self.assertEqual(key.get_bits(), 256)
+ self.assertEqual(key.get_name(), "ecdsa-sha2-nistp256")
+
+ key = ECDSAKey.generate(bits=256)
+ msg = key.sign_ssh_data(b"jerri blank")
+ msg.rewind()
+ self.assertTrue(key.verify_ssh_sig(b"jerri blank", msg))
+ self.assertEqual(key.get_bits(), 256)
+ self.assertEqual(key.get_name(), "ecdsa-sha2-nistp256")
+
+ key = ECDSAKey.generate(bits=384)
+ msg = key.sign_ssh_data(b"jerri blank")
+ msg.rewind()
+ self.assertTrue(key.verify_ssh_sig(b"jerri blank", msg))
+ self.assertEqual(key.get_bits(), 384)
+ self.assertEqual(key.get_name(), "ecdsa-sha2-nistp384")
+
+ key = ECDSAKey.generate(bits=521)
+ msg = key.sign_ssh_data(b"jerri blank")
+ msg.rewind()
+ self.assertTrue(key.verify_ssh_sig(b"jerri blank", msg))
+ self.assertEqual(key.get_bits(), 521)
+ self.assertEqual(key.get_name(), "ecdsa-sha2-nistp521")
+
+ def test_load_ecdsa_256(self):
+ key = ECDSAKey.from_private_key_file(_support("ecdsa-256.key"))
+ self.assertEqual("ecdsa-sha2-nistp256", key.get_name())
+ exp_ecdsa = b(FINGER_ECDSA_256.split()[1].replace(":", ""))
+ my_ecdsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_ecdsa, my_ecdsa)
+ self.assertEqual(PUB_ECDSA_256.split()[1], key.get_base64())
+ self.assertEqual(256, key.get_bits())
+
+ s = StringIO()
+ key.write_private_key(s)
+ self.assertEqual(ECDSA_PRIVATE_OUT_256, s.getvalue())
+ s.seek(0)
+ key2 = ECDSAKey.from_private_key(s)
+ self.assertEqual(key, key2)
+
+ def test_load_ecdsa_password_256(self):
+ key = ECDSAKey.from_private_key_file(
+ _support("test_ecdsa_password_256.key"), b"television"
+ )
+ self.assertEqual("ecdsa-sha2-nistp256", key.get_name())
+ exp_ecdsa = b(FINGER_ECDSA_256.split()[1].replace(":", ""))
+ my_ecdsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_ecdsa, my_ecdsa)
+ self.assertEqual(PUB_ECDSA_256.split()[1], key.get_base64())
+ self.assertEqual(256, key.get_bits())
+
+ def test_compare_ecdsa_256(self):
+ # verify that the private & public keys compare equal
+ key = ECDSAKey.from_private_key_file(_support("ecdsa-256.key"))
+ self.assertEqual(key, key)
+ pub = ECDSAKey(data=key.asbytes())
+ self.assertTrue(key.can_sign())
+ self.assertTrue(not pub.can_sign())
+ self.assertEqual(key, pub)
+
+ def test_sign_ecdsa_256(self):
+ # verify that the rsa private key can sign and verify
+ key = ECDSAKey.from_private_key_file(_support("ecdsa-256.key"))
+ msg = key.sign_ssh_data(b"ice weasels")
+ self.assertTrue(type(msg) is Message)
+ msg.rewind()
+ self.assertEqual("ecdsa-sha2-nistp256", msg.get_text())
+ # ECDSA signatures, like DSS signatures, tend to be different
+ # each time, so we can't compare against a "known correct"
+ # signature.
+ # Even the length of the signature can change.
+
+ msg.rewind()
+ pub = ECDSAKey(data=key.asbytes())
+ self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg))
+
+ def test_load_ecdsa_384(self):
+ key = ECDSAKey.from_private_key_file(_support("test_ecdsa_384.key"))
+ self.assertEqual("ecdsa-sha2-nistp384", key.get_name())
+ exp_ecdsa = b(FINGER_ECDSA_384.split()[1].replace(":", ""))
+ my_ecdsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_ecdsa, my_ecdsa)
+ self.assertEqual(PUB_ECDSA_384.split()[1], key.get_base64())
+ self.assertEqual(384, key.get_bits())
+
+ s = StringIO()
+ key.write_private_key(s)
+ self.assertEqual(ECDSA_PRIVATE_OUT_384, s.getvalue())
+ s.seek(0)
+ key2 = ECDSAKey.from_private_key(s)
+ self.assertEqual(key, key2)
+
+ def test_load_ecdsa_password_384(self):
+ key = ECDSAKey.from_private_key_file(
+ _support("test_ecdsa_password_384.key"), b"television"
+ )
+ self.assertEqual("ecdsa-sha2-nistp384", key.get_name())
+ exp_ecdsa = b(FINGER_ECDSA_384.split()[1].replace(":", ""))
+ my_ecdsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_ecdsa, my_ecdsa)
+ self.assertEqual(PUB_ECDSA_384.split()[1], key.get_base64())
+ self.assertEqual(384, key.get_bits())
+
+ def test_load_ecdsa_transmutes_crypto_exceptions(self):
+ path = _support("ecdsa-256.key")
+ # TODO: nix unittest for pytest
+ for exception in (TypeError("onoz"), UnsupportedAlgorithm("oops")):
+ with patch(
+ "paramiko.ecdsakey.serialization.load_der_private_key"
+ ) as loader:
+ loader.side_effect = exception
+ with pytest.raises(SSHException, match=str(exception)):
+ ECDSAKey.from_private_key_file(path)
+
+ def test_compare_ecdsa_384(self):
+ # verify that the private & public keys compare equal
+ key = ECDSAKey.from_private_key_file(_support("test_ecdsa_384.key"))
+ self.assertEqual(key, key)
+ pub = ECDSAKey(data=key.asbytes())
+ self.assertTrue(key.can_sign())
+ self.assertTrue(not pub.can_sign())
+ self.assertEqual(key, pub)
+
+ def test_sign_ecdsa_384(self):
+ # verify that the rsa private key can sign and verify
+ key = ECDSAKey.from_private_key_file(_support("test_ecdsa_384.key"))
+ msg = key.sign_ssh_data(b"ice weasels")
+ self.assertTrue(type(msg) is Message)
+ msg.rewind()
+ self.assertEqual("ecdsa-sha2-nistp384", msg.get_text())
+ # ECDSA signatures, like DSS signatures, tend to be different
+ # each time, so we can't compare against a "known correct"
+ # signature.
+ # Even the length of the signature can change.
+
+ msg.rewind()
+ pub = ECDSAKey(data=key.asbytes())
+ self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg))
+
+ def test_load_ecdsa_521(self):
+ key = ECDSAKey.from_private_key_file(_support("test_ecdsa_521.key"))
+ self.assertEqual("ecdsa-sha2-nistp521", key.get_name())
+ exp_ecdsa = b(FINGER_ECDSA_521.split()[1].replace(":", ""))
+ my_ecdsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_ecdsa, my_ecdsa)
+ self.assertEqual(PUB_ECDSA_521.split()[1], key.get_base64())
+ self.assertEqual(521, key.get_bits())
+
+ s = StringIO()
+ key.write_private_key(s)
+ # Different versions of OpenSSL (SSLeay versions 0x1000100f and
+ # 0x1000207f for instance) use different apparently valid (as far as
+ # ssh-keygen is concerned) padding. So we can't check the actual value
+ # of the pem encoded key.
+ s.seek(0)
+ key2 = ECDSAKey.from_private_key(s)
+ self.assertEqual(key, key2)
+
+ def test_load_ecdsa_password_521(self):
+ key = ECDSAKey.from_private_key_file(
+ _support("test_ecdsa_password_521.key"), b"television"
+ )
+ self.assertEqual("ecdsa-sha2-nistp521", key.get_name())
+ exp_ecdsa = b(FINGER_ECDSA_521.split()[1].replace(":", ""))
+ my_ecdsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_ecdsa, my_ecdsa)
+ self.assertEqual(PUB_ECDSA_521.split()[1], key.get_base64())
+ self.assertEqual(521, key.get_bits())
+
+ def test_compare_ecdsa_521(self):
+ # verify that the private & public keys compare equal
+ key = ECDSAKey.from_private_key_file(_support("test_ecdsa_521.key"))
+ self.assertEqual(key, key)
+ pub = ECDSAKey(data=key.asbytes())
+ self.assertTrue(key.can_sign())
+ self.assertTrue(not pub.can_sign())
+ self.assertEqual(key, pub)
+
+ def test_sign_ecdsa_521(self):
+ # verify that the rsa private key can sign and verify
+ key = ECDSAKey.from_private_key_file(_support("test_ecdsa_521.key"))
+ msg = key.sign_ssh_data(b"ice weasels")
+ self.assertTrue(type(msg) is Message)
+ msg.rewind()
+ self.assertEqual("ecdsa-sha2-nistp521", msg.get_text())
+ # ECDSA signatures, like DSS signatures, tend to be different
+ # each time, so we can't compare against a "known correct"
+ # signature.
+ # Even the length of the signature can change.
+
+ msg.rewind()
+ pub = ECDSAKey(data=key.asbytes())
+ self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg))
+
+ def test_load_openssh_format_RSA_key(self):
+ key = RSAKey.from_private_key_file(
+ _support("test_rsa_openssh.key"), b"television"
+ )
+ self.assertEqual("ssh-rsa", key.get_name())
+ self.assertEqual(PUB_RSA_2K_OPENSSH.split()[1], key.get_base64())
+ self.assertEqual(2048, key.get_bits())
+ exp_rsa = b(FINGER_RSA_2K_OPENSSH.split()[1].replace(":", ""))
+ my_rsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_rsa, my_rsa)
+
+ def test_loading_openssh_RSA_keys_uses_correct_p_q(self):
+ # Re #1723 - not the most elegant test but given how deep it is...
+ with patch(
+ "paramiko.rsakey.rsa.RSAPrivateNumbers", wraps=RSAPrivateNumbers
+ ) as spy:
+ # Load key
+ RSAKey.from_private_key_file(
+ _support("test_rsa_openssh.key"), b"television"
+ )
+ # Ensure spy saw the correct P and Q values as derived from
+ # hardcoded test private key value
+ kwargs = spy.call_args[1]
+ assert kwargs["p"] == RSA_2K_OPENSSH_P
+ assert kwargs["q"] == RSA_2K_OPENSSH_Q
+
+ def test_load_openssh_format_DSS_key(self):
+ key = DSSKey.from_private_key_file(
+ _support("test_dss_openssh.key"), b"television"
+ )
+ self.assertEqual("ssh-dss", key.get_name())
+ self.assertEqual(PUB_DSS_1K_OPENSSH.split()[1], key.get_base64())
+ self.assertEqual(1024, key.get_bits())
+ exp_rsa = b(FINGER_DSS_1K_OPENSSH.split()[1].replace(":", ""))
+ my_rsa = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_rsa, my_rsa)
+
+ def test_load_openssh_format_EC_key(self):
+ key = ECDSAKey.from_private_key_file(
+ _support("test_ecdsa_384_openssh.key"), b"television"
+ )
+ self.assertEqual("ecdsa-sha2-nistp384", key.get_name())
+ self.assertEqual(PUB_EC_384_OPENSSH.split()[1], key.get_base64())
+ self.assertEqual(384, key.get_bits())
+ exp_fp = b(FINGER_EC_384_OPENSSH.split()[1].replace(":", ""))
+ my_fp = hexlify(key.get_fingerprint())
+ self.assertEqual(exp_fp, my_fp)
+
+ def test_salt_size(self):
+ # Read an existing encrypted private key
+ file_ = _support("test_rsa_password.key")
+ password = "television"
+ newfile = file_ + ".new"
+ newpassword = "radio"
+ key = RSAKey(filename=file_, password=password)
+ # Write out a newly re-encrypted copy with a new password.
+ # When the bug under test exists, this will ValueError.
+ try:
+ key.write_private_key_file(newfile, password=newpassword)
+ self.assert_keyfile_is_encrypted(newfile)
+ # Verify the inner key data still matches (when no ValueError)
+ key2 = RSAKey(filename=newfile, password=newpassword)
+ self.assertEqual(key, key2)
+ finally:
+ os.remove(newfile)
+
+ def test_load_openssh_format_RSA_nopad(self):
+ # check just not exploding with 'Invalid key'
+ RSAKey.from_private_key_file(_support("test_rsa_openssh_nopad.key"))
+
+ def test_stringification(self):
+ key = RSAKey.from_private_key_file(_support("rsa.key"))
+ comparable = TEST_KEY_BYTESTR
+ self.assertEqual(str(key), comparable)
+
+ def test_ed25519(self):
+ key1 = Ed25519Key.from_private_key_file(_support("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("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)
+
+ # No point testing on systems that never exhibited the bug originally
+ @pytest.mark.skipif(
+ not is_low_entropy(), reason="Not a low-entropy system"
+ )
+ def test_ed25519_32bit_collision(self):
+ # Re: 2021.10.19 security report email: two different private keys
+ # which Paramiko compared as equal on low-entropy platforms.
+ original = Ed25519Key.from_private_key_file(
+ _support("badhash_key1.ed25519.key")
+ )
+ generated = Ed25519Key.from_private_key_file(
+ _support("badhash_key2.ed25519.key")
+ )
+ assert original != generated
+
+ def test_ed25519_nonbytes_password(self):
+ # https://github.com/paramiko/paramiko/issues/1039
+ 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_ed25519_load_from_file_obj(self):
+ with open(_support("ed25519.key")) as pkey_fileobj:
+ key = Ed25519Key.from_private_key(pkey_fileobj)
+ self.assertEqual(key, key)
+ self.assertTrue(key.can_sign())
+
+ def test_keyfile_is_actually_encrypted(self):
+ # Read an existing encrypted private key
+ file_ = _support("test_rsa_password.key")
+ password = "television"
+ newfile = file_ + ".new"
+ newpassword = "radio"
+ key = RSAKey(filename=file_, password=password)
+ # Write out a newly re-encrypted copy with a new password.
+ # When the bug under test exists, this will ValueError.
+ try:
+ key.write_private_key_file(newfile, password=newpassword)
+ self.assert_keyfile_is_encrypted(newfile)
+ finally:
+ os.remove(newfile)
+
+ @patch("paramiko.pkey.os")
+ def _test_keyfile_race(self, os_, exists):
+ # Re: CVE-2022-24302
+ password = "television"
+ newpassword = "radio"
+ source = _support("test_ecdsa_384.key")
+ new = source + ".new"
+ # Mock setup
+ os_.path.exists.return_value = exists
+ # Attach os flag values to mock
+ for attr, value in vars(os).items():
+ if attr.startswith("O_"):
+ setattr(os_, attr, value)
+ # Load fixture key
+ key = ECDSAKey(filename=source, password=password)
+ key._write_private_key = Mock()
+ # Write out in new location
+ key.write_private_key_file(new, password=newpassword)
+ # Expected open via os module
+ os_.open.assert_called_once_with(
+ new, flags=os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode=o600
+ )
+ os_.fdopen.assert_called_once_with(os_.open.return_value, "w")
+ assert (
+ key._write_private_key.call_args[0][0]
+ == os_.fdopen.return_value.__enter__.return_value
+ )
+
+ def test_new_keyfiles_avoid_file_descriptor_race_on_chmod(self):
+ self._test_keyfile_race(exists=False)
+
+ def test_existing_keyfiles_still_work_ok(self):
+ self._test_keyfile_race(exists=True)
+
+ def test_new_keyfiles_avoid_descriptor_race_integration(self):
+ # Integration-style version of above
+ password = "television"
+ newpassword = "radio"
+ source = _support("test_ecdsa_384.key")
+ new = source + ".new"
+ # Load fixture key
+ key = ECDSAKey(filename=source, password=password)
+ try:
+ # Write out in new location
+ key.write_private_key_file(new, password=newpassword)
+ # Test mode
+ assert stat.S_IMODE(os.stat(new).st_mode) == o600
+ # Prove can open with new password
+ reloaded = ECDSAKey(filename=new, password=newpassword)
+ assert reloaded == key
+ finally:
+ if os.path.exists(new):
+ os.unlink(new)
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
new file mode 100644
index 0000000..22c2c9c
--- /dev/null
+++ b/tests/test_proxy.py
@@ -0,0 +1,150 @@
+import signal
+import socket
+
+from unittest.mock import patch
+from pytest import raises
+
+from paramiko import ProxyCommand, ProxyCommandFailure
+
+
+class TestProxyCommand:
+ @patch("paramiko.proxy.subprocess")
+ def test_init_takes_command_string(self, subprocess):
+ ProxyCommand(command_line="do a thing")
+ subprocess.Popen.assert_called_once_with(
+ ["do", "a", "thing"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ bufsize=0,
+ )
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ def test_send_writes_to_process_stdin_returning_length(self, Popen):
+ proxy = ProxyCommand("hi")
+ written = proxy.send(b"data")
+ Popen.return_value.stdin.write.assert_called_once_with(b"data")
+ assert written == len(b"data")
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ def test_send_raises_ProxyCommandFailure_on_error(self, Popen):
+ Popen.return_value.stdin.write.side_effect = IOError(0, "whoops")
+ with raises(ProxyCommandFailure) as info:
+ ProxyCommand("hi").send("data")
+ assert info.value.command == "hi"
+ assert info.value.error == "whoops"
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ @patch("paramiko.proxy.os.read")
+ @patch("paramiko.proxy.select")
+ def test_recv_reads_from_process_stdout_returning_bytes(
+ self, select, os_read, Popen
+ ):
+ stdout = Popen.return_value.stdout
+ select.return_value = [stdout], None, None
+ fileno = stdout.fileno.return_value
+ # Force os.read to return smaller-than-requested chunks
+ os_read.side_effect = [b"was", b"t", b"e", b"of ti", b"me"]
+ proxy = ProxyCommand("hi")
+ # Ask for 5 bytes (ie b"waste")
+ data = proxy.recv(5)
+ # Ensure we got "waste" stitched together
+ assert data == b"waste"
+ # Ensure the calls happened in the sizes expected (starting with the
+ # initial "I want all 5 bytes", followed by "I want whatever I believe
+ # should be left after what I've already read", until done)
+ assert [x[0] for x in os_read.call_args_list] == [
+ (fileno, 5), # initial
+ (fileno, 2), # I got 3, want 2 more
+ (fileno, 1), # I've now got 4, want 1 more
+ ]
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ @patch("paramiko.proxy.os.read")
+ @patch("paramiko.proxy.select")
+ def test_recv_returns_buffer_on_timeout_if_any_read(
+ self, select, os_read, Popen
+ ):
+ stdout = Popen.return_value.stdout
+ select.return_value = [stdout], None, None
+ fileno = stdout.fileno.return_value
+ os_read.side_effect = [b"was", socket.timeout]
+ proxy = ProxyCommand("hi")
+ data = proxy.recv(5)
+ assert data == b"was" # not b"waste"
+ assert os_read.call_args[0] == (fileno, 2)
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ @patch("paramiko.proxy.os.read")
+ @patch("paramiko.proxy.select")
+ def test_recv_raises_timeout_if_nothing_read(self, select, os_read, Popen):
+ stdout = Popen.return_value.stdout
+ select.return_value = [stdout], None, None
+ fileno = stdout.fileno.return_value
+ os_read.side_effect = socket.timeout
+ proxy = ProxyCommand("hi")
+ with raises(socket.timeout):
+ proxy.recv(5)
+ assert os_read.call_args[0] == (fileno, 5)
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ @patch("paramiko.proxy.os.read")
+ @patch("paramiko.proxy.select")
+ def test_recv_raises_ProxyCommandFailure_on_non_timeout_error(
+ self, select, os_read, Popen
+ ):
+ select.return_value = [Popen.return_value.stdout], None, None
+ os_read.side_effect = IOError(0, "whoops")
+ with raises(ProxyCommandFailure) as info:
+ ProxyCommand("hi").recv(5)
+ assert info.value.command == "hi"
+ assert info.value.error == "whoops"
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ @patch("paramiko.proxy.os.kill")
+ def test_close_kills_subprocess(self, os_kill, Popen):
+ proxy = ProxyCommand("hi")
+ proxy.close()
+ os_kill.assert_called_once_with(Popen.return_value.pid, signal.SIGTERM)
+
+ @patch("paramiko.proxy.subprocess.Popen")
+ def test_closed_exposes_whether_subprocess_has_exited(self, Popen):
+ proxy = ProxyCommand("hi")
+ Popen.return_value.returncode = None
+ assert proxy.closed is False
+ assert proxy._closed is False
+ Popen.return_value.returncode = 0
+ assert proxy.closed is True
+ assert proxy._closed is True
+
+ @patch("paramiko.proxy.time.time")
+ @patch("paramiko.proxy.subprocess.Popen")
+ @patch("paramiko.proxy.os.read")
+ @patch("paramiko.proxy.select")
+ def test_timeout_affects_whether_timeout_is_raised(
+ self, select, os_read, Popen, time
+ ):
+ stdout = Popen.return_value.stdout
+ select.return_value = [stdout], None, None
+ # Base case: None timeout means no timing out
+ os_read.return_value = b"meh"
+ proxy = ProxyCommand("hello")
+ assert proxy.timeout is None
+ # Implicit 'no raise' check
+ assert proxy.recv(3) == b"meh"
+ # Use settimeout to set timeout, and it is honored
+ time.side_effect = [0, 10] # elapsed > 7
+ proxy = ProxyCommand("ohnoz")
+ proxy.settimeout(7)
+ assert proxy.timeout == 7
+ with raises(socket.timeout):
+ proxy.recv(3)
+
+ @patch("paramiko.proxy.subprocess", new=None)
+ @patch("paramiko.proxy.subprocess_import_error", new=ImportError("meh"))
+ def test_raises_subprocess_ImportErrors_at_runtime(self):
+ # Not an ideal test, but I don't know of a non-bad way to fake out
+ # module-time ImportErrors. So we mock the symptoms. Meh!
+ with raises(ImportError) as info:
+ ProxyCommand("hi!!!")
+ assert str(info.value) == "meh"
diff --git a/tests/test_rsa.key.pub b/tests/test_rsa.key.pub
new file mode 100644
index 0000000..bfa1e15
--- /dev/null
+++ b/tests/test_rsa.key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=
diff --git a/tests/test_rsa_openssh.key b/tests/test_rsa_openssh.key
new file mode 100644
index 0000000..6077c10
--- /dev/null
+++ b/tests/test_rsa_openssh.key
@@ -0,0 +1,28 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABD0R3hOFS
+FMb2SJeo5h8QPNAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDF+Dpr54DX
+0WdeTDpNAMdkCWEkl3OXtNgf58qlN1gX572OLBqLf0zT4bHstUEpU3piazph/rSWcUMuBo
+D46tZ6jiH7H9b9Pem2eYQWaELDDkM+v9BMbEy5rMbFRLol5OtEvPFqneyEAanPOgvd8t3y
+yhSev9QVusakzJ8j8LGgrA8huYZ+Srnw0shEWLG70KUKCh3rG0QIvA8nfhtUOisr2Gp+F0
+YxMGb5gwBlQYAYE5l6u1SjZ7hNjyNosjK+wRBFgFFBYVpkZKJgWoK9w4ijFyzMZTucnZMq
+KOKAjIJvHfKBf2/cEfYxSq1EndqTqjYsd9T7/s2vcn1OH5a0wkERAAAD0JnzCJYfDeiUQ6
+9LOAb6/NnhKvFjCdBYal60MfLcLBHvzHLJvTneQ4f1Vknq8xEVmRba7SDSfwaEybP/1FsP
+SGH6FNKA5gKllemgmcaUVr3wtNPtjX4WgsyHcwCRgHmOiyNrUj0OZR5wbZabHIIyirl4wa
+LBz8Jb3GalKEagtyWsBKDCKHCFNzh8xmsT1SWhnC7baRyC8e3krQm9hGbNhpj6Q5AtN3ql
+wBVamUp0eKxkt70mKBKI4v3DR8KqrEndeK6d0cegVEkE67fqa99a5J3uSDC8mglKrHiKEs
+dU1dh/bOF/H3aFpINlRwvlZ95Opby7rG0BHgbZONq0+VUnABVzNTM5Xd5UKjjCF28CrQBf
+XS6WeHeUx2zHtOmL1xdePk+Bii+SSUl3pLa4SDwX4nV95cSPx8vMm8dJEruxad6+MPoSuy
+Oyho89jqUTSgC/RPejuTgrnB3WbzE5SJb+V3zMata0J1wxbNfYKG9U+VucUZhP4+jzfNqH
+B/v8JqtuxnqR8NjPsK2+8wJxebL2KVNjKOm//6P3KSDsavpscGpVWOM06zUlwWCB26W3pP
+X/+xO9aR5wiBteFKoJG1waziIjqhOJSmvq+I/texUKEUd/eEFNt10Ubc0zy0sRYVN8rIRJ
+masQzCYuUylDzCa4ar1s4qngBZzWL2PRkPuXuhoHuT0J5no174GR6+6EAYZZhnq0tkYrhZ
+Ar0tQ4CDlI235a3MPHzvABuwYuWys1tBuLAb+6Gc6CmCiQ+mhojfQUBYG5T65iRFA5UQsH
+O1RLEC3yasxGcBI6d0J/fwOP/YLktNu3AeUumr0N9Xgf02DlBNwd+4GOI0LcQvl/3J8ppo
+bamTppKPEZ2d32VNEO+Z6Zx5DlIVm5gDeMvIvdwap445VnhL3ZZH2NCkAcXM9+0WH+Quas
+JCAMgPYiP9FzF+8Onmj2OmhgIVj/9eanhS3/GLrRC4xCvER2V7PwgB0I5qY110BPEttDyo
+IvYE51kvtdW447SK7HZywJnkyw2RNm+29dvWJJwSQckUHuZkXEtmEPk0ePL3yf2NH5XYJc
+pXX6Zac0KemCPIHr8l7GogE4Rb2BBTqddkegb9piz6QTAPcQnn+GuMFG06IBhUrgcMEQ8x
+UOXYUUrT5HvSxWUcgaYH1nfC3bTWmDaodw8/HQKyF6c44rujO2s2NLFOCAyQMUNdhh3lfD
+yHYLO7xYkP6xzzkpk+2lwBoeYdQdAwlKN/XqC8ZhBfwTdem/1hh1BpQJFbbFftWxU8gxxi
+iuI+vmlsuIsxKoGCq8YXuophx62lo=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/test_rsa_openssh_nopad.key b/tests/test_rsa_openssh_nopad.key
new file mode 100644
index 0000000..61ac1b1
--- /dev/null
+++ b/tests/test_rsa_openssh_nopad.key
@@ -0,0 +1,27 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAQEAnyMwWSwrbJxxQZWMJO5xR6eAA9De4t3GViqDRaQt/BgsvzZ14SUz
+aOL/A370fKxhx/JLIOOGA0o5B0/ct+CL7XFqMi5r5+iA9VcIeYKKtoAkrEvRnagNW0WVWx
+thTnE01g8Pb7fDqzI2cBuBNZ2vGNm2m4UTGC8/kl/0ES1V3KqA7lPlTrkTYg9L/ornvVHc
+c8gEbMwx9XXVRzbWiuDE176ojrudY9CZduVSOgW+HK3rKkqLBs/91jv0zUK0oqTQBLR7E2
+V2GWPDU4BjlHTtYr0jpKOGDr1DLu4+NiD/mX+tGMdH6ehbDii0kXmOUaZjs4OxuK3XA/gi
+KZLdj1jQQwAAA7iNnvAVjZ7wFQAAAAdzc2gtcnNhAAABAQCfIzBZLCtsnHFBlYwk7nFHp4
+AD0N7i3cZWKoNFpC38GCy/NnXhJTNo4v8DfvR8rGHH8ksg44YDSjkHT9y34IvtcWoyLmvn
+6ID1Vwh5goq2gCSsS9GdqA1bRZVbG2FOcTTWDw9vt8OrMjZwG4E1na8Y2babhRMYLz+SX/
+QRLVXcqoDuU+VOuRNiD0v+iue9UdxzyARszDH1ddVHNtaK4MTXvqiOu51j0Jl25VI6Bb4c
+resqSosGz/3WO/TNQrSipNAEtHsTZXYZY8NTgGOUdO1ivSOko4YOvUMu7j42IP+Zf60Yx0
+fp6FsOKLSReY5RpmOzg7G4rdcD+CIpkt2PWNBDAAAAAwEAAQAAAQEAnmMbn+VCYxth7fC2
+R5u6y6J+201sSUiKOwCdHxdFXX+CKd4+fRPVkzM6tXQKSnwX5jXVaKqLm4KoOArYl3q6Sl
+1zYParF2plz8oL+URgYzwvQ/1CaDP29zzOZptdwgESoWrj5kF0UlPrsrDtbTvAJm+qPCe6
+1XtRPpKaDO6eYr0PM2QTElZy3mDBUBvu816LdG/ZtnB9g5UsocT5mmhpHTHdjrpwNu5TBe
+ACVodDn5Fu66OlrrnQi4IPCAWKJ1YuzEkZqLhs1L3oMHACsmzrLjzW74SjY4kWTTvGiC6i
+tDoycycThk9EGLGNso99Q1fe84/OZUff7aI3yK9KvLL7oQAAAIEAh2+XrJXSBx/v9E3aJH
+ncgQH1snXr7LcSRqcWicHdbm8JsOTT3TkyXHGlSZ2rr/Y0u5V1ZSO6roJLrAHsDJzx0x0U
+xE/5mpzhD+yIKQwnWkZFLzYEnYDFdXDMzmghUIik9AW7n9dtS8UtVFGaL6Vs2YCOuLqeT9
+nZUkm3UUZ+7QIAAACBAM23DFjQ0/Op2ri7fJA2qFBdXqoJdNHuyYEIrKbB6XaaSUz52+IB
+MbccxEz3vPsHh69tZoJ+xZNbFJe9wdmbF+DQpoukHkJnzpk/pUq8LjQMzZfwv41X8zqaq4
+AOA7g27Rk8aKewhCXjhkr0hHEaSiuqIIindFaFti5sQMi2mtkXAAAAgQDGCXkpuKZK61p9
+L6G5yZSQBCgVtm0iQEbyDXWHjy/GqLtxJjqdyaRK57hXGjbzgJJraSy+sNP9uv2QOvyZvB
+3XaPWwUYVQ34WyibCqqUaPiHxX7T1lZV+asbwgbmSqYtH5dUEJ8zT572mCwxnRjX63PwDo
+5vBbR/qAW5lvRYsltQAAAAFh
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/test_rsa_password.key b/tests/test_rsa_password.key
new file mode 100644
index 0000000..7713049
--- /dev/null
+++ b/tests/test_rsa_password.key
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,DAA422E8A5A8EFB7
+
++nssHGmWl91IcmGiE6DdCIqGvAP04tuLh60wLjWBvdjtF9CjztPnF57xe+6pBk7o
+YgF/Ry3ik9ZV9rHNcRXifDKM9crxtYlpUlkM2C0SP89sXaO0P1Q1yCnrtZUwDIKO
+BNV8et5X7+AGMFsy/nmv0NFMrbpoG03Dppsloecd29NTRlIXwxHRFyHxy6BdEib/
+Dn0mEVbwg3dTvKrd/sODWR9hRwpDGM9nkEbUNJCh7vMwFKkIZZF8yqFvmGckuO5C
+HZkDJ6RkEDYrSZJAavQaiOPF5bu3cHughRfnrIKVrQuTTDiWjwX9Ny8e4p4k7dy7
+rLpbPhtxUOUbpOF7T1QxljDi1Tcq3Ebk3kN/ZLPRFnDrJfyUx+m9BXmAa78Wxs/l
+KaS8DTkYykd3+EGOeJFjZg2bvgqil4V+5JIt/+MQ5pZ/ui7i4GcH2bvZyGAbrXzP
+3LipSAdN5RG+fViLe3HUtfCx4ZAgtU78TWJrLk2FwKQGglFxKLnswp+IKZb09rZV
+uxmG4pPLUnH+mMYdiy5ugzj+5C8iZ0/IstpHVmO6GWROfedpJ82eMztTOtdhfMep
+8Z3HwAwkDtksL7Gq9klb0Wq5+uRlBWetixddAvnmqXNzYhaANWcAF/2a2Hz06Rb0
+e6pe/g0Ek5KV+6YI+D+oEblG0Sr+d4NtxtDTmIJKNVkmzlhI2s53bHp6txCb5JWJ
+S8mKLPBBBzaNXYd3odDvGXguuxUntWSsD11KyR6B9DXMIfWQW5dT7hp5kTMGlXWJ
+lD2hYab13DCCuAkwVTdpzhHYLZyxLYoSu05W6z8SAOs=
+-----END RSA PRIVATE KEY-----
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
new file mode 100644
index 0000000..7fd274b
--- /dev/null
+++ b/tests/test_sftp.py
@@ -0,0 +1,832 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+some unit tests to make sure sftp works.
+
+a real actual sftp server is contacted, and a new folder is created there to
+do test file operations in (so no existing files will be harmed).
+"""
+
+import os
+import socket
+import sys
+import warnings
+from binascii import hexlify
+from io import StringIO
+from tempfile import mkstemp
+
+import pytest
+
+from paramiko.common import o777, o600, o666, o644
+from paramiko.sftp_attr import SFTPAttributes
+from paramiko.util import b, u
+from tests import requireNonAsciiLocale
+
+from ._util import needs_builtin
+from ._util import slow
+
+
+ARTICLE = """
+Insulin sensitivity and liver insulin receptor structure in ducks from two
+genera
+
+T. Constantine, B. Chevalier, M. Derouet and J. Simon
+Station de Recherches Avicoles, Institut National de la Recherche Agronomique,
+Nouzilly, France.
+
+Insulin sensitivity and liver insulin receptor structure were studied in
+5-wk-old ducks from two genera (Muscovy and Pekin). In the fasting state, both
+duck types were equally resistant to exogenous insulin compared with chicken.
+Despite the low potency of duck insulin, the number of insulin receptors was
+lower in Muscovy duck and similar in Pekin duck and chicken liver membranes.
+After 125I-insulin cross-linking, the size of the alpha-subunit of the
+receptors from the three species was 135,000. Wheat germ agglutinin-purified
+receptors from the three species were contaminated by an active and unusual
+adenosinetriphosphatase (ATPase) contaminant (highest activity in Muscovy
+duck). Sequential purification of solubilized receptor from both duck types on
+lentil and then wheat germ agglutinin lectins led to a fraction of receptors
+very poor in ATPase activity that exhibited a beta-subunit size (95,000) and
+tyrosine kinase activity similar to those of ATPase-free chicken insulin
+receptors. Therefore the ducks from the two genera exhibit an alpha-beta-
+structure for liver insulin receptors and a clear difference in the number of
+liver insulin receptors. Their sensitivity to insulin is, however, similarly
+decreased compared with chicken.
+"""
+
+
+# Here is how unicode characters are encoded over 1 to 6 bytes in utf-8
+# U-00000000 - U-0000007F:
+# 0xxxxxxx
+# U-00000080 - U-000007FF:
+# 110xxxxx 10xxxxxx
+# U-00000800 - U-0000FFFF:
+# 1110xxxx 10xxxxxx 10xxxxxx
+# U-00010000 - U-001FFFFF:
+# 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+# U-00200000 - U-03FFFFFF:
+# 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+# U-04000000 - U-7FFFFFFF:
+# 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+# Note that: hex(int('11000011',2)) == '0xc3'
+# Thus, the following 2-bytes sequence is not valid utf8: "invalid continuation
+# byte"
+NON_UTF8_DATA = b"\xC3\xC3"
+
+unicode_folder = "\u00fcnic\u00f8de"
+utf8_folder = b"/\xc3\xbcnic\xc3\xb8\x64\x65"
+
+
+@slow
+class TestSFTP:
+ def test_file(self, sftp):
+ """
+ verify that we can create a file.
+ """
+ f = sftp.open(sftp.FOLDER + "/test", "w")
+ try:
+ assert f.stat().st_size == 0
+ finally:
+ f.close()
+ sftp.remove(sftp.FOLDER + "/test")
+
+ def test_close(self, sftp):
+ """
+ Verify that SFTP session close() causes a socket error on next action.
+ """
+ sftp.close()
+ with pytest.raises(socket.error, match="Socket is closed"):
+ sftp.open(sftp.FOLDER + "/test2", "w")
+
+ def test_sftp_can_be_used_as_context_manager(self, sftp):
+ """
+ verify that the sftp session is closed when exiting the context manager
+ """
+ with sftp:
+ pass
+ with pytest.raises(socket.error, match="Socket is closed"):
+ sftp.open(sftp.FOLDER + "/test2", "w")
+
+ def test_write(self, sftp):
+ """
+ verify that a file can be created and written, and the size is correct.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/duck.txt", "w") as f:
+ f.write(ARTICLE)
+ assert sftp.stat(sftp.FOLDER + "/duck.txt").st_size == 1486
+ finally:
+ sftp.remove(sftp.FOLDER + "/duck.txt")
+
+ def test_sftp_file_can_be_used_as_context_manager(self, sftp):
+ """
+ verify that an opened file can be used as a context manager
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/duck.txt", "w") as f:
+ f.write(ARTICLE)
+ assert sftp.stat(sftp.FOLDER + "/duck.txt").st_size == 1486
+ finally:
+ sftp.remove(sftp.FOLDER + "/duck.txt")
+
+ def test_append(self, sftp):
+ """
+ verify that a file can be opened for append, and tell() still works.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/append.txt", "w") as f:
+ f.write("first line\nsecond line\n")
+ assert f.tell() == 23
+
+ with sftp.open(sftp.FOLDER + "/append.txt", "a+") as f:
+ f.write("third line!!!\n")
+ assert f.tell() == 37
+ assert f.stat().st_size == 37
+ f.seek(-26, f.SEEK_CUR)
+ assert f.readline() == "second line\n"
+ finally:
+ sftp.remove(sftp.FOLDER + "/append.txt")
+
+ def test_rename(self, sftp):
+ """
+ verify that renaming a file works.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/first.txt", "w") as f:
+ f.write("content!\n")
+ sftp.rename(
+ sftp.FOLDER + "/first.txt", sftp.FOLDER + "/second.txt"
+ )
+ with pytest.raises(IOError, match="No such file"):
+ sftp.open(sftp.FOLDER + "/first.txt", "r")
+ with sftp.open(sftp.FOLDER + "/second.txt", "r") as f:
+ f.seek(-6, f.SEEK_END)
+ assert u(f.read(4)) == "tent"
+ finally:
+ # TODO: this is gross, make some sort of 'remove if possible' / 'rm
+ # -f' a-like, jeez
+ try:
+ sftp.remove(sftp.FOLDER + "/first.txt")
+ except:
+ pass
+ try:
+ sftp.remove(sftp.FOLDER + "/second.txt")
+ except:
+ pass
+
+ def testa_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")
+ with pytest.raises(IOError): # actual message seems generic
+ sftp.rename(sftp.FOLDER + "/a", sftp.FOLDER + "/b")
+
+ # 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 "two" == data, err
+
+ finally:
+ try:
+ sftp.remove(sftp.FOLDER + "/a")
+ except:
+ pass
+ try:
+ sftp.remove(sftp.FOLDER + "/b")
+ except:
+ pass
+
+ def test_folder(self, sftp):
+ """
+ create a temporary folder, verify that we can create a file in it, then
+ remove the folder and verify that we can't create a file in it anymore.
+ """
+ sftp.mkdir(sftp.FOLDER + "/subfolder")
+ sftp.open(sftp.FOLDER + "/subfolder/test", "w").close()
+ sftp.remove(sftp.FOLDER + "/subfolder/test")
+ sftp.rmdir(sftp.FOLDER + "/subfolder")
+ # shouldn't be able to create that file if dir removed
+ with pytest.raises(IOError, match="No such file"):
+ sftp.open(sftp.FOLDER + "/subfolder/test")
+
+ def test_listdir(self, sftp):
+ """
+ verify that a folder can be created, a bunch of files can be placed in
+ it, and those files show up in sftp.listdir.
+ """
+ try:
+ sftp.open(sftp.FOLDER + "/duck.txt", "w").close()
+ sftp.open(sftp.FOLDER + "/fish.txt", "w").close()
+ sftp.open(sftp.FOLDER + "/tertiary.py", "w").close()
+
+ x = sftp.listdir(sftp.FOLDER)
+ assert len(x) == 3
+ assert "duck.txt" in x
+ assert "fish.txt" in x
+ assert "tertiary.py" in x
+ assert "random" not in x
+ finally:
+ sftp.remove(sftp.FOLDER + "/duck.txt")
+ sftp.remove(sftp.FOLDER + "/fish.txt")
+ sftp.remove(sftp.FOLDER + "/tertiary.py")
+
+ def test_listdir_iter(self, sftp):
+ """
+ listdir_iter version of above test
+ """
+ try:
+ sftp.open(sftp.FOLDER + "/duck.txt", "w").close()
+ sftp.open(sftp.FOLDER + "/fish.txt", "w").close()
+ sftp.open(sftp.FOLDER + "/tertiary.py", "w").close()
+
+ x = [x.filename for x in sftp.listdir_iter(sftp.FOLDER)]
+ assert len(x) == 3
+ assert "duck.txt" in x
+ assert "fish.txt" in x
+ assert "tertiary.py" in x
+ assert "random" not in x
+ finally:
+ sftp.remove(sftp.FOLDER + "/duck.txt")
+ sftp.remove(sftp.FOLDER + "/fish.txt")
+ sftp.remove(sftp.FOLDER + "/tertiary.py")
+
+ @requireNonAsciiLocale()
+ def test_listdir_in_locale(self, sftp):
+ """Test listdir under a locale that uses non-ascii text."""
+ sftp.open(sftp.FOLDER + "/canard.txt", "w").close()
+ try:
+ folder_contents = sftp.listdir(sftp.FOLDER)
+ assert ["canard.txt"] == folder_contents
+ finally:
+ sftp.remove(sftp.FOLDER + "/canard.txt")
+
+ def test_setstat(self, sftp):
+ """
+ verify that the setstat functions (chown, chmod, utime, truncate) work.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/special", "w") as f:
+ f.write("x" * 1024)
+
+ stat = sftp.stat(sftp.FOLDER + "/special")
+ sftp.chmod(sftp.FOLDER + "/special", (stat.st_mode & ~o777) | o600)
+ stat = sftp.stat(sftp.FOLDER + "/special")
+ expected_mode = o600
+ if sys.platform == "win32":
+ # chmod not really functional on windows
+ expected_mode = o666
+ if sys.platform == "cygwin":
+ # even worse.
+ expected_mode = o644
+ assert stat.st_mode & o777 == expected_mode
+ assert stat.st_size == 1024
+
+ mtime = stat.st_mtime - 3600
+ atime = stat.st_atime - 1800
+ sftp.utime(sftp.FOLDER + "/special", (atime, mtime))
+ stat = sftp.stat(sftp.FOLDER + "/special")
+ assert stat.st_mtime == mtime
+ if sys.platform not in ("win32", "cygwin"):
+ assert stat.st_atime == atime
+
+ # can't really test chown, since we'd have to know a valid uid.
+
+ sftp.truncate(sftp.FOLDER + "/special", 512)
+ stat = sftp.stat(sftp.FOLDER + "/special")
+ assert stat.st_size == 512
+ finally:
+ sftp.remove(sftp.FOLDER + "/special")
+
+ def test_fsetstat(self, sftp):
+ """
+ verify that the fsetstat functions (chown, chmod, utime, truncate)
+ work on open files.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/special", "w") as f:
+ f.write("x" * 1024)
+
+ with sftp.open(sftp.FOLDER + "/special", "r+") as f:
+ stat = f.stat()
+ f.chmod((stat.st_mode & ~o777) | o600)
+ stat = f.stat()
+
+ expected_mode = o600
+ if sys.platform == "win32":
+ # chmod not really functional on windows
+ expected_mode = o666
+ if sys.platform == "cygwin":
+ # even worse.
+ expected_mode = o644
+ assert stat.st_mode & o777 == expected_mode
+ assert stat.st_size == 1024
+
+ mtime = stat.st_mtime - 3600
+ atime = stat.st_atime - 1800
+ f.utime((atime, mtime))
+ stat = f.stat()
+ assert stat.st_mtime == mtime
+ if sys.platform not in ("win32", "cygwin"):
+ assert stat.st_atime == atime
+
+ # can't really test chown, since we'd have to know a valid uid.
+
+ f.truncate(512)
+ stat = f.stat()
+ assert stat.st_size == 512
+ finally:
+ sftp.remove(sftp.FOLDER + "/special")
+
+ def test_readline_seek(self, sftp):
+ """
+ create a text file and write a bunch of text into it. then count the
+ lines in the file, and seek around to retrieve particular lines. this
+ should verify that read buffering and 'tell' work well together, and
+ that read buffering is reset on 'seek'.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/duck.txt", "w") as f:
+ f.write(ARTICLE)
+
+ with sftp.open(sftp.FOLDER + "/duck.txt", "r+") as f:
+ line_number = 0
+ loc = 0
+ pos_list = []
+ for line in f:
+ line_number += 1
+ pos_list.append(loc)
+ loc = f.tell()
+ assert f.seekable()
+ f.seek(pos_list[6], f.SEEK_SET)
+ assert f.readline(), "Nouzilly == France.\n"
+ f.seek(pos_list[17], f.SEEK_SET)
+ assert f.readline()[:4] == "duck"
+ f.seek(pos_list[10], f.SEEK_SET)
+ expected = "duck types were equally resistant to exogenous insulin compared with chicken.\n" # noqa
+ assert f.readline() == expected
+ finally:
+ sftp.remove(sftp.FOLDER + "/duck.txt")
+
+ def test_write_seek(self, sftp):
+ """
+ Create a text file, seek back, change it, and verify.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/testing.txt", "w") as f:
+ f.write("hello kitty.\n")
+ f.seek(-5, f.SEEK_CUR)
+ f.write("dd")
+
+ assert sftp.stat(sftp.FOLDER + "/testing.txt").st_size == 13
+ with sftp.open(sftp.FOLDER + "/testing.txt", "r") as f:
+ data = f.read(20)
+ assert data == b"hello kiddy.\n"
+ finally:
+ sftp.remove(sftp.FOLDER + "/testing.txt")
+
+ def test_symlink(self, sftp):
+ """
+ create a symlink and then check that lstat doesn't follow it.
+ """
+ if not hasattr(os, "symlink"):
+ # skip symlink tests on windows
+ return
+
+ try:
+ with sftp.open(sftp.FOLDER + "/original.txt", "w") as f:
+ f.write("original\n")
+ sftp.symlink("original.txt", sftp.FOLDER + "/link.txt")
+ assert sftp.readlink(sftp.FOLDER + "/link.txt") == "original.txt"
+
+ with sftp.open(sftp.FOLDER + "/link.txt", "r") as f:
+ assert f.readlines() == ["original\n"]
+
+ cwd = sftp.normalize(".")
+ if cwd[-1] == "/":
+ cwd = cwd[:-1]
+ abs_path = cwd + "/" + sftp.FOLDER + "/original.txt"
+ sftp.symlink(abs_path, sftp.FOLDER + "/link2.txt")
+ assert abs_path == sftp.readlink(sftp.FOLDER + "/link2.txt")
+
+ assert sftp.lstat(sftp.FOLDER + "/link.txt").st_size == 12
+ assert sftp.stat(sftp.FOLDER + "/link.txt").st_size == 9
+ # the sftp server may be hiding extra path members from us, so the
+ # length may be longer than we expect:
+ assert sftp.lstat(sftp.FOLDER + "/link2.txt").st_size >= len(
+ abs_path
+ )
+ assert sftp.stat(sftp.FOLDER + "/link2.txt").st_size == 9
+ assert sftp.stat(sftp.FOLDER + "/original.txt").st_size == 9
+ finally:
+ try:
+ sftp.remove(sftp.FOLDER + "/link.txt")
+ except:
+ pass
+ try:
+ sftp.remove(sftp.FOLDER + "/link2.txt")
+ except:
+ pass
+ try:
+ sftp.remove(sftp.FOLDER + "/original.txt")
+ except:
+ pass
+
+ def test_flush_seek(self, sftp):
+ """
+ verify that buffered writes are automatically flushed on seek.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/happy.txt", "w", 1) as f:
+ f.write("full line.\n")
+ f.write("partial")
+ f.seek(9, f.SEEK_SET)
+ f.write("?\n")
+
+ with sftp.open(sftp.FOLDER + "/happy.txt", "r") as f:
+ assert f.readline() == u("full line?\n")
+ assert f.read(7) == b"partial"
+ finally:
+ try:
+ sftp.remove(sftp.FOLDER + "/happy.txt")
+ except:
+ pass
+
+ def test_realpath(self, sftp):
+ """
+ test that realpath is returning something non-empty and not an
+ error.
+ """
+ pwd = sftp.normalize(".")
+ assert len(pwd) > 0
+ f = sftp.normalize("./" + sftp.FOLDER)
+ assert len(f) > 0
+ assert os.path.join(pwd, sftp.FOLDER) == f
+
+ def test_mkdir(self, sftp):
+ """
+ verify that mkdir/rmdir work.
+ """
+ sftp.mkdir(sftp.FOLDER + "/subfolder")
+ with pytest.raises(IOError): # generic msg only
+ sftp.mkdir(sftp.FOLDER + "/subfolder")
+ sftp.rmdir(sftp.FOLDER + "/subfolder")
+ with pytest.raises(IOError, match="No such file"):
+ sftp.rmdir(sftp.FOLDER + "/subfolder")
+
+ def test_chdir(self, sftp):
+ """
+ verify that chdir/getcwd work.
+ """
+ root = sftp.normalize(".")
+ if root[-1] != "/":
+ root += "/"
+ try:
+ sftp.mkdir(sftp.FOLDER + "/alpha")
+ sftp.chdir(sftp.FOLDER + "/alpha")
+ sftp.mkdir("beta")
+ assert root + sftp.FOLDER + "/alpha" == sftp.getcwd()
+ assert ["beta"] == sftp.listdir(".")
+
+ sftp.chdir("beta")
+ with sftp.open("fish", "w") as f:
+ f.write("hello\n")
+ sftp.chdir("..")
+ assert ["fish"] == sftp.listdir("beta")
+ sftp.chdir("..")
+ assert ["fish"] == sftp.listdir("alpha/beta")
+ finally:
+ sftp.chdir(root)
+ try:
+ sftp.unlink(sftp.FOLDER + "/alpha/beta/fish")
+ except:
+ pass
+ try:
+ sftp.rmdir(sftp.FOLDER + "/alpha/beta")
+ except:
+ pass
+ try:
+ sftp.rmdir(sftp.FOLDER + "/alpha")
+ except:
+ pass
+
+ def test_get_put(self, sftp):
+ """
+ verify that get/put work.
+ """
+ warnings.filterwarnings("ignore", "tempnam.*")
+
+ fd, localname = mkstemp()
+ os.close(fd)
+ text = b"All I wanted was a plastic bunny rabbit.\n"
+ with open(localname, "wb") as f:
+ f.write(text)
+ saved_progress = []
+
+ def progress_callback(x, y):
+ saved_progress.append((x, y))
+
+ sftp.put(localname, sftp.FOLDER + "/bunny.txt", progress_callback)
+
+ with sftp.open(sftp.FOLDER + "/bunny.txt", "rb") as f:
+ assert text == f.read(128)
+ assert [(41, 41)] == saved_progress
+
+ os.unlink(localname)
+ fd, localname = mkstemp()
+ os.close(fd)
+ saved_progress = []
+ sftp.get(sftp.FOLDER + "/bunny.txt", localname, progress_callback)
+
+ with open(localname, "rb") as f:
+ assert text == f.read(128)
+ assert [(41, 41)] == saved_progress
+
+ os.unlink(localname)
+ sftp.unlink(sftp.FOLDER + "/bunny.txt")
+
+ def test_get_without_prefetch(self, sftp):
+ """
+ Create a 4MB file. Verify that pull works without prefetching
+ using a lager file.
+ """
+
+ sftp_filename = sftp.FOLDER + "/dummy_file"
+ num_chars = 1024 * 1024 * 4
+
+ fd, localname = mkstemp()
+ os.close(fd)
+
+ with open(localname, "wb") as f:
+ f.write(b"0" * num_chars)
+
+ sftp.put(localname, sftp_filename)
+
+ os.unlink(localname)
+ fd, localname = mkstemp()
+ os.close(fd)
+
+ sftp.get(sftp_filename, localname, prefetch=False)
+
+ assert os.stat(localname).st_size == num_chars
+
+ os.unlink(localname)
+ sftp.unlink(sftp_filename)
+
+ def test_check(self, sftp):
+ """
+ verify that file.check() works against our own server.
+ (it's an sftp extension that we support, and may be the only ones who
+ support it.)
+ """
+ with sftp.open(sftp.FOLDER + "/kitty.txt", "w") as f:
+ f.write("here kitty kitty" * 64)
+
+ try:
+ with sftp.open(sftp.FOLDER + "/kitty.txt", "r") as f:
+ sum = f.check("sha1")
+ assert (
+ "91059CFC6615941378D413CB5ADAF4C5EB293402"
+ == u(hexlify(sum)).upper()
+ )
+ sum = f.check("md5", 0, 512)
+ assert (
+ "93DE4788FCA28D471516963A1FE3856A"
+ == u(hexlify(sum)).upper()
+ )
+ sum = f.check("md5", 0, 0, 510)
+ expected = "EB3B45B8CD55A0707D99B177544A319F373183D241432BB2157AB9E46358C4AC90370B5CADE5D90336FC1716F90B36D6" # noqa
+ assert u(hexlify(sum)).upper() == expected
+ finally:
+ sftp.unlink(sftp.FOLDER + "/kitty.txt")
+
+ def test_x_flag(self, sftp):
+ """
+ verify that the 'x' flag works when opening a file.
+ """
+ sftp.open(sftp.FOLDER + "/unusual.txt", "wx").close()
+
+ try:
+ with pytest.raises(IOError):
+ sftp.open(sftp.FOLDER + "/unusual.txt", "wx")
+ finally:
+ sftp.unlink(sftp.FOLDER + "/unusual.txt")
+
+ def test_utf8(self, sftp):
+ """
+ verify that unicode strings are encoded into utf8 correctly.
+ """
+ with sftp.open(sftp.FOLDER + "/something", "w") as f:
+ f.write("okay")
+ try:
+ sftp.rename(
+ sftp.FOLDER + "/something", sftp.FOLDER + "/" + unicode_folder
+ )
+ sftp.open(b(sftp.FOLDER) + utf8_folder, "r")
+ finally:
+ sftp.unlink(b(sftp.FOLDER) + utf8_folder)
+
+ def test_utf8_chdir(self, sftp):
+ sftp.mkdir(sftp.FOLDER + "/" + unicode_folder)
+ try:
+ sftp.chdir(sftp.FOLDER + "/" + unicode_folder)
+ with sftp.open("something", "w") as f:
+ f.write("okay")
+ sftp.unlink("something")
+ finally:
+ sftp.chdir()
+ sftp.rmdir(sftp.FOLDER + "/" + unicode_folder)
+
+ def test_bad_readv(self, sftp):
+ """
+ verify that readv at the end of the file doesn't essplode.
+ """
+ sftp.open(sftp.FOLDER + "/zero", "w").close()
+ try:
+ with sftp.open(sftp.FOLDER + "/zero", "r") as f:
+ f.readv([(0, 12)])
+
+ with sftp.open(sftp.FOLDER + "/zero", "r") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+ f.read(100)
+ finally:
+ sftp.unlink(sftp.FOLDER + "/zero")
+
+ def test_put_without_confirm(self, sftp):
+ """
+ verify that get/put work without confirmation.
+ """
+ warnings.filterwarnings("ignore", "tempnam.*")
+
+ fd, localname = mkstemp()
+ os.close(fd)
+ text = b"All I wanted was a plastic bunny rabbit.\n"
+ with open(localname, "wb") as f:
+ f.write(text)
+ saved_progress = []
+
+ def progress_callback(x, y):
+ saved_progress.append((x, y))
+
+ res = sftp.put(
+ localname, sftp.FOLDER + "/bunny.txt", progress_callback, False
+ )
+
+ assert SFTPAttributes().attr == res.attr
+
+ with sftp.open(sftp.FOLDER + "/bunny.txt", "r") as f:
+ assert text == f.read(128)
+ assert (41, 41) == saved_progress[-1]
+
+ os.unlink(localname)
+ sftp.unlink(sftp.FOLDER + "/bunny.txt")
+
+ def test_getcwd(self, sftp):
+ """
+ verify that chdir/getcwd work.
+ """
+ assert sftp.getcwd() is None
+ root = sftp.normalize(".")
+ if root[-1] != "/":
+ root += "/"
+ try:
+ sftp.mkdir(sftp.FOLDER + "/alpha")
+ sftp.chdir(sftp.FOLDER + "/alpha")
+ assert sftp.getcwd() == "/" + sftp.FOLDER + "/alpha"
+ finally:
+ sftp.chdir(root)
+ try:
+ sftp.rmdir(sftp.FOLDER + "/alpha")
+ except:
+ pass
+
+ def test_seek_append(self, sftp):
+ """
+ verify that seek doesn't affect writes during append.
+
+ does not work except through paramiko. :( openssh fails.
+ """
+ try:
+ with sftp.open(sftp.FOLDER + "/append.txt", "a") as f:
+ f.write("first line\nsecond line\n")
+ f.seek(11, f.SEEK_SET)
+ f.write("third line\n")
+
+ with sftp.open(sftp.FOLDER + "/append.txt", "r") as f:
+ assert f.stat().st_size == 34
+ assert f.readline() == "first line\n"
+ assert f.readline() == "second line\n"
+ assert f.readline() == "third line\n"
+ finally:
+ sftp.remove(sftp.FOLDER + "/append.txt")
+
+ def test_putfo_empty_file(self, sftp):
+ """
+ Send an empty file and confirm it is sent.
+ """
+ target = sftp.FOLDER + "/empty file.txt"
+ stream = StringIO()
+ try:
+ attrs = sftp.putfo(stream, target)
+ # the returned attributes should not be null
+ assert attrs is not None
+ finally:
+ sftp.remove(target)
+
+ # TODO: this test doesn't actually fail if the regression (removing '%'
+ # expansion to '%%' within sftp.py's def _log()) is removed - stacktraces
+ # appear but they're clearly emitted from subthreads that have no error
+ # handling. No point running it until that is fixed somehow.
+ @pytest.mark.skip("Doesn't prove anything right now")
+ def test_file_with_percent(self, sftp):
+ """
+ verify that we can create a file with a '%' in the filename.
+ ( it needs to be properly escaped by _log() )
+ """
+ f = sftp.open(sftp.FOLDER + "/test%file", "w")
+ try:
+ assert f.stat().st_size == 0
+ finally:
+ f.close()
+ sftp.remove(sftp.FOLDER + "/test%file")
+
+ def test_non_utf8_data(self, sftp):
+ """Test write() and read() of non utf8 data"""
+ try:
+ with sftp.open(f"{sftp.FOLDER}/nonutf8data", "w") as f:
+ f.write(NON_UTF8_DATA)
+ with sftp.open(f"{sftp.FOLDER}/nonutf8data", "r") as f:
+ data = f.read()
+ assert data == NON_UTF8_DATA
+ with sftp.open(f"{sftp.FOLDER}/nonutf8data", "wb") as f:
+ f.write(NON_UTF8_DATA)
+ with sftp.open(f"{sftp.FOLDER}/nonutf8data", "rb") as f:
+ data = f.read()
+ assert data == NON_UTF8_DATA
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/nonutf8data")
+
+ @requireNonAsciiLocale("LC_TIME")
+ def test_sftp_attributes_locale_time(self, sftp):
+ """Test SFTPAttributes under a locale with non-ascii time strings."""
+ some_stat = os.stat(sftp.FOLDER)
+ sftp_attributes = SFTPAttributes.from_stat(some_stat, u("a_directory"))
+ assert b"a_directory" in sftp_attributes.asbytes()
+
+ def test_sftp_attributes_empty_str(self, sftp):
+ sftp_attributes = SFTPAttributes()
+ assert (
+ str(sftp_attributes)
+ == "?--------- 1 0 0 0 (unknown date) ?"
+ )
+
+ @needs_builtin("buffer")
+ def test_write_buffer(self, sftp):
+ """Test write() using a buffer instance."""
+ data = 3 * b"A potentially large block of data to chunk up.\n"
+ try:
+ with sftp.open(f"{sftp.FOLDER}/write_buffer", "wb") as f:
+ for offset in range(0, len(data), 8):
+ f.write(buffer(data, offset, 8)) # noqa
+
+ with sftp.open(f"{sftp.FOLDER}/write_buffer", "rb") as f:
+ assert f.read() == data
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/write_buffer")
+
+ @needs_builtin("memoryview")
+ def test_write_memoryview(self, sftp):
+ """Test write() using a memoryview instance."""
+ data = 3 * b"A potentially large block of data to chunk up.\n"
+ try:
+ with sftp.open(f"{sftp.FOLDER}/write_memoryview", "wb") as f:
+ view = memoryview(data)
+ for offset in range(0, len(data), 8):
+ f.write(view[offset : offset + 8])
+
+ with sftp.open(f"{sftp.FOLDER}/write_memoryview", "rb") as f:
+ assert f.read() == data
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/write_memoryview")
diff --git a/tests/test_sftp_big.py b/tests/test_sftp_big.py
new file mode 100644
index 0000000..7d1110c
--- /dev/null
+++ b/tests/test_sftp_big.py
@@ -0,0 +1,416 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+some unit tests to make sure sftp works well with large files.
+
+a real actual sftp server is contacted, and a new folder is created there to
+do test file operations in (so no existing files will be harmed).
+"""
+
+import random
+import struct
+import sys
+import time
+
+from paramiko.common import o660
+
+from ._util import slow, wait_until
+
+
+@slow
+class TestBigSFTP:
+ def test_lots_of_files(self, sftp):
+ """
+ create a bunch of files over the same session.
+ """
+ numfiles = 100
+ try:
+ for i in range(numfiles):
+ target = f"{sftp.FOLDER}/file{i}.txt"
+ with sftp.open(target, "w", 1) as f:
+ f.write(f"this is file #{i}.\n")
+ sftp.chmod(target, o660)
+
+ # now make sure every file is there, by creating a list of filenmes
+ # and reading them in random order.
+ numlist = list(range(numfiles))
+ while len(numlist) > 0:
+ r = numlist[random.randint(0, len(numlist) - 1)]
+ with sftp.open(f"{sftp.FOLDER}/file{r}.txt") as f:
+ assert f.readline() == f"this is file #{r}.\n"
+ numlist.remove(r)
+ finally:
+ for i in range(numfiles):
+ try:
+ sftp.remove(f"{sftp.FOLDER}/file{i}.txt")
+ except:
+ pass
+
+ def test_big_file(self, sftp):
+ """
+ write a 1MB file with no buffering.
+ """
+ kblob = 1024 * b"x"
+ start = time.time()
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "w") as f:
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+ end = time.time()
+ sys.stderr.write(f"{round(end - start)}s")
+
+ start = time.time()
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "r") as f:
+ for n in range(1024):
+ data = f.read(1024)
+ assert data == kblob
+
+ end = time.time()
+ sys.stderr.write(f"{round(end - start)}s")
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_big_file_pipelined(self, sftp):
+ """
+ write a 1MB file, with no linefeeds, using pipelining.
+ """
+ kblob = bytes().join([struct.pack(">H", n) for n in range(512)])
+ start = time.time()
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "wb") as f:
+ f.set_pipelined(True)
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+ end = time.time()
+ sys.stderr.write(f"{round(end - start)}s")
+
+ start = time.time()
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "rb") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+
+ # read on odd boundaries to make sure the bytes aren't getting
+ # scrambled
+ n = 0
+ k2blob = kblob + kblob
+ chunk = 629
+ size = 1024 * 1024
+ while n < size:
+ if n + chunk > size:
+ chunk = size - n
+ data = f.read(chunk)
+ offset = n % 1024
+ assert data == k2blob[offset : offset + chunk]
+ n += chunk
+
+ end = time.time()
+ sys.stderr.write(f"{round(end - start)}s")
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_prefetch_seek(self, sftp):
+ kblob = bytes().join([struct.pack(">H", n) for n in range(512)])
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "wb") as f:
+ f.set_pipelined(True)
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+
+ start = time.time()
+ k2blob = kblob + kblob
+ chunk = 793
+ for i in range(10):
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "rb") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+ base_offset = (512 * 1024) + 17 * random.randint(
+ 1000, 2000
+ )
+ offsets = [base_offset + j * chunk for j in range(100)]
+ # randomly seek around and read them out
+ for j in range(100):
+ offset = offsets[random.randint(0, len(offsets) - 1)]
+ offsets.remove(offset)
+ f.seek(offset)
+ data = f.read(chunk)
+ n_offset = offset % 1024
+ assert data == k2blob[n_offset : n_offset + chunk]
+ offset += chunk
+ end = time.time()
+ sys.stderr.write(f"{round(end - start)}s")
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_readv_seek(self, sftp):
+ kblob = bytes().join([struct.pack(">H", n) for n in range(512)])
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "wb") as f:
+ f.set_pipelined(True)
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+
+ start = time.time()
+ k2blob = kblob + kblob
+ chunk = 793
+ for i in range(10):
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "rb") as f:
+ base_offset = (512 * 1024) + 17 * random.randint(
+ 1000, 2000
+ )
+ # make a bunch of offsets and put them in random order
+ offsets = [base_offset + j * chunk for j in range(100)]
+ readv_list = []
+ for j in range(100):
+ o = offsets[random.randint(0, len(offsets) - 1)]
+ offsets.remove(o)
+ readv_list.append((o, chunk))
+ ret = f.readv(readv_list)
+ for i in range(len(readv_list)):
+ offset = readv_list[i][0]
+ n_offset = offset % 1024
+ assert next(ret) == k2blob[n_offset : n_offset + chunk]
+ end = time.time()
+ sys.stderr.write(f"{round(end - start)}s")
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_lots_of_prefetching(self, sftp):
+ """
+ prefetch a 1MB file a bunch of times, discarding the file object
+ without using it, to verify that paramiko doesn't get confused.
+ """
+ kblob = 1024 * b"x"
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "w") as f:
+ f.set_pipelined(True)
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+
+ for i in range(10):
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "r") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "r") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+ for n in range(1024):
+ data = f.read(1024)
+ assert data == kblob
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_prefetch_readv(self, sftp):
+ """
+ verify that prefetch and readv don't conflict with each other.
+ """
+ kblob = bytes().join([struct.pack(">H", n) for n in range(512)])
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "wb") as f:
+ f.set_pipelined(True)
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "rb") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+ data = f.read(1024)
+ assert data == kblob
+
+ chunk_size = 793
+ base_offset = 512 * 1024
+ k2blob = kblob + kblob
+ chunks = [
+ (base_offset + (chunk_size * i), chunk_size)
+ for i in range(20)
+ ]
+ for data in f.readv(chunks):
+ offset = base_offset % 1024
+ assert chunk_size == len(data)
+ assert k2blob[offset : offset + chunk_size] == data
+ base_offset += chunk_size
+
+ sys.stderr.write(" ")
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_large_readv(self, sftp):
+ """
+ verify that a very large readv is broken up correctly and still
+ returned as a single blob.
+ """
+ kblob = bytes().join([struct.pack(">H", n) for n in range(512)])
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "wb") as f:
+ f.set_pipelined(True)
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "rb") as f:
+ data = list(f.readv([(23 * 1024, 128 * 1024)]))
+ assert len(data) == 1
+ data = data[0]
+ assert len(data) == 128 * 1024
+
+ sys.stderr.write(" ")
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_big_file_big_buffer(self, sftp):
+ """
+ write a 1MB file, with no linefeeds, and a big buffer.
+ """
+ mblob = 1024 * 1024 * "x"
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "w", 128 * 1024) as f:
+ f.write(mblob)
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+
+ def test_big_file_renegotiate(self, sftp):
+ """
+ write a 1MB file, forcing key renegotiation in the middle.
+ """
+ t = sftp.sock.get_transport()
+ t.packetizer.REKEY_BYTES = 512 * 1024
+ k32blob = 32 * 1024 * "x"
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "w", 128 * 1024) as f:
+ for i in range(32):
+ f.write(k32blob)
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+ assert t.H != t.session_id
+
+ # try to read it too.
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "r", 128 * 1024) as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+ total = 0
+ while total < 1024 * 1024:
+ total += len(f.read(32 * 1024))
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
+ t.packetizer.REKEY_BYTES = pow(2, 30)
+
+ def test_prefetch_limit(self, sftp):
+ """
+ write a 1MB file and prefetch with a limit
+ """
+ kblob = 1024 * b"x"
+ start = time.time()
+
+ def expect_prefetch_extents(file, expected_extents):
+ with file._prefetch_lock:
+ assert len(file._prefetch_extents) == expected_extents
+
+ try:
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "w") as f:
+ for n in range(1024):
+ f.write(kblob)
+ if n % 128 == 0:
+ sys.stderr.write(".")
+ sys.stderr.write(" ")
+
+ assert (
+ sftp.stat(f"{sftp.FOLDER}/hongry.txt").st_size == 1024 * 1024
+ )
+ end = time.time()
+ sys.stderr.write(f"{round(end - start)}s")
+
+ # read with prefetch, no limit
+ # expecting 32 requests (32k * 32 == 1M)
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "rb") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size)
+ wait_until(lambda: expect_prefetch_extents(f, 32))
+
+ # read with prefetch, limiting to 5 simultaneous requests
+ with sftp.open(f"{sftp.FOLDER}/hongry.txt", "rb") as f:
+ file_size = f.stat().st_size
+ f.prefetch(file_size, 5)
+ wait_until(lambda: expect_prefetch_extents(f, 5))
+ for n in range(1024):
+ with f._prefetch_lock:
+ assert len(f._prefetch_extents) <= 5
+ data = f.read(1024)
+ assert data == kblob
+
+ if n % 128 == 0:
+ sys.stderr.write(".")
+
+ finally:
+ sftp.remove(f"{sftp.FOLDER}/hongry.txt")
diff --git a/tests/test_ssh_exception.py b/tests/test_ssh_exception.py
new file mode 100644
index 0000000..1628986
--- /dev/null
+++ b/tests/test_ssh_exception.py
@@ -0,0 +1,75 @@
+import pickle
+import unittest
+
+from paramiko import RSAKey
+from paramiko.ssh_exception import (
+ NoValidConnectionsError,
+ BadAuthenticationType,
+ PartialAuthentication,
+ ChannelException,
+ BadHostKeyException,
+ ProxyCommandFailure,
+)
+
+
+class NoValidConnectionsErrorTest(unittest.TestCase):
+ def test_pickling(self):
+ # Regression test for https://github.com/paramiko/paramiko/issues/617
+ exc = NoValidConnectionsError({("127.0.0.1", "22"): Exception()})
+ new_exc = pickle.loads(pickle.dumps(exc))
+ self.assertEqual(type(exc), type(new_exc))
+ self.assertEqual(str(exc), str(new_exc))
+ self.assertEqual(exc.args, new_exc.args)
+
+ def test_error_message_for_single_host(self):
+ exc = NoValidConnectionsError({("127.0.0.1", "22"): Exception()})
+ assert "Unable to connect to port 22 on 127.0.0.1" in str(exc)
+
+ def test_error_message_for_two_hosts(self):
+ exc = NoValidConnectionsError(
+ {("127.0.0.1", "22"): Exception(), ("::1", "22"): Exception()}
+ )
+ assert "Unable to connect to port 22 on 127.0.0.1 or ::1" in str(exc)
+
+ def test_error_message_for_multiple_hosts(self):
+ exc = NoValidConnectionsError(
+ {
+ ("127.0.0.1", "22"): Exception(),
+ ("::1", "22"): Exception(),
+ ("10.0.0.42", "22"): Exception(),
+ }
+ )
+ exp = "Unable to connect to port 22 on 10.0.0.42, 127.0.0.1 or ::1"
+ assert exp in str(exc)
+
+
+class ExceptionStringDisplayTest(unittest.TestCase):
+ def test_BadAuthenticationType(self):
+ exc = BadAuthenticationType(
+ "Bad authentication type", ["ok", "also-ok"]
+ )
+ expected = "Bad authentication type; allowed types: ['ok', 'also-ok']"
+ assert str(exc) == expected
+
+ def test_PartialAuthentication(self):
+ exc = PartialAuthentication(["ok", "also-ok"])
+ expected = "Partial authentication; allowed types: ['ok', 'also-ok']"
+ assert str(exc) == expected
+
+ def test_BadHostKeyException(self):
+ got_key = RSAKey.generate(2048)
+ wanted_key = RSAKey.generate(2048)
+ exc = BadHostKeyException("myhost", got_key, wanted_key)
+ expected = "Host key for server 'myhost' does not match: got '{}', expected '{}'" # noqa
+ assert str(exc) == expected.format(
+ got_key.get_base64(), wanted_key.get_base64()
+ )
+
+ def test_ProxyCommandFailure(self):
+ exc = ProxyCommandFailure("man squid", 7)
+ expected = 'ProxyCommand("man squid") returned nonzero exit status: 7'
+ assert str(exc) == expected
+
+ def test_ChannelException(self):
+ exc = ChannelException(17, "whatever")
+ assert str(exc) == "ChannelException(17, 'whatever')"
diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py
new file mode 100644
index 0000000..b441a22
--- /dev/null
+++ b/tests/test_ssh_gss.py
@@ -0,0 +1,160 @@
+# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.de>
+#
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Unit Tests for the GSS-API / SSPI SSHv2 Authentication (gssapi-with-mic)
+"""
+
+import socket
+import threading
+
+import paramiko
+
+from ._util import _support, needs_gssapi, KerberosTestCase, update_env
+from .test_client import FINGERPRINTS
+
+
+class NullServer(paramiko.ServerInterface):
+ def get_allowed_auths(self, username):
+ return "gssapi-with-mic,publickey"
+
+ def check_auth_gssapi_with_mic(
+ self, username, gss_authenticated=paramiko.AUTH_FAILED, cc_file=None
+ ):
+ if gss_authenticated == paramiko.AUTH_SUCCESSFUL:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def enable_auth_gssapi(self):
+ return True
+
+ def check_auth_publickey(self, username, key):
+ try:
+ expected = FINGERPRINTS[key.get_name()]
+ except KeyError:
+ return paramiko.AUTH_FAILED
+ else:
+ if key.get_fingerprint() == expected:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def check_channel_request(self, kind, chanid):
+ return paramiko.OPEN_SUCCEEDED
+
+ def check_channel_exec_request(self, channel, command):
+ if command != b"yes":
+ return False
+ return True
+
+
+@needs_gssapi
+class GSSAuthTest(KerberosTestCase):
+ def setUp(self):
+ # TODO: username and targ_name should come from os.environ or whatever
+ # the approved pytest method is for runtime-configuring test data.
+ self.username = self.realm.user_princ
+ self.hostname = socket.getfqdn(self.realm.hostname)
+ self.sockl = socket.socket()
+ self.sockl.bind((self.realm.hostname, 0))
+ self.sockl.listen(1)
+ self.addr, self.port = self.sockl.getsockname()
+ self.event = threading.Event()
+ update_env(self, self.realm.env)
+ thread = threading.Thread(target=self._run)
+ thread.start()
+
+ def tearDown(self):
+ for attr in "tc ts socks sockl".split():
+ if hasattr(self, attr):
+ getattr(self, attr).close()
+
+ def _run(self):
+ self.socks, addr = self.sockl.accept()
+ self.ts = paramiko.Transport(self.socks)
+ host_key = paramiko.RSAKey.from_private_key_file(_support("rsa.key"))
+ self.ts.add_server_key(host_key)
+ server = NullServer()
+ self.ts.start_server(self.event, server)
+
+ def _test_connection(self, **kwargs):
+ """
+ (Most) kwargs get passed directly into SSHClient.connect().
+
+ The exception is ... no exception yet
+ """
+ host_key = paramiko.RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.WarningPolicy())
+ self.tc.get_host_keys().add(
+ f"[{self.addr}]:{self.port}", "ssh-rsa", public_host_key
+ )
+ self.tc.connect(
+ hostname=self.addr,
+ port=self.port,
+ username=self.username,
+ gss_host=self.hostname,
+ gss_auth=True,
+ **kwargs,
+ )
+
+ self.event.wait(1.0)
+ self.assert_(self.event.is_set())
+ self.assert_(self.ts.is_active())
+ self.assertEquals(self.username, self.ts.get_username())
+ self.assertEquals(True, self.ts.is_authenticated())
+
+ stdin, stdout, stderr = self.tc.exec_command("yes")
+ schan = self.ts.accept(1.0)
+
+ schan.send("Hello there.\n")
+ schan.send_stderr("This is on stderr.\n")
+ schan.close()
+
+ self.assertEquals("Hello there.\n", stdout.readline())
+ self.assertEquals("", stdout.readline())
+ self.assertEquals("This is on stderr.\n", stderr.readline())
+ self.assertEquals("", stderr.readline())
+
+ stdin.close()
+ stdout.close()
+ stderr.close()
+
+ def test_gss_auth(self):
+ """
+ Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication
+ (gssapi-with-mic) in client and server mode.
+ """
+ self._test_connection(allow_agent=False, look_for_keys=False)
+
+ def test_auth_trickledown(self):
+ """
+ Failed gssapi-with-mic doesn't prevent subsequent key from succeeding
+ """
+ self.hostname = (
+ "this_host_does_not_exists_and_causes_a_GSSAPI-exception"
+ )
+ self._test_connection(
+ key_filename=[_support("rsa.key")],
+ allow_agent=False,
+ look_for_keys=False,
+ )
diff --git a/tests/test_transport.py b/tests/test_transport.py
new file mode 100644
index 0000000..67e2eb4
--- /dev/null
+++ b/tests/test_transport.py
@@ -0,0 +1,1446 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for the ssh2 protocol in Transport.
+"""
+
+
+from binascii import hexlify
+import itertools
+import select
+import socket
+import time
+import threading
+import random
+import sys
+import unittest
+from unittest.mock import Mock
+
+from paramiko import (
+ AuthHandler,
+ ChannelException,
+ IncompatiblePeer,
+ MessageOrderError,
+ Packetizer,
+ RSAKey,
+ SSHException,
+ SecurityOptions,
+ ServiceRequestingTransport,
+ Transport,
+)
+from paramiko.auth_handler import AuthOnlyHandler
+from paramiko import OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
+from paramiko.common import (
+ DEFAULT_MAX_PACKET_SIZE,
+ DEFAULT_WINDOW_SIZE,
+ MAX_WINDOW_SIZE,
+ MIN_PACKET_SIZE,
+ MIN_WINDOW_SIZE,
+ MSG_CHANNEL_OPEN,
+ MSG_DEBUG,
+ MSG_IGNORE,
+ MSG_KEXINIT,
+ MSG_UNIMPLEMENTED,
+ MSG_USERAUTH_SUCCESS,
+ byte_chr,
+ cMSG_CHANNEL_WINDOW_ADJUST,
+ cMSG_UNIMPLEMENTED,
+)
+from paramiko.message import Message
+
+from ._util import (
+ needs_builtin,
+ _support,
+ requires_sha1_signing,
+ slow,
+ server,
+ _disable_sha2,
+ _disable_sha1,
+ TestServer as NullServer,
+)
+from ._loop import LoopSocket
+from pytest import mark, raises
+
+
+LONG_BANNER = """\
+Welcome to the super-fun-land BBS, where our MOTD is the primary thing we
+provide. All rights reserved. Offer void in Tennessee. Stunt drivers were
+used. Do not attempt at home. Some restrictions apply.
+
+Happy birthday to Commie the cat!
+
+Note: An SSH banner may eventually appear.
+
+Maybe.
+"""
+
+# Faux 'packet type' we do not implement and are unlikely ever to (but which is
+# technically "within spec" re RFC 4251
+MSG_FUGGEDABOUTIT = 253
+
+
+class TransportTest(unittest.TestCase):
+ # TODO: this can get nuked once ServiceRequestingTransport becomes the
+ # only Transport, as it has this baked in.
+ _auth_handler_class = AuthHandler
+
+ def setUp(self):
+ self.socks = LoopSocket()
+ self.sockc = LoopSocket()
+ self.sockc.link(self.socks)
+ self.tc = Transport(self.sockc)
+ self.ts = Transport(self.socks)
+
+ def tearDown(self):
+ self.tc.close()
+ self.ts.close()
+ self.socks.close()
+ self.sockc.close()
+
+ # TODO: unify with newer contextmanager
+ def setup_test_server(
+ self, client_options=None, server_options=None, connect_kwargs=None
+ ):
+ host_key = RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = RSAKey(data=host_key.asbytes())
+ self.ts.add_server_key(host_key)
+
+ if client_options is not None:
+ client_options(self.tc.get_security_options())
+ if server_options is not None:
+ server_options(self.ts.get_security_options())
+
+ event = threading.Event()
+ self.server = NullServer()
+ self.assertTrue(not event.is_set())
+ self.ts.start_server(event, self.server)
+ if connect_kwargs is None:
+ connect_kwargs = dict(
+ hostkey=public_host_key,
+ username="slowdive",
+ password="pygmalion",
+ )
+ self.tc.connect(**connect_kwargs)
+ event.wait(1.0)
+ self.assertTrue(event.is_set())
+ self.assertTrue(self.ts.is_active())
+
+ def test_security_options(self):
+ o = self.tc.get_security_options()
+ self.assertEqual(type(o), SecurityOptions)
+ self.assertTrue(("aes256-cbc", "aes192-cbc") != o.ciphers)
+ o.ciphers = ("aes256-cbc", "aes192-cbc")
+ self.assertEqual(("aes256-cbc", "aes192-cbc"), o.ciphers)
+ try:
+ o.ciphers = ("aes256-cbc", "made-up-cipher")
+ self.assertTrue(False)
+ except ValueError:
+ pass
+ try:
+ o.ciphers = 23
+ self.assertTrue(False)
+ except TypeError:
+ pass
+
+ def testb_security_options_reset(self):
+ o = self.tc.get_security_options()
+ # should not throw any exceptions
+ o.ciphers = o.ciphers
+ o.digests = o.digests
+ o.key_types = o.key_types
+ o.kex = o.kex
+ o.compression = o.compression
+
+ def test_compute_key(self):
+ self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929 # noqa
+ self.tc.H = b"\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3" # noqa
+ self.tc.session_id = self.tc.H
+ key = self.tc._compute_key("C", 32)
+ self.assertEqual(
+ b"207E66594CA87C44ECCBA3B3CD39FDDB378E6FDB0F97C54B2AA0CFBF900CD995", # noqa
+ hexlify(key).upper(),
+ )
+
+ def test_simple(self):
+ """
+ verify that we can establish an ssh link with ourselves across the
+ loopback sockets. this is hardly "simple" but it's simpler than the
+ later tests. :)
+ """
+ host_key = RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = RSAKey(data=host_key.asbytes())
+ self.ts.add_server_key(host_key)
+ event = threading.Event()
+ server = NullServer()
+ self.assertTrue(not event.is_set())
+ self.assertEqual(None, self.tc.get_username())
+ self.assertEqual(None, self.ts.get_username())
+ self.assertEqual(False, self.tc.is_authenticated())
+ self.assertEqual(False, self.ts.is_authenticated())
+ self.ts.start_server(event, server)
+ self.tc.connect(
+ hostkey=public_host_key, username="slowdive", password="pygmalion"
+ )
+ event.wait(1.0)
+ self.assertTrue(event.is_set())
+ self.assertTrue(self.ts.is_active())
+ self.assertEqual("slowdive", self.tc.get_username())
+ self.assertEqual("slowdive", self.ts.get_username())
+ self.assertEqual(True, self.tc.is_authenticated())
+ self.assertEqual(True, self.ts.is_authenticated())
+
+ def test_long_banner(self):
+ """
+ verify that a long banner doesn't mess up the handshake.
+ """
+ host_key = RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = RSAKey(data=host_key.asbytes())
+ self.ts.add_server_key(host_key)
+ event = threading.Event()
+ server = NullServer()
+ self.assertTrue(not event.is_set())
+ self.socks.send(LONG_BANNER)
+ self.ts.start_server(event, server)
+ self.tc.connect(
+ hostkey=public_host_key, username="slowdive", password="pygmalion"
+ )
+ event.wait(1.0)
+ self.assertTrue(event.is_set())
+ self.assertTrue(self.ts.is_active())
+
+ def test_special(self):
+ """
+ verify that the client can demand odd handshake settings, and can
+ renegotiate keys in mid-stream.
+ """
+
+ def force_algorithms(options):
+ options.ciphers = ("aes256-cbc",)
+ options.digests = ("hmac-md5-96",)
+
+ self.setup_test_server(client_options=force_algorithms)
+ self.assertEqual("aes256-cbc", self.tc.local_cipher)
+ self.assertEqual("aes256-cbc", self.tc.remote_cipher)
+ self.assertEqual(12, self.tc.packetizer.get_mac_size_out())
+ self.assertEqual(12, self.tc.packetizer.get_mac_size_in())
+
+ self.tc.send_ignore(1024)
+ self.tc.renegotiate_keys()
+ self.ts.send_ignore(1024)
+
+ @slow
+ def test_keepalive(self):
+ """
+ verify that the keepalive will be sent.
+ """
+ self.setup_test_server()
+ self.assertEqual(None, getattr(self.server, "_global_request", None))
+ self.tc.set_keepalive(1)
+ time.sleep(2)
+ self.assertEqual("keepalive@lag.net", self.server._global_request)
+
+ def test_exec_command(self):
+ """
+ verify that exec_command() does something reasonable.
+ """
+ self.setup_test_server()
+
+ chan = self.tc.open_session()
+ schan = self.ts.accept(1.0)
+ try:
+ chan.exec_command(
+ b"command contains \xfc and is not a valid UTF-8 string"
+ )
+ self.assertTrue(False)
+ except SSHException:
+ pass
+
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ schan = self.ts.accept(1.0)
+ schan.send("Hello there.\n")
+ schan.send_stderr("This is on stderr.\n")
+ schan.close()
+
+ f = chan.makefile()
+ self.assertEqual("Hello there.\n", f.readline())
+ self.assertEqual("", f.readline())
+ f = chan.makefile_stderr()
+ self.assertEqual("This is on stderr.\n", f.readline())
+ self.assertEqual("", f.readline())
+
+ # now try it with combined stdout/stderr
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ schan = self.ts.accept(1.0)
+ schan.send("Hello there.\n")
+ schan.send_stderr("This is on stderr.\n")
+ schan.close()
+
+ chan.set_combine_stderr(True)
+ f = chan.makefile()
+ self.assertEqual("Hello there.\n", f.readline())
+ self.assertEqual("This is on stderr.\n", f.readline())
+ self.assertEqual("", f.readline())
+
+ def test_channel_can_be_used_as_context_manager(self):
+ """
+ verify that exec_command() does something reasonable.
+ """
+ self.setup_test_server()
+
+ with self.tc.open_session() as chan:
+ with self.ts.accept(1.0) as schan:
+ chan.exec_command("yes")
+ schan.send("Hello there.\n")
+ schan.close()
+
+ f = chan.makefile()
+ self.assertEqual("Hello there.\n", f.readline())
+ self.assertEqual("", f.readline())
+
+ def test_invoke_shell(self):
+ """
+ verify that invoke_shell() does something reasonable.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.invoke_shell()
+ schan = self.ts.accept(1.0)
+ chan.send("communist j. cat\n")
+ f = schan.makefile()
+ self.assertEqual("communist j. cat\n", f.readline())
+ chan.close()
+ self.assertEqual("", f.readline())
+
+ def test_channel_exception(self):
+ """
+ verify that ChannelException is thrown for a bad open-channel request.
+ """
+ self.setup_test_server()
+ try:
+ self.tc.open_channel("bogus")
+ self.fail("expected exception")
+ except ChannelException as e:
+ self.assertTrue(e.code == OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED)
+
+ def test_exit_status(self):
+ """
+ verify that get_exit_status() works.
+ """
+ self.setup_test_server()
+
+ chan = self.tc.open_session()
+ schan = self.ts.accept(1.0)
+ chan.exec_command("yes")
+ schan.send("Hello there.\n")
+ self.assertTrue(not chan.exit_status_ready())
+ # trigger an EOF
+ schan.shutdown_read()
+ schan.shutdown_write()
+ schan.send_exit_status(23)
+ schan.close()
+
+ f = chan.makefile()
+ self.assertEqual("Hello there.\n", f.readline())
+ self.assertEqual("", f.readline())
+ count = 0
+ while not chan.exit_status_ready():
+ time.sleep(0.1)
+ count += 1
+ if count > 50:
+ raise Exception("timeout")
+ self.assertEqual(23, chan.recv_exit_status())
+ chan.close()
+
+ def test_select(self):
+ """
+ verify that select() on a channel works.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.invoke_shell()
+ schan = self.ts.accept(1.0)
+
+ # nothing should be ready
+ r, w, e = select.select([chan], [], [], 0.1)
+ self.assertEqual([], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+
+ schan.send("hello\n")
+
+ # something should be ready now (give it 1 second to appear)
+ for i in range(10):
+ r, w, e = select.select([chan], [], [], 0.1)
+ if chan in r:
+ break
+ time.sleep(0.1)
+ self.assertEqual([chan], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+
+ self.assertEqual(b"hello\n", chan.recv(6))
+
+ # and, should be dead again now
+ r, w, e = select.select([chan], [], [], 0.1)
+ self.assertEqual([], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+
+ schan.close()
+
+ # detect eof?
+ for i in range(10):
+ r, w, e = select.select([chan], [], [], 0.1)
+ if chan in r:
+ break
+ time.sleep(0.1)
+ self.assertEqual([chan], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+ self.assertEqual(b"", chan.recv(16))
+
+ # make sure the pipe is still open for now...
+ p = chan._pipe
+ self.assertEqual(False, p._closed)
+ chan.close()
+ # ...and now is closed.
+ self.assertEqual(True, p._closed)
+
+ def test_renegotiate(self):
+ """
+ verify that a transport can correctly renegotiate mid-stream.
+ """
+ self.setup_test_server()
+ self.tc.packetizer.REKEY_BYTES = 16384
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ schan = self.ts.accept(1.0)
+
+ self.assertEqual(self.tc.H, self.tc.session_id)
+ for i in range(20):
+ chan.send("x" * 1024)
+ chan.close()
+
+ # allow a few seconds for the rekeying to complete
+ for i in range(50):
+ if self.tc.H != self.tc.session_id:
+ break
+ time.sleep(0.1)
+ self.assertNotEqual(self.tc.H, self.tc.session_id)
+
+ schan.close()
+
+ def test_compression(self):
+ """
+ verify that zlib compression is basically working.
+ """
+
+ def force_compression(o):
+ o.compression = ("zlib",)
+
+ self.setup_test_server(force_compression, force_compression)
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ schan = self.ts.accept(1.0)
+
+ bytes = self.tc.packetizer._Packetizer__sent_bytes
+ chan.send("x" * 1024)
+ bytes2 = self.tc.packetizer._Packetizer__sent_bytes
+ block_size = self.tc._cipher_info[self.tc.local_cipher]["block-size"]
+ mac_size = self.tc._mac_info[self.tc.local_mac]["size"]
+ # tests show this is actually compressed to *52 bytes*! including
+ # packet overhead! nice!! :)
+ self.assertTrue(bytes2 - bytes < 1024)
+ self.assertEqual(16 + block_size + mac_size, bytes2 - bytes)
+
+ chan.close()
+ schan.close()
+
+ def test_x11(self):
+ """
+ verify that an x11 port can be requested and opened.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ schan = self.ts.accept(1.0)
+
+ requested = []
+
+ def handler(c, addr_port):
+ addr, port = addr_port
+ requested.append((addr, port))
+ self.tc._queue_incoming_channel(c)
+
+ self.assertEqual(
+ None, getattr(self.server, "_x11_screen_number", None)
+ )
+ cookie = chan.request_x11(0, single_connection=True, handler=handler)
+ self.assertEqual(0, self.server._x11_screen_number)
+ self.assertEqual("MIT-MAGIC-COOKIE-1", self.server._x11_auth_protocol)
+ self.assertEqual(cookie, self.server._x11_auth_cookie)
+ self.assertEqual(True, self.server._x11_single_connection)
+
+ x11_server = self.ts.open_x11_channel(("localhost", 6093))
+ x11_client = self.tc.accept()
+ self.assertEqual("localhost", requested[0][0])
+ self.assertEqual(6093, requested[0][1])
+
+ x11_server.send("hello")
+ self.assertEqual(b"hello", x11_client.recv(5))
+
+ x11_server.close()
+ x11_client.close()
+ chan.close()
+ schan.close()
+
+ def test_reverse_port_forwarding(self):
+ """
+ verify that a client can ask the server to open a reverse port for
+ forwarding.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ self.ts.accept(1.0)
+
+ requested = []
+
+ def handler(c, origin_addr_port, server_addr_port):
+ requested.append(origin_addr_port)
+ requested.append(server_addr_port)
+ self.tc._queue_incoming_channel(c)
+
+ port = self.tc.request_port_forward("127.0.0.1", 0, handler)
+ self.assertEqual(port, self.server._listen.getsockname()[1])
+
+ cs = socket.socket()
+ cs.connect(("127.0.0.1", port))
+ ss, _ = self.server._listen.accept()
+ sch = self.ts.open_forwarded_tcpip_channel(
+ ss.getsockname(), ss.getpeername()
+ )
+ cch = self.tc.accept()
+
+ sch.send("hello")
+ self.assertEqual(b"hello", cch.recv(5))
+ sch.close()
+ cch.close()
+ ss.close()
+ cs.close()
+
+ # now cancel it.
+ self.tc.cancel_port_forward("127.0.0.1", port)
+ self.assertTrue(self.server._listen is None)
+
+ def test_port_forwarding(self):
+ """
+ verify that a client can forward new connections from a locally-
+ forwarded port.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ self.ts.accept(1.0)
+
+ # open a port on the "server" that the client will ask to forward to.
+ greeting_server = socket.socket()
+ greeting_server.bind(("127.0.0.1", 0))
+ greeting_server.listen(1)
+ greeting_port = greeting_server.getsockname()[1]
+
+ cs = self.tc.open_channel(
+ "direct-tcpip", ("127.0.0.1", greeting_port), ("", 9000)
+ )
+ sch = self.ts.accept(1.0)
+ cch = socket.socket()
+ cch.connect(self.server._tcpip_dest)
+
+ ss, _ = greeting_server.accept()
+ ss.send(b"Hello!\n")
+ ss.close()
+ sch.send(cch.recv(8192))
+ sch.close()
+
+ self.assertEqual(b"Hello!\n", cs.recv(7))
+ cs.close()
+
+ def test_stderr_select(self):
+ """
+ verify that select() on a channel works even if only stderr is
+ receiving data.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.invoke_shell()
+ schan = self.ts.accept(1.0)
+
+ # nothing should be ready
+ r, w, e = select.select([chan], [], [], 0.1)
+ self.assertEqual([], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+
+ schan.send_stderr("hello\n")
+
+ # something should be ready now (give it 1 second to appear)
+ for i in range(10):
+ r, w, e = select.select([chan], [], [], 0.1)
+ if chan in r:
+ break
+ time.sleep(0.1)
+ self.assertEqual([chan], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+
+ self.assertEqual(b"hello\n", chan.recv_stderr(6))
+
+ # and, should be dead again now
+ r, w, e = select.select([chan], [], [], 0.1)
+ self.assertEqual([], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+
+ schan.close()
+ chan.close()
+
+ def test_send_ready(self):
+ """
+ verify that send_ready() indicates when a send would not block.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.invoke_shell()
+ schan = self.ts.accept(1.0)
+
+ self.assertEqual(chan.send_ready(), True)
+ total = 0
+ K = "*" * 1024
+ limit = 1 + (64 * 2**15)
+ while total < limit:
+ chan.send(K)
+ total += len(K)
+ if not chan.send_ready():
+ break
+ self.assertTrue(total < limit)
+
+ schan.close()
+ chan.close()
+ self.assertEqual(chan.send_ready(), True)
+
+ def test_rekey_deadlock(self):
+ """
+ Regression test for deadlock when in-transit messages are received
+ after MSG_KEXINIT is sent
+
+ Note: When this test fails, it may leak threads.
+ """
+
+ # Test for an obscure deadlocking bug that can occur if we receive
+ # certain messages while initiating a key exchange.
+ #
+ # The deadlock occurs as follows:
+ #
+ # In the main thread:
+ # 1. The user's program calls Channel.send(), which sends
+ # MSG_CHANNEL_DATA to the remote host.
+ # 2. Packetizer discovers that REKEY_BYTES has been exceeded, and
+ # sets the __need_rekey flag.
+ #
+ # In the Transport thread:
+ # 3. Packetizer notices that the __need_rekey flag is set, and raises
+ # NeedRekeyException.
+ # 4. In response to NeedRekeyException, the transport thread sends
+ # MSG_KEXINIT to the remote host.
+ #
+ # On the remote host (using any SSH implementation):
+ # 5. The MSG_CHANNEL_DATA is received, and MSG_CHANNEL_WINDOW_ADJUST
+ # is sent.
+ # 6. The MSG_KEXINIT is received, and a corresponding MSG_KEXINIT is
+ # sent.
+ #
+ # In the main thread:
+ # 7. The user's program calls Channel.send().
+ # 8. Channel.send acquires Channel.lock, then calls
+ # Transport._send_user_message().
+ # 9. Transport._send_user_message waits for Transport.clear_to_send
+ # to be set (i.e., it waits for re-keying to complete).
+ # Channel.lock is still held.
+ #
+ # In the Transport thread:
+ # 10. MSG_CHANNEL_WINDOW_ADJUST is received; Channel._window_adjust
+ # is called to handle it.
+ # 11. Channel._window_adjust tries to acquire Channel.lock, but it
+ # blocks because the lock is already held by the main thread.
+ #
+ # The result is that the Transport thread never processes the remote
+ # host's MSG_KEXINIT packet, because it becomes deadlocked while
+ # handling the preceding MSG_CHANNEL_WINDOW_ADJUST message.
+
+ # We set up two separate threads for sending and receiving packets,
+ # while the main thread acts as a watchdog timer. If the timer
+ # expires, a deadlock is assumed.
+
+ class SendThread(threading.Thread):
+ def __init__(self, chan, iterations, done_event):
+ threading.Thread.__init__(
+ self, None, None, self.__class__.__name__
+ )
+ self.daemon = True
+ self.chan = chan
+ self.iterations = iterations
+ self.done_event = done_event
+ self.watchdog_event = threading.Event()
+ self.last = None
+
+ def run(self):
+ try:
+ for i in range(1, 1 + self.iterations):
+ if self.done_event.is_set():
+ break
+ self.watchdog_event.set()
+ # print i, "SEND"
+ self.chan.send("x" * 2048)
+ finally:
+ self.done_event.set()
+ self.watchdog_event.set()
+
+ class ReceiveThread(threading.Thread):
+ def __init__(self, chan, done_event):
+ threading.Thread.__init__(
+ self, None, None, self.__class__.__name__
+ )
+ self.daemon = True
+ self.chan = chan
+ self.done_event = done_event
+ self.watchdog_event = threading.Event()
+
+ def run(self):
+ try:
+ while not self.done_event.is_set():
+ if self.chan.recv_ready():
+ chan.recv(65536)
+ self.watchdog_event.set()
+ else:
+ if random.randint(0, 1):
+ time.sleep(random.randint(0, 500) / 1000.0)
+ finally:
+ self.done_event.set()
+ self.watchdog_event.set()
+
+ self.setup_test_server()
+ self.ts.packetizer.REKEY_BYTES = 2048
+
+ chan = self.tc.open_session()
+ chan.exec_command("yes")
+ schan = self.ts.accept(1.0)
+
+ # Monkey patch the client's Transport._handler_table so that the client
+ # sends MSG_CHANNEL_WINDOW_ADJUST whenever it receives an initial
+ # MSG_KEXINIT. This is used to simulate the effect of network latency
+ # on a real MSG_CHANNEL_WINDOW_ADJUST message.
+ self.tc._handler_table = (
+ self.tc._handler_table.copy()
+ ) # copy per-class dictionary
+ _negotiate_keys = self.tc._handler_table[MSG_KEXINIT]
+
+ def _negotiate_keys_wrapper(self, m):
+ if self.local_kex_init is None: # Remote side sent KEXINIT
+ # Simulate in-transit MSG_CHANNEL_WINDOW_ADJUST by sending it
+ # before responding to the incoming MSG_KEXINIT.
+ m2 = Message()
+ m2.add_byte(cMSG_CHANNEL_WINDOW_ADJUST)
+ m2.add_int(chan.remote_chanid)
+ m2.add_int(1) # bytes to add
+ self._send_message(m2)
+ return _negotiate_keys(self, m)
+
+ self.tc._handler_table[MSG_KEXINIT] = _negotiate_keys_wrapper
+
+ # Parameters for the test
+ iterations = 500 # The deadlock does not happen every time, but it
+ # should after many iterations.
+ timeout = 5
+
+ # This event is set when the test is completed
+ done_event = threading.Event()
+
+ # Start the sending thread
+ st = SendThread(schan, iterations, done_event)
+ st.start()
+
+ # Start the receiving thread
+ rt = ReceiveThread(chan, done_event)
+ rt.start()
+
+ # Act as a watchdog timer, checking
+ deadlocked = False
+ while not deadlocked and not done_event.is_set():
+ for event in (st.watchdog_event, rt.watchdog_event):
+ event.wait(timeout)
+ if done_event.is_set():
+ break
+ if not event.is_set():
+ deadlocked = True
+ break
+ event.clear()
+
+ # Tell the threads to stop (if they haven't already stopped). Note
+ # that if one or more threads are deadlocked, they might hang around
+ # forever (until the process exits).
+ done_event.set()
+
+ # Assertion: We must not have detected a timeout.
+ self.assertFalse(deadlocked)
+
+ # Close the channels
+ schan.close()
+ chan.close()
+
+ def test_sanitze_packet_size(self):
+ """
+ verify that we conform to the rfc of packet and window sizes.
+ """
+ for val, correct in [
+ (4095, MIN_PACKET_SIZE),
+ (None, DEFAULT_MAX_PACKET_SIZE),
+ (2**32, MAX_WINDOW_SIZE),
+ ]:
+ self.assertEqual(self.tc._sanitize_packet_size(val), correct)
+
+ def test_sanitze_window_size(self):
+ """
+ verify that we conform to the rfc of packet and window sizes.
+ """
+ for val, correct in [
+ (32767, MIN_WINDOW_SIZE),
+ (None, DEFAULT_WINDOW_SIZE),
+ (2**32, MAX_WINDOW_SIZE),
+ ]:
+ self.assertEqual(self.tc._sanitize_window_size(val), correct)
+
+ @slow
+ def test_handshake_timeout(self):
+ """
+ verify that we can get a handshake timeout.
+ """
+ # Tweak client Transport instance's Packetizer instance so
+ # its read_message() sleeps a bit. This helps prevent race conditions
+ # where the client Transport's timeout timer thread doesn't even have
+ # time to get scheduled before the main client thread finishes
+ # handshaking with the server.
+ # (Doing this on the server's transport *sounds* more 'correct' but
+ # actually doesn't work nearly as well for whatever reason.)
+ class SlowPacketizer(Packetizer):
+ def read_message(self):
+ time.sleep(1)
+ return super().read_message()
+
+ # NOTE: prettttty sure since the replaced .packetizer Packetizer is now
+ # no longer doing anything with its copy of the socket...everything'll
+ # be fine. Even tho it's a bit squicky.
+ self.tc.packetizer = SlowPacketizer(self.tc.sock)
+ # Continue with regular test red tape.
+ host_key = RSAKey.from_private_key_file(_support("rsa.key"))
+ public_host_key = RSAKey(data=host_key.asbytes())
+ self.ts.add_server_key(host_key)
+ event = threading.Event()
+ server = NullServer()
+ self.assertTrue(not event.is_set())
+ self.tc.handshake_timeout = 0.000000000001
+ self.ts.start_server(event, server)
+ self.assertRaises(
+ EOFError,
+ self.tc.connect,
+ hostkey=public_host_key,
+ username="slowdive",
+ password="pygmalion",
+ )
+
+ def test_select_after_close(self):
+ """
+ verify that select works when a channel is already closed.
+ """
+ self.setup_test_server()
+ chan = self.tc.open_session()
+ chan.invoke_shell()
+ schan = self.ts.accept(1.0)
+ schan.close()
+
+ # give client a moment to receive close notification
+ time.sleep(0.1)
+
+ r, w, e = select.select([chan], [], [], 0.1)
+ self.assertEqual([chan], r)
+ self.assertEqual([], w)
+ self.assertEqual([], e)
+
+ def test_channel_send_misc(self):
+ """
+ verify behaviours sending various instances to a channel
+ """
+ self.setup_test_server()
+ text = "\xa7 slice me nicely"
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # TypeError raised on non string or buffer type
+ self.assertRaises(TypeError, chan.send, object())
+ self.assertRaises(TypeError, chan.sendall, object())
+
+ # sendall() accepts a unicode instance
+ chan.sendall(text)
+ expected = text.encode("utf-8")
+ self.assertEqual(sfile.read(len(expected)), expected)
+
+ @needs_builtin("buffer")
+ def test_channel_send_buffer(self):
+ """
+ verify sending buffer instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b"some test data\n whole"
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts buffer instances
+ sent = 0
+ while sent < len(data):
+ sent += chan.send(buffer(data, sent, 8)) # noqa
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a buffer instance
+ chan.sendall(buffer(data)) # noqa
+ self.assertEqual(sfile.read(len(data)), data)
+
+ @needs_builtin("memoryview")
+ def test_channel_send_memoryview(self):
+ """
+ verify sending memoryview instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b"some test data\n whole"
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts memoryview slices
+ sent = 0
+ view = memoryview(data)
+ while sent < len(view):
+ sent += chan.send(view[sent : sent + 8])
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a memoryview instance
+ chan.sendall(memoryview(data))
+ self.assertEqual(sfile.read(len(data)), data)
+
+ def test_server_rejects_open_channel_without_auth(self):
+ try:
+ self.setup_test_server(connect_kwargs={})
+ self.tc.open_session()
+ except ChannelException as e:
+ assert e.code == OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
+ else:
+ assert False, "Did not raise ChannelException!"
+
+ def test_server_rejects_arbitrary_global_request_without_auth(self):
+ self.setup_test_server(connect_kwargs={})
+ # NOTE: this dummy global request kind would normally pass muster
+ # from the test server.
+ self.tc.global_request("acceptable")
+ # Global requests never raise exceptions, even on failure (not sure why
+ # this was the original design...ugh.) Best we can do to tell failure
+ # happened is that the client transport's global_response was set back
+ # to None; if it had succeeded, it would be the response Message.
+ err = "Unauthed global response incorrectly succeeded!"
+ assert self.tc.global_response is None, err
+
+ def test_server_rejects_port_forward_without_auth(self):
+ # NOTE: at protocol level port forward requests are treated same as a
+ # regular global request, but Paramiko server implements a special-case
+ # method for it, so it gets its own test. (plus, THAT actually raises
+ # an exception on the client side, unlike the general case...)
+ self.setup_test_server(connect_kwargs={})
+ try:
+ self.tc.request_port_forward("localhost", 1234)
+ except SSHException as e:
+ assert "forwarding request denied" in str(e)
+ else:
+ assert False, "Did not raise SSHException!"
+
+ def _send_unimplemented(self, server_is_sender):
+ self.setup_test_server()
+ sender, recipient = self.tc, self.ts
+ if server_is_sender:
+ sender, recipient = self.ts, self.tc
+ recipient._send_message = Mock()
+ msg = Message()
+ msg.add_byte(cMSG_UNIMPLEMENTED)
+ sender._send_message(msg)
+ # TODO: I hate this but I literally don't see a good way to know when
+ # the recipient has received the sender's message (there are no
+ # existing threading events in play that work for this), esp in this
+ # case where we don't WANT a response (as otherwise we could
+ # potentially try blocking on the sender's receipt of a reply...maybe).
+ time.sleep(0.1)
+ assert not recipient._send_message.called
+
+ def test_server_does_not_respond_to_MSG_UNIMPLEMENTED(self):
+ self._send_unimplemented(server_is_sender=False)
+
+ def test_client_does_not_respond_to_MSG_UNIMPLEMENTED(self):
+ self._send_unimplemented(server_is_sender=True)
+
+ def _send_client_message(self, message_type):
+ self.setup_test_server(connect_kwargs={})
+ self.ts._send_message = Mock()
+ # NOTE: this isn't 100% realistic (most of these message types would
+ # have actual other fields in 'em) but it suffices to test the level of
+ # message dispatch we're interested in here.
+ msg = Message()
+ # TODO: really not liking the whole cMSG_XXX vs MSG_XXX duality right
+ # now, esp since the former is almost always just byte_chr(the
+ # latter)...but since that's the case...
+ msg.add_byte(byte_chr(message_type))
+ self.tc._send_message(msg)
+ # No good way to actually wait for server action (see above tests re:
+ # MSG_UNIMPLEMENTED). Grump.
+ time.sleep(0.1)
+
+ def _expect_unimplemented(self):
+ # Ensure MSG_UNIMPLEMENTED was sent (implies it hit end of loop instead
+ # of truly handling the given message).
+ # NOTE: When bug present, this will actually be the first thing that
+ # fails (since in many cases actual message handling doesn't involve
+ # sending a message back right away).
+ assert self.ts._send_message.call_count == 1
+ reply = self.ts._send_message.call_args[0][0]
+ reply.rewind() # Because it's pre-send, not post-receive
+ assert reply.get_byte() == cMSG_UNIMPLEMENTED
+
+ def test_server_transports_reject_client_message_types(self):
+ # TODO: handle Transport's own tables too, not just its inner auth
+ # handler's table. See TODOs in auth_handler.py
+ some_handler = self._auth_handler_class(self.tc)
+ for message_type in some_handler._client_handler_table:
+ self._send_client_message(message_type)
+ self._expect_unimplemented()
+ # Reset for rest of loop
+ self.tearDown()
+ self.setUp()
+
+ def test_server_rejects_client_MSG_USERAUTH_SUCCESS(self):
+ self._send_client_message(MSG_USERAUTH_SUCCESS)
+ # Sanity checks
+ assert not self.ts.authenticated
+ assert not self.ts.auth_handler.authenticated
+ # Real fix's behavior
+ self._expect_unimplemented()
+
+ def test_can_override_packetizer_used(self):
+ class MyPacketizer(Packetizer):
+ pass
+
+ # control case
+ assert Transport(sock=LoopSocket()).packetizer.__class__ is Packetizer
+ # overridden case
+ tweaked = Transport(sock=LoopSocket(), packetizer_class=MyPacketizer)
+ assert tweaked.packetizer.__class__ is MyPacketizer
+
+
+# TODO: for now this is purely a regression test. It needs actual tests of the
+# intentional new behavior too!
+class ServiceRequestingTransportTest(TransportTest):
+ _auth_handler_class = AuthOnlyHandler
+
+ def setUp(self):
+ # Copypasta (Transport init is load-bearing)
+ self.socks = LoopSocket()
+ self.sockc = LoopSocket()
+ self.sockc.link(self.socks)
+ # New class who dis
+ self.tc = ServiceRequestingTransport(self.sockc)
+ self.ts = ServiceRequestingTransport(self.socks)
+
+
+class AlgorithmDisablingTests(unittest.TestCase):
+ def test_preferred_lists_default_to_private_attribute_contents(self):
+ t = Transport(sock=Mock())
+ assert t.preferred_ciphers == t._preferred_ciphers
+ assert t.preferred_macs == t._preferred_macs
+ assert t.preferred_keys == tuple(
+ t._preferred_keys
+ + tuple(
+ "{}-cert-v01@openssh.com".format(x) for x in t._preferred_keys
+ )
+ )
+ assert t.preferred_kex == t._preferred_kex
+
+ def test_preferred_lists_filter_disabled_algorithms(self):
+ t = Transport(
+ sock=Mock(),
+ disabled_algorithms={
+ "ciphers": ["aes128-cbc"],
+ "macs": ["hmac-md5"],
+ "keys": ["ssh-dss"],
+ "kex": ["diffie-hellman-group14-sha256"],
+ },
+ )
+ assert "aes128-cbc" in t._preferred_ciphers
+ assert "aes128-cbc" not in t.preferred_ciphers
+ assert "hmac-md5" in t._preferred_macs
+ assert "hmac-md5" not in t.preferred_macs
+ assert "ssh-dss" in t._preferred_keys
+ assert "ssh-dss" not in t.preferred_keys
+ assert "ssh-dss-cert-v01@openssh.com" not in t.preferred_keys
+ assert "diffie-hellman-group14-sha256" in t._preferred_kex
+ assert "diffie-hellman-group14-sha256" not in t.preferred_kex
+
+ def test_implementation_refers_to_public_algo_lists(self):
+ t = Transport(
+ sock=Mock(),
+ disabled_algorithms={
+ "ciphers": ["aes128-cbc"],
+ "macs": ["hmac-md5"],
+ "keys": ["ssh-dss"],
+ "kex": ["diffie-hellman-group14-sha256"],
+ "compression": ["zlib"],
+ },
+ )
+ # Enable compression cuz otherwise disabling one option for it makes no
+ # sense...
+ t.use_compression(True)
+ # Effectively a random spot check, but kex init touches most/all of the
+ # algorithm lists so it's a good spot.
+ t._send_message = Mock()
+ t._send_kex_init()
+ # Cribbed from Transport._parse_kex_init, which didn't feel worth
+ # refactoring given all the vars involved :(
+ m = t._send_message.call_args[0][0]
+ m.rewind()
+ m.get_byte() # the msg type
+ m.get_bytes(16) # cookie, discarded
+ kexen = m.get_list()
+ server_keys = m.get_list()
+ ciphers = m.get_list()
+ m.get_list()
+ macs = m.get_list()
+ m.get_list()
+ compressions = m.get_list()
+ # OK, now we can actually check that our disabled algos were not
+ # included (as this message includes the full lists)
+ assert "aes128-cbc" not in ciphers
+ assert "hmac-md5" not in macs
+ assert "ssh-dss" not in server_keys
+ assert "diffie-hellman-group14-sha256" not in kexen
+ assert "zlib" not in compressions
+
+
+class TestSHA2SignatureKeyExchange(unittest.TestCase):
+ # NOTE: these all rely on the default server() hostkey being RSA
+ # NOTE: these rely on both sides being properly implemented re: agreed-upon
+ # hostkey during kex being what's actually used. Truly proving that eg
+ # SHA512 was used, is quite difficult w/o super gross hacks. However, there
+ # are new tests in test_pkey.py which use known signature blobs to prove
+ # the SHA2 family was in fact used!
+
+ @requires_sha1_signing
+ def test_base_case_ssh_rsa_still_used_as_fallback(self):
+ # Prove that ssh-rsa is used if either, or both, participants have SHA2
+ # algorithms disabled
+ for which in ("init", "client_init", "server_init"):
+ with server(**{which: _disable_sha2}) as (tc, _):
+ assert tc.host_key_type == "ssh-rsa"
+
+ def test_kex_with_sha2_512(self):
+ # It's the default!
+ with server() as (tc, _):
+ assert tc.host_key_type == "rsa-sha2-512"
+
+ def test_kex_with_sha2_256(self):
+ # No 512 -> you get 256
+ with server(
+ init=dict(disabled_algorithms=dict(keys=["rsa-sha2-512"]))
+ ) as (tc, _):
+ assert tc.host_key_type == "rsa-sha2-256"
+
+ def _incompatible_peers(self, client_init, server_init):
+ with server(
+ client_init=client_init, server_init=server_init, catch_error=True
+ ) as (tc, ts, err):
+ # If neither side blew up then that's bad!
+ assert err is not None
+ # If client side blew up first, it'll be straightforward
+ if isinstance(err, IncompatiblePeer):
+ pass
+ # If server side blew up first, client sees EOF & we need to check
+ # the server transport for its saved error (otherwise it can only
+ # appear in log output)
+ elif isinstance(err, EOFError):
+ assert ts.saved_exception is not None
+ assert isinstance(ts.saved_exception, IncompatiblePeer)
+ # If it was something else, welp
+ else:
+ raise err
+
+ def test_client_sha2_disabled_server_sha1_disabled_no_match(self):
+ self._incompatible_peers(
+ client_init=_disable_sha2, server_init=_disable_sha1
+ )
+
+ def test_client_sha1_disabled_server_sha2_disabled_no_match(self):
+ self._incompatible_peers(
+ client_init=_disable_sha1, server_init=_disable_sha2
+ )
+
+ def test_explicit_client_hostkey_not_limited(self):
+ # Be very explicit about the hostkey on BOTH ends,
+ # and ensure it still ends up choosing sha2-512.
+ # (This is a regression test vs previous implementation which overwrote
+ # the entire preferred-hostkeys structure when given an explicit key as
+ # a client.)
+ hostkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ connect = dict(
+ hostkey=hostkey, username="slowdive", password="pygmalion"
+ )
+ with server(hostkey=hostkey, connect=connect) as (tc, _):
+ assert tc.host_key_type == "rsa-sha2-512"
+
+
+class TestExtInfo(unittest.TestCase):
+ def test_ext_info_handshake_exposed_in_client_kexinit(self):
+ with server() as (tc, _):
+ # NOTE: this is latest KEXINIT /sent by us/ (Transport retains it)
+ kex = tc._get_latest_kex_init()
+ # flag in KexAlgorithms list
+ assert "ext-info-c" in kex["kex_algo_list"]
+ # data stored on Transport after hearing back from a compatible
+ # server (such as ourselves in server mode)
+ assert tc.server_extensions == {
+ "server-sig-algs": b"ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss" # noqa
+ }
+
+ def test_client_uses_server_sig_algs_for_pubkey_auth(self):
+ privkey = RSAKey.from_private_key_file(_support("rsa.key"))
+ with server(
+ pubkeys=[privkey],
+ connect=dict(pkey=privkey),
+ server_init=dict(
+ disabled_algorithms=dict(pubkeys=["rsa-sha2-512"])
+ ),
+ ) as (tc, _):
+ assert tc.is_authenticated()
+ # Client settled on 256 despite itself not having 512 disabled (and
+ # otherwise, 512 would have been earlier in the preferred list)
+ assert tc._agreed_pubkey_algorithm == "rsa-sha2-256"
+
+
+class BadSeqPacketizer(Packetizer):
+ def read_message(self):
+ cmd, msg = super().read_message()
+ # Only mess w/ seqno if kexinit.
+ if cmd is MSG_KEXINIT:
+ # NOTE: this is /only/ the copy of the seqno which gets
+ # transmitted up from Packetizer; it's not modifying
+ # Packetizer's own internal seqno. For these tests,
+ # modifying the latter isn't required, and is also harder
+ # to do w/o triggering MAC mismatches.
+ msg.seqno = 17 # arbitrary nonzero int
+ return cmd, msg
+
+
+class TestStrictKex:
+ def test_kex_algos_includes_kex_strict_c(self):
+ with server() as (tc, _):
+ kex = tc._get_latest_kex_init()
+ assert "kex-strict-c-v00@openssh.com" in kex["kex_algo_list"]
+
+ @mark.parametrize(
+ "server_active,client_active",
+ itertools.product([True, False], repeat=2),
+ )
+ def test_mode_agreement(self, server_active, client_active):
+ with server(
+ server_init=dict(strict_kex=server_active),
+ client_init=dict(strict_kex=client_active),
+ ) as (tc, ts):
+ if server_active and client_active:
+ assert tc.agreed_on_strict_kex is True
+ assert ts.agreed_on_strict_kex is True
+ else:
+ assert tc.agreed_on_strict_kex is False
+ assert ts.agreed_on_strict_kex is False
+
+ def test_mode_advertised_by_default(self):
+ # NOTE: no explicit strict_kex overrides...
+ with server() as (tc, ts):
+ assert all(
+ (
+ tc.advertise_strict_kex,
+ tc.agreed_on_strict_kex,
+ ts.advertise_strict_kex,
+ ts.agreed_on_strict_kex,
+ )
+ )
+
+ @mark.parametrize(
+ "ptype",
+ (
+ # "normal" but definitely out-of-order message
+ MSG_CHANNEL_OPEN,
+ # Normally ignored, but not in this case
+ MSG_IGNORE,
+ # Normally triggers debug parsing, but not in this case
+ MSG_DEBUG,
+ # Normally ignored, but...you get the idea
+ MSG_UNIMPLEMENTED,
+ # Not real, so would normally trigger us /sending/
+ # MSG_UNIMPLEMENTED, but...
+ MSG_FUGGEDABOUTIT,
+ ),
+ )
+ def test_MessageOrderError_non_kex_messages_in_initial_kex(self, ptype):
+ class AttackTransport(Transport):
+ # Easiest apparent spot on server side which is:
+ # - late enough for both ends to have handshook on strict mode
+ # - early enough to be in the window of opportunity for Terrapin
+ # attack; essentially during actual kex, when the engine is
+ # waiting for things like MSG_KEXECDH_REPLY (for eg curve25519).
+ def _negotiate_keys(self, m):
+ self.clear_to_send_lock.acquire()
+ try:
+ self.clear_to_send.clear()
+ finally:
+ self.clear_to_send_lock.release()
+ if self.local_kex_init is None:
+ # remote side wants to renegotiate
+ self._send_kex_init()
+ self._parse_kex_init(m)
+ # Here, we would normally kick over to kex_engine, but instead
+ # we want the server to send the OOO message.
+ m = Message()
+ m.add_byte(byte_chr(ptype))
+ # rest of packet unnecessary...
+ self._send_message(m)
+
+ with raises(MessageOrderError):
+ with server(server_transport_factory=AttackTransport) as (tc, _):
+ pass # above should run and except during connect()
+
+ def test_SSHException_raised_on_out_of_order_messages_when_not_strict(
+ self,
+ ):
+ # This is kind of dumb (either situation is still fatal!) but whatever,
+ # may as well be strict with our new strict flag...
+ with raises(SSHException) as info: # would be true either way, but
+ with server(
+ client_init=dict(strict_kex=False),
+ ) as (tc, _):
+ tc._expect_packet(MSG_KEXINIT)
+ tc.open_session()
+ assert info.type is SSHException # NOT MessageOrderError!
+
+ def test_error_not_raised_when_kexinit_not_seq_0_but_unstrict(self):
+ with server(
+ client_init=dict(
+ # Disable strict kex
+ strict_kex=False,
+ # Give our clientside a packetizer that sets all kexinit
+ # Message objects to have .seqno==17, which would trigger the
+ # new logic if we'd forgotten to wrap it in strict-kex check
+ packetizer_class=BadSeqPacketizer,
+ ),
+ ):
+ pass # kexinit happens at connect...
+
+ def test_MessageOrderError_raised_when_kexinit_not_seq_0_and_strict(self):
+ with raises(MessageOrderError):
+ with server(
+ # Give our clientside a packetizer that sets all kexinit
+ # Message objects to have .seqno==17, which should trigger the
+ # new logic (given we are NOT disabling strict-mode)
+ client_init=dict(packetizer_class=BadSeqPacketizer),
+ ):
+ pass # kexinit happens at connect...
+
+ def test_sequence_numbers_reset_on_newkeys_when_strict(self):
+ with server(defer=True) as (tc, ts):
+ # When in strict mode, these should all be zero or close to it
+ # (post-kexinit, pre-auth).
+ # Server->client will be 1 (EXT_INFO got sent after NEWKEYS)
+ assert tc.packetizer._Packetizer__sequence_number_in == 1
+ assert ts.packetizer._Packetizer__sequence_number_out == 1
+ # Client->server will be 0
+ assert tc.packetizer._Packetizer__sequence_number_out == 0
+ assert ts.packetizer._Packetizer__sequence_number_in == 0
+
+ def test_sequence_numbers_not_reset_on_newkeys_when_not_strict(self):
+ with server(defer=True, client_init=dict(strict_kex=False)) as (
+ tc,
+ ts,
+ ):
+ # When not in strict mode, these will all be ~3-4 or so
+ # (post-kexinit, pre-auth). Not encoding exact values as it will
+ # change anytime we mess with the test harness...
+ assert tc.packetizer._Packetizer__sequence_number_in != 0
+ assert tc.packetizer._Packetizer__sequence_number_out != 0
+ assert ts.packetizer._Packetizer__sequence_number_in != 0
+ assert ts.packetizer._Packetizer__sequence_number_out != 0
+
+ def test_sequence_number_rollover_detected(self):
+ class RolloverTransport(Transport):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # Induce an about-to-rollover seqno, such that it rolls over
+ # during initial kex.
+ setattr(
+ self.packetizer,
+ "_Packetizer__sequence_number_in",
+ sys.maxsize,
+ )
+ setattr(
+ self.packetizer,
+ "_Packetizer__sequence_number_out",
+ sys.maxsize,
+ )
+
+ with raises(
+ SSHException,
+ match=r"Sequence number rolled over during initial kex!",
+ ):
+ with server(
+ client_init=dict(
+ # Disable strict kex - this should happen always
+ strict_kex=False,
+ ),
+ # Transport which tickles its packetizer seqno's
+ transport_factory=RolloverTransport,
+ ):
+ pass # kexinit happens at connect...
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..a2a8224
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,136 @@
+# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
+#
+# 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 distributed 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Some unit tests for utility functions.
+"""
+
+from binascii import hexlify
+import os
+from hashlib import sha1
+import unittest
+
+import paramiko
+import paramiko.util
+from paramiko.util import safe_string
+
+
+test_hosts_file = """\
+secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\
+9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\
+D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc=
+happy.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\
+BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
+5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
+"""
+
+
+class UtilTest(unittest.TestCase):
+ def test_imports(self):
+ """
+ verify that all the classes can be imported from paramiko.
+ """
+ for name in (
+ "Agent",
+ "AgentKey",
+ "AuthenticationException",
+ "AuthFailure",
+ "AuthHandler",
+ "AuthResult",
+ "AuthSource",
+ "AuthStrategy",
+ "AutoAddPolicy",
+ "BadAuthenticationType",
+ "BufferedFile",
+ "Channel",
+ "ChannelException",
+ "ConfigParseError",
+ "CouldNotCanonicalize",
+ "DSSKey",
+ "HostKeys",
+ "InMemoryPrivateKey",
+ "Message",
+ "MissingHostKeyPolicy",
+ "NoneAuth",
+ "OnDiskPrivateKey",
+ "Password",
+ "PasswordRequiredException",
+ "PrivateKey",
+ "RSAKey",
+ "RejectPolicy",
+ "SFTP",
+ "SFTPAttributes",
+ "SFTPClient",
+ "SFTPError",
+ "SFTPFile",
+ "SFTPHandle",
+ "SFTPServer",
+ "SFTPServerInterface",
+ "SSHClient",
+ "SSHConfig",
+ "SSHConfigDict",
+ "SSHException",
+ "SecurityOptions",
+ "ServerInterface",
+ "SourceResult",
+ "SubsystemHandler",
+ "Transport",
+ "WarningPolicy",
+ "util",
+ ):
+ assert name in dir(paramiko)
+
+ def test_generate_key_bytes(self):
+ key_bytes = paramiko.util.generate_key_bytes(
+ sha1, b"ABCDEFGH", "This is my secret passphrase.", 64
+ )
+ hexy = "".join([f"{byte:02x}" for byte in key_bytes])
+ hexpected = "9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b" # noqa
+ assert hexy == hexpected
+
+ def test_host_keys(self):
+ with open("hostfile.temp", "w") as f:
+ f.write(test_hosts_file)
+ try:
+ hostdict = paramiko.util.load_host_keys("hostfile.temp")
+ assert 2 == len(hostdict)
+ assert 1 == len(list(hostdict.values())[0])
+ assert 1 == len(list(hostdict.values())[1])
+ fp = hexlify(
+ hostdict["secure.example.com"]["ssh-rsa"].get_fingerprint()
+ ).upper()
+ assert b"E6684DB30E109B67B70FF1DC5C7F1363" == fp
+ finally:
+ os.unlink("hostfile.temp")
+
+ def test_clamp_value(self):
+ assert 32768 == paramiko.util.clamp_value(32767, 32768, 32769)
+ assert 32767 == paramiko.util.clamp_value(32767, 32765, 32769)
+ assert 32769 == paramiko.util.clamp_value(32767, 32770, 32769)
+
+ def test_safe_string(self):
+ vanilla = b"vanilla"
+ has_bytes = b"has \7\3 bytes"
+ safe_vanilla = safe_string(vanilla)
+ safe_has_bytes = safe_string(has_bytes)
+ expected_bytes = b"has %07%03 bytes"
+ err = "{!r} != {!r}"
+ msg = err.format(safe_vanilla, vanilla)
+ assert safe_vanilla == vanilla, msg
+ msg = err.format(safe_has_bytes, expected_bytes)
+ assert safe_has_bytes == expected_bytes, msg