Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-asyncssh for openSUSE:Factory 
checked in at 2023-10-05 20:04:58
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asyncssh (Old)
 and      /work/SRC/openSUSE:Factory/.python-asyncssh.new.28202 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-asyncssh"

Thu Oct  5 20:04:58 2023 rev:24 rq:1115789 version:2.14.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asyncssh/python-asyncssh.changes  
2023-07-03 17:42:15.224530767 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-asyncssh.new.28202/python-asyncssh.changes   
    2023-10-05 20:06:09.181215623 +0200
@@ -1,0 +2,25 @@
+Thu Oct  5 09:42:35 UTC 2023 - Dirk Müller <dmuel...@suse.com>
+
+- update to 2.14.0:
+  * Added support for a new accept_handler argument when setting
+    up local port forwarding, allowing the client host and port to
+    be validated and/or logged for each new forwarded connection.
+  * Added an option to disable expensive RSA private key checks
+    when using OpenSSL 3.x. Functions that read private keys have
+    been modified to include a new unsafe_skip_rsa_key_validation
+    argument which can be used to avoid these additional checks,
+    if you are loading keys from a trusted source.
+  * Added host information into AsyncSSH exceptions when host key
+    validation fails, and a few other improvements related to
+    X.509 certificate validation errors.
+  * Fixed a regression which prevented keys loaded into an SSH
+    agent with a certificate from working correctly beginning in
+    AsyncSSH after version 2.5.0.
+  * Fixed an issue which was triggering an internal exception
+    when shutting down server sessions with the line editor enabled
+    which could cause some output to be lost on exit, especially when
+    running on Windows.
+  * Fixed a documentation error in SSHClientConnectionOptions and
+    SSHServerConnectionOptions.
+
+-------------------------------------------------------------------

Old:
----
  asyncssh-2.13.2.tar.gz

New:
----
  asyncssh-2.14.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-asyncssh.spec ++++++
--- /var/tmp/diff_new_pack.2czv4J/_old  2023-10-05 20:06:10.345257676 +0200
+++ /var/tmp/diff_new_pack.2czv4J/_new  2023-10-05 20:06:10.349257820 +0200
@@ -19,7 +19,7 @@
 %define skip_python2 1
 %define skip_python36 1
 Name:           python-asyncssh
-Version:        2.13.2
+Version:        2.14.0
 Release:        0
 Summary:        Asynchronous SSHv2 client and server library
 License:        EPL-2.0 OR GPL-2.0-or-later

++++++ asyncssh-2.13.2.tar.gz -> asyncssh-2.14.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/.github/workflows/run_tests.yml 
new/asyncssh-2.14.0/.github/workflows/run_tests.yml
--- old/asyncssh-2.13.2/.github/workflows/run_tests.yml 2022-12-27 
22:30:36.000000000 +0100
+++ new/asyncssh-2.14.0/.github/workflows/run_tests.yml 2023-10-01 
02:35:42.000000000 +0200
@@ -111,7 +111,7 @@
           V = sys.version_info
           p = platform.system().lower()
           subprocess.run(
-              ['tox', '-e', f'py{V.major}{V.minor}-{p}', '--', '-ra'],
+              ['tox', 'run', '-e', f'py{V.major}{V.minor}-{p}', '--', '-ra'],
               check=True)
 
       - name: Upload coverage data
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/PKG-INFO new/asyncssh-2.14.0/PKG-INFO
--- old/asyncssh-2.13.2/PKG-INFO        2023-06-22 05:08:52.759388400 +0200
+++ new/asyncssh-2.14.0/PKG-INFO        2023-10-01 03:06:55.014456500 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: asyncssh
-Version: 2.13.2
+Version: 2.14.0
 Summary: AsyncSSH: Asynchronous SSHv2 client and server library
 Home-page: http://asyncssh.timeheart.net
 Author: Ron Frederick
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/__init__.py 
new/asyncssh-2.14.0/asyncssh/__init__.py
--- old/asyncssh-2.13.2/asyncssh/__init__.py    2022-08-11 02:01:29.000000000 
+0200
+++ new/asyncssh-2.14.0/asyncssh/__init__.py    2023-10-01 02:35:42.000000000 
+0200
@@ -44,6 +44,7 @@
 
 from .connection import SSHAcceptor, SSHClientConnection, SSHServerConnection
 from .connection import SSHClientConnectionOptions, SSHServerConnectionOptions
+from .connection import SSHAcceptHandler
 from .connection import create_connection, create_server, connect, listen
 from .connection import connect_reverse, listen_reverse, get_server_host_key
 from .connection import get_server_auth_methods, run_client, run_server
@@ -86,6 +87,8 @@
 from .public_key import load_keypairs, load_public_keys, load_certificates
 from .public_key import load_resident_keys
 
+from .rsa import set_default_skip_rsa_key_validation
+
 from .scp import scp
 
 from .session import DataType, SSHClientSession, SSHServerSession
@@ -164,5 +167,5 @@
     'read_certificate_list', 'read_known_hosts', 'read_private_key',
     'read_private_key_list', 'read_public_key', 'read_public_key_list',
     'run_client', 'run_server', 'scp', 'set_debug_level', 'set_log_level',
-    'set_sftp_log_level',
+    'set_sftp_log_level', 'set_default_skip_rsa_key_validation',
 ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/agent.py 
new/asyncssh-2.14.0/asyncssh/agent.py
--- old/asyncssh-2.13.2/asyncssh/agent.py       2023-02-18 23:52:45.000000000 
+0100
+++ new/asyncssh-2.14.0/asyncssh/agent.py       2023-10-01 02:35:42.000000000 
+0200
@@ -149,6 +149,18 @@
         self._is_cert = is_cert
         self._flags = 0
 
+    @property
+    def has_cert(self) -> bool:
+        """ Return if this key pair has an associated cert"""
+
+        return self._is_cert
+
+    @property
+    def has_x509_chain(self) -> bool:
+        """ Return if this key pair has an associated X.509 cert chain"""
+
+        return False
+
     def set_certificate(self, cert: SSHCertificate) -> None:
         """Set certificate to use with this key"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/connection.py 
new/asyncssh-2.14.0/asyncssh/connection.py
--- old/asyncssh-2.13.2/asyncssh/connection.py  2023-06-22 04:57:39.000000000 
+0200
+++ new/asyncssh-2.14.0/asyncssh/connection.py  2023-10-01 02:35:42.000000000 
+0200
@@ -233,6 +233,7 @@
 
 _VersionArg = DefTuple[BytesOrStr]
 
+SSHAcceptHandler = Callable[[str, int], MaybeAwait[bool]]
 
 # SSH service names
 _USERAUTH_SERVICE = b'ssh-userauth'
@@ -2886,10 +2887,10 @@
         return SSHForwarder(cast(SSHForwarder, peer))
 
     @async_context_manager
-    async def forward_local_port(self, listen_host: str,
-                                 listen_port: int,
-                                 dest_host: str,
-                                 dest_port: int) -> SSHListener:
+    async def forward_local_port(
+            self, listen_host: str, listen_port: int,
+            dest_host: str, dest_port: int,
+            accept_handler: Optional[SSHAcceptHandler] = None) -> SSHListener:
         """Set up local port forwarding
 
            This method is a coroutine which attempts to set up port
