[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