[Python-modules-commits] [python-asyncssh] 02/07: New upstream release.
Vincent Bernat
bernat at moszumanska.debian.org
Mon Sep 18 12:03:30 UTC 2017
This is an automated email from the git hooks/post-receive script.
bernat pushed a commit to branch debian/master
in repository python-asyncssh.
commit 8cff4f8fb2eb39bbe554dd63439520e477ce9455
Author: Vincent Bernat <bernat at debian.org>
Date: Mon Sep 18 12:04:27 2017 +0200
New upstream release.
---
.travis.yml | 2 +-
README.rst | 16 +-
asyncssh/__init__.py | 4 +-
asyncssh/asn1.py | 43 ++
asyncssh/auth_keys.py | 138 +++--
asyncssh/channel.py | 113 +++--
asyncssh/connection.py | 352 ++++++++++---
asyncssh/crypto/__init__.py | 9 +-
asyncssh/crypto/pyca/dsa.py | 27 +-
asyncssh/crypto/pyca/ec.py | 31 +-
asyncssh/crypto/pyca/misc.py | 25 +
asyncssh/crypto/pyca/rsa.py | 32 +-
asyncssh/crypto/pyca/x509.py | 354 +++++++++++++
asyncssh/dsa.py | 19 +-
asyncssh/ecdsa.py | 14 +-
asyncssh/ed25519.py | 8 +-
asyncssh/known_hosts.py | 109 +++-
asyncssh/process.py | 123 +++--
asyncssh/public_key.py | 1105 ++++++++++++++++++++++++++++++++---------
asyncssh/rsa.py | 22 +-
asyncssh/version.py | 2 +-
docs/api.rst | 239 ++++++---
docs/changes.rst | 29 ++
docs/conf.py | 2 +-
examples/show_environment.py | 7 +-
pylintrc | 2 +-
setup.py | 1 +
tests/server.py | 125 ++++-
tests/test_asn1.py | 8 +-
tests/test_auth_keys.py | 138 +++--
tests/test_connection.py | 185 ++++++-
tests/test_connection_auth.py | 282 ++++++++---
tests/test_known_hosts.py | 139 ++++--
tests/test_process.py | 40 +-
tests/test_public_key.py | 592 +++++++++++++++++++---
tests/test_sftp.py | 2 +-
tests/test_x509.py | 289 +++++++++++
tests/util.py | 6 +
tox.ini | 1 +
39 files changed, 3761 insertions(+), 874 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 16c130f..2042de3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -63,4 +63,4 @@ matrix:
- pyenv local 3.5.2
- pyenv rehash
-script: tox
+script: travis_wait 60 tox
diff --git a/README.rst b/README.rst
index 47b3925..df927fc 100644
--- a/README.rst
+++ b/README.rst
@@ -48,6 +48,9 @@ Features
* Password, public key, and keyboard-interactive user authentication methods
* Many types and formats of `public keys and certificates`__
+
+ * Including support for X.509 certificates as defined in RFC 6187
+
* Support for accessing keys managed by `ssh-agent`__ on UNIX systems
* Including agent forwarding support on both the client and the server
@@ -123,6 +126,9 @@ functionality:
* Install libnettle from http://www.lysator.liu.se/~nisse/nettle/
if you want support for UMAC cryptographic hashes.
+* Install pyOpenSSL from https://pypi.python.org/pypi/pyOpenSSL
+ if you want support for X.509 certificate authentication.
+
* Install pypiwin32 from https://pypi.python.org/pypi/pypiwin32
if you want support for using the Pageant agent or support for
GSSAPI key exchange and authentication on Windows.
@@ -133,19 +139,21 @@ easy to install any or all of these dependencies:
| bcrypt
| gssapi
| libnacl
+ | pyOpenSSL
| pypiwin32
-For example, to install bcrypt, gssapi, and libnacl on UNIX, you can run:
+For example, to install bcrypt, gssapi, libnacl, and pyOpenSSL on UNIX,
+you can run:
::
- pip install 'asyncssh[bcrypt,gssapi,libnacl]'
+ pip install 'asyncssh[bcrypt,gssapi,libnacl,pyOpenSSL]'
-To install bcrypt, libnacl, and pypiwin32 on Windows, you can run:
+To install bcrypt, libnacl, pyOpenSSL, and pypiwin32 on Windows, you can run:
::
- pip install 'asyncssh[bcrypt,libnacl,pypiwin32]'
+ pip install 'asyncssh[bcrypt,libnacl,pyOpenSSL,pypiwin32]'
Note that you will still need to manually install the libsodium library
listed above for libnacl to work correctly and/or libnettle for UMAC
diff --git a/asyncssh/__init__.py b/asyncssh/__init__.py
index 3e13b6b..264349c 100644
--- a/asyncssh/__init__.py
+++ b/asyncssh/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2013-2016 by Ron Frederick <ronf at timeheart.net>.
+# Copyright (c) 2013-2017 by Ron Frederick <ronf at timeheart.net>.
# All rights reserved.
#
# This program and the accompanying materials are made available under
@@ -60,7 +60,7 @@ from .public_key import import_public_key, import_certificate
from .public_key import read_private_key, read_public_key, read_certificate
from .public_key import read_private_key_list, read_public_key_list
from .public_key import read_certificate_list
-from .public_key import load_keypairs, load_public_keys
+from .public_key import load_keypairs, load_public_keys, load_certificates
from .scp import scp
diff --git a/asyncssh/asn1.py b/asyncssh/asn1.py
index f26654b..c724f29 100644
--- a/asyncssh/asn1.py
+++ b/asyncssh/asn1.py
@@ -43,6 +43,7 @@ OBJECT_IDENTIFIER = 0x06
UTF8_STRING = 0x0c
SEQUENCE = 0x10
SET = 0x11
+IA5_STRING = 0x16
# pylint: enable=bad-whitespace
@@ -457,6 +458,48 @@ class BitString:
return cls(content[1:], unused=content[0])
+ at DERTag(IA5_STRING)
+class IA5String:
+ """An ASCII string value"""
+
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return self.value
+
+ def __repr__(self):
+ return "IA5String('%s')" % self.value
+
+ def __eq__(self, other):
+ return isinstance(other, type(self)) and self.value == other.value
+
+ def __hash__(self):
+ return hash(self.value)
+
+ def encode(self):
+ """Encode a DER IA5 string"""
+
+ # ASN.1 defines this type as only containing ASCII characters, but
+ # some tools expecting ASN.1 allow IA5Strings to contain UTF-8
+ # characters, so we leave it up to the caller whether to resrict
+ # the data to plain ASCII or not.
+
+ if isinstance(self.value, str):
+ return self.value.encode('utf-8')
+ else:
+ return self.value
+
+ @classmethod
+ def decode(cls, constructed, content):
+ """Decode a DER IA5 string"""
+
+ if constructed:
+ raise ASN1DecodeError('IA5 STRING should not be constructed')
+
+ return cls(content.decode('utf-8'))
+
+
@DERTag(OBJECT_IDENTIFIER)
class ObjectIdentifier:
"""An object identifier (OID) value
diff --git a/asyncssh/auth_keys.py b/asyncssh/auth_keys.py
index 14e4438..ae963cc 100644
--- a/asyncssh/auth_keys.py
+++ b/asyncssh/auth_keys.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2015 by Ron Frederick <ronf at timeheart.net>.
+# Copyright (c) 2015-2017 by Ron Frederick <ronf at timeheart.net>.
# All rights reserved.
#
# This program and the accompanying materials are made available under
@@ -14,25 +14,66 @@
import socket
+try:
+ from .crypto import X509NamePattern
+ _x509_available = True
+except ImportError: # pragma: no cover
+ _x509_available = False
+
from .misc import ip_address
from .pattern import HostPatternList, WildcardPatternList
-from .public_key import import_public_key, KeyImportError
+from .public_key import KeyImportError, import_public_key
+from .public_key import import_certificate, import_certificate_subject
class _SSHAuthorizedKeyEntry:
"""An entry in an SSH authorized_keys list"""
def __init__(self, line):
+ self.key = None
+ self.cert = None
self.options = {}
try:
- self.key = import_public_key(line)
+ self._import_key_or_cert(line)
return
except KeyImportError:
pass
line = self._parse_options(line)
- self.key = import_public_key(line)
+ self._import_key_or_cert(line)
+
+ def _import_key_or_cert(self, line):
+ """Import key or certificate in this entry"""
+
+ try:
+ self.key = import_public_key(line)
+ return
+ except KeyImportError:
+ pass
+
+ try:
+ self.cert = import_certificate(line)
+
+ if ('cert-authority' in self.options and
+ self.cert.subject != self.cert.issuer):
+ raise ValueError('X.509 cert-authority entries must '
+ 'contain a root CA certificate')
+
+ return
+ except KeyImportError:
+ pass
+
+ if 'cert-authority' not in self.options:
+ try:
+ self.key = None
+ self.cert = None
+ self._add_subject('subject', import_certificate_subject(line))
+ return
+ except KeyImportError:
+ pass
+
+ raise KeyImportError('Unrecognized key, certificate, or subject')
def _set_string(self, option, value):
"""Set an option with a string value"""
@@ -73,12 +114,19 @@ class _SSHAuthorizedKeyEntry:
self.options.setdefault(option, []).append(WildcardPatternList(value))
+ def _add_subject(self, option, value):
+ """Add an X.509 subject pattern"""
+
+ if _x509_available: # pragma: no branch
+ self.options.setdefault(option, []).append(X509NamePattern(value))
+
_handlers = {
'command': _set_string,
'environment': _add_environment,
'from': _add_from,
'permitopen': _add_permitopen,
- 'principals': _add_principals
+ 'principals': _add_principals,
+ 'subject': _add_subject
}
def _add_option(self):
@@ -134,6 +182,37 @@ class _SSHAuthorizedKeyEntry:
return line[idx:].strip()
+ def match_options(self, client_addr, cert_principals, cert_subject=None):
+ """Match "from", "principals" and "subject" options in entry"""
+
+ from_patterns = self.options.get('from')
+
+ if from_patterns:
+ client_host, _ = socket.getnameinfo((client_addr, 0),
+ socket.NI_NUMERICSERV)
+ client_ip = ip_address(client_addr)
+
+ if not all(pattern.matches(client_host, client_addr, client_ip)
+ for pattern in from_patterns):
+ return False
+
+ principal_patterns = self.options.get('principals')
+
+ if cert_principals is not None and principal_patterns is not None:
+ if not all(any(pattern.matches(principal)
+ for principal in cert_principals)
+ for pattern in principal_patterns):
+ return False
+
+ subject_patterns = self.options.get('subject')
+
+ if cert_subject is not None and subject_patterns is not None:
+ if not all(pattern.matches(cert_subject)
+ for pattern in subject_patterns):
+ return False
+
+ return True
+
class SSHAuthorizedKeys:
"""An SSH authorized keys list"""
@@ -141,6 +220,7 @@ class SSHAuthorizedKeys:
def __init__(self, data):
self._user_entries = []
self._ca_entries = []
+ self._x509_entries = []
for line in data.splitlines():
line = line.strip()
@@ -152,42 +232,42 @@ class SSHAuthorizedKeys:
except KeyImportError:
continue
- if 'cert-authority' in entry.options:
- self._ca_entries.append(entry)
+ if entry.key:
+ if 'cert-authority' in entry.options:
+ self._ca_entries.append(entry)
+ else:
+ self._user_entries.append(entry)
else:
- self._user_entries.append(entry)
+ self._x509_entries.append(entry)
- if not self._user_entries and not self._ca_entries:
- raise ValueError('No valid keys found')
+ if (not self._user_entries and not self._ca_entries and
+ not self._x509_entries):
+ raise ValueError('No valid entries found')
def validate(self, key, client_addr, cert_principals=None, ca=False):
"""Return whether a public key or CA is valid for authentication"""
for entry in self._ca_entries if ca else self._user_entries:
- if entry.key != key:
- continue
-
- from_patterns = entry.options.get('from')
- if from_patterns is not None:
- client_host, _ = socket.getnameinfo((client_addr, 0),
- socket.NI_NUMERICSERV)
- client_ip = ip_address(client_addr)
+ if (entry.key == key and
+ entry.match_options(client_addr, cert_principals)):
+ return entry.options
- if not all(pattern.matches(client_host, client_addr, client_ip)
- for pattern in from_patterns):
- continue
+ return None
- principal_patterns = entry.options.get('principals')
- if cert_principals is not None and principal_patterns is not None:
- if not all(any(pattern.matches(principal)
- for principal in cert_principals)
- for pattern in principal_patterns):
- continue
+ def validate_x509(self, cert, client_addr):
+ """Return whether an X.509 certificate is valid for authentication"""
- return entry.options
+ for entry in self._x509_entries:
+ if (entry.cert and 'cert-authority' not in entry.options and
+ (cert.key != entry.cert.key or
+ cert.subject != entry.cert.subject)):
+ continue # pragma: no cover (work around bug in coverage tool)
- return None
+ if entry.match_options(client_addr, cert.user_principals,
+ cert.subject):
+ return entry.options, entry.cert
+ return None, None
def import_authorized_keys(data):
"""Import SSH authorized keys
diff --git a/asyncssh/channel.py b/asyncssh/channel.py
index a32080b..e9b4c03 100644
--- a/asyncssh/channel.py
+++ b/asyncssh/channel.py
@@ -54,6 +54,10 @@ class SSHChannel(SSHPacketHandler):
self._encoding = encoding
self._extra = {'connection': conn}
+ self._env = {}
+ self._command = None
+ self._subsystem = None
+
self._send_state = 'closed'
self._send_chan = None
self._send_window = None
@@ -812,6 +816,58 @@ class SSHChannel(SSHPacketHandler):
self._recv_paused = False
self._flush_recv_buf()
+ def get_environment(self):
+ """Return the environment for this session
+
+ This method returns the environment set by the client when
+ the session was opened. On the server, calls to this method
+ should only be made after :meth:`session_started
+ <SSHServerSession.session_started>` has been called on the
+ :class:`SSHServerSession`. When using the stream-based API,
+ calls to this can be made at any time after the handler
+ function has started up.
+
+ :returns: A dictionary containing the environment variables
+ set by the client
+
+ """
+
+ return self._env
+
+ def get_command(self):
+ """Return the command the client requested to execute, if any
+
+ This method returns the command the client requested to
+ execute when the session was opened, if any. If the client
+ did not request that a command be executed, this method
+ will return ``None``. On the server, alls to this method
+ should only be made after :meth:`session_started
+ <SSHServerSession.session_started>` has been called on the
+ :class:`SSHServerSession`. When using the stream-based API,
+ calls to this can be made at any time after the handler
+ function has started up.
+
+ """
+
+ return self._command
+
+ def get_subsystem(self):
+ """Return the subsystem the client requested to open, if any
+
+ This method returns the subsystem the client requested to
+ open when the session was opened, if any. If the client
+ did not request that a subsystem be opened, this method will
+ return ``None``. On the server, calls to this method should
+ only be made after :meth:`session_started
+ <SSHServerSession.session_started>` has been called on the
+ :class:`SSHServerSession`. When using the stream-based API,
+ calls to this can be made at any time after the handler
+ function has started up.
+
+ """
+
+ return self._subsystem
+
class SSHClientChannel(SSHChannel):
"""SSH client channel"""
@@ -846,6 +902,10 @@ class SSHClientChannel(SSHChannel):
self._session = session_factory()
self._session.connection_made(self)
+ self._env = env
+ self._command = command
+ self._subsystem = subsystem
+
for name, value in env.items():
self._send_request(b'env', String(str(name)), String(str(value)))
@@ -1080,12 +1140,11 @@ class SSHServerChannel(SSHChannel):
super().__init__(conn, loop, encoding, window, max_pktsize)
+ self._env = conn.get_key_option('environment', {})
+
self._allow_pty = allow_pty
self._line_editor = line_editor
self._line_history = line_history
- self._env = self._conn.get_key_option('environment', {})
- self._command = None
- self._subsystem = None
self._term_type = None
self._term_size = (0, 0, 0, 0)
self._term_modes = {}
@@ -1303,54 +1362,6 @@ class SSHServerChannel(SSHChannel):
return self._session.break_received(msec)
- def get_environment(self):
- """Return the environment for this session
-
- This method returns the environment set by the client
- when the session was opened. Calls to this method should
- only be made after :meth:`session_started
- <SSHServerSession.session_started>` has been called on
- the :class:`SSHServerSession`.
-
- :returns: A dictionary containing the environment variables
- set by the client
-
- """
-
- return self._env
-
- def get_command(self):
- """Return the command the client requested to execute, if any
-
- This method returns the command the client requested to
- execute when the session was opened, if any. If the client
- did not request that a command be executed, this method
- will return ``None``. Calls to this method should only be made
- after :meth:`session_started <SSHServerSession.session_started>`
- has been called on the :class:`SSHServerSession`. When using
- the stream-based API, calls to this can be made at any time
- after the handler function has started up.
-
- """
-
- return self._command
-
- def get_subsystem(self):
- """Return the subsystem the client requested to open, if any
-
- This method returns the subsystem the client requested to
- open when the session was opened, if any. If the client
- did not request that a subsystem be opened, this method will
- return ``None``. Calls to this method should only be made
- after :meth:`session_started <SSHServerSession.session_started>`
- has been called on the :class:`SSHServerSession`. When using
- the stream-based API, calls to this can be made at any time
- after the handler function has started up.
-
- """
-
- return self._subsystem
-
def get_terminal_type(self):
"""Return the terminal type for this session
diff --git a/asyncssh/connection.py b/asyncssh/connection.py
index 753ffc2..80987c2 100644
--- a/asyncssh/connection.py
+++ b/asyncssh/connection.py
@@ -87,9 +87,10 @@ from .packet import PacketDecodeError, SSHPacket, SSHPacketHandler
from .process import PIPE, SSHClientProcess, SSHServerProcess
from .public_key import CERT_TYPE_HOST, CERT_TYPE_USER, KeyImportError
-from .public_key import get_public_key_algs, get_certificate_algs
from .public_key import decode_ssh_public_key, decode_ssh_certificate
-from .public_key import load_keypairs
+from .public_key import get_public_key_algs, get_certificate_algs
+from .public_key import get_x509_certificate_algs
+from .public_key import load_keypairs, load_certificates
from .saslprep import saslprep, SASLPrepError
@@ -171,7 +172,8 @@ def _select_algs(alg_type, algs, possible_algs, none_value=None):
raise ValueError('No %s algorithms selected' % alg_type)
-def _validate_algs(kex_algs, enc_algs, mac_algs, cmp_algs, sig_algs):
+def _validate_algs(kex_algs, enc_algs, mac_algs, cmp_algs,
+ sig_algs, allow_x509):
"""Validate requested algorithms"""
kex_algs = _select_algs('key exchange', kex_algs, get_kex_algs())
@@ -179,7 +181,11 @@ def _validate_algs(kex_algs, enc_algs, mac_algs, cmp_algs, sig_algs):
mac_algs = _select_algs('MAC', mac_algs, get_mac_algs())
cmp_algs = _select_algs('compression', cmp_algs,
get_compression_algs(), b'none')
- sig_algs = _select_algs('signature', sig_algs, get_public_key_algs())
+
+ allowed_sig_algs = get_x509_certificate_algs() if allow_x509 else []
+ allowed_sig_algs = allowed_sig_algs + get_public_key_algs()
+
+ sig_algs = _select_algs('signature', sig_algs, allowed_sig_algs)
return kex_algs, enc_algs, mac_algs, cmp_algs, sig_algs
@@ -192,7 +198,6 @@ class SSHConnection(SSHPacketHandler):
rekey_bytes, rekey_seconds, server):
self._protocol_factory = protocol_factory
self._loop = loop
- self._tasks = set()
self._transport = None
self._peer_addr = None
self._owner = None
@@ -330,10 +335,6 @@ class SSHConnection(SSHPacketHandler):
def _cleanup(self, exc):
"""Clean up this connection"""
- if self._auth:
- self._auth.cancel()
- self._auth = None
-
for chan in list(self._channels.values()):
chan.process_connection_close(exc)
@@ -363,15 +364,12 @@ class SSHConnection(SSHPacketHandler):
self._loop.call_soon(self._cleanup, exc)
- @asyncio.coroutine
- def _run_task(self, coro):
- """Run an async task, catching and reporting any errors"""
-
- task = asyncio.Task.current_task(self._loop)
+ def _reap_task(self, task):
+ """Collect result of an async task, reporting errors"""
# pylint: disable=broad-except
try:
- yield from coro
+ task.result()
except asyncio.CancelledError:
pass
except DisconnectError as exc:
@@ -380,13 +378,11 @@ class SSHConnection(SSHPacketHandler):
except Exception:
self.internal_error()
- self._tasks.remove(task)
-
def create_task(self, coro):
"""Create an asynchronous task which catches and reports errors"""
- task = create_task(self._run_task(coro), loop=self._loop)
- self._tasks.add(task)
+ task = create_task(coro, loop=self._loop)
+ task.add_done_callback(self._reap_task)
return task
def is_client(self):
@@ -1605,9 +1601,6 @@ class SSHConnection(SSHPacketHandler):
yield from self._close_event.wait()
- yield from asyncio.gather(*self._tasks, return_exceptions=True,
- loop=self._loop)
-
def disconnect(self, code, reason, lang=DEFAULT_LANG):
"""Disconnect the SSH connection
@@ -1919,6 +1912,7 @@ class SSHClientConnection(SSHConnection):
def __init__(self, client_factory, loop, client_version, kex_algs,
encryption_algs, mac_algs, compression_algs, signature_algs,
rekey_bytes, rekey_seconds, host, port, known_hosts,
+ x509_trusted_certs, x509_trusted_cert_paths, x509_purposes,
username, password, client_keys, gss_host, gss_delegate_creds,
agent, agent_path, auth_waiter):
super().__init__(client_factory, loop, client_version, kex_algs,
@@ -1929,6 +1923,9 @@ class SSHClientConnection(SSHConnection):
self._host = host
self._port = port if port != _DEFAULT_PORT else None
self._known_hosts = known_hosts
+ self._x509_trusted_certs = x509_trusted_certs
+ self._x509_trusted_cert_paths = x509_trusted_cert_paths
+ self._x509_purposes = x509_purposes
self._username = saslprep(username)
self._password = password
self._client_keys = client_keys
@@ -1947,6 +1944,10 @@ class SSHClientConnection(SSHConnection):
self._server_ca_keys = set()
self._revoked_server_keys = set()
+ self._x509_revoked_certs = []
+ self._x509_trusted_subjects = []
+ self._x509_revoked_subjects = []
+
self._kbdint_password_auth = False
self._remote_listeners = {}
@@ -1958,8 +1959,6 @@ class SSHClientConnection(SSHConnection):
if self._known_hosts is None:
self._server_host_keys = None
self._server_ca_keys = None
- self._revoked_server_keys = None
- self._server_host_key_algs = None
else:
if not self._known_hosts:
self._known_hosts = os.path.join(os.path.expanduser('~'),
@@ -1968,26 +1967,46 @@ class SSHClientConnection(SSHConnection):
known_hosts = match_known_hosts(self._known_hosts, self._host,
self._peer_addr, self._port)
- server_host_keys, server_ca_keys, revoked_server_keys = known_hosts
+ server_host_keys, server_ca_keys, revoked_server_keys, \
+ server_x509_certs, revoked_x509_certs, \
+ server_x509_subjects, revoked_x509_subjects = known_hosts
self._server_host_keys = set()
self._server_host_key_algs = []
- self._server_ca_keys = set(server_ca_keys)
- if server_ca_keys:
- self._server_host_key_algs.extend(get_certificate_algs())
-
- self._revoked_server_keys = set(revoked_server_keys)
-
for key in server_host_keys:
self._server_host_keys.add(key)
if key.algorithm not in self._server_host_key_algs:
self._server_host_key_algs.extend(key.sig_algorithms)
+ if server_ca_keys:
+ self._server_host_key_algs = \
+ get_certificate_algs() + self._server_host_key_algs
+
+ self._server_ca_keys = set(server_ca_keys)
+ self._revoked_server_keys = set(revoked_server_keys)
+
+ if self._x509_trusted_certs is not None:
+ self._x509_trusted_certs = list(self._x509_trusted_certs)
+ self._x509_trusted_certs.extend(server_x509_certs)
+
+ 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._x509_revoked_certs = set(revoked_x509_certs)
+
+ self._x509_trusted_subjects = server_x509_subjects
+ self._x509_revoked_subjects = revoked_x509_subjects
+
if not self._server_host_key_algs:
if self._known_hosts is None:
- self._server_host_key_algs = (get_public_key_algs() +
- get_certificate_algs())
+ 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:
@@ -2016,6 +2035,61 @@ class SSHClientConnection(SSHConnection):
super()._cleanup(exc)
+ def _validate_server_openssh_certificate(self, cert):
+ """Validate the server's OpenSSH certificate"""
+
+ if cert.signing_key in self._revoked_server_keys:
+ raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
+ 'Revoked server CA key')
+
+ if self._server_ca_keys is not None and \
+ cert.signing_key not in self._server_ca_keys:
+ raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
+ 'Untrusted server CA key')
+
+ try:
+ cert.validate(CERT_TYPE_HOST, self._host)
+ except ValueError as exc:
+ raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
+ str(exc)) from None
+
+ return cert.key
+
+ def _validate_server_x509_certificate_chain(self, cert):
+ """Validate the server's X.509 certificate"""
+
+ if (self._x509_revoked_subjects and
+ any(pattern.matches(cert.subject)
+ for pattern in self._x509_revoked_subjects)):
+ raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
+ 'Revoked server X.509 subject name')
+
+ if (self._x509_trusted_subjects and
+ not any(pattern.matches(cert.subject)
+ for pattern in self._x509_trusted_subjects)):
+ raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
+ 'Untrusted server X.509 subject name')
+
+ try:
+ # Only validate hostname against X.509 certificate host
+ # principals when there are no X.509 trusted subject
+ # entries matched in known_hosts.
+ if self._x509_trusted_subjects:
+ host_principal = None
+ else:
+ host_principal = self._host
+
+ cert.validate_chain(self._x509_trusted_certs,
+ self._x509_trusted_cert_paths,
+ self._x509_revoked_certs,
+ self._x509_purposes,
+ host_principal=host_principal)
+ except ValueError as exc:
+ raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
+ str(exc)) from None
+
+ return cert.key
+
def validate_server_host_key(self, data):
"""Validate and return the server's host key"""
@@ -2024,31 +2098,17 @@ class SSHClientConnection(SSHConnection):
except KeyImportError:
pass
else:
- if self._revoked_server_keys is not None and \
- cert.signing_key in self._revoked_server_keys:
- raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
- 'Revoked server CA key')
-
- if self._server_ca_keys is not None and \
- cert.signing_key not in self._server_ca_keys:
- raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
- 'Untrusted server CA key')
-
- try:
- cert.validate(CERT_TYPE_HOST, self._host)
- except ValueError as exc:
- raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
- str(exc)) from None
-
- return cert.key
+ if cert.is_x509_chain:
+ return self._validate_server_x509_certificate_chain(cert)
+ else:
+ return self._validate_server_openssh_certificate(cert)
try:
key = decode_ssh_public_key(data)
except KeyImportError:
pass
else:
- if self._revoked_server_keys is not None and \
- key in self._revoked_server_keys:
+ if key in self._revoked_server_keys:
raise DisconnectError(DISC_HOST_KEY_NOT_VERIFYABLE,
'Revoked server host key')
@@ -3126,10 +3186,12 @@ class SSHServerConnection(SSHConnection):
def __init__(self, server_factory, loop, server_version, kex_algs,
encryption_algs, mac_algs, compression_algs, signature_algs,
rekey_bytes, rekey_seconds, server_host_keys,
- authorized_client_keys, gss_host, allow_pty, line_editor,
- line_history, x11_forwarding, x11_auth_path, agent_forwarding,
- process_factory, session_factory, session_encoding,
- sftp_factory, allow_scp, window, max_pktsize, login_timeout):
+ authorized_client_keys, x509_trusted_certs,
+ x509_trusted_cert_paths, x509_purposes, gss_host, allow_pty,
+ line_editor, line_history, x11_forwarding, x11_auth_path,
+ agent_forwarding, process_factory, session_factory,
+ session_encoding, sftp_factory, allow_scp, window,
+ max_pktsize, login_timeout):
super().__init__(server_factory, loop, server_version, kex_algs,
encryption_algs, mac_algs, compression_algs,
signature_algs, rekey_bytes, rekey_seconds,
@@ -3138,6 +3200,9 @@ class SSHServerConnection(SSHConnection):
self._server_host_keys = server_host_keys
self._server_host_key_algs = server_host_keys.keys()
self._client_keys = authorized_client_keys
+ self._x509_trusted_certs = x509_trusted_certs
+ self._x509_trusted_cert_paths = x509_trusted_cert_paths
+ self._x509_purposes = x509_purposes
self._allow_pty = allow_pty
self._line_editor = line_editor
self._line_history = line_history
@@ -3261,13 +3326,8 @@ class SSHServerConnection(SSHConnection):
return result
@asyncio.coroutine
- def _validate_client_certificate(self, username, key_data):
- """Validate a client certificate for the specified user"""
-
- try:
- cert = decode_ssh_certificate(key_data)
- except KeyImportError:
- return None
+ def _validate_openssh_certificate(self, username, cert):
+ """Validate an OpenSSH client certificate for the specified user"""
options = None
@@ -3308,6 +3368,52 @@ class SSHServerConnection(SSHConnection):
return cert.key
@asyncio.coroutine
+ def _validate_x509_certificate_chain(self, username, cert):
+ """Validate an X.509 client certificate for the specified user"""
+
+ if not self._client_keys:
+ return None
+
+ options, trusted_cert = \
+ self._client_keys.validate_x509(cert, self._peer_addr)
+
+ if options is None:
+ return None
+
+ self._key_options = options
+
+ if self.get_key_option('principals'):
+ username = None
+
+ if trusted_cert:
+ trusted_certs = self._x509_trusted_certs + [trusted_cert]
+ else:
+ trusted_certs = self._x509_trusted_certs
+
+ try:
+ cert.validate_chain(trusted_certs, self._x509_trusted_cert_paths,
+ None, self._x509_purposes,
+ user_principal=username)
+ except ValueError:
+ return None
+
+ return cert.key
+
+ @asyncio.coroutine
+ def _validate_client_certificate(self, username, key_data):
+ """Validate a client certificate for the specified user"""
+
+ try:
+ cert = decode_ssh_certificate(key_data)
+ except KeyImportError:
+ return None
+
+ if cert.is_x509_chain:
+ return self._validate_x509_certificate_chain(username, cert)
+ else:
+ return self._validate_openssh_certificate(username, cert)
+
+ @asyncio.coroutine
def _validate_client_public_key(self, username, key_data):
"""Validate a client public key for the specified user"""
@@ -4159,7 +4265,9 @@ class SSHServerConnection(SSHConnection):
@asyncio.coroutine
def create_connection(client_factory, host, port=_DEFAULT_PORT, *,
loop=None, tunnel=None, family=0, flags=0,
- local_addr=None, known_hosts=(), username=None,
+ local_addr=None, known_hosts=(), x509_trusted_certs=(),
+ x509_trusted_cert_paths=(),
+ x509_purposes='secureShellServer', username=None,
password=None, client_keys=(), passphrase=None,
gss_host=(), gss_delegate_creds=False,
agent_path=(), agent_forwarding=False,
@@ -4228,6 +4336,34 @@ def create_connection(client_factory, host, port=_DEFAULT_PORT, *,
the keys will be looked up in the file :file:`.ssh/known_hosts`.
If this is explicitly set to ``None``, server host key validation
will be disabled.
+ :param x509_trusted_certs: (optional)
+ A list of certificates which should be trusted for X.509 server
+ certificate authentication. If no trusted certificates are
+ specified, an attempt will be made to load them from the file
+ :file:`.ssh/ca-bundle.crt`. If this argument is explicitly set
+ to ``None``, X.509 server certificate authentication will not
+ be performed.
+
+ .. note:: X.509 certificates to trust can also be provided
+ through a :ref:`known_hosts <KnownHosts>` file
+ if they are converted into OpenSSH format.
+ This allows their trust to be limited to only
+ specific host names.
+ :param x509_trusted_cert_paths: (optional)
+ A list of path names to "hash directories" containing certificates
+ which should be trusted for X.509 server certificate authentication.
+ Each certificate should be in a separate file with a name of the
+ form *hash.number*, where *hash* is the OpenSSL hash value of the
+ certificate subject name and *number* is an integer counting up
+ from zero if multiple certificates have the same hash. If no
+ paths are specified, an attempt with be made to use the directory
+ :file:`.ssh/crt` as a certificate hash directory.
+ :param x509_purposes: (optional)
+ A list of purposes allowed in the ExtendedKeyUsage of a
+ certificate used for X.509 server certificate authentication,
+ defulting to 'secureShellServer'. If this argument is explicitly
+ set to ``None``, the server certificate's ExtendedKeyUsage will
+ not be checked.
:param str username: (optional)
Username to authenticate as on the server. If not specified,
the currently logged in user on the local machine will be used.
@@ -4305,6 +4441,9 @@ def create_connection(client_factory, host, port=_DEFAULT_PORT, *,
:type flags: flags to pass to :meth:`getaddrinfo() <socket.getaddrinfo>`
:type local_addr: tuple of str and int
:type known_hosts: *see* :ref:`SpecifyingKnownHosts`
+ :type x509_trusted_certs: *see* :ref:`SpecifyingCertificates`
... 6365 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-asyncssh.git
More information about the Python-modules-commits
mailing list