@@ -2906,10 +2907,17 @@
                The hostname or address to forward the connections to
            :param dest_port:
                The port number to forward the connections to
+           :param accept_handler:
+               A `callable` or coroutine which takes arguments of the
+               original host and port of the client and decides whether
+               or not to allow connection forwarding, returning `True` to
+               accept the connection and begin forwarding or `False` to
+               reject and close it.
            :type listen_host: `str`
            :type listen_port: `int`
            :type dest_host: `str`
            :type dest_port: `int`
+           :type accept_handler: `callable` or coroutine
 
            :returns: :class:`SSHListener`
 
@@ -2923,6 +2931,21 @@
                     Tuple[SSHTCPChannel[bytes], SSHTCPSession[bytes]]:
             """Forward a local connection over SSH"""
 
+            if accept_handler:
+                result = accept_handler(orig_host, orig_port)
+
+                if inspect.isawaitable(result):
+                    result = await cast(Awaitable[bool], result)
+
+                if not result:
+                    self.logger.info('Request for TCP forwarding from '
+                                     '%s to %s denied by application',
+                                     (orig_host, orig_port),
+                                     (dest_host, dest_port))
+
+                    raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED,
+                                           'Connection forwarding denied')
+
             return (await self.create_connection(session_factory,
                                                  dest_host, dest_port,
                                                  orig_host, orig_port))
@@ -3240,7 +3263,12 @@
                 self._host_key_alias or self._host,
                 self._peer_addr, self._port, key_data)
         except ValueError as exc:
-            raise HostKeyNotVerifiable(str(exc)) from None
+            host = self._host
+
+            if self._host_key_alias:
+                host += f' with alias {self._host_key_alias}'
+
+            raise HostKeyNotVerifiable(f'{exc} for host {host}') from None
 
         self._server_host_key = host_key
         return host_key
@@ -3288,7 +3316,9 @@
 
         self.logger.info('Auth failed for user %s', self._username)
 
-        self._force_close(PermissionDenied('Permission denied'))
+        self._force_close(PermissionDenied('Permission denied for user '
+                                           f'{self._username} on host '
+                                           f'{self._host}'))
 
     def gss_kex_auth_requested(self) -> bool:
         """Return whether to allow GSS key exchange authentication or not"""
@@ -4688,9 +4718,9 @@
                                     **kwargs) # type: ignore
 
     @async_context_manager
