Hello community,
here is the log from the commit of package python-asyncssh for openSUSE:Factory checked in at 2019-02-02 21:49:30
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asyncssh (Old)
and /work/SRC/openSUSE:Factory/.python-asyncssh.new.28833 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asyncssh"
Sat Feb 2 21:49:30 2019 rev:2 rq:670364 version:1.15.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asyncssh/python-asyncssh.changes 2018-11-10 16:55:27.672041169 +0100
+++ /work/SRC/openSUSE:Factory/.python-asyncssh.new.28833/python-asyncssh.changes 2019-02-02 21:49:36.119962485 +0100
@@ -1,0 +2,28 @@
+Thu Jan 31 13:08:53 UTC 2019 - Ondřej Súkup
+
+- update to 1.15.1
+ * Added callback-based host validation in SSHClient, allowing callers to decide
+ programmatically whether to trust server host keys and certificates rather
+ than having to provide a list of trusted values in advance.
+ * Changed SSH client code to only load the default known hosts file if if exists.
+ Previously an error was returned if a known_hosts value wasn't specified
+ and the default known_hosts file didn't exist. For host validate to work in
+ this case, verification callbacks must be implemented or other forms
+ of validation such as X.509 trusted CAs or GSS-based key exchange must be used.
+ * Fixed known hosts validation to completely disable certificate checks when
+ known_hosts is set to None.
+ * Switched curve25519 key exchange to use the PyCA implementation
+ * Added get_fingerprint() method to return a fingerprint of an SSHKey.
+ * Added the ability to pass keyword arguments provided in the scp() command
+ through to asyncssh.connect() calls it makes, allowing things like custom
+ credentials to be specified.
+ * Added support for a reuse_port argument in create_server().
+ * Added support for "soft" EOF when line editing in enabled
+ * Added support for the Windows 10 OpenSSH ssh-agent.
+ * Reworked scoped link-local IPv6 address normalization to work better on Linux systems.
+ * Fixed a problem preserving directory structure in recursive scp().
+ * Fixed SFTP chmod tests to avoid attempting to set the sticky bit on a plain file
+ * Updated note in SSHClientChannel's send_signal() documentation to reflect
+ that OpenSSH 7.9 and later should now support processing of signal messages.
+
+-------------------------------------------------------------------
Old:
----
asyncssh-1.14.0.tar.gz
New:
----
asyncssh-1.15.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-asyncssh.spec ++++++
--- /var/tmp/diff_new_pack.Ps7jCp/_old 2019-02-02 21:49:36.703961979 +0100
+++ /var/tmp/diff_new_pack.Ps7jCp/_new 2019-02-02 21:49:36.731961955 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-asyncssh
#
-# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -12,35 +12,36 @@
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.
-# Please submit bugfixes or comments via http://bugs.opensuse.org/
+# Please submit bugfixes or comments via https://bugs.opensuse.org/
+#
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-asyncssh
-Version: 1.14.0
+Version: 1.15.1
Release: 0
-License: EPL-2.0 OR GPL-2.0-or-later
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
-Url: http://asyncssh.timeheart.net
+License: EPL-2.0 OR GPL-2.0-or-later
Group: Development/Languages/Python
+Url: http://asyncssh.timeheart.net
Source: https://files.pythonhosted.org/packages/source/a/asyncssh/asyncssh-%{version}.tar.gz
-BuildRequires: python-rpm-macros
-BuildRequires: %{python_module setuptools}
BuildRequires: %{python_module bcrypt}
-BuildRequires: %{python_module libnacl}
+BuildRequires: %{python_module cryptography >= 1.5}
BuildRequires: %{python_module gssapi}
+BuildRequires: %{python_module libnacl}
BuildRequires: %{python_module pyOpenSSL}
+BuildRequires: %{python_module setuptools}
BuildRequires: %{python_module uvloop}
-BuildRequires: %{python_module cryptography >= 1.5}
BuildRequires: fdupes
-BuildRequires: openssl
BuildRequires: openssh
-Requires: python-cryptography >= 1.5
+BuildRequires: openssl
+BuildRequires: python-rpm-macros
Requires: python-bcrypt
+Requires: python-cryptography >= 1.5
+Requires: python-gssapi
Requires: python-libnacl
Requires: python-pyOpenSSL
-Requires: python-gssapi
Recommends: python-uvloop
BuildArch: noarch
@@ -65,7 +66,6 @@
%check
%python_exec setup.py test
-
%files %{python_files}
%license LICENSE COPYRIGHT
%doc README.rst
++++++ asyncssh-1.14.0.tar.gz -> asyncssh-1.15.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/PKG-INFO new/asyncssh-1.15.1/PKG-INFO
--- old/asyncssh-1.14.0/PKG-INFO 2018-09-08 23:46:35.000000000 +0200
+++ new/asyncssh-1.15.1/PKG-INFO 2019-01-21 22:30:22.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 1.14.0
+Version: 1.15.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
@@ -218,8 +218,8 @@
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
-Provides-Extra: pyOpenSSL
-Provides-Extra: bcrypt
Provides-Extra: pypiwin32
-Provides-Extra: libnacl
Provides-Extra: gssapi
+Provides-Extra: libnacl
+Provides-Extra: bcrypt
+Provides-Extra: pyOpenSSL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/agent.py new/asyncssh-1.15.1/asyncssh/agent.py
--- old/asyncssh-1.14.0/asyncssh/agent.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/agent.py 2018-11-29 06:11:31.000000000 +0100
@@ -452,6 +452,9 @@
def lock(self, passphrase):
"""Lock the agent using the specified passphrase
+ .. note:: The lock and unlock actions don't appear to be
+ supported on the Windows 10 OpenSSH agent.
+
:param passphrase:
The passphrase required to later unlock the agent
:type passphrase: `str`
@@ -474,6 +477,9 @@
def unlock(self, passphrase):
"""Unlock the agent using the specified passphrase
+ .. note:: The lock and unlock actions don't appear to be
+ supported on the Windows 10 OpenSSH agent.
+
:param passphrase:
The passphrase to use to unlock the agent
:type passphrase: `str`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/agent_win32.py new/asyncssh-1.15.1/asyncssh/agent_win32.py
--- old/asyncssh-1.14.0/asyncssh/agent_win32.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/agent_win32.py 2018-11-29 06:11:31.000000000 +0100
@@ -27,24 +27,35 @@
import ctypes
import ctypes.wintypes
import errno
-import mmapfile
-import win32api
-import win32con
-import win32ui
+import os
+
+try:
+ import mmapfile
+ import win32api
+ import win32con
+ import win32ui
+ _pywin32_available = True
+except ImportError:
+ _pywin32_available = False
_AGENT_COPYDATA_ID = 0x804e50ba
_AGENT_MAX_MSGLEN = 8192
_AGENT_NAME = 'Pageant'
+_DEFAULT_OPENSSH_PATH = r'\\.\pipe\openssh-ssh-agent'
+
def _find_agent_window():
"""Find and return the Pageant window"""
- try:
- return win32ui.FindWindow(_AGENT_NAME, _AGENT_NAME)
- except win32ui.error:
- raise OSError(errno.ENOENT, 'Agent not found') from None
+ if _pywin32_available:
+ try:
+ return win32ui.FindWindow(_AGENT_NAME, _AGENT_NAME)
+ except win32ui.error:
+ raise OSError(errno.ENOENT, 'Agent not found') from None
+ else:
+ raise OSError(errno.ENOENT, 'PyWin32 not installed') from None
class _CopyDataStruct(ctypes.Structure):
@@ -112,12 +123,55 @@
self._mapfile = None
+class _W10OpenSSHTransport:
+ """Transport to connect to OpenSSH agent on Windows 10"""
+
+ def __init__(self, agent_path):
+ self._agentfile = open(agent_path, 'r+b')
+
+ def write(self, data):
+ """Write request data to OpenSSH agent"""
+
+ self._agentfile.write(data)
+
+ @asyncio.coroutine
+ def readexactly(self, n):
+ """Read response data from OpenSSH agent"""
+
+ result = self._agentfile.read(n)
+
+ if len(result) != n:
+ raise asyncio.IncompleteReadError(result, n)
+
+ return result
+
+ def close(self):
+ """Close the connection to OpenSSH"""
+
+ if self._agentfile:
+ self._agentfile.close()
+ self._agentfile = None
+
+
@asyncio.coroutine
def open_agent(loop, agent_path):
- """Open a connection to the Pageant agent"""
+ """Open a connection to the Pageant or Windows 10 OpenSSH agent"""
# pylint: disable=unused-argument
- _find_agent_window()
- transport = _PageantTransport()
+ transport = None
+
+ if not agent_path:
+ agent_path = os.environ.get('SSH_AUTH_SOCK', None)
+
+ if not agent_path:
+ try:
+ _find_agent_window()
+ transport = _PageantTransport()
+ except OSError:
+ agent_path = _DEFAULT_OPENSSH_PATH
+
+ if not transport:
+ transport = _W10OpenSSHTransport(agent_path)
+
return transport, transport
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/channel.py new/asyncssh-1.15.1/asyncssh/channel.py
--- old/asyncssh-1.14.0/asyncssh/channel.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/channel.py 2018-11-29 06:11:31.000000000 +0100
@@ -1266,12 +1266,12 @@
process or service. Signal names should be as described in
section 6.10 of :rfc:`RFC 4254 <4254#section-6.10>`.
- .. note:: OpenSSH's SSH server implementation does not
- currently support this message, so attempts to
+ .. note:: OpenSSH's SSH server implementation prior to version
+ 7.9 does not support this message, so attempts to
use :meth:`send_signal`, :meth:`terminate`, or
- :meth:`kill` with an OpenSSH SSH server will
- end up being ignored. This is currently being
- tracked in OpenSSH `bug 1424`__.
+ :meth:`kill` with an older OpenSSH SSH server will
+ end up being ignored. This was tracked in OpenSSH
+ `bug 1424`__.
__ https://bugzilla.mindrot.org/show_bug.cgi?id=1424
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/client.py new/asyncssh-1.15.1/asyncssh/client.py
--- old/asyncssh-1.14.0/asyncssh/client.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/client.py 2019-01-21 20:44:11.000000000 +0100
@@ -107,6 +107,87 @@
pass # pragma: no cover
+ def validate_host_public_key(self, host, addr, port, key):
+ """Return whether key is an authorized key for this host
+
+ Server host key validation can be supported by passing known
+ host keys in the `known_hosts` argument of
+ :func:`create_connection`. However, for more flexibility
+ in matching on the allowed set of keys, this method can be
+ implemented by the application to do the matching itself. It
+ should return `True` if the specified key is a valid host key
+ for the server being connected to.
+
+ By default, this method returns `False` for all host keys.
+
+ .. note:: This function only needs to report whether the
+ public key provided is a valid key for this
+ host. If it is, AsyncSSH will verify that the
+ server possesses the corresponding private key
+ before allowing the validation to succeed.
+
+ :param host:
+ The hostname of the target host
+ :param addr:
+ The IP address of the target host
+ :param port:
+ The port number on the target host
+ :param key:
+ The public key sent by the server
+ :type host: `str`
+ :type addr: `str`
+ :type port: `int`
+ :type key: :class:`SSHKey` *public key*
+
+ :returns: A `bool` indicating if the specified key is a valid
+ key for the target host
+
+ """
+
+ return False # pragma: no cover
+
+ def validate_host_ca_key(self, host, addr, port, key):
+ """Return whether key is an authorized CA key for this host
+
+ Server host certificate validation can be supported by passing
+ known host CA keys in the `known_hosts` argument of
+ :func:`create_connection`. However, for more flexibility
+ in matching on the allowed set of keys, this method can be
+ implemented by the application to do the matching itself. It
+ should return `True` if the specified key is a valid certificate
+ authority key for the server being connected to.
+
+ By default, this method returns `False` for all CA keys.
+
+ .. note:: This function only needs to report whether the
+ public key provided is a valid CA key for this
+ host. If it is, AsyncSSH will verify that the
+ certificate is valid, that the host is one of
+ the valid principals for the certificate, and
+ that the server possesses the private key
+ corresponding to the public key in the certificate
+ before allowing the validation to succeed.
+
+ :param host:
+ The hostname of the target host
+ :param addr:
+ The IP address of the target host
+ :param port:
+ The port number on the target host
+ :param key:
+ The public key which signed the certificate sent by the server
+ :type host: `str`
+ :type addr: `str`
+ :type port: `int`
+ :type key: :class:`SSHKey` *public key*
+
+ :returns: A `bool` indicating if the specified key is a valid
+ CA key for the target host
+
+ """
+
+ return False # pragma: no cover
+
def auth_banner_received(self, msg, lang):
"""An incoming authentication banner was received
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/connection.py new/asyncssh-1.15.1/asyncssh/connection.py
--- old/asyncssh-1.14.0/asyncssh/connection.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/connection.py 2019-01-21 20:44:11.000000000 +0100
@@ -489,17 +489,19 @@
self._x509_trusted_subjects = trusted_x509_subjects
self._x509_revoked_subjects = revoked_x509_subjects
- def _validate_openssh_host_certificate(self, host, cert):
+ def _validate_openssh_host_certificate(self, host, addr, port, cert):
"""Validate an OpenSSH host certificate"""
- if cert.signing_key in self._revoked_host_keys:
- raise ValueError('Host CA key is revoked')
+ if self._trusted_ca_keys is not None:
+ if cert.signing_key in self._revoked_host_keys:
+ raise ValueError('Host CA key is revoked')
+
+ if cert.signing_key not in self._trusted_ca_keys and \
+ not self._owner.validate_host_ca_key(host, addr, port,
+ cert.signing_key):
+ raise ValueError('Host CA key is not trusted')
- if self._trusted_ca_keys is not None and \
- cert.signing_key not in self._trusted_ca_keys:
- raise ValueError('Host CA key is not trusted')
-
- cert.validate(CERT_TYPE_HOST, host)
+ cert.validate(CERT_TYPE_HOST, host)
return cert.key
@@ -530,7 +532,7 @@
return cert.key
- def _validate_host_key(self, host, key_data):
+ def _validate_host_key(self, host, addr, port, key_data):
"""Validate and return a trusted host key"""
try:
@@ -541,19 +543,22 @@
if cert.is_x509_chain:
return self._validate_x509_host_certificate_chain(host, cert)
else:
- return self._validate_openssh_host_certificate(host, cert)
+ return self._validate_openssh_host_certificate(host, addr,
+ port, cert)
try:
key = decode_ssh_public_key(key_data)
except KeyImportError:
pass
else:
- if key in self._revoked_host_keys:
- raise ValueError('Host key is revoked')
-
- if self._trusted_host_keys is not None and \
- key not in self._trusted_host_keys:
- raise ValueError('Host key is not trusted')
+ if self._trusted_host_keys is not None:
+ if key in self._revoked_host_keys:
+ raise ValueError('Host key is revoked')
+
+ if key not in self._trusted_host_keys and \
+ not self._owner.validate_host_public_key(host, addr,
+ port, key):
+ raise ValueError('Host key is not trusted')
return key
@@ -582,8 +587,6 @@
self._connection_made()
self._owner.connection_made(self)
self._send_version()
- except DisconnectError as exc:
- self._loop.call_soon(self.connection_lost, exc)
except Exception:
self._loop.call_soon(self.internal_error, sys.exc_info())
@@ -2230,8 +2233,14 @@
self._trusted_ca_keys = None
else:
if not self._known_hosts:
- self._known_hosts = os.path.join(os.path.expanduser('~'),
- '.ssh', 'known_hosts')
+ default_known_hosts = os.path.join(os.path.expanduser('~'),
+ '.ssh', 'known_hosts')
+
+ if (os.path.isfile(default_known_hosts) and
+ os.access(default_known_hosts, os.R_OK)):
+ self._known_hosts = default_known_hosts
+ else:
+ self._known_hosts = b''
port = self._port if self._port != _DEFAULT_PORT else None
@@ -2244,25 +2253,14 @@
self._server_host_key_algs = \
get_certificate_algs() + self._server_host_key_algs
- if self._x509_trusted_certs is not None:
- if self._x509_trusted_certs or self._x509_trusted_cert_paths:
- self._server_host_key_algs = \
- get_x509_certificate_algs() + \
- self._server_host_key_algs
-
if not self._server_host_key_algs:
- if self._known_hosts is None:
- self._server_host_key_algs = (get_certificate_algs() +
- get_public_key_algs())
-
- if self._x509_trusted_certs is not None:
- self._server_host_key_algs = \
- get_x509_certificate_algs() + self._server_host_key_algs
- elif self._gss:
- self._server_host_key_algs = [b'null']
- else:
- raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
- 'No trusted server host keys available')
+ self._server_host_key_algs = (get_certificate_algs() +
+ get_public_key_algs())
+
+ if self._x509_trusted_certs is not None:
+ if self._x509_trusted_certs or self._x509_trusted_cert_paths:
+ self._server_host_key_algs = \
+ get_x509_certificate_algs() + self._server_host_key_algs
self.logger.info('Connection to %s succeeded', (self._host, self._port))
@@ -2311,9 +2309,11 @@
"""Validate and return the server's host key"""
try:
- return self._validate_host_key(self._host, key_data)
+ return self._validate_host_key(self._host, self._peer_addr,
+ self._port, key_data)
except ValueError as exc:
- raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE, str(exc))
+ raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
+ str(exc)) from None
def try_next_auth(self):
"""Attempt client authentication using the next compatible method"""
@@ -3690,7 +3690,8 @@
def host_based_auth_supported(self):
"""Return whether or not host based authentication is supported"""
- return bool(self._known_client_hosts)
+ return (bool(self._known_client_hosts) or
+ self._owner.host_based_auth_supported())
@asyncio.coroutine
def validate_host_based_auth(self, username, key_data, client_host,
@@ -3711,11 +3712,13 @@
self.logger.info('Client host mismatch: received %s, '
'resolved %s', client_host, resolved_host)
- self._match_known_hosts(self._known_client_hosts, resolved_host,
- self._peer_addr, None)
+ if self._known_client_hosts:
+ self._match_known_hosts(self._known_client_hosts, resolved_host,
+ self._peer_addr, None)
try:
- key = self._validate_host_key(client_host, key_data)
+ key = self._validate_host_key(resolved_host, self._peer_addr,
+ self._peer_port, key_data)
except ValueError as exc:
self.logger.debug1('Invalid host key: %s', exc)
return False
@@ -5112,11 +5115,12 @@
@asyncio.coroutine
def create_server(server_factory, host=None, port=_DEFAULT_PORT, *,
loop=None, family=0, flags=socket.AI_PASSIVE, backlog=100,
- reuse_address=None, server_host_keys=None, passphrase=None,
- known_client_hosts=None, trust_client_host=False,
- authorized_client_keys=None, x509_trusted_certs=(),
- x509_trusted_cert_paths=(), x509_purposes='secureShellClient',
- gss_host=(), allow_pty=True, line_editor=True,
+ reuse_address=None, reuse_port=None, server_host_keys=None,
+ passphrase=None, known_client_hosts=None,
+ trust_client_host=False, authorized_client_keys=None,
+ x509_trusted_certs=(), x509_trusted_cert_paths=(),
+ x509_purposes='secureShellClient', gss_host=(),
+ allow_pty=True, line_editor=True,
line_history=_DEFAULT_LINE_HISTORY,
x11_forwarding=False, x11_auth_path=None,
agent_forwarding=True, process_factory=None,
@@ -5158,6 +5162,12 @@
Whether or not to reuse a local socket in the TIME_WAIT state
without waiting for its natural timeout to expire. If not
specified, this will be automatically set to `True` on UNIX.
+ :param reuse_port: (optional)
+ Whether or not to allow this socket to be bound to the same
+ port other existing sockets are bound to, so long as they all
+ set this flag when being created. If not specified, the
+ default is to not allow this. This option is not supported
+ on Windows.
:param server_host_keys: (optional)
A list of private keys and optional certificates which can be
used by the server as a host key. Either this argument or
@@ -5310,6 +5320,7 @@
:type flags: flags to pass to :meth:`getaddrinfo() `
:type backlog: `int`
:type reuse_address: `bool`
+ :type reuse_port: `bool`
:type server_host_keys: *see* :ref:`SpecifyingPrivateKeys`
:type passphrase: `str`
:type known_client_hosts: *see* :ref:`SpecifyingKnownHosts`
@@ -5411,7 +5422,8 @@
return (yield from loop.create_server(conn_factory, host, port,
family=family, flags=flags,
backlog=backlog,
- reuse_address=reuse_address))
+ reuse_address=reuse_address,
+ reuse_port=reuse_port))
@async_context_manager
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/crypto/curve25519.py new/asyncssh-1.15.1/asyncssh/crypto/curve25519.py
--- old/asyncssh-1.14.0/asyncssh/crypto/curve25519.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/crypto/curve25519.py 2019-01-21 20:44:11.000000000 +0100
@@ -20,47 +20,28 @@
"""Curve25519 key exchange handler primitives"""
-import ctypes
-import os
+from cryptography.exceptions import InternalError
+from cryptography.hazmat.primitives.asymmetric import x25519
-try:
- from libnacl import nacl
- _CURVE25519_BYTES = nacl.crypto_scalarmult_curve25519_bytes()
- _CURVE25519_SCALARBYTES = nacl.crypto_scalarmult_curve25519_scalarbytes()
+class Curve25519DH:
+ """Curve25519 Diffie Hellman implementation"""
- _curve25519 = nacl.crypto_scalarmult_curve25519
- _curve25519_base = nacl.crypto_scalarmult_curve25519_base
-except (ImportError, OSError, AttributeError): # pragma: no cover
- pass
-else:
- class Curve25519DH:
- """Curve25519 Diffie Hellman implementation"""
+ def __init__(self):
+ self._priv_key = x25519.X25519PrivateKey.generate()
- def __init__(self):
- self._private = os.urandom(_CURVE25519_SCALARBYTES)
+ def get_public(self):
+ """Return the public key to send in the handshake"""
- def get_public(self):
- """Return the public key to send in the handshake"""
+ return self._priv_key.public_key().public_bytes()
- public = ctypes.create_string_buffer(_CURVE25519_BYTES)
+ def get_shared(self, peer_public):
+ """Return the shared key from the peer's public key"""
- if _curve25519_base(public, self._private) != 0:
- # This error is never returned by libsodium
- raise ValueError('Curve25519 failed') # pragma: no cover
+ try:
+ peer_key = x25519.X25519PublicKey.from_public_bytes(peer_public)
+ except InternalError:
+ raise ValueError('Invalid curve25519 public key') from None
- return public.raw
-
- def get_shared(self, peer_public):
- """Return the shared key from the peer's public key"""
-
- if len(peer_public) != _CURVE25519_BYTES:
- raise AssertionError('Invalid curve25519 public key size')
-
- shared = ctypes.create_string_buffer(_CURVE25519_BYTES)
-
- if _curve25519(shared, self._private, peer_public) != 0:
- # This error is never returned by libsodium
- raise ValueError('Curve25519 failed') # pragma: no cover
-
- return int.from_bytes(shared.raw, 'big')
+ shared = self._priv_key.exchange(peer_key)
+ return int.from_bytes(shared, 'big')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/editor.py new/asyncssh-1.15.1/asyncssh/editor.py
--- old/asyncssh-1.14.0/asyncssh/editor.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/editor.py 2018-11-29 06:11:31.000000000 +0100
@@ -280,7 +280,7 @@
"""Erase character to the right, or send EOF if input line is empty"""
if not self._line:
- self._session.eof_received()
+ self._session.soft_eof_received()
else:
self._erase_right()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/kex_ecdh.py new/asyncssh-1.15.1/asyncssh/kex_ecdh.py
--- old/asyncssh-1.14.0/asyncssh/kex_ecdh.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/kex_ecdh.py 2019-01-21 20:44:11.000000000 +0100
@@ -83,7 +83,7 @@
try:
k = self._priv.get_shared(self._client_pub)
- except (AssertionError, ValueError):
+ except ValueError:
raise DisconnectError(DISC_PROTOCOL_ERROR,
'Invalid kex init msg') from None
@@ -113,7 +113,7 @@
try:
k = self._priv.get_shared(self._server_pub)
- except (AssertionError, ValueError):
+ except ValueError:
raise DisconnectError(DISC_PROTOCOL_ERROR,
'Invalid kex reply msg') from None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/misc.py new/asyncssh-1.15.1/asyncssh/misc.py
--- old/asyncssh-1.14.0/asyncssh/misc.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/misc.py 2018-11-29 06:11:31.000000000 +0100
@@ -100,17 +100,30 @@
"""Normalize scoped IP address
The ipaddress module doesn't handle scoped addresses properly,
- so we strip off the CIDR suffix here and normalize scoped IP
- addresses using socket.inet_pton before we pass them into
- ipaddress.
+ so we normalize scoped IP addresses using socket.getaddrinfo
+ before we pass them into ip_address/ip_network.
"""
- for family in (socket.AF_INET, socket.AF_INET6):
- try:
- return socket.inet_ntop(family, socket.inet_pton(family, addr))
- except (ValueError, socket.error):
- pass
+ try:
+ addrinfo = socket.getaddrinfo(addr, None, family=socket.AF_UNSPEC,
+ type=socket.SOCK_STREAM,
+ flags=socket.AI_NUMERICHOST)[0]
+ except socket.gaierror:
+ return addr
+
+ if addrinfo[0] == socket.AF_INET6:
+ sa = addrinfo[4]
+ addr = sa[0]
+
+ idx = addr.find('%')
+ if idx >= 0: # pragma: no cover
+ addr = addr[:idx]
+
+ ip = ipaddress.ip_address(addr)
+
+ if ip.is_link_local:
+ addr = str(ipaddress.ip_address(int(ip) | (sa[3] << 96)))
return addr
@@ -338,6 +351,18 @@
self.signal = signal
+class SoftEOFReceived(Exception):
+ """SSH soft EOF request received
+
+ This exception is raised on an SSH server stdin stream when the
+ client sends an EOF from within the line editor on the channel.
+
+ """
+
+ def __init__(self):
+ super().__init__('Soft EOF')
+
+
class TerminalSizeChanged(Exception):
"""SSH terminal size change notification received
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/public_key.py new/asyncssh-1.15.1/asyncssh/public_key.py
--- old/asyncssh-1.14.0/asyncssh/public_key.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/public_key.py 2019-01-21 20:44:11.000000000 +0100
@@ -22,6 +22,7 @@
import binascii
from datetime import datetime, timedelta
+from hashlib import md5, sha1, sha256, sha384, sha512
import os
from pathlib import Path, PurePath
import re
@@ -57,6 +58,9 @@
_DEFAULT_HOST_KEY_FILES = ('ssh_host_ed25519_key', 'ssh_host_ecdsa_key',
'ssh_host_rsa_key', 'ssh_host_dsa_key')
+_hashes = {'md5': md5, 'sha1': sha1, 'sha256': sha256,
+ 'sha384': sha384, 'sha512': sha512}
+
_public_key_algs = []
_certificate_algs = []
_x509_certificate_algs = []
@@ -294,6 +298,39 @@
self._comment = comment or None
+ def get_fingerprint(self, hash_name='sha256'):
+ """Get the fingerprint of this key
+
+ Available hashes include:
+
+ md5, sha1, sha256, sha384, sha512
+
+ :param hash_name: (optional)
+ The hash algorithm to use to construct the fingerprint.
+ :type hash_name: `str`
+
+ :returns: `str`
+
+ :raises: :exc:`ValueError` if the hash name is invalid
+
+ """
+
+ try:
+ hash_alg = _hashes[hash_name]
+ except KeyError:
+ raise ValueError('Unknown hash algorithm') from None
+
+ h = hash_alg(self.public_data)
+
+ if hash_name == 'md5':
+ fp = h.hexdigest()
+ fp_text = ':'.join(fp[i:i+2] for i in range(0, len(fp), 2))
+ else:
+ fp = h.digest()
+ fp_text = binascii.b2a_base64(fp).decode('ascii')[:-1].strip('=')
+
+ return hash_name.upper() + ':' + fp_text
+
def sign_der(self, data, sig_algorithm):
"""Abstract method to compute a DER-encoded signature"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/scp.py new/asyncssh-1.15.1/asyncssh/scp.py
--- old/asyncssh-1.14.0/asyncssh/scp.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/scp.py 2018-11-29 06:11:31.000000000 +0100
@@ -58,7 +58,7 @@
@asyncio.coroutine
-def _parse_path(path):
+def _parse_path(path, **kwargs):
"""Convert an SCP path into an SSHClientConnection and path"""
from . import connect
@@ -77,10 +77,10 @@
if isinstance(conn, (str, bytes)):
close_conn = True
- conn = yield from connect(conn)
+ conn = yield from connect(conn, **kwargs)
elif isinstance(conn, tuple):
close_conn = True
- conn = yield from connect(*conn)
+ conn = yield from connect(*conn, **kwargs)
else:
close_conn = False
@@ -552,6 +552,7 @@
self.send_ok()
elif action == b'E':
self.send_ok()
+ break
elif action in b'CD':
try:
attrs.permissions, size, name = _parse_cd_args(args)
@@ -757,7 +758,8 @@
@asyncio.coroutine
def scp(srcpaths, dstpath=None, *, preserve=False, recurse=False,
- block_size=SFTP_BLOCK_SIZE, progress_handler=None, error_handler=None):
+ block_size=SFTP_BLOCK_SIZE, progress_handler=None,
+ error_handler=None, **kwargs):
"""Copy files using SCP
This function is a coroutine which copies one or more files or
@@ -831,6 +833,10 @@
wants the copy to completely stop. Otherwise, after an error, the
copy will continue starting with the next file.
+ If any other keyword arguments are specified, they will be passed
+ to the AsyncSSH connect() call when attempting to open any new SSH
+ connections needed to perform the file transfer.
+
:param srcpaths:
The paths of the source files or directories to copy
:param dstpath: (optional)
@@ -863,11 +869,12 @@
must_be_dir = len(srcpaths) > 1
- dstconn, dstpath, close_dst = yield from _parse_path(dstpath)
+ dstconn, dstpath, close_dst = yield from _parse_path(dstpath, **kwargs)
try:
for srcpath in srcpaths:
- srcconn, srcpath, close_src = yield from _parse_path(srcpath)
+ srcconn, srcpath, close_src = yield from _parse_path(srcpath,
+ **kwargs)
try:
if srcconn and dstconn:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/server.py new/asyncssh-1.15.1/asyncssh/server.py
--- old/asyncssh-1.14.0/asyncssh/server.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/server.py 2019-01-21 20:44:11.000000000 +0100
@@ -27,7 +27,7 @@
Applications may subclass this when implementing an SSH server to
provide custom authentication and request handlers.
- THe method :meth:`begin_auth` can be overridden decide whether
+ The method :meth:`begin_auth` can be overridden decide whether
or not authentication is required, and additional callbacks are
provided for each form of authentication in cases where authentication
information is not provided in the call to :func:`create_server`.
@@ -172,6 +172,122 @@
host_domain = host_principal.rsplit('@')[-1]
return user_principal == username + '@' + host_domain
+ def host_based_auth_supported(self):
+ """Return whether or not host-based authentication is supported
+
+ This method should return `True` if client host-based
+ authentication is supported. Applications wishing to support
+ it must have this method return `True` and implement
+ :meth:`validate_host_public_key` and/or :meth:`validate_host_ca_key`
+ to return whether or not the key provided by the client is valid
+ for the client host being authenticated.
+
+ By default, it returns `False` indicating the client host
+ based authentication is not supported.
+
+ :returns: A `bool` indicating if host-based authentication is
+ supported or not
+
+ """
+
+ return False # pragma: no cover
+
+ def validate_host_public_key(self, client_host, client_addr,
+ client_port, key):
+ """Return whether key is an authorized host key for this client host
+
+ Host key based client authentication can be supported by
+ passing authorized host keys in the `known_client_hosts`
+ argument of :func:`create_server`. However, for more flexibility
+ in matching on the allowed set of keys, this method can be
+ implemented by the application to do the matching itself. It
+ should return `True` if the specified key is a valid host key
+ for the client host being authenticated.
+
+ This method may be called multiple times with different keys
+ provided by the client. Applications should precompute as
+ much as possible in the :meth:`begin_auth` method so that
+ this function can quickly return whether the key provided is
+ in the list.
+
+ By default, this method returns `False` for all client host keys.
+
+ .. note:: This function only needs to report whether the
+ public key provided is a valid key for this client
+ host. If it is, AsyncSSH will verify that the
+ client possesses the corresponding private key
+ before allowing the authentication to succeed.
+
+ :param client_host:
+ The hostname of the client host
+ :param client_addr:
+ The IP address of the client host
+ :param client_port:
+ The port number on the client host
+ :param key:
+ The host public key sent by the client
+ :type client_host: `str`
+ :type client_addr: `str`
+ :type client_port: `int`
+ :type key: :class:`SSHKey` *public key*
+
+ :returns: A `bool` indicating if the specified key is a valid
+ key for the client host being authenticated
+
+ """
+
+ return False # pragma: no cover
+
+ def validate_host_ca_key(self, client_host, client_addr,
+ client_port, key):
+ """Return whether key is an authorized CA key for this client host
+
+ Certificate based client host authentication can be
+ supported by passing authorized host CA keys in the
+ `known_client_hosts` argument of :func:`create_server`.
+ However, for more flexibility in matching on the allowed
+ set of keys, this method can be implemented by the application
+ to do the matching itself. It should return `True` if the
+ specified key is a valid certificate authority key for the
+ client host being authenticated.
+
+ This method may be called multiple times with different keys
+ provided by the client. Applications should precompute as
+ much as possible in the :meth:`begin_auth` method so that
+ this function can quickly return whether the key provided is
+ in the list.
+
+ By default, this method returns `False` for all CA keys.
+
+ .. note:: This function only needs to report whether the
+ public key provided is a valid CA key for this
+ client host. If it is, AsyncSSH will verify that
+ the certificate is valid, that the client host is
+ one of the valid principals for the certificate,
+ and that the client possesses the private key
+ corresponding to the public key in the certificate
+ before allowing the authentication to succeed.
+
+ :param client_host:
+ The hostname of the client host
+ :param client_addr:
+ The IP address of the client host
+ :param client_port:
+ The port number on the client host
+ :param key:
+ The public key which signed the certificate sent by the client
+ :type client_host: `str`
+ :type client_addr: `str`
+ :type client_port: `int`
+ :type key: :class:`SSHKey` *public key*
+
+ :returns: A `bool` indicating if the specified key is a valid
+ CA key for the client host being authenticated
+
+ """
+
+ return False # pragma: no cover
+
def validate_host_based_user(self, username, client_host, client_username):
"""Return whether remote host and user is authorized for this user
@@ -211,8 +327,9 @@
This method should return `True` if client public key
authentication is supported. Applications wishing to support
it must have this method return `True` and implement
- :meth:`validate_public_key` to return whether or not the key
- provided by the client is valid for the user being authenticated.
+ :meth:`validate_public_key` and/or :meth:`validate_ca_key`
+ to return whether or not the key provided by the client is
+ valid for the user being authenticated.
By default, it returns `False` indicating the client public
key authentication is not supported.
@@ -227,7 +344,7 @@
def validate_public_key(self, username, key):
"""Return whether key is an authorized client key for this user
- Basic key-based client authentication can be supported by
+ Key based client authentication can be supported by
passing authorized keys in the `authorized_client_keys`
argument of :func:`create_server`, or by calling
:meth:`set_authorized_keys
@@ -272,8 +389,8 @@
def validate_ca_key(self, username, key):
"""Return whether key is an authorized CA key for this user
- Basic key-based client authentication can be supported by
- passing authorized keys in the `authorized_client_keys`
+ Certificate based client authentication can be supported by
+ passing authorized CA keys in the `authorized_client_keys`
argument of :func:`create_server`, or by calling
:meth:`set_authorized_keys
` on the server
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/stream.py new/asyncssh-1.15.1/asyncssh/stream.py
--- old/asyncssh-1.14.0/asyncssh/stream.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/stream.py 2018-11-29 06:11:31.000000000 +0100
@@ -23,7 +23,8 @@
import asyncio
from .constants import EXTENDED_DATA_STDERR
-from .misc import BreakReceived, SignalReceived, TerminalSizeChanged
+from .misc import BreakReceived, SignalReceived
+from .misc import SoftEOFReceived, TerminalSizeChanged
from .misc import async_iterator, python35
from .session import SSHClientSession, SSHServerSession
from .session import SSHTCPSession, SSHUNIXSession
@@ -450,7 +451,13 @@
if data:
break
else:
- raise recv_buf.pop(0)
+ exc = recv_buf.pop(0)
+
+ if isinstance(exc, SoftEOFReceived):
+ n = 0
+ break
+ else:
+ raise exc
l = len(recv_buf[0])
if n > 0 and l > n:
@@ -468,7 +475,7 @@
continue
if n == 0 or (n > 0 and data and not exact) or \
- self._eof_received:
+ (n < 0 and recv_buf) or self._eof_received:
break
yield from self._block_read(datatype)
@@ -503,7 +510,12 @@
self._recv_buf_len -= buflen
raise asyncio.IncompleteReadError(buf, None)
else:
- raise recv_buf.pop(0)
+ exc = recv_buf.pop(0)
+
+ if isinstance(exc, SoftEOFReceived):
+ return buf
+ else:
+ raise exc
buf += recv_buf[curbuf]
start = max(buflen + 1 - seplen, 0)
@@ -629,6 +641,12 @@
self._recv_buf[None].append(SignalReceived(signal))
self._unblock_read(None)
+ def soft_eof_received(self):
+ """Handle an incoming soft EOF on the channel"""
+
+ self._recv_buf[None].append(SoftEOFReceived())
+ self._unblock_read(None)
+
def terminal_size_changed(self, width, height, pixwidth, pixheight):
"""Handle an incoming terminal size change on the channel"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh/version.py new/asyncssh-1.15.1/asyncssh/version.py
--- old/asyncssh-1.14.0/asyncssh/version.py 2018-09-08 23:12:08.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh/version.py 2019-01-21 20:44:11.000000000 +0100
@@ -26,4 +26,4 @@
__url__ = 'http://asyncssh.timeheart.net'
-__version__ = '1.14.0'
+__version__ = '1.15.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh.egg-info/PKG-INFO new/asyncssh-1.15.1/asyncssh.egg-info/PKG-INFO
--- old/asyncssh-1.14.0/asyncssh.egg-info/PKG-INFO 2018-09-08 23:46:35.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh.egg-info/PKG-INFO 2019-01-21 22:30:22.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 1.14.0
+Version: 1.15.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
@@ -218,8 +218,8 @@
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
-Provides-Extra: pyOpenSSL
-Provides-Extra: bcrypt
Provides-Extra: pypiwin32
-Provides-Extra: libnacl
Provides-Extra: gssapi
+Provides-Extra: libnacl
+Provides-Extra: bcrypt
+Provides-Extra: pyOpenSSL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/asyncssh.egg-info/requires.txt new/asyncssh-1.15.1/asyncssh.egg-info/requires.txt
--- old/asyncssh-1.14.0/asyncssh.egg-info/requires.txt 2018-09-08 23:46:35.000000000 +0200
+++ new/asyncssh-1.15.1/asyncssh.egg-info/requires.txt 2019-01-21 22:30:22.000000000 +0100
@@ -1,4 +1,4 @@
-cryptography>=1.5
+cryptography>=2.0
[bcrypt]
bcrypt>=3.1.3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/pylintrc new/asyncssh-1.15.1/pylintrc
--- old/asyncssh-1.14.0/pylintrc 2018-05-20 19:14:31.000000000 +0200
+++ new/asyncssh-1.15.1/pylintrc 2019-01-21 20:44:11.000000000 +0100
@@ -95,7 +95,7 @@
bad-functions=map,filter
# Good variable names which should always be accepted, separated by a comma
-good-names=a,av,b,c,ca,ch,cn,f,fs,g,h,i,ip,iv,j,k,l,n,r,s,sa,t,v,x,y,_
+good-names=a,av,b,c,ca,ch,cn,f,fp,fs,g,h,i,ip,iv,j,k,l,n,r,s,sa,t,v,x,y,_
# Bad variable names which should always be refused, separated by a comma
bad-names=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/setup.py new/asyncssh-1.15.1/setup.py
--- old/asyncssh-1.14.0/setup.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/setup.py 2019-01-21 20:44:11.000000000 +0100
@@ -51,7 +51,7 @@
description = doclines[0],
long_description = long_description,
platforms = 'Any',
- install_requires = ['cryptography >= 1.5'],
+ install_requires = ['cryptography >= 2.0'],
extras_require = {
'bcrypt': ['bcrypt >= 3.1.3'],
'gssapi': ['gssapi >= 1.2.0'],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/tests/test_connection.py new/asyncssh-1.15.1/tests/test_connection.py
--- old/asyncssh-1.14.0/tests/test_connection.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/tests/test_connection.py 2019-01-21 20:44:11.000000000 +0100
@@ -23,6 +23,7 @@
import asyncio
from copy import copy
import os
+import sys
import unittest
from unittest.mock import patch
@@ -148,6 +149,30 @@
return super().verify_and_decrypt(header, data + b'\xff', mac)
+class _ValidateHostKeyClient(asyncssh.SSHClient):
+ """Test server host key/CA validation callbacks"""
+
+ def __init__(self, host_key=None, ca_key=None):
+ self._host_key = \
+ asyncssh.read_public_key(host_key) if host_key else None
+ self._ca_key = \
+ asyncssh.read_public_key(ca_key) if ca_key else None
+
+ def validate_host_public_key(self, host, addr, port, key):
+ """Return whether key is an authorized key for this host"""
+
+ # pylint: disable=unused-argument
+
+ return key == self._host_key
+
+ def validate_host_ca_key(self, host, addr, port, key):
+ """Return whether key is an authorized CA key for this host"""
+
+ # pylint: disable=unused-argument
+
+ return key == self._ca_key
+
+
class _PreAuthRequestClient(asyncssh.SSHClient):
"""Test sending a request prior to auth complete"""
@@ -349,6 +374,33 @@
yield from asyncssh.listen(server_host_keys=['skey', 'skey'])
@asynctest
+ def test_known_hosts_not_present(self):
+ """Test connecting with default known hosts file not present"""
+
+ try:
+ os.rename(os.path.join('.ssh', 'known_hosts'),
+ os.path.join('.ssh', 'known_hosts.save'))
+
+ with self.assertRaises(asyncssh.DisconnectError):
+ yield from self.connect()
+ finally:
+ os.rename(os.path.join('.ssh', 'known_hosts.save'),
+ os.path.join('.ssh', 'known_hosts'))
+
+ @unittest.skipIf(sys.platform == 'win32', 'skip chmod tests on Windows')
+ @asynctest
+ def test_known_hosts_not_readable(self):
+ """Test connecting with default known hosts file not readable"""
+
+ try:
+ os.chmod(os.path.join('.ssh', 'known_hosts'), 0)
+
+ with self.assertRaises(asyncssh.DisconnectError):
+ yield from self.connect()
+ finally:
+ os.chmod(os.path.join('.ssh', 'known_hosts'), 0o644)
+
+ @asynctest
def test_known_hosts_none(self):
"""Test connecting with known hosts checking disabled"""
@@ -447,11 +499,19 @@
yield from conn.wait_closed()
@asynctest
- def test_untrusted_known_hosts_key(self):
- """Test untrusted server host key"""
+ def test_validate_host_ca_callback(self):
+ """Test callback to validate server CA key"""
- with self.assertRaises(asyncssh.DisconnectError):
- yield from self.connect(known_hosts=(['ckey.pub'], [], []))
+ def client_factory():
+ """Return an SSHClient which can validate the sevrer CA key"""
+
+ return _ValidateHostKeyClient(ca_key='skey.pub')
+
+ conn, _ = yield from self.create_connection(client_factory,
+ known_hosts=([], [], []))
+
+ conn.close()
+ yield from conn.wait_closed()
@asynctest
def test_untrusted_known_hosts_ca(self):
@@ -461,6 +521,32 @@
yield from self.connect(known_hosts=([], ['ckey.pub'], []))
@asynctest
+ def test_untrusted_host_key_callback(self):
+ """Test callback to validate server host key returning failure"""
+
+ def client_factory():
+ """Return an SSHClient which can validate the sevrer host key"""
+
+ return _ValidateHostKeyClient(host_key='ckey.pub')
+
+ with self.assertRaises(asyncssh.DisconnectError):
+ yield from self.create_connection(client_factory,
+ known_hosts=([], [], []))
+
+ @asynctest
+ def test_untrusted_host_ca_callback(self):
+ """Test callback to validate server CA key returning failure"""
+
+ def client_factory():
+ """Return an SSHClient which can validate the sevrer CA key"""
+
+ return _ValidateHostKeyClient(ca_key='ckey.pub')
+
+ with self.assertRaises(asyncssh.DisconnectError):
+ yield from self.create_connection(client_factory,
+ known_hosts=([], [], []))
+
+ @asynctest
def test_revoked_known_hosts_key(self):
"""Test revoked server host key"""
@@ -1257,6 +1343,48 @@
yield from self.connect()
+class _TestServerWithoutCert(ServerTestCase):
+ """Unit tests with a server that advertises a host key instead of a cert"""
+
+ @classmethod
+ @asyncio.coroutine
+ def start_server(cls):
+ """Start an SSH server to connect to"""
+
+ return (yield from cls.create_server(server_host_keys=[('skey', None)]))
+
+ @asynctest
+ def test_validate_host_key_callback(self):
+ """Test callback to validate server host key"""
+
+ def client_factory():
+ """Return an SSHClient which can validate the sevrer host key"""
+
+ return _ValidateHostKeyClient(host_key='skey.pub')
+
+ conn, _ = yield from self.create_connection(client_factory,
+ known_hosts=([], [], []))
+
+ conn.close()
+ yield from conn.wait_closed()
+
+ @asynctest
+ def test_untrusted_known_hosts_key(self):
+ """Test untrusted server host key"""
+
+ with self.assertRaises(asyncssh.DisconnectError):
+ yield from self.connect(known_hosts=(['ckey.pub'], [], []))
+
+ @asynctest
+ def test_known_hosts_none_with_key(self):
+ """Test disabled known hosts checking with server host key"""
+
+ with (yield from self.connect(known_hosts=None)) as conn:
+ pass
+
+ yield from conn.wait_closed()
+
+
class _TestServerInternalError(ServerTestCase):
"""Unit test for server internal error during auth"""
@@ -1310,6 +1438,15 @@
with self.assertRaises(asyncssh.DisconnectError):
yield from self.connect(known_hosts=([], ['skey.pub'], []))
+ @asynctest
+ def test_known_hosts_none_with_expired_cert(self):
+ """Test disabled known hosts checking with expired host certificate"""
+
+ with (yield from self.connect(known_hosts=None)) as conn:
+ pass
+
+ yield from conn.wait_closed()
+
class _TestCustomClientVersion(ServerTestCase):
"""Unit test for custom SSH client version"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/tests/test_connection_auth.py new/asyncssh-1.15.1/tests/test_connection_auth.py
--- old/asyncssh-1.14.0/tests/test_connection_auth.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/tests/test_connection_auth.py 2019-01-21 20:44:11.000000000 +0100
@@ -65,9 +65,39 @@
class _HostBasedServer(Server):
"""Server for testing host-based authentication"""
+ def __init__(self, host_key=None, ca_key=None):
+ super().__init__()
+
+ self._host_key = \
+ asyncssh.read_public_key(host_key) if host_key else None
+ self._ca_key = \
+ asyncssh.read_public_key(ca_key) if ca_key else None
+
+ def host_based_auth_supported(self):
+ """Return whether or not host based authentication is supported"""
+
+ return True
+
+ def validate_host_public_key(self, client_host, client_addr,
+ client_port, key):
+ """Return whether key is an authorized key for this host"""
+
+ # pylint: disable=unused-argument
+
+ return key == self._host_key
+
+ def validate_host_ca_key(self, client_host, client_addr, client_port, key):
+ """Return whether key is an authorized CA key for this host"""
+
+ # pylint: disable=unused-argument
+
+ return key == self._ca_key
+
def validate_host_based_user(self, username, client_host, client_username):
"""Return whether remote host and user is authorized for this user"""
+ # pylint: disable=unused-argument
+
return client_username == 'user'
@@ -655,6 +685,61 @@
client_username='user')
+@patch_getnameinfo
+class _TestCallbackHostBasedAuth(ServerTestCase):
+ """Unit tests for host-based authentication using callback"""
+
+ @classmethod
+ @asyncio.coroutine
+ def start_server(cls):
+ """Start an SSH server which supports host-based authentication"""
+
+ def server_factory():
+ """Return an SSHServer which can validate the client host key"""
+
+ return _HostBasedServer(host_key='skey.pub', ca_key='skey.pub')
+
+ return (yield from cls.create_server(server_factory))
+
+ @asynctest
+ def test_validate_client_host_callback(self):
+ """Test using callback to validate client host key"""
+
+ with (yield from self.connect(username='user',
+ client_host_keys=[('skey', None)],
+ client_username='user')) as conn:
+ pass
+
+ yield from conn.wait_closed()
+
+ @asynctest
+ def test_validate_client_host_ca_callback(self):
+ """Test using callback to validate client host CA key"""
+
+ with (yield from self.connect(username='user', client_host_keys='skey',
+ client_username='user')) as conn:
+ pass
+
+ yield from conn.wait_closed()
+
+ @asynctest
+ def test_untrusted_client_host_callback(self):
+ """Test callback to validate client host key returning failure"""
+
+ with self.assertRaises(asyncssh.DisconnectError):
+ yield from self.connect(username='user',
+ client_host_keys=[('ckey', None)],
+ client_username='user')
+
+ @asynctest
+ def test_untrusted_client_host_ca_callback(self):
+ """Test callback to validate client host CA key returning failure"""
+
+ with self.assertRaises(asyncssh.DisconnectError):
+ yield from self.connect(username='user', client_host_keys='ckey',
+ client_username='user')
+
+
@patch_getnameinfo
class _TestKeysignHostBasedAuth(ServerTestCase):
"""Unit tests for host-based authentication using ssh-keysign"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/tests/test_editor.py new/asyncssh-1.15.1/tests/test_editor.py
--- old/asyncssh-1.14.0/tests/test_editor.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/tests/test_editor.py 2018-11-29 06:11:31.000000000 +0100
@@ -61,6 +61,18 @@
stdout.close()
+def _handle_soft_eof(stdin, stdout, stderr):
+ """Accept input using read() and echo it back"""
+
+ # pylint: disable=unused-argument
+
+ while not stdin.at_eof():
+ data = yield from stdin.read()
+ stdout.write(data or 'EOF\n')
+
+ stdout.close()
+
+
class _CheckEditor(ServerTestCase):
"""Utility functions for AsyncSSH line editor unit tests"""
@@ -268,3 +280,37 @@
"""Test changing the terminal width"""
yield from self.check_input('abc\n', 'abc\n', set_width=True)
+
+
+class _TestEditorSoftEOF(_CheckEditor):
+ """Unit tests for AsyncSSH line editor sending soft EOF"""
+
+ @classmethod
+ @asyncio.coroutine
+ def start_server(cls):
+ """Start an SSH server for the tests to use"""
+
+ return (yield from cls.create_server(session_factory=_handle_soft_eof))
+
+ @asynctest
+ def test_editor_soft_eof(self):
+ """Test editor sending soft EOF"""
+
+ with (yield from self.connect()) as conn:
+ process = yield from conn.create_process(term_type='ansi')
+
+ process.stdin.write('\x04')
+
+ self.assertEqual((yield from process.stdout.readline()), 'EOF\r\n')
+
+ process.stdin.write('abc\n\x04')
+
+ self.assertEqual((yield from process.stdout.readline()), 'abc\r\n')
+ self.assertEqual((yield from process.stdout.readline()), 'abc\r\n')
+ self.assertEqual((yield from process.stdout.readline()), 'EOF\r\n')
+
+ process.stdin.write('abc\n')
+ process.stdin.write_eof()
+
+ self.assertEqual((yield from process.stdout.read()),
+ 'abc\r\nabc\r\n')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/tests/test_known_hosts.py new/asyncssh-1.15.1/tests/test_known_hosts.py
--- old/asyncssh-1.14.0/tests/test_known_hosts.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/tests/test_known_hosts.py 2018-11-29 06:11:31.000000000 +0100
@@ -209,6 +209,18 @@
for patlists in no_match:
self.check_hosts(patlists, ([], [], [], [], [], [], []))
+ def test_scoped_addr(self):
+ """Test match on scoped addresses"""
+
+ self.check_hosts((['fe80::1%1'], [], [], [], [], [], []),
+ ([0], [], [], [], [], [], []), addr='fe80::1%1')
+ self.check_hosts((['fe80::%1/64'], [], [], [], [], [], []),
+ ([0], [], [], [], [], [], []), addr='fe80::1%1')
+ self.check_hosts((['fe80::1%2'], [], [], [], [], [], []),
+ ([], [], [], [], [], [], []), addr='fe80::1%1')
+ self.check_hosts((['2001:2::%3/64'], [], [], [], [], [], []),
+ ([0], [], [], [], [], [], []), addr='2001:2::1')
+
def test_missing_key(self):
"""Test for line with missing key data"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/tests/test_public_key.py new/asyncssh-1.15.1/tests/test_public_key.py
--- old/asyncssh-1.14.0/tests/test_public_key.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/tests/test_public_key.py 2019-01-21 20:44:11.000000000 +0100
@@ -354,6 +354,16 @@
self.assertEqual(keylist[0], newkey)
self.assertEqual(keylist[1], newkey)
+ for hash_name in ('md5', 'sha1', 'sha256', 'sha384', 'sha512'):
+ fp = newkey.get_fingerprint(hash_name)
+
+ if _openssh_available: # pragma: no branch
+ keygen_fp = run('ssh-keygen -l -E %s -f sshpub' % hash_name)
+ self.assertEqual(fp, keygen_fp.decode('ascii').split()[1])
+
+ with self.assertRaises(ValueError):
+ newkey.get_fingerprint('xxx')
+
def check_certificate(self, cert_type, format_name):
"""Check for a certificate match"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.14.0/tests/test_sftp.py new/asyncssh-1.15.1/tests/test_sftp.py
--- old/asyncssh-1.14.0/tests/test_sftp.py 2018-09-08 23:11:28.000000000 +0200
+++ new/asyncssh-1.15.1/tests/test_sftp.py 2018-11-27 05:17:29.000000000 +0100
@@ -957,8 +957,8 @@
try:
self._create_file('file')
- yield from sftp.chmod('file', 0o1234)
- self.assertEqual(stat.S_IMODE(os.stat('file').st_mode), 0o1234)
+ yield from sftp.chmod('file', 0o4321)
+ self.assertEqual(stat.S_IMODE(os.stat('file').st_mode), 0o4321)
finally:
remove('file')