-    async def forward_local_port_to_path(self, listen_host: str,
-                                         listen_port: int,
-                                         dest_path: str) -> SSHListener:
+    async def forward_local_port_to_path(
+            self, listen_host: str, listen_port: int, dest_path: str,
+            accept_handler: Optional[SSHAcceptHandler] = None) -> SSHListener:
         """Set up local TCP port forwarding to a remote UNIX domain socket
 
            This method is a coroutine which attempts to set up port
@@ -4705,9 +4735,16 @@
                The port number on the local host to listen on
            :param dest_path:
                The path on the remote host to forward the connections to
+           :param accept_handler:
+               A `callable` or coroutine which takes arguments of the
+               original host and port of the client and decides whether
+               or not to allow connection forwarding, returning `True` to
+               accept the connection and begin forwarding or `False` to
+               reject and close it.
            :type listen_host: `str`
            :type listen_port: `int`
            :type dest_path: `str`
+           :type accept_handler: `callable` or coroutine
 
            :returns: :class:`SSHListener`
 
@@ -4717,10 +4754,24 @@
 
         async def tunnel_connection(
                 session_factory: SSHUNIXSessionFactory[bytes],
-                _orig_host: str, _orig_port: int) -> \
+                orig_host: str, orig_port: int) -> \
                     Tuple[SSHUNIXChannel[bytes], SSHUNIXSession[bytes]]:
             """Forward a local connection over SSH"""
 
+            if accept_handler:
+                result = accept_handler(orig_host, orig_port)
+
+                if inspect.isawaitable(result):
+                    result = await cast(Awaitable[bool], result)
+
+                if not result:
+                    self.logger.info('Request for TCP forwarding from '
+                                     '%s to %s denied by application',
+                                     (orig_host, orig_port), dest_path)
+
+                    raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED,
+                                           'Connection forwarding denied')
+
             return (await self.create_unix_connection(session_factory,
                                                       dest_path))
 
@@ -5730,6 +5781,10 @@
             if listener is True:
                 listener = await self.forward_local_port(
                     listen_host, listen_port, listen_host, listen_port)
+            elif callable(listener):
+                listener = await self.forward_local_port(
+                    listen_host, listen_port,
+                    listen_host, listen_port, listener)
         except OSError:
             self.logger.debug1('Failed to create TCP listener')
             self._report_global_response(False)
@@ -6548,7 +6603,8 @@
         if x509_trusted_cert_paths:
             for path in x509_trusted_cert_paths:
                 if not Path(path).is_dir():
-                    raise ValueError('Path not a directory: ' + str(path))
+                    raise ValueError('X.509 trusted certificate path not '
+                                     f'a directory: {path}')
 
         self.x509_trusted_certs = x509_trusted_certs
         self.x509_trusted_cert_paths = x509_trusted_cert_paths
@@ -6972,7 +7028,7 @@
            build up a configuration. When an option is not explicitly
            specified, its value will be pulled from this options object
            (if present) before falling back to the default value.
-       :type client_factory: `callable` returning :class:`SSHClientConnection`
+       :type client_factory: `callable` returning :class:`SSHClient`
        :type proxy_command: `str` or `list` of `str`
        :type known_hosts: *see* :ref:`SpecifyingKnownHosts`
        :type host_key_alias: `str`
@@ -7621,7 +7677,7 @@
            build up a configuration. When an option is not explicitly
            specified, its value will be pulled from this options object
            (if present) before falling back to the default value.
-       :type server_factory: `callable` returning :class:`SSHServerConnection`
+       :type server_factory: `callable` returning :class:`SSHServer`
        :type proxy_command: `str` or `list` of `str`
        :type family: `socket.AF_UNSPEC`, `socket.AF_INET`, or `socket.AF_INET6`
        :type server_host_keys: *see* :ref:`SpecifyingPrivateKeys`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/crypto/rsa.py 
new/asyncssh-2.14.0/asyncssh/crypto/rsa.py
--- old/asyncssh-2.13.2/asyncssh/crypto/rsa.py  2023-02-18 23:52:45.000000000 
+0100
+++ new/asyncssh-2.14.0/asyncssh/crypto/rsa.py  2023-10-01 02:35:42.000000000 
+0200
@@ -98,12 +98,14 @@
 
     @classmethod
     def construct(cls, n: int, e: int, d: int, p: int, q: int,
-                  dmp1: int, dmq1: int, iqmp: int) -> 'RSAPrivateKey':
+                  dmp1: int, dmq1: int, iqmp: int,
+                  skip_validation: bool) -> 'RSAPrivateKey':
         """Construct an RSA private key"""
 
         pub = rsa.RSAPublicNumbers(e, n)
         priv = rsa.RSAPrivateNumbers(p, q, d, dmp1, dmq1, iqmp, pub)
-        priv_key = priv.private_key()
+        priv_key = priv.private_key(
+            unsafe_skip_rsa_key_validation=skip_validation)
 
         return cls(priv_key, pub, priv)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/crypto/x509.py 
new/asyncssh-2.14.0/asyncssh/crypto/x509.py
--- old/asyncssh-2.13.2/asyncssh/crypto/x509.py 2023-06-22 04:57:39.000000000 
+0200
+++ new/asyncssh-2.14.0/asyncssh/crypto/x509.py 2023-10-01 02:35:42.000000000 
+0200
@@ -308,7 +308,7 @@
                                                None)
             x509_ctx.verify_certificate()
         except crypto.X509StoreContextError as exc:
-            raise ValueError(str(exc)) from None
+            raise ValueError(f'X.509 chain validation error: {exc}') from None
 
 
 def generate_x509_certificate(signing_key: PyCAKey, key: PyCAKey,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/editor.py 
new/asyncssh-2.14.0/asyncssh/editor.py
--- old/asyncssh-2.13.2/asyncssh/editor.py      2023-06-22 04:57:39.000000000 
+0200
+++ new/asyncssh-2.14.0/asyncssh/editor.py      2023-10-01 02:35:42.000000000 
+0200
@@ -704,8 +704,10 @@
                     self._ring_bell()
 
             self._bell_rung = False
-            self._chan.write(''.join(self._outbuf))
-            self._outbuf.clear()
+
+            if self._outbuf:
+                self._chan.write(''.join(self._outbuf))
+                self._outbuf.clear()
         else:
             self._session.data_received(data, datatype)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/public_key.py 
new/asyncssh-2.14.0/asyncssh/public_key.py
--- old/asyncssh-2.13.2/asyncssh/public_key.py  2023-06-22 04:57:39.000000000 
+0200
+++ new/asyncssh-2.14.0/asyncssh/public_key.py  2023-10-01 02:35:42.000000000 
+0200
@@ -262,13 +262,13 @@
 
     @classmethod
     def make_private(cls, key_params: object) -> 'SSHKey':
-        """Construct an RSA private key"""
+        """Construct a private key"""
 
         raise NotImplementedError
 
     @classmethod
     def make_public(cls, key_params: object) -> 'SSHKey':
-        """Construct an RSA public key"""
+        """Construct a public key"""
 
         raise NotImplementedError
 
@@ -2096,6 +2096,18 @@
 
         return self._key_type
 
+    @property
+    def has_cert(self) -> bool:
+        """ Return if this key pair has an associated cert"""
+
+        return bool(self._cert)
+
+    @property
+    def has_x509_chain(self) -> bool:
+        """ Return if this key pair has an associated X.509 cert chain"""
+
+        return self._cert.is_x509_chain if self._cert else False
+
     def get_algorithm(self) -> str:
         """Return the algorithm associated with this key pair"""
 
@@ -2194,9 +2206,9 @@
 
         self.sig_algorithm = sig_algorithm
 
-        if not self._cert:
+        if not self.has_cert:
             self.algorithm = sig_algorithm
-        elif self._cert.is_x509_chain:
+        elif self.has_x509_chain:
             self.algorithm = sig_algorithm
 
             cert = cast('SSHX509CertificateChain', self._cert)
@@ -2402,7 +2414,9 @@
     return None, (), len(data)
 
 
-def _decode_pkcs1_private(pem_name: bytes, key_data: object) -> SSHKey:
+def _decode_pkcs1_private(
+        pem_name: bytes, key_data: object,
+        unsafe_skip_rsa_key_validation: Optional[bool]) -> SSHKey:
     """Decode a PKCS#1 format private key"""
 
     handler = _pem_map.get(pem_name)
@@ -2415,6 +2429,10 @@
         raise KeyImportError('Invalid %s private key' %
                              pem_name.decode('ascii'))
 
+    if pem_name == b'RSA':
+        key_params = cast(Tuple, key_params) + \
+            (unsafe_skip_rsa_key_validation,)
+
     return handler.make_private(key_params)
 
 
@@ -2434,7 +2452,9 @@
     return handler.make_public(key_params)
 
 
-def _decode_pkcs8_private(key_data: object) -> SSHKey:
+def _decode_pkcs8_private(
+        key_data: object,
+        unsafe_skip_rsa_key_validation: Optional[bool]) -> SSHKey:
     """Decode a PKCS#8 format private key"""
 
     if (isinstance(key_data, tuple) and len(key_data) >= 3 and
@@ -2455,6 +2475,10 @@
                                  handler.pem_name.decode('ascii')
                                  if handler.pem_name else 'PKCS#8')
 
+        if alg == ObjectIdentifier('1.2.840.113549.1.1.1'):
+            key_params = cast(Tuple, key_params) + \
+                (unsafe_skip_rsa_key_validation,)
+
         return handler.make_private(key_params)
     else:
         raise KeyImportError('Invalid PKCS#8 private key')
@@ -2486,8 +2510,9 @@
         raise KeyImportError('Invalid PKCS#8 public key')
 
 
-def _decode_openssh_private(data: bytes,
-                            passphrase: Optional[BytesOrStr]) -> SSHKey:
+def _decode_openssh_private(
+        data: bytes, passphrase: Optional[BytesOrStr],
+        unsafe_skip_rsa_key_validation: Optional[bool]) -> SSHKey:
     """Decode an OpenSSH format private key"""
 
     try:
@@ -2578,6 +2603,10 @@
         if len(pad) >= block_size or pad != bytes(range(1, len(pad) + 1)):
             raise KeyImportError('Invalid OpenSSH private key')
 
+        if alg == b'ssh-rsa':
+            key_params = cast(Tuple, key_params) + \
+                (unsafe_skip_rsa_key_validation,)
+
         key = handler.make_private(key_params)
         key.set_comment(comment)
         return key
@@ -2609,8 +2638,9 @@
         raise KeyImportError('Invalid OpenSSH private key') from None
 
 
-def _decode_der_private(key_data: object,
-                        passphrase: Optional[BytesOrStr]) -> SSHKey:
+def _decode_der_private(
+        key_data: object, passphrase: Optional[BytesOrStr],
+        unsafe_skip_rsa_key_validation: Optional[bool]) -> SSHKey:
     """Decode a DER format private key"""
 
     # First, if there's a passphrase, try to decrypt PKCS#8
@@ -2623,7 +2653,7 @@
 
     # Then, try to decode PKCS#8
     try:
-        return _decode_pkcs8_private(key_data)
+        return _decode_pkcs8_private(key_data, unsafe_skip_rsa_key_validation)
     except KeyImportError:
         # PKCS#8 failed - try PKCS#1 instead
         pass
@@ -2631,7 +2661,8 @@
     # If that fails, try each of the possible PKCS#1 encodings
     for pem_name in _pem_map:
         try:
-            return _decode_pkcs1_private(pem_name, key_data)
+            return _decode_pkcs1_private(pem_name, key_data,
+                                         unsafe_skip_rsa_key_validation)
         except KeyImportError:
             # Try the next PKCS#1 encoding
             pass
@@ -2667,13 +2698,15 @@
     return SSHX509Certificate.construct_from_der(data, comment)
 
 
-def _decode_pem_private(pem_name: bytes, headers: Mapping[bytes, bytes],
-                        data: bytes, passphrase: Optional[BytesOrStr]) -> \
-        SSHKey:
+def _decode_pem_private(
+        pem_name: bytes, headers: Mapping[bytes, bytes],
+        data: bytes, passphrase: Optional[BytesOrStr],
+        unsafe_skip_rsa_key_validation: Optional[bool]) -> SSHKey:
     """Decode a PEM format private key"""
 
     if pem_name == b'OPENSSH':
-        return _decode_openssh_private(data, passphrase)
+        return _decode_openssh_private(data, passphrase,
+                                       unsafe_skip_rsa_key_validation)
 
     if headers.get(b'Proc-Type') == b'4,ENCRYPTED':
         if passphrase is None:
@@ -2715,9 +2748,10 @@
                                  'private key') from None
 
     if pem_name:
-        return _decode_pkcs1_private(pem_name, key_data)
+        return _decode_pkcs1_private(pem_name, key_data,
+                                     unsafe_skip_rsa_key_validation)
     else:
-        return _decode_pkcs8_private(key_data)
+        return _decode_pkcs8_private(key_data, unsafe_skip_rsa_key_validation)
 
 
 def _decode_pem_public(pem_name: bytes, data: bytes) -> SSHKey:
@@ -2750,8 +2784,10 @@
     return SSHX509Certificate.construct_from_der(data)
 
 
-def _decode_private(data: bytes, passphrase: Optional[BytesOrStr]) -> \
-        Tuple[Optional[SSHKey], Optional[int]]:
+def _decode_private(
+        data: bytes, passphrase: Optional[BytesOrStr],
+        unsafe_skip_rsa_key_validation: Optional[bool]) -> \
+            Tuple[Optional[SSHKey], Optional[int]]:
     """Decode a private key"""
 
     fmt, key_info, end = _match_next(data, b'PRIVATE KEY')
@@ -2759,10 +2795,12 @@
     key: Optional[SSHKey]
 
     if fmt == 'der':
-        key = _decode_der_private(key_info[0], passphrase)
+        key = _decode_der_private(key_info[0], passphrase,
+                                  unsafe_skip_rsa_key_validation)
     elif fmt == 'pem':
         pem_name, headers, data = key_info
-        key = _decode_pem_private(pem_name, headers, data, passphrase)
+        key = _decode_pem_private(pem_name, headers, data, passphrase,
+                                  unsafe_skip_rsa_key_validation)
     else:
         key = None
 
@@ -2799,7 +2837,7 @@
         if fmt == 'pem' and key_info[0] == b'OPENSSH':
             key = _decode_openssh_public(key_info[2])
         else:
-            key, _ = _decode_private(data, None)
+            key, _ = _decode_private(data, None, False)
 
             if key:
                 key = key.convert_to_public()
@@ -2836,14 +2874,16 @@
     return cert, end
 
 
-def _decode_private_list(data: bytes, passphrase: Optional[BytesOrStr]) -> \
-        Sequence[SSHKey]:
+def _decode_private_list(
+        data: bytes, passphrase: Optional[BytesOrStr],
+        unsafe_skip_rsa_key_validation: Optional[bool]) -> Sequence[SSHKey]:
     """Decode a private key list"""
 
     keys: List[SSHKey] = []
 
     while data:
-        key, end = _decode_private(data, passphrase)
+        key, end = _decode_private(data, passphrase,
+                                   unsafe_skip_rsa_key_validation)
 
         if key:
             keys.append(key)
@@ -3122,8 +3162,9 @@
     key.set_comment(comment)
     return key
 
-def import_private_key(data: BytesOrStr,
-                       passphrase: Optional[BytesOrStr] = None) -> SSHKey:
+def import_private_key(
+        data: BytesOrStr, passphrase: Optional[BytesOrStr] = None,
+        unsafe_skip_rsa_key_validation: Optional[bool] = None) -> SSHKey:
     """Import a private key
 
        This function imports a private key encoded in PKCS#1 or PKCS#8 DER
@@ -3134,8 +3175,13 @@
            The data to import.
        :param passphrase: (optional)
            The passphrase to use to decrypt the key.
+       :param unsafe_skip_rsa_key_validation: (optional)
+           Whether or not to skip key validation when loading RSA private
+           keys, defaulting to performing these checks unless changed by
+           calling :func:`set_default_skip_rsa_key_validation`.
        :type data: `bytes` or ASCII `str`
        :type passphrase: `str` or `bytes`
+       :type unsafe_skip_rsa_key_validation: bool
 
        :returns: An :class:`SSHKey` private key
 
@@ -3147,7 +3193,7 @@
         except UnicodeEncodeError:
             raise KeyImportError('Invalid encoding for key') from None
 
-    key, _ = _decode_private(data, passphrase)
+    key, _ = _decode_private(data, passphrase, unsafe_skip_rsa_key_validation)
 
     if key:
         return key
@@ -3155,12 +3201,14 @@
         raise KeyImportError('Invalid private key')
 
 
-def import_private_key_and_certs(data: bytes,
-                                 passphrase: Optional[BytesOrStr] = None) -> \
-        Tuple[SSHKey, Optional[SSHX509CertificateChain]]:
+def import_private_key_and_certs(
+        data: bytes, passphrase: Optional[BytesOrStr] = None,
+        unsafe_skip_rsa_key_validation: Optional[bool] = None) -> \
+            Tuple[SSHKey, Optional[SSHX509CertificateChain]]:
     """Import a private key and optional certificate chain"""
 
-    key, end = _decode_private(data, passphrase)
+    key, end = _decode_private(data, passphrase,
+                               unsafe_skip_rsa_key_validation)
 
     if key:
         return key, import_certificate_chain(data[end:])
@@ -3256,8 +3304,9 @@
     raise KeyImportError('Invalid certificate subject')
 
 
-def read_private_key(filename: FilePath,
-                     passphrase: Optional[BytesOrStr] = None) -> SSHKey:
+def read_private_key(
+        filename: FilePath, passphrase: Optional[BytesOrStr] = None,
+        unsafe_skip_rsa_key_validation: Optional[bool] = None) -> SSHKey:
     """Read a private key from a file
 
        This function reads a private key from a file. See the function
@@ -3268,26 +3317,34 @@
            The file to read the key from.
        :param passphrase: (optional)
            The passphrase to use to decrypt the key.
+       :param unsafe_skip_rsa_key_validation: (optional)
+           Whether or not to skip key validation when loading RSA private
+           keys, defaulting to performing these checks unless changed by
+           calling :func:`set_default_skip_rsa_key_validation`.
        :type filename: :class:`PurePath <pathlib.PurePath>` or `str`
        :type passphrase: `str` or `bytes`
+       :type unsafe_skip_rsa_key_validation: bool
 
        :returns: An :class:`SSHKey` private key
 
     """
 
-    key = import_private_key(read_file(filename), passphrase)
+    key = import_private_key(read_file(filename), passphrase,
+                             unsafe_skip_rsa_key_validation)
 
     key.set_filename(filename)
 
     return key
 
 
-def read_private_key_and_certs(filename: FilePath,
-                               passphrase: Optional[BytesOrStr] = None) -> \
-        Tuple[SSHKey, Optional[SSHX509CertificateChain]]:
+def read_private_key_and_certs(
+        filename: FilePath, passphrase: Optional[BytesOrStr] = None,
+        unsafe_skip_rsa_key_validation: Optional[bool] = None) -> \
+            Tuple[SSHKey, Optional[SSHX509CertificateChain]]:
     """Read a private key and optional certificate chain from a file"""
 
-    key, cert = import_private_key_and_certs(read_file(filename), passphrase)
+    key, cert = import_private_key_and_certs(read_file(filename), passphrase,
+                                             unsafe_skip_rsa_key_validation)
 
     key.set_filename(filename)
 
@@ -3334,9 +3391,10 @@
     return import_certificate(read_file(filename))
 
 
-def read_private_key_list(filename: FilePath,
-                          passphrase: Optional[BytesOrStr] = None) -> \
-        Sequence[SSHKey]:
+def read_private_key_list(
+        filename: FilePath, passphrase: Optional[BytesOrStr] = None,
+        unsafe_skip_rsa_key_validation: Optional[bool] = None) -> \
+            Sequence[SSHKey]:
     """Read a list of private keys from a file
 
        This function reads a list of private keys from a file. See the
@@ -3348,14 +3406,20 @@
            The file to read the keys from.
        :param passphrase: (optional)
            The passphrase to use to decrypt the keys.
+       :param unsafe_skip_rsa_key_validation: (optional)
+           Whether or not to skip key validation when loading RSA private
+           keys, defaulting to performing these checks unless changed by
+           calling :func:`set_default_skip_rsa_key_validation`.
        :type filename: :class:`PurePath <pathlib.PurePath>` or `str`
        :type passphrase: `str` or `bytes`
+       :type unsafe_skip_rsa_key_validation: bool
 
        :returns: A list of :class:`SSHKey` private keys
 
     """
 
-    keys = _decode_private_list(read_file(filename), passphrase)
+    keys = _decode_private_list(read_file(filename), passphrase,
+                                unsafe_skip_rsa_key_validation)
 
     for key in keys:
         key.set_filename(filename)
@@ -3404,10 +3468,12 @@
     return _decode_certificate_list(read_file(filename))
 
 
-def load_keypairs(keylist: KeyPairListArg,
-                  passphrase: Optional[BytesOrStr] = None,
-                  certlist: CertListArg = (), skip_public: bool = False,
-                  ignore_encrypted: bool = False) -> Sequence[SSHKeyPair]:
+def load_keypairs(
+        keylist: KeyPairListArg, passphrase: Optional[BytesOrStr] = None,
+        certlist: CertListArg = (), skip_public: bool = False,
+        ignore_encrypted: bool = False,
+        unsafe_skip_rsa_key_validation: Optional[bool] = None) -> \
+            Sequence[SSHKeyPair]:
     """Load SSH private keys and optional matching certificates
 
        This function loads a list of SSH keys and optional matching
@@ -3427,10 +3493,15 @@
            An internal parameter used to skip public keys and certificates
            when IdentitiesOnly and IdentityFile are used to specify a
            mixture of private and public keys.
+       :param unsafe_skip_rsa_key_validation: (optional)
+           Whether or not to skip key validation when loading RSA private
+           keys, defaulting to performing these checks unless changed by
+           calling :func:`set_default_skip_rsa_key_validation`.
        :type keylist: *see* :ref:`SpecifyingPrivateKeys`
        :type passphrase: `str` or `bytes`
        :type certlist: *see* :ref:`SpecifyingCertificates`
        :type skip_public: `bool`
+       :type unsafe_skip_rsa_key_validation: bool
 
        :returns: A list of :class:`SSHKeyPair` objects
 
@@ -3444,7 +3515,8 @@
 
     if isinstance(keylist, (PurePath, str)):
         try:
-            priv_keys = read_private_key_list(keylist, passphrase)
+            priv_keys = read_private_key_list(keylist, passphrase,
+                                              unsafe_skip_rsa_key_validation)
             keys_to_load = [keylist] if len(priv_keys) <= 1 else priv_keys
         except KeyImportError:
             keys_to_load = [keylist]
@@ -3472,21 +3544,25 @@
                 key_prefix = str(key_to_load)
 
                 if allow_certs:
-                    key, certs_to_load = \
-                        read_private_key_and_certs(key_to_load, passphrase)
+                    key, certs_to_load = read_private_key_and_certs(
+                        key_to_load, passphrase,
+                        unsafe_skip_rsa_key_validation)
 
                     if not certs_to_load:
                         certs_to_load = key_prefix + '-cert.pub'
                 else:
-                    key = read_private_key(key_to_load, passphrase)
+                    key = read_private_key(key_to_load, passphrase,
+                                           unsafe_skip_rsa_key_validation)
 
                 pubkey_to_load = key_prefix + '.pub'
             elif isinstance(key_to_load, bytes):
                 if allow_certs:
-                    key, certs_to_load = \
-                        import_private_key_and_certs(key_to_load, passphrase)
+                    key, certs_to_load = import_private_key_and_certs(
+                        key_to_load, passphrase,
+                        unsafe_skip_rsa_key_validation)
                 else:
-                    key = import_private_key(key_to_load, passphrase)
+                    key = import_private_key(key_to_load, passphrase,
+                                             unsafe_skip_rsa_key_validation)
             else:
                 key = key_to_load
         except KeyImportError as exc:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/rsa.py 
new/asyncssh-2.14.0/asyncssh/rsa.py
--- old/asyncssh-2.13.2/asyncssh/rsa.py 2023-06-22 04:57:39.000000000 +0200
+++ new/asyncssh-2.14.0/asyncssh/rsa.py 2023-10-01 02:35:42.000000000 +0200
@@ -43,9 +43,49 @@
 
 
 _PrivateKeyArgs = Tuple[int, int, int, int, int, int, int, int]
+_PrivateKeyConstructArgs = Tuple[int, int, int, int, int, int, int, int, bool]
 _PublicKeyArgs = Tuple[int, int]
 
 
+_default_skip_rsa_key_validation = False
+
+
+def set_default_skip_rsa_key_validation(skip_validation: bool) -> None:
+    """Set whether to disable RSA key validation in OpenSSL
+
+       OpenSSL 3.x does additional validation when loading RSA keys
+       as an added security measure. However, the result is that
+       loading a key can take significantly longer than it did before.
+
+       If all your RSA keys are coming from a trusted source, you can
+       call this function with a value of `True` to default to skipping
+       these checks on RSA keys, reducing the cost back down to what it
+       was in earlier releases.
+
+       This can also be set on a case by case basis by using the new
+       `unsafe_skip_rsa_key_validation` argument on the functions used
+       to load keys. This will only affect loading keys of type RSA.
+
+       .. note:: The extra cost only applies to loading existing keys, and
+                 not to generating new keys. Also, in cases where a key is
+                 used repeatedly, it can be loaded once into an `SSHKey`
+                 object and reused without having to pay the cost each time.
+                 So, this call should not be needed in most applications.
+
+                 If an application does need this, it is strongly
+                 recommended that the `unsafe_skip_rsa_key_validation`
+                 argument be used rather than using this function to
+                 change the default behavior for all load operations.
+
+    """
+
+    # pylint: disable=global-statement
+
+    global _default_skip_rsa_key_validation
+
+    _default_skip_rsa_key_validation = skip_validation
+
+
 class RSAKey(SSHKey):
     """Handler for RSA public key encryption"""
 
@@ -94,9 +134,14 @@
     def make_private(cls, key_params: object) -> SSHKey:
         """Construct an RSA private key"""
 
-        n, e, d, p, q, dmp1, dmq1, iqmp = cast(_PrivateKeyArgs, key_params)
+        n, e, d, p, q, dmp1, dmq1, iqmp, unsafe_skip_rsa_key_validation = \
+            cast(_PrivateKeyConstructArgs, key_params)
+
+        if unsafe_skip_rsa_key_validation is None:
+            unsafe_skip_rsa_key_validation = _default_skip_rsa_key_validation
 
-        return cls(RSAPrivateKey.construct(n, e, d, p, q, dmp1, dmq1, iqmp))
+        return cls(RSAPrivateKey.construct(n, e, d, p, q, dmp1, dmq1, iqmp,
+                                           unsafe_skip_rsa_key_validation))
 
     @classmethod
     def make_public(cls, key_params: object) -> SSHKey:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/server.py 
new/asyncssh-2.14.0/asyncssh/server.py
--- old/asyncssh-2.13.2/asyncssh/server.py      2023-06-22 04:57:39.000000000 
+0200
+++ new/asyncssh-2.14.0/asyncssh/server.py      2023-10-01 02:35:42.000000000 
+0200
@@ -31,7 +31,7 @@
 
 if TYPE_CHECKING:
     # pylint: disable=cyclic-import
-    from .connection import SSHServerConnection
+    from .connection import SSHServerConnection, SSHAcceptHandler
     from .channel import SSHServerChannel, SSHTCPChannel, SSHUNIXChannel
     from .session import SSHServerSession, SSHTCPSession, SSHUNIXSession
 
@@ -45,7 +45,7 @@
 _NewUNIXSession = Union[bool, 'SSHUNIXSession', SSHSocketSessionFactory,
                         Tuple['SSHUNIXChannel', 'SSHUNIXSession'],
                         Tuple['SSHUNIXChannel', SSHSocketSessionFactory]]
-_NewListener = Union[bool, SSHListener]
+_NewListener = Union[bool, 'SSHAcceptHandler', SSHListener]
 
 
 class SSHServer:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh/version.py 
new/asyncssh-2.14.0/asyncssh/version.py
--- old/asyncssh-2.13.2/asyncssh/version.py     2023-06-22 04:57:55.000000000 
+0200
+++ new/asyncssh-2.14.0/asyncssh/version.py     2023-10-01 03:04:52.000000000 
+0200
@@ -26,4 +26,4 @@
 
 __url__ = 'http://asyncssh.timeheart.net'
 
-__version__ = '2.13.2'
+__version__ = '2.14.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh.egg-info/PKG-INFO 
new/asyncssh-2.14.0/asyncssh.egg-info/PKG-INFO
--- old/asyncssh-2.13.2/asyncssh.egg-info/PKG-INFO      2023-06-22 
05:08:52.000000000 +0200
+++ new/asyncssh-2.14.0/asyncssh.egg-info/PKG-INFO      2023-10-01 
03:06:54.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: asyncssh
-Version: 2.13.2
+Version: 2.14.0
 Summary: AsyncSSH: Asynchronous SSHv2 client and server library
 Home-page: http://asyncssh.timeheart.net
 Author: Ron Frederick
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/asyncssh.egg-info/requires.txt 
new/asyncssh-2.14.0/asyncssh.egg-info/requires.txt
--- old/asyncssh-2.13.2/asyncssh.egg-info/requires.txt  2023-06-22 
05:08:52.000000000 +0200
+++ new/asyncssh-2.14.0/asyncssh.egg-info/requires.txt  2023-10-01 
03:06:54.000000000 +0200
@@ -1,4 +1,4 @@
-cryptography>=3.1
+cryptography>=39.0
 typing_extensions>=3.6
 
 [bcrypt]
@@ -17,7 +17,7 @@
 python-pkcs11>=0.7.0
 
 [pyOpenSSL]
-pyOpenSSL>=17.0.0
+pyOpenSSL>=23.0.0
 
 [pywin32]
 pywin32>=227
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/docs/api.rst 
new/asyncssh-2.14.0/docs/api.rst
--- old/asyncssh-2.13.2/docs/api.rst    2023-06-22 04:57:39.000000000 +0200
+++ new/asyncssh-2.14.0/docs/api.rst    2023-10-01 02:35:42.000000000 +0200
@@ -1617,6 +1617,11 @@
 
 .. index:: SSH agent support
 
+set_default_skip_rsa_key_validation
+-----------------------------------
+
+.. autofunction:: set_default_skip_rsa_key_validation
+
 SSH Agent Support
 =================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/docs/changes.rst 
new/asyncssh-2.14.0/docs/changes.rst
--- old/asyncssh-2.13.2/docs/changes.rst        2023-06-22 04:59:01.000000000 
+0200
+++ new/asyncssh-2.14.0/docs/changes.rst        2023-10-01 03:04:52.000000000 
+0200
@@ -3,6 +3,44 @@
 Change Log
 ==========
 
+Release 2.14.0 (30 Sep 2023)
+----------------------------
+
+* Added support for a new accept_handler argument when setting up
+  local port forwarding, allowing the client host and port to be
+  validated and/or logged for each new forwarded connection. An
+  accept handler can also be returned from the server_requested
+  function to provide this functionality when acting as a server.
+  Thanks go to GitHub user zgxkbtl for suggesting this feature.
+
+* Added an option to disable expensive RSA private key checks when
+  using OpenSSL 3.x. Functions that read private keys have been
+  modified to include a new unsafe_skip_rsa_key_validation argument
+  which can be used to avoid these additional checks, if you are
+  loading keys from a trusted source.
+
+* Added host information into AsyncSSH exceptions when host key
+  validation fails, and a few other improvements related to X.509
+  certificate validation errors. Thanks go to Peter Moore for
+  suggesting this and providing an example.
+
+* Fixed a regression which prevented keys loaded into an SSH agent
+  with a certificate from working correctly beginning in AsyncSSH
+  after version 2.5.0. Thanks go to GitHub user htol for reporting
+  this issue and suggesting the commit which caused the problem.
+
+* Fixed an issue which was triggering an internal exception when
+  shutting down server sessions with the line editor enabled which
+  could cause some output to be lost on exit, especially when running
+  on Windows. Thanks go to GitHub user jerrbe for reporting this issue.
+
+* Fixed an issue in a unit test seen in Python 3.12 beta. Thanks go
+  to Georg Sauthoff for providing this fix.
+
+* Fixed a documentation error in SSHClientConnectionOptions and
+  SSHServerConnectionOptions. Thanks go to GitHub user bowenerchen
+  for reporting this issue.
+
 Release 2.13.2 (21 Jun 2023)
 ----------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/setup.py new/asyncssh-2.14.0/setup.py
--- old/asyncssh-2.13.2/setup.py        2022-04-16 22:13:09.000000000 +0200
+++ new/asyncssh-2.14.0/setup.py        2023-10-01 02:35:42.000000000 +0200
@@ -57,14 +57,14 @@
       long_description = long_description,
       platforms = 'Any',
       python_requires = '>= 3.6',
-      install_requires = ['cryptography >= 3.1', 'typing_extensions >= 3.6'],
+      install_requires = ['cryptography >= 39.0', 'typing_extensions >= 3.6'],
       extras_require = {
           'bcrypt':     ['bcrypt >= 3.1.3'],
           'fido2':      ['fido2 >= 0.9.2'],
           'gssapi':     ['gssapi >= 1.2.0'],
           'libnacl':    ['libnacl >= 1.4.2'],
           'pkcs11':     ['python-pkcs11 >= 0.7.0'],
-          'pyOpenSSL':  ['pyOpenSSL >= 17.0.0'],
+          'pyOpenSSL':  ['pyOpenSSL >= 23.0.0'],
           'pywin32':    ['pywin32 >= 227']
       },
       packages = ['asyncssh', 'asyncssh.crypto'],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/tests/test_connection.py 
new/asyncssh-2.14.0/tests/test_connection.py
--- old/asyncssh-2.13.2/tests/test_connection.py        2023-06-22 
04:57:39.000000000 +0200
+++ new/asyncssh-2.14.0/tests/test_connection.py        2023-10-01 
02:35:42.000000000 +0200
@@ -2068,6 +2068,13 @@
             await self.connect()
 
     @asynctest
+    async def test_host_key_unknown(self):
+        """Test unknown host key alias"""
+
+        with self.assertRaises(asyncssh.HostKeyNotVerifiable):
+            await self.connect(host_key_alias='unknown')
+
+    @asynctest
     async def test_host_key_match(self):
         """Test host key match"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/tests/test_forward.py 
new/asyncssh-2.14.0/tests/test_forward.py
--- old/asyncssh-2.13.2/tests/test_forward.py   2022-12-27 22:30:36.000000000 
+0100
+++ new/asyncssh-2.14.0/tests/test_forward.py   2023-10-01 02:35:42.000000000 
+0200
@@ -183,6 +183,18 @@
             return listen_host != 'fail'
 
 
+class _TCPAcceptHandlerServer(Server):
+    """Server for testing forwarding accept handler"""
+
+    async def server_requested(self, listen_host, listen_port):
+        """Handle a request to create a new socket listener"""
+
+        def accept_handler(_orig_host: str, _orig_port: int) -> bool:
+            return True
+
+        return accept_handler
+
+
 class _UNIXConnectionServer(Server):
     """Server for testing direct and forwarded UNIX domain connections"""
 
@@ -594,6 +606,39 @@
                 await self._check_local_connection(listener.get_port(),
                                                    delay=0.1)
 
+    @asynctest
+    async def test_forward_local_port_accept_handler(self):
+        """Test forwarding of a local port with an accept handler"""
+
+        def accept_handler(_orig_host: str, _orig_port: int) -> bool:
+            return True
+
+        async with self.connect() as conn:
+            async with conn.forward_local_port('', 0, '', 7,
+                                               accept_handler) as listener:
+                await self._check_local_connection(listener.get_port(),
+                                                   delay=0.1)
+
+    @asynctest
+    async def test_forward_local_port_accept_handler_denial(self):
+        """Test forwarding of a local port with an accept handler denial"""
+
+        async def accept_handler(_orig_host: str, _orig_port: int) -> bool:
+            return False
+
+        async with self.connect() as conn:
+            async with conn.forward_local_port('', 0, '', 7,
+                                               accept_handler) as listener:
+                listen_port = listener.get_port()
+
+                reader, writer = await asyncio.open_connection('127.0.0.1',
+                                                               listen_port)
+
+                self.assertEqual((await reader.read()), b'')
+
+                writer.close()
+                await maybe_wait_closed(writer)
+
     @unittest.skipIf(sys.platform == 'win32',
                      'skip UNIX domain socket tests on Windows')
     @asynctest
@@ -855,6 +900,33 @@
             await listener.wait_closed()
 
 
+class _TestTCPForwardingAcceptHandler(_CheckForwarding):
+    """Unit tests for TCP forwarding with accept handler"""
+
+    @classmethod
+    async def start_server(cls):
+        """Start an SSH server which supports TCP connection forwarding"""
+
+        return await cls.create_server(
+            _TCPAcceptHandlerServer, authorized_client_keys='authorized_keys')
+
+    @asynctest
+    async def test_forward_remote_port_accept_handler(self):
+        """Test forwarding of a remote port with accept handler"""
+
+        server = await asyncio.start_server(echo, None, 0,
+                                            family=socket.AF_INET)
+        server_port = server.sockets[0].getsockname()[1]
+
+        async with self.connect() as conn:
+            async with conn.forward_remote_port(
+                    '', 0, '127.0.0.1', server_port) as listener:
+                await self._check_local_connection(listener.get_port())
+
+        server.close()
+        await server.wait_closed()
+
+
 class _TestAsyncTCPForwarding(_TestTCPForwarding):
     """Unit tests for AsyncSSH TCP connection forwarding with async return"""
 
@@ -1000,6 +1072,39 @@
         os.remove('local')
 
     @asynctest
+    async def test_forward_local_port_to_path_accept_handler(self):
+        """Test forwarding of port to UNIX path with accept handler"""
+
+        def accept_handler(_orig_host: str, _orig_port: int) -> bool:
+            return True
+
+        async with self.connect() as conn:
+            async with conn.forward_local_port_to_path(
+                    '', 0, '/echo', accept_handler) as listener:
+                await self._check_local_connection(listener.get_port(),
+                                                   delay=0.1)
+
+    @asynctest
+    async def test_forward_local_port_to_path_accept_handler_denial(self):
+        """Test forwarding of port to UNIX path with accept handler denial"""
+
+        async def accept_handler(_orig_host: str, _orig_port: int) -> bool:
+            return False
+
+        async with self.connect() as conn:
+            async with conn.forward_local_port_to_path(
+                    '', 0, '/echo', accept_handler) as listener:
+                listen_port = listener.get_port()
+
+                reader, writer = await asyncio.open_connection('127.0.0.1',
+                                                               listen_port)
+
+                self.assertEqual((await reader.read()), b'')
+
+                writer.close()
+                await maybe_wait_closed(writer)
+
+    @asynctest
     async def test_forward_local_port_to_path(self):
         """Test forwarding of a local port to a remote UNIX domain socket"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/tests/test_process.py 
new/asyncssh-2.14.0/tests/test_process.py
--- old/asyncssh-2.13.2/tests/test_process.py   2022-12-30 17:45:50.000000000 
+0100
+++ new/asyncssh-2.14.0/tests/test_process.py   2023-10-01 02:35:42.000000000 
+0200
@@ -902,7 +902,7 @@
             proc1.stdin.write(data)
             proc1.stdin.write_eof()
 
-            stdout_data, _ = await proc2.communicate()
+            stdout_data = await proc2.stdout.read()
 
         self.assertEqual(stdout_data, data.encode('ascii'))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/tests/util.py 
new/asyncssh-2.14.0/tests/util.py
--- old/asyncssh-2.13.2/tests/util.py   2023-06-22 04:57:39.000000000 +0200
+++ new/asyncssh-2.14.0/tests/util.py   2023-10-01 02:35:42.000000000 +0200
@@ -32,8 +32,7 @@
 
 from unittest.mock import patch
 
-from cryptography.hazmat.backends.openssl import backend
-
+from asyncssh import set_default_skip_rsa_key_validation
 from asyncssh.gss import gss_available
 from asyncssh.logging import logger
 from asyncssh.misc import ConnectionLost, SignalReceived
@@ -77,15 +76,10 @@
 # pylint: enable=no-member
 
 
-# pylint: disable=protected-access
-
-# Disable RSA key blinding to speed up unit tests
-backend._rsa_skip_check_key = True
-
-# pylint: enable=protected-access
-
 _test_keys = {}
 
+set_default_skip_rsa_key_validation(True)
+
 
 def asynctest(coro):
     """Decorator for async tests, for use with AsyncTestCase"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.13.2/tox.ini new/asyncssh-2.14.0/tox.ini
--- old/asyncssh-2.13.2/tox.ini 2022-12-27 22:30:36.000000000 +0100
+++ new/asyncssh-2.14.0/tox.ini 2023-10-01 02:35:42.000000000 +0200
@@ -26,6 +26,7 @@
     windows: win32
 usedevelop = True
 setenv =
+    PIP_USE_PEP517 = 1
     COVERAGE_FILE = .coverage.{envname}
 commands =
     {envpython} -m pytest --cov --cov-report=term-missing:skip-covered 
{posargs}

Reply via email to