mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Align SSL APIs and transport behavior with Python 3.15 (#9167)
Refresh ssl.py and test_ssl.py through update_lib from CPython d220409a13b77de7a6c30abe16ea90db01528edc without local test changes. Implement rustls cipher-suite, group and signature policy, negotiated-group reporting, EOF and session-policy behavior. Keep OpenSSL and wasm API imports compatible, with genuine version-gated queries and honest unsupported capabilities. Repair OpenSSL callback ownership and traversal, callback exceptions, EOF and shutdown, PHA, session destruction, and system-error decoding exposed by canonical coverage. Preserve existing I/O locking; the inherited optional-backend full-duplex hang remains a separate driver prerequisite. Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
1 parent
19d2fe3af7
commit
fb76135ec8
14 files changed
+2638
-854
No files matched your search
Vendored
+84
-27
@@ -13,6 +13,9 @@ Exceptions:
|
||||
|
||||
Functions:
|
||||
|
||||
get_sigalgs -- return a list of all available TLS signature
|
||||
algorithms (requires OpenSSL 3.4 or later)
|
||||
|
||||
cert_time_to_seconds -- convert time string used for certificate
|
||||
notBefore and notAfter functions to integer
|
||||
seconds past the Epoch (the time values
|
||||
@@ -107,16 +110,12 @@ from _ssl import (
|
||||
)
|
||||
from _ssl import txt2obj as _txt2obj, nid2obj as _nid2obj
|
||||
from _ssl import RAND_status, RAND_add, RAND_bytes
|
||||
try:
|
||||
from _ssl import RAND_egd
|
||||
except ImportError:
|
||||
# RAND_egd is not supported on some platforms
|
||||
pass
|
||||
from _ssl import get_sigalgs
|
||||
|
||||
|
||||
from _ssl import (
|
||||
HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_SSLv2, HAS_SSLv3, HAS_TLSv1,
|
||||
HAS_TLSv1_1, HAS_TLSv1_2, HAS_TLSv1_3, HAS_PSK, HAS_PHA
|
||||
HAS_TLSv1_1, HAS_TLSv1_2, HAS_TLSv1_3, HAS_PSK, HAS_PSK_TLS13, HAS_PHA
|
||||
)
|
||||
from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION
|
||||
|
||||
@@ -151,7 +150,8 @@ _IntEnum._convert_(
|
||||
source=_ssl)
|
||||
|
||||
PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS
|
||||
_PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()}
|
||||
_PROTOCOL_NAMES = frozendict({
|
||||
value: name for name, value in _SSLMethod.__members__.items()})
|
||||
|
||||
_SSLv2_IF_EXISTS = getattr(_SSLMethod, 'PROTOCOL_SSLv2', None)
|
||||
|
||||
@@ -373,6 +373,20 @@ def _ipaddress_match(cert_ipaddress, host_ip):
|
||||
return ip == host_ip
|
||||
|
||||
|
||||
def _check_sslobject_params(server_side, context=None, server_hostname=None, session=None):
|
||||
"""Raises a ValueError if SSLObject._create() parameters aren't valid.
|
||||
"""
|
||||
if server_side:
|
||||
if server_hostname:
|
||||
raise ValueError("server_hostname can only be specified "
|
||||
"in client mode")
|
||||
if session is not None:
|
||||
raise ValueError("session can only be specified in "
|
||||
"client mode")
|
||||
if context.check_hostname and not server_hostname:
|
||||
raise ValueError("check_hostname requires server_hostname")
|
||||
|
||||
|
||||
DefaultVerifyPaths = namedtuple("DefaultVerifyPaths",
|
||||
"cafile capath openssl_cafile_env openssl_cafile openssl_capath_env "
|
||||
"openssl_capath")
|
||||
@@ -813,6 +827,8 @@ class SSLObject:
|
||||
@classmethod
|
||||
def _create(cls, incoming, outgoing, server_side=False,
|
||||
server_hostname=None, session=None, context=None):
|
||||
_check_sslobject_params(server_side=server_side, context=context,
|
||||
server_hostname=server_hostname, session=session)
|
||||
self = cls.__new__(cls)
|
||||
sslobj = context._wrap_bio(
|
||||
incoming, outgoing, server_side=server_side,
|
||||
@@ -931,6 +947,18 @@ class SSLObject:
|
||||
ssl_version, secret_bits)``."""
|
||||
return self._sslobj.cipher()
|
||||
|
||||
def group(self):
|
||||
"""Return the currently selected key agreement group name."""
|
||||
return self._sslobj.group()
|
||||
|
||||
def client_sigalg(self):
|
||||
"""Return the selected client authentication signature algorithm."""
|
||||
return self._sslobj.client_sigalg()
|
||||
|
||||
def server_sigalg(self):
|
||||
"""Return the selected server handshake signature algorithm."""
|
||||
return self._sslobj.server_sigalg()
|
||||
|
||||
def shared_ciphers(self):
|
||||
"""Return a list of ciphers shared by the client during the handshake or
|
||||
None if this is not a valid server connection.
|
||||
@@ -975,6 +1003,10 @@ def _sslcopydoc(func):
|
||||
return func
|
||||
|
||||
|
||||
class _GiveupOnSSLSendfile(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SSLSocket(socket):
|
||||
"""This class implements a subtype of socket.socket that wraps
|
||||
the underlying OS socket in an SSL context when necessary, and
|
||||
@@ -993,15 +1025,8 @@ class SSLSocket(socket):
|
||||
context=None, session=None):
|
||||
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
|
||||
raise NotImplementedError("only stream sockets are supported")
|
||||
if server_side:
|
||||
if server_hostname:
|
||||
raise ValueError("server_hostname can only be specified "
|
||||
"in client mode")
|
||||
if session is not None:
|
||||
raise ValueError("session can only be specified in "
|
||||
"client mode")
|
||||
if context.check_hostname and not server_hostname:
|
||||
raise ValueError("check_hostname requires server_hostname")
|
||||
_check_sslobject_params(server_side=server_side, context=context,
|
||||
server_hostname=server_hostname, session=session)
|
||||
|
||||
sock_timeout = sock.gettimeout()
|
||||
kwargs = dict(
|
||||
@@ -1206,6 +1231,30 @@ class SSLSocket(socket):
|
||||
else:
|
||||
return self._sslobj.cipher()
|
||||
|
||||
@_sslcopydoc
|
||||
def group(self):
|
||||
self._checkClosed()
|
||||
if self._sslobj is None:
|
||||
return None
|
||||
else:
|
||||
return self._sslobj.group()
|
||||
|
||||
@_sslcopydoc
|
||||
def client_sigalg(self):
|
||||
self._checkClosed()
|
||||
if self._sslobj is None:
|
||||
return None
|
||||
else:
|
||||
return self._sslobj.client_sigalg()
|
||||
|
||||
@_sslcopydoc
|
||||
def server_sigalg(self):
|
||||
self._checkClosed()
|
||||
if self._sslobj is None:
|
||||
return None
|
||||
else:
|
||||
return self._sslobj.server_sigalg()
|
||||
|
||||
@_sslcopydoc
|
||||
def shared_ciphers(self):
|
||||
self._checkClosed()
|
||||
@@ -1266,15 +1315,26 @@ class SSLSocket(socket):
|
||||
return super().sendall(data, flags)
|
||||
|
||||
def sendfile(self, file, offset=0, count=None):
|
||||
"""Send a file, possibly by using os.sendfile() if this is a
|
||||
clear-text socket. Return the total number of bytes sent.
|
||||
"""Send a file, possibly by using an efficient sendfile() call if
|
||||
the system supports it. Return the total number of bytes sent.
|
||||
"""
|
||||
if self._sslobj is not None:
|
||||
return self._sendfile_use_send(file, offset, count)
|
||||
else:
|
||||
# os.sendfile() works with plain sockets only
|
||||
if self._sslobj is None:
|
||||
return super().sendfile(file, offset, count)
|
||||
|
||||
if not self._sslobj.uses_ktls_for_send():
|
||||
return self._sendfile_use_send(file, offset, count)
|
||||
|
||||
sendfile = getattr(self._sslobj, "sendfile", None)
|
||||
if sendfile is None:
|
||||
return self._sendfile_use_send(file, offset, count)
|
||||
|
||||
try:
|
||||
return self._sendfile_zerocopy(
|
||||
sendfile, _GiveupOnSSLSendfile, file, offset, count,
|
||||
)
|
||||
except _GiveupOnSSLSendfile:
|
||||
return self._sendfile_use_send(file, offset, count)
|
||||
|
||||
def recv(self, buflen=1024, flags=0):
|
||||
self._checkClosed()
|
||||
if self._sslobj is not None:
|
||||
@@ -1484,11 +1544,8 @@ def DER_cert_to_PEM_cert(der_cert_bytes):
|
||||
"""Takes a certificate in binary DER format and returns the
|
||||
PEM version of it as a string."""
|
||||
|
||||
f = str(base64.standard_b64encode(der_cert_bytes), 'ASCII', 'strict')
|
||||
ss = [PEM_HEADER]
|
||||
ss += [f[i:i+64] for i in range(0, len(f), 64)]
|
||||
ss.append(PEM_FOOTER + '\n')
|
||||
return '\n'.join(ss)
|
||||
f = str(base64.b64encode(der_cert_bytes, wrapcol=64), 'ASCII')
|
||||
return f'{PEM_HEADER}\n{f}\n{PEM_FOOTER}\n'
|
||||
|
||||
def PEM_cert_to_DER_cert(pem_cert_string):
|
||||
"""Takes a certificate in ASCII PEM format and returns the
|
||||
|
||||
Vendored
+580
-50
@@ -49,7 +49,17 @@ Py_DEBUG_WIN32 = support.Py_DEBUG and sys.platform == 'win32'
|
||||
PROTOCOLS = sorted(ssl._PROTOCOL_NAMES)
|
||||
HOST = socket_helper.HOST
|
||||
IS_AWS_LC = "AWS-LC" in ssl.OPENSSL_VERSION
|
||||
IS_LIBRESSL = "LibreSSL" in ssl.OPENSSL_VERSION
|
||||
IS_OPENSSL_3_0_0 = ssl.OPENSSL_VERSION_INFO >= (3, 0, 0)
|
||||
CAN_GET_SELECTED_OPENSSL_GROUP = ssl.OPENSSL_VERSION_INFO >= (3, 2)
|
||||
CAN_IGNORE_UNKNOWN_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 3)
|
||||
CAN_GET_AVAILABLE_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 5)
|
||||
CAN_GET_AVAILABLE_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 4)
|
||||
# LibreSSL does not provide SSL_CTX_set1_(client_)sigalgs_list().
|
||||
CAN_SET_CLIENT_SIGALGS = not IS_AWS_LC and not IS_LIBRESSL
|
||||
CAN_SET_SERVER_SIGALGS = not IS_LIBRESSL
|
||||
CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 3)
|
||||
CAN_GET_SELECTED_OPENSSL_SIGALG = ssl.OPENSSL_VERSION_INFO >= (3, 5)
|
||||
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')
|
||||
|
||||
HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename')
|
||||
@@ -136,6 +146,7 @@ REMOTE_HOST = "self-signed.pythontest.net"
|
||||
EMPTYCERT = data_file("nullcert.pem")
|
||||
BADCERT = data_file("badcert.pem")
|
||||
NONEXISTINGCERT = data_file("XXXnonexisting.pem")
|
||||
NONEXISTINGKEY = data_file("XXXnonexistingkey.pem")
|
||||
BADKEY = data_file("badkey.pem")
|
||||
NOKIACERT = data_file("nokia.pem")
|
||||
NULLBYTECERT = data_file("nullbytecert.pem")
|
||||
@@ -276,7 +287,7 @@ def make_test_context(
|
||||
check_hostname=None,
|
||||
cert_reqs=ssl.CERT_NONE,
|
||||
ca_certs=None, certfile=None, keyfile=None,
|
||||
ciphers=None,
|
||||
ciphers=None, ciphersuites=None,
|
||||
min_version=None, max_version=None,
|
||||
):
|
||||
if server_side:
|
||||
@@ -300,6 +311,8 @@ def make_test_context(
|
||||
|
||||
if ciphers is not None:
|
||||
context.set_ciphers(ciphers)
|
||||
if ciphersuites is not None:
|
||||
context.set_ciphersuites(ciphersuites)
|
||||
|
||||
if min_version is not None:
|
||||
context.minimum_version = min_version
|
||||
@@ -316,7 +329,7 @@ def test_wrap_socket(
|
||||
check_hostname=None,
|
||||
cert_reqs=ssl.CERT_NONE,
|
||||
ca_certs=None, certfile=None, keyfile=None,
|
||||
ciphers=None,
|
||||
ciphers=None, ciphersuites=None,
|
||||
min_version=None, max_version=None,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -325,7 +338,7 @@ def test_wrap_socket(
|
||||
check_hostname=check_hostname,
|
||||
cert_reqs=cert_reqs,
|
||||
ca_certs=ca_certs, certfile=certfile, keyfile=keyfile,
|
||||
ciphers=ciphers,
|
||||
ciphers=ciphers, ciphersuites=ciphersuites,
|
||||
min_version=min_version, max_version=max_version,
|
||||
)
|
||||
if not server_side:
|
||||
@@ -336,7 +349,8 @@ def test_wrap_socket(
|
||||
USE_SAME_TEST_CONTEXT = False
|
||||
_TEST_CONTEXT = None
|
||||
|
||||
def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
|
||||
def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True,
|
||||
client_cert=None):
|
||||
"""Create context
|
||||
|
||||
client_context, server_context, hostname = testing_context()
|
||||
@@ -363,6 +377,10 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
|
||||
if server_chain:
|
||||
server_context.load_verify_locations(SIGNING_CA)
|
||||
|
||||
if client_cert:
|
||||
client_context.load_cert_chain(client_cert)
|
||||
server_context.verify_mode = ssl.CERT_REQUIRED
|
||||
|
||||
if USE_SAME_TEST_CONTEXT:
|
||||
if _TEST_CONTEXT is not None:
|
||||
_TEST_CONTEXT = client_context, server_context, hostname
|
||||
@@ -384,6 +402,34 @@ def do_ssl_object_handshake(sslobject, outgoing, max_retry=25):
|
||||
return data
|
||||
|
||||
|
||||
def connected_bio_pair(client_context, server_context, hostname, max_retry=5):
|
||||
"""Handshake a client and a server SSLObject against each other.
|
||||
|
||||
Everything happens in memory, so this needs no socket and no thread.
|
||||
Returns the two objects followed by their four BIOs, in the order
|
||||
client, server, c_in, c_out, s_in, s_out.
|
||||
"""
|
||||
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
|
||||
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
|
||||
client = client_context.wrap_bio(c_in, c_out, server_hostname=hostname)
|
||||
server = server_context.wrap_bio(s_in, s_out, server_side=True)
|
||||
|
||||
# Loop on the handshake for a bit to get it settled
|
||||
for _ in range(max_retry):
|
||||
with contextlib.suppress(ssl.SSLWantReadError):
|
||||
client.do_handshake()
|
||||
if c_out.pending:
|
||||
s_in.write(c_out.read())
|
||||
with contextlib.suppress(ssl.SSLWantReadError):
|
||||
server.do_handshake()
|
||||
if s_out.pending:
|
||||
c_in.write(s_out.read())
|
||||
# Now the handshakes should be complete (don't raise WantReadError)
|
||||
client.do_handshake()
|
||||
server.do_handshake()
|
||||
return client, server, c_in, c_out, s_in, s_out
|
||||
|
||||
|
||||
class BasicSocketTests(unittest.TestCase):
|
||||
|
||||
def test_constants(self):
|
||||
@@ -396,7 +442,7 @@ class BasicSocketTests(unittest.TestCase):
|
||||
ssl.OP_NO_COMPRESSION
|
||||
self.assertEqual(ssl.HAS_SNI, True)
|
||||
self.assertEqual(ssl.HAS_ECDH, True)
|
||||
self.assertEqual(ssl.HAS_TLSv1_2, True)
|
||||
self.assertIsInstance(ssl.HAS_TLSv1_2, bool)
|
||||
self.assertEqual(ssl.HAS_TLSv1_3, True)
|
||||
ssl.OP_NO_SSLv2
|
||||
ssl.OP_NO_SSLv3
|
||||
@@ -587,11 +633,11 @@ class BasicSocketTests(unittest.TestCase):
|
||||
# Some sanity checks follow
|
||||
# >= 1.1.1
|
||||
self.assertGreaterEqual(n, 0x10101000)
|
||||
# < 4.0
|
||||
self.assertLess(n, 0x40000000)
|
||||
# < 5.0
|
||||
self.assertLess(n, 0x50000000)
|
||||
major, minor, fix, patch, status = t
|
||||
self.assertGreaterEqual(major, 1)
|
||||
self.assertLess(major, 4)
|
||||
self.assertLess(major, 5)
|
||||
self.assertGreaterEqual(minor, 0)
|
||||
self.assertLess(minor, 256)
|
||||
self.assertGreaterEqual(fix, 0)
|
||||
@@ -657,12 +703,14 @@ class BasicSocketTests(unittest.TestCase):
|
||||
ssl.OP_NO_TLSv1_2,
|
||||
ssl.OP_NO_TLSv1_3
|
||||
]
|
||||
protocols = [
|
||||
ssl.PROTOCOL_TLSv1,
|
||||
ssl.PROTOCOL_TLSv1_1,
|
||||
ssl.PROTOCOL_TLSv1_2,
|
||||
ssl.PROTOCOL_TLS
|
||||
]
|
||||
protocols = []
|
||||
if hasattr(ssl, 'PROTOCOL_TLSv1'):
|
||||
protocols.append(ssl.PROTOCOL_TLSv1)
|
||||
if hasattr(ssl, 'PROTOCOL_TLSv1_1'):
|
||||
protocols.append(ssl.PROTOCOL_TLSv1_1)
|
||||
if hasattr(ssl, 'PROTOCOL_TLSv1_2'):
|
||||
protocols.append(ssl.PROTOCOL_TLSv1_2)
|
||||
protocols.append(ssl.PROTOCOL_TLS)
|
||||
versions = [
|
||||
ssl.TLSVersion.SSLv3,
|
||||
ssl.TLSVersion.TLSv1,
|
||||
@@ -1027,6 +1075,58 @@ class ContextTests(unittest.TestCase):
|
||||
len(intersection), 2, f"\ngot: {sorted(names)}\nexpected: {sorted(expected)}"
|
||||
)
|
||||
|
||||
def test_set_groups(self):
|
||||
ctx = ssl.create_default_context()
|
||||
# We use P-256 and P-384 (FIPS 186-4) that are alloed by OpenSSL
|
||||
# even if FIPS module is enabled. Ignoring unknown groups is only
|
||||
# supported since OpenSSL 3.3.
|
||||
self.assertIsNone(ctx.set_groups('P-256:P-384'))
|
||||
|
||||
self.assertRaises(ssl.SSLError, ctx.set_groups, 'P-256:foo')
|
||||
if CAN_IGNORE_UNKNOWN_OPENSSL_GROUPS:
|
||||
self.assertIsNone(ctx.set_groups('P-256:?foo'))
|
||||
|
||||
@unittest.skipUnless(CAN_GET_AVAILABLE_OPENSSL_GROUPS,
|
||||
"OpenSSL version doesn't support getting groups")
|
||||
def test_get_groups(self):
|
||||
ctx = ssl.create_default_context()
|
||||
# By default, only return official IANA names.
|
||||
self.assertNotIn('P-256', ctx.get_groups())
|
||||
self.assertIn('P-256', ctx.get_groups(include_aliases=True))
|
||||
|
||||
@unittest.skipUnless(CAN_GET_AVAILABLE_OPENSSL_SIGALGS,
|
||||
"SSL library doesn't support getting sigalgs")
|
||||
def test_get_sigalgs(self):
|
||||
self.assertIn('rsa_pss_rsae_sha256', ssl.get_sigalgs())
|
||||
|
||||
@unittest.skipUnless(CAN_SET_CLIENT_SIGALGS,
|
||||
"SSL library doesn't support setting client sigalgs")
|
||||
def test_set_client_sigalgs(self):
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256'))
|
||||
|
||||
self.assertRaises(ssl.SSLError, ctx.set_client_sigalgs,
|
||||
'rsa_pss_rsae_sha256:foo')
|
||||
|
||||
# Ignoring unknown sigalgs is only supported since OpenSSL 3.3.
|
||||
if CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS:
|
||||
self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256:?foo'))
|
||||
|
||||
@unittest.skipUnless(CAN_SET_SERVER_SIGALGS,
|
||||
"SSL library doesn't support setting server sigalgs")
|
||||
def test_set_server_sigalgs(self):
|
||||
ctx = ssl.create_default_context()
|
||||
|
||||
self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256'))
|
||||
|
||||
self.assertRaises(ssl.SSLError, ctx.set_server_sigalgs,
|
||||
'rsa_pss_rsae_sha256:foo')
|
||||
|
||||
# Ignoring unknown sigalgs is only supported since OpenSSL 3.3.
|
||||
if CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS:
|
||||
self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256:?foo'))
|
||||
|
||||
def test_options(self):
|
||||
# Test default SSLContext options
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
@@ -1156,6 +1256,7 @@ class ContextTests(unittest.TestCase):
|
||||
ssl.TLSVersion.TLSv1,
|
||||
ssl.TLSVersion.TLSv1_1,
|
||||
ssl.TLSVersion.TLSv1_2,
|
||||
ssl.TLSVersion.TLSv1_3,
|
||||
ssl.TLSVersion.SSLv3,
|
||||
}
|
||||
)
|
||||
@@ -1169,7 +1270,7 @@ class ContextTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
ctx.minimum_version = 42
|
||||
|
||||
if has_tls_protocol(ssl.PROTOCOL_TLSv1_1):
|
||||
if has_tls_protocol('PROTOCOL_TLSv1_1'):
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1)
|
||||
|
||||
self.assertIn(
|
||||
@@ -1231,6 +1332,11 @@ class ContextTests(unittest.TestCase):
|
||||
with self.assertRaises(OSError) as cm:
|
||||
ctx.load_cert_chain(NONEXISTINGCERT)
|
||||
self.assertEqual(cm.exception.errno, errno.ENOENT)
|
||||
self.assertEqual(cm.exception.filename, NONEXISTINGCERT)
|
||||
with self.assertRaises(OSError) as cm:
|
||||
ctx.load_cert_chain(CERTFILE, keyfile=NONEXISTINGKEY)
|
||||
self.assertEqual(cm.exception.errno, errno.ENOENT)
|
||||
self.assertEqual(cm.exception.filename, NONEXISTINGKEY)
|
||||
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
|
||||
ctx.load_cert_chain(BADCERT)
|
||||
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
|
||||
@@ -1533,19 +1639,27 @@ class ContextTests(unittest.TestCase):
|
||||
gc.collect()
|
||||
self.assertIs(wr(), None)
|
||||
|
||||
@unittest.skipUnless(support.Py_GIL_DISABLED,
|
||||
"test is only useful if the GIL is disabled")
|
||||
@support.skip_if_sanitizer("gh-150191: OpenSSL has an internal data race "
|
||||
"when the SNI callback is replaced during a "
|
||||
"handshake", thread=True)
|
||||
@threading_helper.requires_working_threading()
|
||||
def test_sni_callback_race(self):
|
||||
# Replacing sni_callback while handshakes are in-flight must not
|
||||
# Replacing sni_callback while a handshake is in-flight must not
|
||||
# crash (use-after-free on the callback in free-threaded builds).
|
||||
#
|
||||
# Use a single handshake thread: OpenSSL has internal data races
|
||||
# on shared SSL_CTX state when multiple handshakes run
|
||||
# concurrently against the same context (gh-150191). Concurrency
|
||||
# on the *setter* is what exercises the fix from gh-149816, so
|
||||
# multiple toggler threads race against each other and against
|
||||
# the one handshake worker.
|
||||
client_ctx, server_ctx, hostname = testing_context()
|
||||
|
||||
server_ctx.sni_callback = lambda *a: None
|
||||
done = threading.Event()
|
||||
deadline = time.monotonic() + 0.1
|
||||
|
||||
def do_handshakes():
|
||||
while not done.is_set():
|
||||
while time.monotonic() < deadline:
|
||||
c_in = ssl.MemoryBIO()
|
||||
c_out = ssl.MemoryBIO()
|
||||
s_in = ssl.MemoryBIO()
|
||||
@@ -1572,19 +1686,11 @@ class ContextTests(unittest.TestCase):
|
||||
c_in.write(s_out.read())
|
||||
|
||||
def toggle_callback():
|
||||
while not done.is_set():
|
||||
while time.monotonic() < deadline:
|
||||
server_ctx.sni_callback = lambda *a: None
|
||||
server_ctx.sni_callback = None
|
||||
|
||||
workers = max(4, (os.cpu_count() or 4) * 2)
|
||||
threads = [threading.Thread(target=do_handshakes)
|
||||
for _ in range(workers)]
|
||||
threads.append(threading.Thread(target=toggle_callback))
|
||||
|
||||
with threading_helper.catch_threading_exception() as cm:
|
||||
with threading_helper.start_threads(threads):
|
||||
done.set()
|
||||
self.assertIsNone(cm.exc_value)
|
||||
threading_helper.run_concurrently([do_handshakes] + 4 * [toggle_callback])
|
||||
|
||||
def test_cert_store_stats(self):
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
@@ -1717,23 +1823,24 @@ class ContextTests(unittest.TestCase):
|
||||
self.assertFalse(ctx.check_hostname)
|
||||
self._assert_context_options(ctx)
|
||||
|
||||
if has_tls_protocol(ssl.PROTOCOL_TLSv1):
|
||||
if has_tls_protocol('PROTOCOL_TLSv1'):
|
||||
with warnings_helper.check_warnings():
|
||||
ctx = ssl._create_stdlib_context(ssl.PROTOCOL_TLSv1)
|
||||
self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1)
|
||||
self.assertEqual(ctx.verify_mode, ssl.CERT_NONE)
|
||||
self._assert_context_options(ctx)
|
||||
|
||||
with warnings_helper.check_warnings():
|
||||
ctx = ssl._create_stdlib_context(
|
||||
ssl.PROTOCOL_TLSv1_2,
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
check_hostname=True
|
||||
)
|
||||
self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1_2)
|
||||
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
|
||||
self.assertTrue(ctx.check_hostname)
|
||||
self._assert_context_options(ctx)
|
||||
if has_tls_protocol('PROTOCOL_TLSv1_2'):
|
||||
with warnings_helper.check_warnings():
|
||||
ctx = ssl._create_stdlib_context(
|
||||
ssl.PROTOCOL_TLSv1_2,
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
check_hostname=True
|
||||
)
|
||||
self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1_2)
|
||||
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
|
||||
self.assertTrue(ctx.check_hostname)
|
||||
self._assert_context_options(ctx)
|
||||
|
||||
ctx = ssl._create_stdlib_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLS_SERVER)
|
||||
@@ -1911,6 +2018,10 @@ class SSLErrorTests(unittest.TestCase):
|
||||
|
||||
def test_bad_server_hostname(self):
|
||||
ctx = ssl.create_default_context()
|
||||
# Omitting the name entirely is bad too: this context checks it.
|
||||
with self.assertRaises(ValueError):
|
||||
ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
|
||||
server_hostname=None)
|
||||
with self.assertRaises(ValueError):
|
||||
ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
|
||||
server_hostname="")
|
||||
@@ -1995,6 +2106,64 @@ class SSLObjectTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(TypeError, "public constructor"):
|
||||
ssl.SSLObject(bio, bio)
|
||||
|
||||
def test_check_hostname_requires_server_hostname(self):
|
||||
# wrap_bio() used to accept a context asking for hostname checking
|
||||
# without a name to check against, and then verify the certificate
|
||||
# chain but never the peer's identity, with check_hostname still
|
||||
# reporting True and nothing reporting the check had been skipped.
|
||||
# It must refuse that call, as wrap_socket() already did.
|
||||
client_context, _, hostname = testing_context()
|
||||
self.assertTrue(client_context.check_hostname)
|
||||
|
||||
for server_hostname in (None, ""):
|
||||
with self.subTest(server_hostname=server_hostname):
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"check_hostname requires server_hostname"):
|
||||
client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
|
||||
server_hostname=server_hostname)
|
||||
# The sibling constructor refuses the very same call.
|
||||
with socket.socket() as sock:
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"check_hostname requires server_hostname"):
|
||||
client_context.wrap_socket(
|
||||
sock, server_hostname=server_hostname)
|
||||
|
||||
# A name was all that was missing.
|
||||
client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
|
||||
server_hostname=hostname)
|
||||
|
||||
# Asking for no hostname check remains a way to say so explicitly.
|
||||
context = make_test_context()
|
||||
self.assertFalse(context.check_hostname)
|
||||
context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO())
|
||||
|
||||
def test_server_side_bad_params(self):
|
||||
# A server neither sends a hostname nor resumes a client's session,
|
||||
# so wrap_bio() rejects both in server mode like wrap_socket()
|
||||
client_context, server_context, hostname = testing_context()
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"server_hostname can only be specified in client mode"):
|
||||
server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
|
||||
server_side=True,
|
||||
server_hostname=hostname)
|
||||
|
||||
client, server, *_ = connected_bio_pair(
|
||||
client_context, server_context, hostname)
|
||||
session = client.session
|
||||
self.assertIsNotNone(session)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, "session can only be specified in client mode"):
|
||||
server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
|
||||
server_side=True, session=session)
|
||||
|
||||
# Neither argument is what a server passes, so this still works.
|
||||
server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
|
||||
server_side=True)
|
||||
|
||||
def test_unwrap(self):
|
||||
client_ctx, server_ctx, hostname = testing_context()
|
||||
c_in = ssl.MemoryBIO()
|
||||
@@ -2037,6 +2206,86 @@ class SSLObjectTests(unittest.TestCase):
|
||||
c_in.write(s_out.read())
|
||||
client.unwrap()
|
||||
|
||||
def test_sni_callback_context_released_and_callback_raises(self):
|
||||
# Variant of the test below without a HelloRetryRequest: the callback
|
||||
# switches the connection to another context, drops the last
|
||||
# references to the context that carries it, and raises. The C
|
||||
# callback must not touch that context after the Python callback
|
||||
# returned.
|
||||
client_ctx, server_ctx, hostname = testing_context()
|
||||
leaf_ctx = server_ctx
|
||||
|
||||
def sni_cb(sslobj, server_name, ctx):
|
||||
sslobj.context = leaf_ctx
|
||||
del ctx
|
||||
raise LookupError("no certificate for " + repr(server_name))
|
||||
|
||||
def make_server():
|
||||
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
|
||||
dispatch_ctx.sni_callback = sni_cb
|
||||
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
|
||||
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
|
||||
return server, s_in, s_out
|
||||
|
||||
server, s_in, s_out = make_server()
|
||||
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
|
||||
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
|
||||
with self.assertRaises(ssl.SSLWantReadError):
|
||||
client.do_handshake()
|
||||
s_in.write(c_out.read())
|
||||
with support.catch_unraisable_exception() as cm:
|
||||
with self.assertRaises(ssl.SSLError):
|
||||
server.do_handshake()
|
||||
self.assertIsInstance(cm.unraisable.exc_value, LookupError)
|
||||
self.assertIs(server.context, leaf_ctx)
|
||||
|
||||
def test_sni_callback_context_released_before_second_client_hello(self):
|
||||
# The SSLContext carrying sni_callback may be released by the
|
||||
# application once the callback has switched the connection over to
|
||||
# another context. If the server then sends a HelloRetryRequest, the
|
||||
# second ClientHello makes OpenSSL consult the original SSL_CTX's
|
||||
# servername callback again; that must not use the deallocated
|
||||
# SSLContext object.
|
||||
client_ctx, leaf_ctx, hostname = testing_context()
|
||||
calls = []
|
||||
|
||||
def make_server():
|
||||
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
|
||||
# Force a HelloRetryRequest: the client offers an X25519 key
|
||||
# share first, the server only accepts P-384.
|
||||
dispatch_ctx.set_ecdh_curve("secp384r1")
|
||||
def sni_cb(sslobj, server_name, ctx):
|
||||
calls.append(server_name)
|
||||
sslobj.context = leaf_ctx
|
||||
dispatch_ctx.sni_callback = sni_cb
|
||||
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
|
||||
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
|
||||
return server, s_in, s_out, weakref.ref(dispatch_ctx)
|
||||
|
||||
# After this only the C-level SSL object references dispatch_ctx.
|
||||
server, s_in, s_out, dispatch_ref = make_server()
|
||||
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
|
||||
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
|
||||
for _ in range(10):
|
||||
for obj, out, peer_in in ((client, c_out, s_in),
|
||||
(server, s_out, c_in)):
|
||||
try:
|
||||
obj.do_handshake()
|
||||
except ssl.SSLWantReadError:
|
||||
pass
|
||||
if out.pending:
|
||||
peer_in.write(out.read())
|
||||
client.do_handshake()
|
||||
server.do_handshake()
|
||||
support.gc_collect()
|
||||
self.assertIsNone(dispatch_ref())
|
||||
self.assertGreaterEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0], hostname)
|
||||
self.assertIs(server.context, leaf_ctx)
|
||||
self.assertIsNotNone(client.cipher())
|
||||
|
||||
class SimpleBackgroundTests(unittest.TestCase):
|
||||
"""Tests that connect to a simple server running in the background"""
|
||||
|
||||
@@ -2417,6 +2666,68 @@ class SimpleBackgroundTests(unittest.TestCase):
|
||||
self.assertRaises(ssl.SSLEOFError, sslobj.read)
|
||||
|
||||
|
||||
@unittest.skipUnless(has_tls_version('TLSv1_3'), "TLS 1.3 is not available")
|
||||
class SimpleBackgroundTestsTLS_1_3(unittest.TestCase):
|
||||
"""Tests that connect to a simple server running in the background."""
|
||||
|
||||
def setUp(self):
|
||||
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ciphers = [cipher['name'] for cipher in server_ctx.get_ciphers()
|
||||
if cipher['protocol'] == 'TLSv1.3']
|
||||
|
||||
if not ciphers:
|
||||
self.skipTest("No cipher supports TLSv1.3")
|
||||
|
||||
self.matching_cipher = ciphers[0]
|
||||
# Some tests need at least two ciphers, and are responsible
|
||||
# to skip themselves if matching_cipher == mismatched_cipher.
|
||||
self.mismatched_cipher = ciphers[-1]
|
||||
|
||||
server_ctx.set_ciphersuites(self.matching_cipher)
|
||||
server_ctx.load_cert_chain(SIGNED_CERTFILE)
|
||||
server = ThreadedEchoServer(context=server_ctx)
|
||||
self.enterContext(server)
|
||||
self.server_addr = (HOST, server.port)
|
||||
|
||||
def test_ciphersuites(self):
|
||||
# Test unrecognized TLS 1.3 cipher suite name
|
||||
with (
|
||||
socket.socket(socket.AF_INET) as sock,
|
||||
self.assertRaisesRegex(ssl.SSLError,
|
||||
"No cipher suite can be selected")
|
||||
):
|
||||
test_wrap_socket(sock, cert_reqs=ssl.CERT_NONE,
|
||||
ciphersuites="XXX",
|
||||
min_version=ssl.TLSVersion.TLSv1_3)
|
||||
|
||||
# Test successful TLS 1.3 handshake
|
||||
with test_wrap_socket(socket.socket(socket.AF_INET),
|
||||
cert_reqs=ssl.CERT_NONE,
|
||||
ciphersuites=self.matching_cipher,
|
||||
min_version=ssl.TLSVersion.TLSv1_3) as s:
|
||||
s.connect(self.server_addr)
|
||||
self.assertEqual(s.cipher()[0], self.matching_cipher)
|
||||
|
||||
def test_ciphersuite_downgrade(self):
|
||||
with test_wrap_socket(socket.socket(socket.AF_INET),
|
||||
cert_reqs=ssl.CERT_NONE,
|
||||
ciphersuites=self.matching_cipher,
|
||||
min_version=ssl.TLSVersion.TLSv1_2,
|
||||
max_version=ssl.TLSVersion.TLSv1_2) as s:
|
||||
s.connect(self.server_addr)
|
||||
self.assertEqual(s.cipher()[1], 'TLSv1.2')
|
||||
|
||||
def test_ciphersuite_mismatch(self):
|
||||
if self.matching_cipher == self.mismatched_cipher:
|
||||
self.skipTest("Multiple TLS 1.3 ciphers are not available")
|
||||
|
||||
with test_wrap_socket(socket.socket(socket.AF_INET),
|
||||
cert_reqs=ssl.CERT_NONE,
|
||||
ciphersuites=self.mismatched_cipher,
|
||||
min_version=ssl.TLSVersion.TLSv1_3) as s:
|
||||
self.assertRaises(ssl.SSLError, s.connect, self.server_addr)
|
||||
|
||||
|
||||
@support.requires_resource('network')
|
||||
class NetworkedTests(unittest.TestCase):
|
||||
|
||||
@@ -2760,6 +3071,36 @@ class ThreadedEchoServer(threading.Thread):
|
||||
def stop(self):
|
||||
self.active = False
|
||||
|
||||
class TestEOFServer(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.listening = threading.Event()
|
||||
self.address = None
|
||||
|
||||
def run(self):
|
||||
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(CERTFILE)
|
||||
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
with server_sock:
|
||||
server_sock.settimeout(support.SHORT_TIMEOUT)
|
||||
server_sock.bind((HOST, 0))
|
||||
server_sock.listen(5)
|
||||
|
||||
self.address = server_sock.getsockname()
|
||||
self.listening.set()
|
||||
|
||||
sock, addr = server_sock.accept()
|
||||
sslconn = context.wrap_socket(sock, server_side=True)
|
||||
with sslconn:
|
||||
request = b''
|
||||
while chunk := sslconn.recv(1024):
|
||||
request += chunk
|
||||
if b'\n' in chunk:
|
||||
break
|
||||
|
||||
sslconn.sendall(b'server\n')
|
||||
sslconn.shutdown(socket.SHUT_WR)
|
||||
|
||||
class AsyncoreEchoServer(threading.Thread):
|
||||
|
||||
# this one's based on asyncore.dispatcher
|
||||
@@ -2921,6 +3262,11 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
|
||||
'session_reused': s.session_reused,
|
||||
'session': s.session,
|
||||
})
|
||||
if CAN_GET_SELECTED_OPENSSL_GROUP:
|
||||
stats.update({'group': s.group()})
|
||||
if CAN_GET_SELECTED_OPENSSL_SIGALG:
|
||||
stats.update({'client_sigalg': s.client_sigalg()})
|
||||
stats.update({'server_sigalg': s.server_sigalg()})
|
||||
s.close()
|
||||
stats['server_alpn_protocols'] = server.selected_alpn_protocols
|
||||
stats['server_shared_ciphers'] = server.shared_ciphers
|
||||
@@ -3629,10 +3975,10 @@ class ThreadedTests(unittest.TestCase):
|
||||
client_options=ssl.OP_NO_TLSv1_2)
|
||||
|
||||
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1_2, 'TLSv1.2')
|
||||
if has_tls_protocol(ssl.PROTOCOL_TLSv1):
|
||||
if has_tls_protocol('PROTOCOL_TLSv1'):
|
||||
try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1, False)
|
||||
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1_2, False)
|
||||
if has_tls_protocol(ssl.PROTOCOL_TLSv1_1):
|
||||
if has_tls_protocol('PROTOCOL_TLSv1_1'):
|
||||
try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1_1, False)
|
||||
try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_TLSv1_2, False)
|
||||
|
||||
@@ -4071,6 +4417,8 @@ class ThreadedTests(unittest.TestCase):
|
||||
with self.assertRaises(OSError):
|
||||
s.connect((HOST, server.port))
|
||||
self.assertIn("NO_SHARED_CIPHER", server.conn_errors[0])
|
||||
self.assertIsNone(s.cipher())
|
||||
self.assertIsNone(s.group())
|
||||
|
||||
def test_version_basic(self):
|
||||
"""
|
||||
@@ -4346,6 +4694,113 @@ class ThreadedTests(unittest.TestCase):
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
|
||||
def test_groups(self):
|
||||
# server secp384r1, client auto
|
||||
client_context, server_context, hostname = testing_context()
|
||||
|
||||
server_context.set_groups("secp384r1")
|
||||
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
stats = server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
if CAN_GET_SELECTED_OPENSSL_GROUP:
|
||||
self.assertEqual(stats['group'], "secp384r1")
|
||||
|
||||
# server auto, client secp384r1
|
||||
client_context, server_context, hostname = testing_context()
|
||||
client_context.set_groups("secp384r1")
|
||||
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
stats = server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
if CAN_GET_SELECTED_OPENSSL_GROUP:
|
||||
self.assertEqual(stats['group'], "secp384r1")
|
||||
|
||||
# server / client curve mismatch
|
||||
client_context, server_context, hostname = testing_context()
|
||||
client_context.set_groups("prime256v1")
|
||||
server_context.set_groups("secp384r1")
|
||||
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
with self.assertRaises(ssl.SSLError):
|
||||
server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
|
||||
@unittest.skipUnless(CAN_SET_CLIENT_SIGALGS,
|
||||
"SSL library doesn't support setting client sigalgs")
|
||||
def test_client_sigalgs(self):
|
||||
# no mutual auth, so cient_sigalg should be None
|
||||
client_context, server_context, hostname = testing_context()
|
||||
stats = server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
if CAN_GET_SELECTED_OPENSSL_SIGALG:
|
||||
self.assertIsNone(stats['client_sigalg'])
|
||||
|
||||
# server auto, client rsa_pss_rsae_sha384
|
||||
sigalg = "rsa_pss_rsae_sha384"
|
||||
client_context, server_context, hostname = \
|
||||
testing_context(client_cert=SIGNED_CERTFILE)
|
||||
client_context.set_client_sigalgs(sigalg)
|
||||
stats = server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
if CAN_GET_SELECTED_OPENSSL_SIGALG:
|
||||
self.assertEqual(stats['client_sigalg'], sigalg)
|
||||
|
||||
@unittest.skipUnless(CAN_SET_CLIENT_SIGALGS,
|
||||
"SSL library doesn't support setting client sigalgs")
|
||||
def test_client_sigalgs_mismatch(self):
|
||||
client_context, server_context, hostname = \
|
||||
testing_context(client_cert=SIGNED_CERTFILE)
|
||||
client_context.set_client_sigalgs("rsa_pss_rsae_sha256")
|
||||
server_context.set_client_sigalgs("rsa_pss_rsae_sha384")
|
||||
|
||||
with self.assertRaises((
|
||||
ssl.SSLError,
|
||||
# On handshake failures, some systems raise a ConnectionResetError.
|
||||
ConnectionResetError,
|
||||
# On handshake failures, macOS may raise a BrokenPipeError.
|
||||
# See https://github.com/python/cpython/issues/139504.
|
||||
BrokenPipeError,
|
||||
)):
|
||||
server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
|
||||
@unittest.skipUnless(CAN_SET_SERVER_SIGALGS,
|
||||
"SSL library doesn't support setting server sigalgs")
|
||||
def test_server_sigalgs(self):
|
||||
# server rsa_pss_rsae_sha384, client auto
|
||||
sigalg = "rsa_pss_rsae_sha384"
|
||||
client_context, server_context, hostname = testing_context()
|
||||
server_context.set_server_sigalgs(sigalg)
|
||||
stats = server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
if CAN_GET_SELECTED_OPENSSL_SIGALG:
|
||||
self.assertEqual(stats['server_sigalg'], sigalg)
|
||||
|
||||
# server auto, client rsa_pss_rsae_sha384
|
||||
client_context, server_context, hostname = testing_context()
|
||||
client_context.set_server_sigalgs(sigalg)
|
||||
stats = server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
if CAN_GET_SELECTED_OPENSSL_SIGALG:
|
||||
self.assertEqual(stats['server_sigalg'], sigalg)
|
||||
|
||||
@unittest.skipUnless(CAN_SET_SERVER_SIGALGS,
|
||||
"SSL library doesn't support setting server sigalgs")
|
||||
def test_server_sigalgs_mismatch(self):
|
||||
client_context, server_context, hostname = testing_context()
|
||||
client_context.set_server_sigalgs("rsa_pss_rsae_sha256")
|
||||
server_context.set_server_sigalgs("rsa_pss_rsae_sha384")
|
||||
with self.assertRaises(ssl.SSLError):
|
||||
server_params_test(client_context, server_context,
|
||||
chatty=True, connectionchatty=True,
|
||||
sni_name=hostname)
|
||||
|
||||
def test_selected_alpn_protocol(self):
|
||||
# selected_alpn_protocol() is None unless ALPN is used.
|
||||
client_context, server_context, hostname = testing_context()
|
||||
@@ -4481,7 +4936,9 @@ class ThreadedTests(unittest.TestCase):
|
||||
sni_name='supermessage')
|
||||
|
||||
# Allow for flexible libssl error messages.
|
||||
regex = "(SSLV3_ALERT_HANDSHAKE_FAILURE|NO_PRIVATE_VALUE)"
|
||||
regex = ("(TLS_ALERT_HANDSHAKE_FAILURE"
|
||||
"|SSLV3_ALERT_HANDSHAKE_FAILURE"
|
||||
"|NO_PRIVATE_VALUE)")
|
||||
self.assertRegex(cm.exception.reason, regex)
|
||||
self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError)
|
||||
|
||||
@@ -4536,19 +4993,30 @@ class ThreadedTests(unittest.TestCase):
|
||||
self.assertRaises(ValueError, s.write, b'hello')
|
||||
|
||||
def test_sendfile(self):
|
||||
"""Try to send a file using kTLS if possible."""
|
||||
TEST_DATA = b"x" * 512
|
||||
with open(os_helper.TESTFN, 'wb') as f:
|
||||
f.write(TEST_DATA)
|
||||
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
|
||||
client_context, server_context, hostname = testing_context()
|
||||
client_context.options |= getattr(ssl, 'OP_ENABLE_KTLS', 0)
|
||||
server = ThreadedEchoServer(context=server_context, chatty=False)
|
||||
with server:
|
||||
with client_context.wrap_socket(socket.socket(),
|
||||
server_hostname=hostname) as s:
|
||||
s.connect((HOST, server.port))
|
||||
# kTLS seems to work only with a connection created before
|
||||
# wrapping `sock` by the SSL context in contrast to calling
|
||||
# `sock.connect()` after the wrapping.
|
||||
with server, socket.create_connection((HOST, server.port)) as sock:
|
||||
with client_context.wrap_socket(
|
||||
sock, server_hostname=hostname
|
||||
) as ssock:
|
||||
if support.verbose:
|
||||
ktls_used = ssock._sslobj.uses_ktls_for_send()
|
||||
print(
|
||||
'kTLS is',
|
||||
'available' if ktls_used else 'unavailable',
|
||||
)
|
||||
with open(os_helper.TESTFN, 'rb') as file:
|
||||
s.sendfile(file)
|
||||
self.assertEqual(s.recv(1024), TEST_DATA)
|
||||
ssock.sendfile(file)
|
||||
self.assertEqual(ssock.recv(1024), TEST_DATA)
|
||||
|
||||
def test_session(self):
|
||||
client_context, server_context, hostname = testing_context()
|
||||
@@ -4727,6 +5195,7 @@ class ThreadedTests(unittest.TestCase):
|
||||
|
||||
@requires_tls_version('TLSv1_3')
|
||||
@unittest.skipUnless(ssl.HAS_PSK, 'TLS-PSK disabled on this OpenSSL build')
|
||||
@unittest.skipUnless(ssl.HAS_PSK_TLS13, 'TLS 1.3 PSK disabled on this OpenSSL build')
|
||||
def test_psk_tls1_3(self):
|
||||
psk = bytes.fromhex('deadbeef')
|
||||
identity_hint = 'identity-hint'
|
||||
@@ -4758,6 +5227,9 @@ class ThreadedTests(unittest.TestCase):
|
||||
with client_context.wrap_socket(socket.socket()) as s:
|
||||
s.connect((HOST, server.port))
|
||||
|
||||
@support.skip_if_sanitizer("gh-150191: OpenSSL races on SSL->rwstate and "
|
||||
"the socket BIO flags with concurrent read "
|
||||
"and write", thread=True)
|
||||
def test_thread_recv_while_main_thread_sends(self):
|
||||
# GH-137583: Locking was added to calls to send() and recv() on SSL
|
||||
# socket objects. This seemed fine at the surface level because those
|
||||
@@ -4794,6 +5266,50 @@ class ThreadedTests(unittest.TestCase):
|
||||
if cm.exc_value is not None:
|
||||
raise cm.exc_value
|
||||
|
||||
def test_got_eof(self):
|
||||
# gh-148292: Test that _ssl._SSLSocket behaves the same on all OpenSSL
|
||||
# versions on calling methods after EOF (after the first SSLEOFError).
|
||||
|
||||
server = TestEOFServer()
|
||||
server.start()
|
||||
if not server.listening.wait(support.SHORT_TIMEOUT):
|
||||
raise RuntimeError("server took too long")
|
||||
self.addCleanup(server.join)
|
||||
|
||||
context = ssl.create_default_context(cafile=CERTFILE)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(support.SHORT_TIMEOUT)
|
||||
sock.connect(server.address)
|
||||
sslsock = context.wrap_socket(sock, server_hostname='localhost')
|
||||
with sslsock:
|
||||
sslsock.sendall(b'client\n')
|
||||
# test the _ssl._SSLSocket object, not ssl.SSLSocket
|
||||
sslobj = sslsock._sslobj
|
||||
|
||||
data = sslobj.read(1024)
|
||||
self.assertEqual(data, b'server\n')
|
||||
|
||||
# The second read gets EOF error and sets got_eof_error to 1
|
||||
with self.assertRaises(ssl.SSLEOFError):
|
||||
sslobj.read(1024)
|
||||
|
||||
# Following read(), sendfile(), write() and do_handshake() calls
|
||||
# must raise SSLEOFError
|
||||
with self.assertRaises(ssl.SSLEOFError):
|
||||
# The _SSLSocket remembers the previous EOF error
|
||||
# and raises again SSLEOFError
|
||||
sslobj.read(1024)
|
||||
if hasattr(sslobj, 'sendfile'):
|
||||
with open(__file__, "rb") as fp:
|
||||
with self.assertRaises(ssl.SSLEOFError):
|
||||
sslobj.sendfile(fp.fileno(), 0, 1)
|
||||
with self.assertRaises(ssl.SSLEOFError):
|
||||
sslobj.write(b'client2\n')
|
||||
with self.assertRaises(ssl.SSLEOFError):
|
||||
sslsock.do_handshake()
|
||||
|
||||
self.assertEqual(sslsock.pending(), 0)
|
||||
|
||||
|
||||
@unittest.skipUnless(has_tls_version('TLSv1_3') and ssl.HAS_PHA,
|
||||
"Test needs TLS 1.3 PHA")
|
||||
@@ -5220,6 +5736,20 @@ class TestSSLDebug(unittest.TestCase):
|
||||
with self.assertRaises(TypeError):
|
||||
client_context._msg_callback = object()
|
||||
|
||||
def test_msg_callback_exception(self):
|
||||
client_context, server_context, hostname = testing_context()
|
||||
|
||||
def msg_cb(conn, direction, version, content_type, msg_type, data):
|
||||
raise RuntimeError("msg_cb exception")
|
||||
|
||||
client_context._msg_callback = msg_cb
|
||||
server = ThreadedEchoServer(context=server_context, chatty=False)
|
||||
with server:
|
||||
with client_context.wrap_socket(socket.socket(),
|
||||
server_hostname=hostname) as s:
|
||||
with self.assertRaisesRegex(RuntimeError, "msg_cb exception"):
|
||||
s.connect((HOST, server.port))
|
||||
|
||||
def test_msg_callback_tls12(self):
|
||||
client_context, server_context, hostname = testing_context()
|
||||
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||
|
||||
@@ -444,9 +444,9 @@ pub fn kx_group_by_openssl_name(name: &str) -> Option<&'static dyn SupportedKxGr
|
||||
|
||||
fn kx_group_openssl_names(name: rustls::NamedGroup) -> &'static [&'static str] {
|
||||
match name {
|
||||
rustls::NamedGroup::secp256r1 => &["prime256v1", "secp256r1"],
|
||||
rustls::NamedGroup::secp384r1 => &["secp384r1", "prime384v1"],
|
||||
rustls::NamedGroup::secp521r1 => &["secp521r1", "prime521v1"],
|
||||
rustls::NamedGroup::secp256r1 => &["prime256v1", "secp256r1", "P-256"],
|
||||
rustls::NamedGroup::secp384r1 => &["secp384r1", "prime384v1", "P-384"],
|
||||
rustls::NamedGroup::secp521r1 => &["secp521r1", "prime521v1", "P-521"],
|
||||
rustls::NamedGroup::X25519 => &["X25519", "x25519"],
|
||||
rustls::NamedGroup::X448 => &["X448", "x448"],
|
||||
rustls::NamedGroup::MLKEM768 => &["MLKEM768"],
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct ProtocolSettings {
|
||||
pub versions: &'static [&'static rustls::SupportedProtocolVersion],
|
||||
pub kx_groups: Option<Vec<&'static dyn rustls::crypto::SupportedKxGroup>>,
|
||||
pub cipher_suites: Option<Vec<rustls::SupportedCipherSuite>>,
|
||||
pub server_sigalgs: Option<Vec<rustls::SignatureScheme>>,
|
||||
pub alpn_protocols: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
@@ -176,12 +177,27 @@ pub fn create_server_config(options: ServerConfigOptions) -> Result<chain::Serve
|
||||
// Add certificate
|
||||
let mut config = if let Some(resolver) = options.cert_resolver {
|
||||
// Use custom cert resolver (e.g., for SNI)
|
||||
let resolver = if let Some(schemes) = options.protocol_settings.server_sigalgs.clone() {
|
||||
Arc::new(super::sigalg::ServerResolver {
|
||||
inner: resolver,
|
||||
schemes,
|
||||
}) as Arc<dyn ResolvesServerCert>
|
||||
} else {
|
||||
resolver
|
||||
};
|
||||
builder.with_cert_resolver(resolver)
|
||||
} else {
|
||||
// Use single certificate
|
||||
builder
|
||||
.with_single_cert(options.cert_chain, options.private_key)
|
||||
.map_err(|e| format!("Failed to set server certificate: {e}"))?
|
||||
let key = CertifiedKey::from_der(
|
||||
options.cert_chain,
|
||||
options.private_key,
|
||||
CryptoExt::get_provider(),
|
||||
)
|
||||
.map_err(|e| format!("Failed to set server certificate: {e}"))?;
|
||||
builder.with_cert_resolver(Arc::new(MultiCertResolver::new(
|
||||
vec![Arc::new(key)],
|
||||
options.protocol_settings.server_sigalgs.as_deref(),
|
||||
)))
|
||||
};
|
||||
|
||||
// Set ALPN protocols with fallback
|
||||
@@ -432,6 +448,15 @@ pub fn create_client_config(options: ClientConfigOptions) -> Result<chain::Clien
|
||||
Arc::new(NoVerifier)
|
||||
};
|
||||
|
||||
let verifier = if let Some(schemes) = options.protocol_settings.server_sigalgs {
|
||||
Arc::new(super::sigalg::ServerVerifier {
|
||||
inner: verifier,
|
||||
schemes,
|
||||
}) as Arc<dyn rustls::client::danger::ServerCertVerifier>
|
||||
} else {
|
||||
verifier
|
||||
};
|
||||
|
||||
// Step 2: Create ClientConfig builder once with the selected verifier
|
||||
let builder = ClientConfig::builder_with_provider(custom_provider)
|
||||
.with_protocol_versions(options.protocol_settings.versions)
|
||||
@@ -481,7 +506,15 @@ pub struct MultiCertResolver {
|
||||
|
||||
impl MultiCertResolver {
|
||||
/// Create a new multi-certificate resolver
|
||||
pub fn new(cert_keys: Vec<Arc<CertifiedKey>>) -> Self {
|
||||
pub fn new(
|
||||
mut cert_keys: Vec<Arc<CertifiedKey>>,
|
||||
schemes: Option<&[rustls::SignatureScheme]>,
|
||||
) -> Self {
|
||||
if let Some(schemes) = schemes {
|
||||
for key in &mut cert_keys {
|
||||
*key = super::sigalg::restrict_key(key, schemes);
|
||||
}
|
||||
}
|
||||
Self { cert_keys }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ impl TlsError {
|
||||
use rustls::PeerIncompatible;
|
||||
match peer_err {
|
||||
PeerIncompatible::NoCipherSuitesInCommon => Self::NoCipherSuites,
|
||||
PeerIncompatible::NoSignatureSchemesInCommon => {
|
||||
Self::Ssl("no signature schemes in common".to_owned())
|
||||
}
|
||||
_ => Self::Eof,
|
||||
}
|
||||
}
|
||||
@@ -66,3 +69,38 @@ impl From<std::io::Error> for TlsError {
|
||||
Self::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn signature_incompatibility_is_not_transport_eof() {
|
||||
let error = TlsError::from_rustls(rustls::Error::PeerIncompatible(
|
||||
rustls::PeerIncompatible::NoSignatureSchemesInCommon,
|
||||
));
|
||||
assert!(!error.is_eof());
|
||||
assert!(matches!(error, TlsError::Ssl(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_negotiation_and_transport_errors_keep_their_classification() {
|
||||
assert!(matches!(
|
||||
TlsError::from_rustls(rustls::Error::PeerIncompatible(
|
||||
rustls::PeerIncompatible::NoCipherSuitesInCommon,
|
||||
)),
|
||||
TlsError::NoCipherSuites
|
||||
));
|
||||
assert!(matches!(
|
||||
TlsError::from_rustls(rustls::Error::PeerIncompatible(
|
||||
rustls::PeerIncompatible::Tls12NotOffered,
|
||||
)),
|
||||
TlsError::Eof
|
||||
));
|
||||
let os_error = std::io::Error::from_raw_os_error(10053);
|
||||
match TlsError::from(os_error) {
|
||||
TlsError::Io(error) => assert_eq!(error.raw_os_error(), Some(10053)),
|
||||
error => panic!("OS error was reclassified: {error:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,34 @@ pub fn sni_alert(description: u8) -> Vec<u8> {
|
||||
vec![21, 3, 3, 0, 2, 2, description]
|
||||
}
|
||||
|
||||
/// Serialize a failed initial `Accepted::into_connection` attempt.
|
||||
///
|
||||
/// No server bytes have been sent at this boundary. Rustls may have queued
|
||||
/// ServerHello before finding that a full handshake has no usable signature,
|
||||
/// or may return a signing-key error without queuing any alert (TLS 1.2).
|
||||
/// Send only a plaintext fatal alert, so the peer cannot start its next flight
|
||||
/// while the server is closing. Keep certificate/session selection in rustls:
|
||||
/// resumed sessions do not need a new certificate signature.
|
||||
pub fn initial_handshake_alert(
|
||||
error: &rustls::Error,
|
||||
mut alert: rustls::server::AcceptedAlert,
|
||||
) -> std::io::Result<Vec<u8>> {
|
||||
// TLS 1.2's emit_server_kx reports the exact General message below when
|
||||
// SigningKey::choose_scheme fails; TLS 1.3 uses PeerIncompatible.
|
||||
let signature_mismatch = matches!(
|
||||
error,
|
||||
rustls::Error::PeerIncompatible(rustls::PeerIncompatible::NoSignatureSchemesInCommon)
|
||||
) || matches!(error, rustls::Error::General(message) if message == "incompatible signing key");
|
||||
if signature_mismatch {
|
||||
return Ok(sni_alert(u8::from(
|
||||
rustls::AlertDescription::HandshakeFailure,
|
||||
)));
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
alert.write_all(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn feed_acceptor(acceptor: &mut Acceptor, bytes: &[u8]) -> std::io::Result<()> {
|
||||
let mut reader = std::io::Cursor::new(bytes);
|
||||
while reader.position() < bytes.len() as u64 {
|
||||
@@ -107,3 +135,322 @@ mod tests {
|
||||
assert_eq!(sni_alert(80), [21, 3, 3, 0, 2, 2, 80]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod signature_tests {
|
||||
use super::super::config::MultiCertResolver;
|
||||
use super::*;
|
||||
use alloc::sync::Arc;
|
||||
use rustls::{
|
||||
ClientConnection, SignatureScheme, client::WebPkiServerVerifier, server::Acceptor,
|
||||
};
|
||||
use rustls::{RootCertStore, SupportedCipherSuite, crypto::CryptoProvider, sign::CertifiedKey};
|
||||
use rustls::{client::ClientConfig, server::ServerConfig};
|
||||
|
||||
fn certificate_key(mut pem: &[u8], provider: &CryptoProvider) -> Arc<CertifiedKey> {
|
||||
let mut private_key_pem = pem;
|
||||
let certificates = rustls_pemfile::certs(&mut pem)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
let key = rustls_pemfile::private_key(&mut private_key_pem)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
Arc::new(CertifiedKey::from_der(certificates, key, provider).unwrap())
|
||||
}
|
||||
|
||||
fn client_config(
|
||||
version: &'static rustls::SupportedProtocolVersion,
|
||||
scheme: SignatureScheme,
|
||||
cipher_suite: Option<SupportedCipherSuite>,
|
||||
) -> Arc<ClientConfig> {
|
||||
let mut provider = rustls::crypto::aws_lc_rs::default_provider();
|
||||
if let Some(suite) = cipher_suite {
|
||||
provider.cipher_suites = vec![suite];
|
||||
}
|
||||
let provider = Arc::new(provider);
|
||||
let mut roots = RootCertStore::empty();
|
||||
let ca = include_bytes!("../../../../Lib/test/certdata/pycacert.pem");
|
||||
for cert in rustls_pemfile::certs(&mut &ca[..]) {
|
||||
roots.add(cert.unwrap()).unwrap();
|
||||
}
|
||||
let verifier =
|
||||
WebPkiServerVerifier::builder_with_provider(Arc::new(roots), provider.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
let config = ClientConfig::builder_with_provider(provider)
|
||||
.with_protocol_versions(&[version])
|
||||
.unwrap()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(super::super::sigalg::ServerVerifier {
|
||||
inner: verifier,
|
||||
schemes: vec![scheme],
|
||||
}))
|
||||
.with_no_client_auth();
|
||||
Arc::new(config)
|
||||
}
|
||||
|
||||
fn client_hello(config: Arc<ClientConfig>) -> (ClientConnection, rustls::server::Accepted) {
|
||||
let mut client = ClientConnection::new(config, "localhost".try_into().unwrap()).unwrap();
|
||||
let mut hello = Vec::new();
|
||||
client.write_tls(&mut hello).unwrap();
|
||||
let mut acceptor = Acceptor::default();
|
||||
super::super::handshake::feed_acceptor(&mut acceptor, &hello).unwrap();
|
||||
(client, acceptor.accept().unwrap().unwrap())
|
||||
}
|
||||
|
||||
fn server_config(
|
||||
version: &'static rustls::SupportedProtocolVersion,
|
||||
include_incompatible_first_key: bool,
|
||||
cipher_suite: Option<SupportedCipherSuite>,
|
||||
scheme: SignatureScheme,
|
||||
) -> Arc<ServerConfig> {
|
||||
let mut provider = rustls::crypto::aws_lc_rs::default_provider();
|
||||
if let Some(suite) = cipher_suite {
|
||||
provider.cipher_suites = vec![suite];
|
||||
}
|
||||
let provider = Arc::new(provider);
|
||||
let mut keys = Vec::new();
|
||||
if include_incompatible_first_key {
|
||||
keys.push(certificate_key(
|
||||
include_bytes!("../../../../Lib/test/certdata/keycertecc.pem"),
|
||||
&provider,
|
||||
));
|
||||
}
|
||||
keys.push(certificate_key(
|
||||
include_bytes!("../../../../Lib/test/certdata/keycert3.pem"),
|
||||
&provider,
|
||||
));
|
||||
Arc::new(
|
||||
ServerConfig::builder_with_provider(provider)
|
||||
.with_protocol_versions(&[version])
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(MultiCertResolver::new(keys, Some(&[scheme])))),
|
||||
)
|
||||
}
|
||||
|
||||
fn assert_signature_mismatch_sends_only_a_fatal_alert(
|
||||
version: &'static rustls::SupportedProtocolVersion,
|
||||
) {
|
||||
let (mut client, accepted) = client_hello(client_config(
|
||||
version,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
None,
|
||||
));
|
||||
let (error, alert) = accepted
|
||||
.into_connection(server_config(
|
||||
version,
|
||||
false,
|
||||
None,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
))
|
||||
.unwrap_err();
|
||||
let bytes = initial_handshake_alert(&error, alert).unwrap();
|
||||
assert!(matches!(
|
||||
super::super::error::TlsError::from_rustls(error),
|
||||
super::super::error::TlsError::Ssl(_)
|
||||
));
|
||||
assert_eq!(bytes.len(), 7, "only a plaintext alert may precede failure");
|
||||
assert_eq!(bytes[0], 21, "no ServerHello or CCS may precede the alert");
|
||||
assert_eq!(&bytes[3..6], &[0, 2, 2]);
|
||||
client.read_tls(&mut &bytes[..]).unwrap();
|
||||
assert!(matches!(
|
||||
client.process_new_packets(),
|
||||
Err(rustls::Error::AlertReceived(_))
|
||||
));
|
||||
assert!(
|
||||
!client.wants_write(),
|
||||
"a rejected client must not queue CCS"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls12_signature_mismatch_sends_only_a_fatal_alert() {
|
||||
assert_signature_mismatch_sends_only_a_fatal_alert(&rustls::version::TLS12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls13_signature_mismatch_sends_only_a_fatal_alert() {
|
||||
assert_signature_mismatch_sends_only_a_fatal_alert(&rustls::version::TLS13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_initial_alert_and_similar_error_text_are_preserved() {
|
||||
for override_error in [
|
||||
None,
|
||||
Some(rustls::Error::General(
|
||||
"incompatible signing key detail".into(),
|
||||
)),
|
||||
] {
|
||||
let (mut client, accepted) = client_hello(client_config(
|
||||
&rustls::version::TLS12,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
None,
|
||||
));
|
||||
let (error, alert) = accepted
|
||||
.into_connection(server_config(
|
||||
&rustls::version::TLS13,
|
||||
false,
|
||||
None,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
))
|
||||
.unwrap_err();
|
||||
let bytes = initial_handshake_alert(&override_error.unwrap_or(error), alert).unwrap();
|
||||
assert_eq!(bytes, sni_alert(70));
|
||||
client.read_tls(&mut &bytes[..]).unwrap();
|
||||
assert!(matches!(
|
||||
client.process_new_packets(),
|
||||
Err(rustls::Error::AlertReceived(
|
||||
rustls::AlertDescription::ProtocolVersion
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disjoint_cipher_suites_keep_the_no_shared_cipher_error() {
|
||||
use rustls::crypto::aws_lc_rs::cipher_suite::{
|
||||
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
};
|
||||
let version = &rustls::version::TLS12;
|
||||
let (_, accepted) = client_hello(client_config(
|
||||
version,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
Some(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
|
||||
));
|
||||
let (error, _) = accepted
|
||||
.into_connection(server_config(
|
||||
version,
|
||||
false,
|
||||
Some(TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384),
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
))
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
super::super::error::TlsError::from_rustls(error),
|
||||
super::super::error::TlsError::NoCipherSuites
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incompatible_certificate_algorithm_keeps_the_no_shared_cipher_error() {
|
||||
use rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256;
|
||||
let version = &rustls::version::TLS12;
|
||||
let (_, accepted) = client_hello(client_config(
|
||||
version,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
Some(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
|
||||
));
|
||||
let (error, _) = accepted
|
||||
.into_connection(server_config(
|
||||
version,
|
||||
false,
|
||||
None,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
))
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
super::super::error::TlsError::from_rustls(error),
|
||||
super::super::error::TlsError::NoCipherSuites
|
||||
));
|
||||
}
|
||||
|
||||
fn finish_handshake(client: &mut ClientConnection, server: &mut rustls::ServerConnection) {
|
||||
for _ in 0..16 {
|
||||
let mut bytes = Vec::new();
|
||||
server.write_tls(&mut bytes).unwrap();
|
||||
if !bytes.is_empty() {
|
||||
client.read_tls(&mut &bytes[..]).unwrap();
|
||||
client.process_new_packets().unwrap();
|
||||
}
|
||||
bytes.clear();
|
||||
client.write_tls(&mut bytes).unwrap();
|
||||
if !bytes.is_empty() {
|
||||
server.read_tls(&mut &bytes[..]).unwrap();
|
||||
server.process_new_packets().unwrap();
|
||||
}
|
||||
if !client.is_handshaking()
|
||||
&& !server.is_handshaking()
|
||||
&& !client.wants_write()
|
||||
&& !server.wants_write()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("in-memory handshake did not finish within 16 exchanges");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_match_selects_a_later_compatible_key() {
|
||||
for version in [&rustls::version::TLS12, &rustls::version::TLS13] {
|
||||
let client_config = client_config(version, SignatureScheme::RSA_PSS_SHA384, None);
|
||||
let server_config = server_config(version, true, None, SignatureScheme::RSA_PSS_SHA384);
|
||||
for expected_kind in [rustls::HandshakeKind::Full, rustls::HandshakeKind::Resumed] {
|
||||
let (mut client, accepted) = client_hello(client_config.clone());
|
||||
let mut server = accepted.into_connection(server_config.clone()).unwrap();
|
||||
finish_handshake(&mut client, &mut server);
|
||||
assert_eq!(client.handshake_kind(), Some(expected_kind));
|
||||
assert_eq!(server.handshake_kind(), Some(expected_kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_resumption_survives_signature_policy_change(
|
||||
version: &'static rustls::SupportedProtocolVersion,
|
||||
) {
|
||||
let client_config = client_config(version, SignatureScheme::RSA_PSS_SHA256, None);
|
||||
let initial_config = server_config(version, false, None, SignatureScheme::RSA_PSS_SHA256);
|
||||
let (mut client, accepted) = client_hello(client_config.clone());
|
||||
let mut server = accepted.into_connection(initial_config.clone()).unwrap();
|
||||
finish_handshake(&mut client, &mut server);
|
||||
assert_eq!(client.handshake_kind(), Some(rustls::HandshakeKind::Full));
|
||||
|
||||
// SSLContext retains its server session cache/ticketer when a policy
|
||||
// change rebuilds the configuration. Change only the signing policy.
|
||||
let mut changed_config = (*initial_config).clone();
|
||||
changed_config.cert_resolver =
|
||||
server_config(version, false, None, SignatureScheme::RSA_PSS_SHA384)
|
||||
.cert_resolver
|
||||
.clone();
|
||||
let changed_config = Arc::new(changed_config);
|
||||
let (mut client, accepted) = client_hello(client_config);
|
||||
let mut server = accepted.into_connection(changed_config.clone()).unwrap();
|
||||
finish_handshake(&mut client, &mut server);
|
||||
assert_eq!(
|
||||
client.handshake_kind(),
|
||||
Some(rustls::HandshakeKind::Resumed)
|
||||
);
|
||||
assert_eq!(
|
||||
server.handshake_kind(),
|
||||
Some(rustls::HandshakeKind::Resumed)
|
||||
);
|
||||
|
||||
// The same changed policy must still reject a fresh client's PSS256
|
||||
// offer; accepting a resumed session must not relax a full handshake.
|
||||
let (mut client, accepted) = client_hello(self::client_config(
|
||||
version,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
None,
|
||||
));
|
||||
let (error, alert) = accepted.into_connection(changed_config).unwrap_err();
|
||||
let bytes = initial_handshake_alert(&error, alert).unwrap();
|
||||
assert_eq!(bytes, sni_alert(40));
|
||||
client.read_tls(&mut &bytes[..]).unwrap();
|
||||
assert!(matches!(
|
||||
client.process_new_packets(),
|
||||
Err(rustls::Error::AlertReceived(
|
||||
rustls::AlertDescription::HandshakeFailure,
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls12_resumption_survives_signature_policy_change() {
|
||||
assert_resumption_survives_signature_policy_change(&rustls::version::TLS12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls13_resumption_survives_signature_policy_change() {
|
||||
assert_resumption_survives_signature_policy_change(&rustls::version::TLS13);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ pub mod providers;
|
||||
#[cfg(feature = "rustls")]
|
||||
pub mod session;
|
||||
#[cfg(feature = "rustls")]
|
||||
pub mod sigalg;
|
||||
#[cfg(feature = "rustls")]
|
||||
pub mod verify;
|
||||
#[cfg(feature = "rustls")]
|
||||
pub mod x509;
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// cspell: ignore sigalgs rsae mldsa
|
||||
|
||||
//! Per-context TLS server signature algorithm selection.
|
||||
|
||||
use alloc::sync::Arc;
|
||||
use rustls::{
|
||||
DigitallySignedStruct, DistinguishedName, Error, SignatureAlgorithm, SignatureScheme,
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
pki_types::{CertificateDer, ServerName, SubjectPublicKeyInfoDer, UnixTime},
|
||||
server::{ClientHello, ResolvesServerCert},
|
||||
sign::{CertifiedKey, Signer, SigningKey},
|
||||
};
|
||||
|
||||
use super::providers::CryptoExt;
|
||||
|
||||
pub fn scheme_name(scheme: SignatureScheme) -> Option<&'static str> {
|
||||
use SignatureScheme::{
|
||||
ECDSA_NISTP256_SHA256, ECDSA_NISTP384_SHA384, ECDSA_NISTP521_SHA512, ECDSA_SHA1_Legacy,
|
||||
ED448, ED25519, ML_DSA_44, ML_DSA_65, ML_DSA_87, RSA_PKCS1_SHA1, RSA_PKCS1_SHA256,
|
||||
RSA_PKCS1_SHA384, RSA_PKCS1_SHA512, RSA_PSS_SHA256, RSA_PSS_SHA384, RSA_PSS_SHA512,
|
||||
};
|
||||
Some(match scheme {
|
||||
RSA_PKCS1_SHA1 => "rsa_pkcs1_sha1",
|
||||
ECDSA_SHA1_Legacy => "ecdsa_sha1",
|
||||
RSA_PKCS1_SHA256 => "rsa_pkcs1_sha256",
|
||||
RSA_PKCS1_SHA384 => "rsa_pkcs1_sha384",
|
||||
RSA_PKCS1_SHA512 => "rsa_pkcs1_sha512",
|
||||
ECDSA_NISTP256_SHA256 => "ecdsa_secp256r1_sha256",
|
||||
ECDSA_NISTP384_SHA384 => "ecdsa_secp384r1_sha384",
|
||||
ECDSA_NISTP521_SHA512 => "ecdsa_secp521r1_sha512",
|
||||
RSA_PSS_SHA256 => "rsa_pss_rsae_sha256",
|
||||
RSA_PSS_SHA384 => "rsa_pss_rsae_sha384",
|
||||
RSA_PSS_SHA512 => "rsa_pss_rsae_sha512",
|
||||
ED25519 => "ed25519",
|
||||
ED448 => "ed448",
|
||||
ML_DSA_44 => "mldsa44",
|
||||
ML_DSA_65 => "mldsa65",
|
||||
ML_DSA_87 => "mldsa87",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_list(list: &str) -> Result<Vec<SignatureScheme>, &'static str> {
|
||||
let supported = CryptoExt::get_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes();
|
||||
let mut selected = Vec::new();
|
||||
for name in list.split(':') {
|
||||
let (name, optional) = name.strip_prefix('?').map_or((name, false), |n| (n, true));
|
||||
let name = match name {
|
||||
"RSA+SHA256" => "rsa_pkcs1_sha256",
|
||||
"RSA+SHA384" => "rsa_pkcs1_sha384",
|
||||
"RSA+SHA512" => "rsa_pkcs1_sha512",
|
||||
"ECDSA+SHA256" => "ecdsa_secp256r1_sha256",
|
||||
"ECDSA+SHA384" => "ecdsa_secp384r1_sha384",
|
||||
"ECDSA+SHA512" => "ecdsa_secp521r1_sha512",
|
||||
other => other,
|
||||
};
|
||||
match supported.iter().find(|s| scheme_name(**s) == Some(name)) {
|
||||
Some(scheme) if !selected.contains(scheme) => selected.push(*scheme),
|
||||
Some(_) => {}
|
||||
None if optional => {}
|
||||
None => return Err("unrecognized signature algorithm"),
|
||||
}
|
||||
}
|
||||
if selected.is_empty() {
|
||||
return Err("unrecognized signature algorithm");
|
||||
}
|
||||
Ok(selected)
|
||||
}
|
||||
|
||||
/// Restrict advertisement and handshake signatures, retaining the complete
|
||||
/// certificate verifier (including trust, hostname and revocation checks).
|
||||
#[derive(Debug)]
|
||||
pub struct ServerVerifier {
|
||||
pub inner: Arc<dyn ServerCertVerifier>,
|
||||
pub schemes: Vec<SignatureScheme>,
|
||||
}
|
||||
|
||||
impl ServerVerifier {
|
||||
fn check_scheme(&self, dss: &DigitallySignedStruct) -> Result<(), Error> {
|
||||
if self.schemes.contains(&dss.scheme) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::General(
|
||||
"server signature algorithm is not enabled".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerCertVerifier for ServerVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &CertificateDer<'_>,
|
||||
intermediates: &[CertificateDer<'_>],
|
||||
server_name: &ServerName<'_>,
|
||||
ocsp_response: &[u8],
|
||||
now: UnixTime,
|
||||
) -> Result<ServerCertVerified, Error> {
|
||||
self.inner
|
||||
.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, Error> {
|
||||
self.check_scheme(dss)?;
|
||||
self.inner.verify_tls12_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, Error> {
|
||||
self.check_scheme(dss)?;
|
||||
self.inner.verify_tls13_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
let supported = self.inner.supported_verify_schemes();
|
||||
self.schemes
|
||||
.iter()
|
||||
.filter(|s| supported.contains(s))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn requires_raw_public_keys(&self) -> bool {
|
||||
self.inner.requires_raw_public_keys()
|
||||
}
|
||||
|
||||
fn root_hint_subjects(&self) -> Option<&[DistinguishedName]> {
|
||||
self.inner.root_hint_subjects()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RestrictedSigningKey {
|
||||
inner: Arc<dyn SigningKey>,
|
||||
schemes: Vec<SignatureScheme>,
|
||||
}
|
||||
|
||||
impl SigningKey for RestrictedSigningKey {
|
||||
fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option<Box<dyn Signer>> {
|
||||
self.schemes
|
||||
.iter()
|
||||
.filter(|s| offered.contains(s))
|
||||
.find_map(|s| self.inner.choose_scheme(&[*s]))
|
||||
}
|
||||
|
||||
fn public_key(&self) -> Option<SubjectPublicKeyInfoDer<'_>> {
|
||||
self.inner.public_key()
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> SignatureAlgorithm {
|
||||
self.inner.algorithm()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerResolver {
|
||||
pub inner: Arc<dyn ResolvesServerCert>,
|
||||
pub schemes: Vec<SignatureScheme>,
|
||||
}
|
||||
|
||||
pub fn restrict_key(key: &CertifiedKey, schemes: &[SignatureScheme]) -> Arc<CertifiedKey> {
|
||||
let mut key = key.clone();
|
||||
key.key = Arc::new(RestrictedSigningKey {
|
||||
inner: key.key,
|
||||
schemes: schemes.to_vec(),
|
||||
});
|
||||
Arc::new(key)
|
||||
}
|
||||
|
||||
impl ResolvesServerCert for ServerResolver {
|
||||
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
|
||||
let key = self.inner.resolve(hello)?;
|
||||
Some(restrict_key(&key, &self.schemes))
|
||||
}
|
||||
|
||||
fn only_raw_public_keys(&self) -> bool {
|
||||
self.inner.only_raw_public_keys()
|
||||
}
|
||||
}
|
||||
@@ -747,6 +747,25 @@ impl ClientCertVerifier for DeferredClientCertVerifier {
|
||||
|
||||
// Public Utility Functions
|
||||
|
||||
/// Preserve which filesystem operation failed, independently of PEM errors.
|
||||
#[derive(Debug)]
|
||||
pub struct CertFileError {
|
||||
pub error: std::io::Error,
|
||||
pub key_file: bool,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CertFileError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
core::fmt::Display::fmt(&self.error, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for CertFileError {
|
||||
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
|
||||
Some(&self.error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load certificate chain and private key from files
|
||||
///
|
||||
/// This function loads a certificate chain from `cert_path` and a private key
|
||||
@@ -771,7 +790,10 @@ pub fn load_cert_chain_from_file(
|
||||
password: Option<&str>,
|
||||
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Box<dyn core::error::Error>> {
|
||||
// Load certificate file - preserve io::Error for errno
|
||||
let cert_contents = crate::fs::read(cert_path)?;
|
||||
let cert_contents = crate::fs::read(cert_path).map_err(|error| CertFileError {
|
||||
error,
|
||||
key_file: false,
|
||||
})?;
|
||||
|
||||
// Parse certificates (PEM format)
|
||||
let mut cert_cursor = std::io::Cursor::new(&cert_contents);
|
||||
@@ -784,7 +806,10 @@ pub fn load_cert_chain_from_file(
|
||||
}
|
||||
|
||||
// Load private key file - preserve io::Error for errno
|
||||
let key_contents = crate::fs::read(key_path)?;
|
||||
let key_contents = crate::fs::read(key_path).map_err(|error| CertFileError {
|
||||
error,
|
||||
key_file: true,
|
||||
})?;
|
||||
|
||||
// Parse private key (supports PKCS8, RSA, EC formats)
|
||||
let private_key = if let Some(pwd) = password {
|
||||
|
||||
@@ -20,6 +20,8 @@ fn main() {
|
||||
(0x3_01_00_00_0, "ossl310"),
|
||||
(0x3_02_00_00_0, "ossl320"),
|
||||
(0x3_03_00_00_0, "ossl330"),
|
||||
(0x3_04_00_00_0, "ossl340"),
|
||||
(0x3_05_00_00_0, "ossl350"),
|
||||
];
|
||||
|
||||
for (_, cfg) in ossl_vers {
|
||||
|
||||
+936
-680
File diff suppressed because it is too large.
Load diff
+382
-88
@@ -1,4 +1,4 @@
|
||||
// spell-checker: ignore ssleof aesccm aesgcm capath getblocking setblocking ENDTLS TLSEXT
|
||||
// spell-checker: ignore ssleof aesccm aesgcm capath getblocking setblocking ENDTLS TLSEXT ktls sigalgs rsae mldsa
|
||||
|
||||
//! Pure Rust SSL/TLS implementation using rustls
|
||||
//!
|
||||
@@ -79,14 +79,14 @@ mod _ssl {
|
||||
|
||||
// Import error types used in this module (others are exposed via pymodule(with(...)))
|
||||
use super::error::{
|
||||
PySSLError, PySSLWantReadError, PySSLWantWriteError, create_ssl_eof_error,
|
||||
PySSLEOFError, PySSLError, PySSLWantReadError, PySSLWantWriteError, create_ssl_eof_error,
|
||||
create_ssl_want_read_error, create_ssl_want_write_error, create_ssl_zero_return_error,
|
||||
};
|
||||
use alloc::sync::Arc;
|
||||
use core::{
|
||||
hash::{Hash, Hasher},
|
||||
hint::cold_path,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use rustpython_vm::exceptions;
|
||||
@@ -281,6 +281,8 @@ mod _ssl {
|
||||
#[pyattr]
|
||||
const HAS_PSK: bool = false; // PSK not supported in rustls
|
||||
#[pyattr]
|
||||
const HAS_PSK_TLS13: bool = false; // External PSK callbacks are not supported.
|
||||
#[pyattr]
|
||||
const HAS_SSLv2: bool = false;
|
||||
#[pyattr]
|
||||
const HAS_SSLv3: bool = false;
|
||||
@@ -325,6 +327,8 @@ mod _ssl {
|
||||
#[pytraverse(skip)]
|
||||
context_identity: Arc<()>,
|
||||
#[pytraverse(skip)]
|
||||
policy_identity: PyRwLock<Arc<()>>,
|
||||
#[pytraverse(skip)]
|
||||
protocol: i32,
|
||||
#[pytraverse(skip)]
|
||||
check_hostname: PyRwLock<bool>,
|
||||
@@ -336,7 +340,7 @@ mod _ssl {
|
||||
verify_flags: PyRwLock<i32>,
|
||||
// Rustls configuration (built lazily)
|
||||
#[pytraverse(skip)]
|
||||
server_config: PyRwLock<Option<chain::ServerConfig>>,
|
||||
server_config: PyRwLock<Option<(Arc<()>, chain::ServerConfig)>>,
|
||||
// Certificate store
|
||||
#[pytraverse(skip)]
|
||||
root_certs: PyRwLock<RootCertStore>,
|
||||
@@ -380,9 +384,11 @@ mod _ssl {
|
||||
keylog_filename: PyRwLock<Option<PyObjectRef>>,
|
||||
#[pytraverse(skip)]
|
||||
key_log: Arc<super::keylog::KeyLog>,
|
||||
// ECDH curve name for key exchange
|
||||
// Explicit key exchange groups, shared by set_groups and set_ecdh_curve.
|
||||
#[pytraverse(skip)]
|
||||
ecdh_curve: PyRwLock<Option<String>>,
|
||||
kx_groups: PyRwLock<Option<Vec<&'static dyn SupportedKxGroup>>>,
|
||||
#[pytraverse(skip)]
|
||||
server_sigalgs: PyRwLock<Option<Vec<rustls::SignatureScheme>>>,
|
||||
// Certificate statistics for cert_store_stats()
|
||||
#[pytraverse(skip)]
|
||||
ca_cert_count: PyRwLock<usize>, // Number of CA certificates
|
||||
@@ -470,6 +476,14 @@ mod _ssl {
|
||||
|
||||
#[pyclass(with(Constructor, Representable), flags(BASETYPE))]
|
||||
impl PySSLContext {
|
||||
fn invalidate_policy(&self) {
|
||||
// Old SSLSession configurations must never override later policy.
|
||||
// Replace the identity after mutation so concurrent snapshots also
|
||||
// become ineligible for resumption once that mutation completes.
|
||||
*self.policy_identity.write() = Arc::new(());
|
||||
*self.server_config.write() = None;
|
||||
}
|
||||
|
||||
fn warn_deprecated_tls_version(version: i32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let version_name = match version {
|
||||
PROTO_SSLv3 => Some("SSLv3"),
|
||||
@@ -505,7 +519,7 @@ mod _ssl {
|
||||
capath_state.directories.push(directory);
|
||||
capath_state.cache = None;
|
||||
drop(capath_state);
|
||||
*self.server_config.write() = None;
|
||||
self.invalidate_policy();
|
||||
}
|
||||
|
||||
fn capath_certificates(&self) -> Arc<Vec<Vec<u8>>> {
|
||||
@@ -571,6 +585,7 @@ mod _ssl {
|
||||
*zelf.verify_mode.write() = CERT_REQUIRED;
|
||||
}
|
||||
}
|
||||
zelf.invalidate_policy();
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
@@ -581,6 +596,7 @@ mod _ssl {
|
||||
#[pygetset(setter)]
|
||||
fn set__host_flags(zelf: &Py<Self>, value: i32) {
|
||||
*zelf.host_flags.write() = value;
|
||||
zelf.invalidate_policy();
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
@@ -600,6 +616,7 @@ mod _ssl {
|
||||
));
|
||||
}
|
||||
*zelf.verify_mode.write() = mode;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -616,6 +633,7 @@ mod _ssl {
|
||||
#[pygetset(setter)]
|
||||
fn set_verify_flags(zelf: &Py<Self>, value: i32) {
|
||||
*zelf.verify_flags.write() = value;
|
||||
zelf.invalidate_policy();
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
@@ -626,6 +644,7 @@ mod _ssl {
|
||||
#[pygetset(setter)]
|
||||
fn set_post_handshake_auth(zelf: &Py<Self>, value: bool) {
|
||||
*zelf.post_handshake_auth.write() = value;
|
||||
zelf.invalidate_policy();
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
@@ -644,6 +663,7 @@ mod _ssl {
|
||||
);
|
||||
}
|
||||
*zelf.num_tickets.write() = value;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -682,6 +702,7 @@ mod _ssl {
|
||||
}
|
||||
|
||||
*zelf.options.write() = value;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -715,6 +736,7 @@ mod _ssl {
|
||||
_ => value,
|
||||
};
|
||||
*zelf.minimum_version.write() = normalized_value;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -748,6 +770,7 @@ mod _ssl {
|
||||
_ => value,
|
||||
};
|
||||
*zelf.maximum_version.write() = normalized_value;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -788,7 +811,9 @@ mod _ssl {
|
||||
|
||||
// If failed and callable exists, invoke it and retry
|
||||
// This implements lazy evaluation: callable only invoked if password is actually needed
|
||||
if result.is_err()
|
||||
if result
|
||||
.as_ref()
|
||||
.is_err_and(|error| !error.is::<rustpython_host_env::ssl::verify::CertFileError>())
|
||||
&& let Some(callable) = password_callable
|
||||
{
|
||||
// Invoke callable - exceptions propagate naturally
|
||||
@@ -826,6 +851,25 @@ mod _ssl {
|
||||
|
||||
// Process result
|
||||
let (certs, key) = result.map_err(|e| {
|
||||
let e = match e.downcast::<rustpython_host_env::ssl::verify::CertFileError>() {
|
||||
Ok(error) => {
|
||||
let filename = if error.key_file {
|
||||
args.keyfile.as_ref().unwrap_or(&args.certfile)
|
||||
} else {
|
||||
&args.certfile
|
||||
};
|
||||
let filename: PyObjectRef = match filename {
|
||||
Either::A(s) => s.clone().into(),
|
||||
Either::B(b) => vm.ctx.new_bytes(b.borrow_buf().to_vec()).into(),
|
||||
};
|
||||
let exc = error.error.into_pyexception(vm);
|
||||
exc.as_object()
|
||||
.set_attr("filename", filename, vm)
|
||||
.expect("OSError.filename is writable");
|
||||
return exc;
|
||||
}
|
||||
Err(error) => error,
|
||||
};
|
||||
// Try to downcast to io::Error to preserve errno information
|
||||
if let Ok(io_err) = e.downcast::<std::io::Error>() {
|
||||
match io_err.kind() {
|
||||
@@ -932,6 +976,7 @@ mod _ssl {
|
||||
|
||||
// Add new cert/key pair as tuple
|
||||
cert_keys.push((Arc::new(certified_key), key));
|
||||
zelf.invalidate_policy();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -981,6 +1026,8 @@ mod _ssl {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
// Loaders may partially update trust before returning an error.
|
||||
let _policy_change = ContextPolicyChange(zelf);
|
||||
// If it's a CRL, just add it (separate lock, no conflict with root_store)
|
||||
if let Some(crl) = crl_opt {
|
||||
zelf.crls.write().push(crl);
|
||||
@@ -1017,7 +1064,7 @@ mod _ssl {
|
||||
if let Some(dir_path) = capath_dir {
|
||||
zelf.add_verify_dir(dir_path);
|
||||
}
|
||||
*zelf.server_config.write() = None;
|
||||
zelf.invalidate_policy();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1142,6 +1189,7 @@ mod _ssl {
|
||||
_purpose: OptionalArg<i32>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let _policy_change = ContextPolicyChange(zelf);
|
||||
let mut store = zelf.root_certs.write();
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -1179,7 +1227,7 @@ mod _ssl {
|
||||
}
|
||||
|
||||
drop(store);
|
||||
*zelf.server_config.write() = None;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1195,6 +1243,7 @@ mod _ssl {
|
||||
alpn_list.push(bytes.borrow_buf().to_vec());
|
||||
}
|
||||
*zelf.alpn_protocols.write() = alpn_list;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1207,6 +1256,7 @@ mod _ssl {
|
||||
let bytes = protos.borrow_buf();
|
||||
let alpn_list = parse_length_prefixed_alpn(&bytes, vm)?;
|
||||
*zelf.alpn_protocols.write() = alpn_list;
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1234,21 +1284,128 @@ mod _ssl {
|
||||
.upcast()
|
||||
})?;
|
||||
|
||||
// TLS 1.3 has a separate OpenSSL setter. Discard whatever this
|
||||
// cipher string happened to match there, then restore exactly the
|
||||
// provider defaults in their preference order.
|
||||
// TLS 1.3 has a separate setter. Preserve its current selection.
|
||||
let current = zelf.selected_ciphers.read();
|
||||
selected_ciphers = cipher::restore_default_tls13(
|
||||
selected_ciphers,
|
||||
CryptoExt::get_ext().default_ciphers_or_provider(),
|
||||
current
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| CryptoExt::get_ext().default_ciphers_or_provider()),
|
||||
);
|
||||
drop(current);
|
||||
|
||||
*zelf.selected_ciphers.write() = Some(selected_ciphers);
|
||||
*zelf.suite_b_kx_groups.write() = suite_b_kx_groups;
|
||||
*zelf.server_config.write() = None;
|
||||
zelf.invalidate_policy();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn set_ciphersuites(
|
||||
zelf: &Py<Self>,
|
||||
ciphersuites: PyUtf8StrRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let list = ciphersuites.as_str();
|
||||
if list.contains('\0') {
|
||||
return Err(vm.new_value_error("embedded null character"));
|
||||
}
|
||||
let mut selected = Vec::new();
|
||||
for name in list.split(':') {
|
||||
if let Some(suite) = CryptoExt::get_ext()
|
||||
.all_ciphers_or_default()
|
||||
.iter()
|
||||
.find(|suite| suite.tls13().is_some() && cipher::describe(suite).name == name)
|
||||
&& !selected.contains(suite)
|
||||
{
|
||||
selected.push(*suite);
|
||||
}
|
||||
}
|
||||
if selected.is_empty() && !list.is_empty() {
|
||||
return Err(vm
|
||||
.new_os_subtype_error(
|
||||
PySSLError::class(&vm.ctx).to_owned(),
|
||||
None,
|
||||
"No cipher suite can be selected.".to_owned(),
|
||||
)
|
||||
.upcast());
|
||||
}
|
||||
let mut current = zelf.selected_ciphers.write();
|
||||
selected.extend(
|
||||
current
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| CryptoExt::get_ext().default_ciphers_or_provider())
|
||||
.iter()
|
||||
.filter(|suite| suite.tls13().is_none())
|
||||
.copied(),
|
||||
);
|
||||
*current = Some(selected);
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn set_groups(
|
||||
zelf: &Py<Self>,
|
||||
grouplist: PyUtf8StrRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let list = grouplist.as_str();
|
||||
if list.contains('\0') {
|
||||
return Err(vm.new_value_error("embedded null character"));
|
||||
}
|
||||
let mut groups: Vec<&'static dyn SupportedKxGroup> = Vec::new();
|
||||
if !list.is_empty() {
|
||||
for name in list.split(':') {
|
||||
let (name, optional) =
|
||||
name.strip_prefix('?').map_or((name, false), |n| (n, true));
|
||||
match cipher::kx_group_by_openssl_name(name) {
|
||||
Some(group) if !groups.iter().any(|g| g.name() == group.name()) => {
|
||||
groups.push(group)
|
||||
}
|
||||
Some(_) => {}
|
||||
None if optional => {}
|
||||
None => {
|
||||
return Err(vm
|
||||
.new_os_subtype_error(
|
||||
PySSLError::class(&vm.ctx).to_owned(),
|
||||
None,
|
||||
"unrecognized group".to_owned(),
|
||||
)
|
||||
.upcast());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*zelf.kx_groups.write() = Some(groups);
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn set_server_sigalgs(
|
||||
zelf: &Py<Self>,
|
||||
sigalgslist: PyUtf8StrRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let list = sigalgslist.as_str();
|
||||
if list.contains('\0') {
|
||||
return Err(vm.new_value_error("embedded null character"));
|
||||
}
|
||||
let schemes = rustpython_host_env::ssl::sigalg::parse_list(list).map_err(|error| {
|
||||
vm.new_os_subtype_error(
|
||||
PySSLError::class(&vm.ctx).to_owned(),
|
||||
None,
|
||||
error.to_owned(),
|
||||
)
|
||||
.upcast()
|
||||
})?;
|
||||
*zelf.server_sigalgs.write() = Some(schemes);
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn get_ciphers(zelf: &Py<Self>, vm: &VirtualMachine) -> PyListRef {
|
||||
// What the last `set_ciphers` selected, or the provider defaults
|
||||
@@ -1556,11 +1713,10 @@ mod _ssl {
|
||||
return Err(vm.new_type_error("ECDH curve name must be str or bytes"));
|
||||
};
|
||||
|
||||
curve_name_to_kx_group(&curve_name).map_err(|error| vm.new_value_error(error))?;
|
||||
|
||||
// Store the curve name to be used during handshake
|
||||
// This will limit the key exchange groups offered/accepted
|
||||
*zelf.ecdh_curve.write() = Some(curve_name);
|
||||
let groups =
|
||||
curve_name_to_kx_group(&curve_name).map_err(|error| vm.new_value_error(error))?;
|
||||
*zelf.kx_groups.write() = Some(groups);
|
||||
zelf.invalidate_policy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1618,6 +1774,7 @@ mod _ssl {
|
||||
server_hostname: PyRwLock::new(hostname),
|
||||
connection: PyMutex::new(None),
|
||||
state: PyMutex::new(TlsState::new(args.server_side)),
|
||||
got_eof_error: AtomicBool::new(false),
|
||||
session_was_reused: PyMutex::new(false),
|
||||
owner: PyRwLock::new(args.owner.map(|o| o.downgrade(None, vm)).transpose()?),
|
||||
session: PyRwLock::new(None),
|
||||
@@ -1698,6 +1855,7 @@ mod _ssl {
|
||||
server_hostname: PyRwLock::new(hostname),
|
||||
connection: PyMutex::new(None),
|
||||
state: PyMutex::new(TlsState::new(server_side)),
|
||||
got_eof_error: AtomicBool::new(false),
|
||||
session_was_reused: PyMutex::new(false),
|
||||
owner: PyRwLock::new(args.owner.map(|o| o.downgrade(None, vm)).transpose()?),
|
||||
session: PyRwLock::new(None),
|
||||
@@ -1983,6 +2141,7 @@ mod _ssl {
|
||||
let shared_session_cache = Arc::new(ParkingRwLock::new(HashMap::new()));
|
||||
Ok(Self {
|
||||
context_identity: Arc::new(()),
|
||||
policy_identity: PyRwLock::new(Arc::new(())),
|
||||
protocol,
|
||||
check_hostname: PyRwLock::new(protocol == PROTOCOL_TLS_CLIENT),
|
||||
host_flags: PyRwLock::new(0),
|
||||
@@ -2004,7 +2163,8 @@ mod _ssl {
|
||||
msg_callback: PyRwLock::new(None),
|
||||
keylog_filename: PyRwLock::new(None),
|
||||
key_log: Arc::new(super::keylog::KeyLog::default()),
|
||||
ecdh_curve: PyRwLock::new(None),
|
||||
kx_groups: PyRwLock::new(None),
|
||||
server_sigalgs: PyRwLock::new(None),
|
||||
ca_cert_count: PyRwLock::new(0),
|
||||
x509_cert_count: PyRwLock::new(0),
|
||||
// Use the shared cache created above
|
||||
@@ -2084,6 +2244,23 @@ mod _ssl {
|
||||
}
|
||||
}
|
||||
|
||||
// Trust loaders can mutate before a later input fails, so invalidate on
|
||||
// both successful and exceptional exits, after their write locks drop.
|
||||
struct ContextPolicyChange<'a>(&'a PySSLContext);
|
||||
|
||||
impl Drop for ContextPolicyChange<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.invalidate_policy();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientConfigSnapshot {
|
||||
config: Arc<rustls::ClientConfig>,
|
||||
context_identity: Arc<()>,
|
||||
policy_identity: Arc<()>,
|
||||
}
|
||||
|
||||
// SSLSocket - represents a TLS-wrapped socket
|
||||
#[pyattr]
|
||||
#[pyclass(name = "_SSLSocket", module = "_ssl", traverse)]
|
||||
@@ -2107,6 +2284,8 @@ mod _ssl {
|
||||
connection: PyMutex<Option<TlsConnection>>,
|
||||
// Includes the saved exception while a rejected handshake sends its alert.
|
||||
state: PyMutex<TlsState>,
|
||||
#[pytraverse(skip)]
|
||||
got_eof_error: AtomicBool,
|
||||
// Session was reused (for session resumption tracking)
|
||||
#[pytraverse(skip)]
|
||||
session_was_reused: PyMutex<bool>,
|
||||
@@ -2117,7 +2296,7 @@ mod _ssl {
|
||||
// Client configuration used by this connection. Retained so the resulting
|
||||
// SSLSession can reuse the same verifier and client credentials.
|
||||
#[pytraverse(skip)]
|
||||
client_config: PyRwLock<Option<Arc<rustls::ClientConfig>>>,
|
||||
client_config: PyRwLock<Option<ClientConfigSnapshot>>,
|
||||
#[pytraverse(skip)]
|
||||
chain_builder: PyRwLock<Option<Arc<VerifiedChainBuilder>>>,
|
||||
#[pytraverse(skip)]
|
||||
@@ -2144,6 +2323,19 @@ mod _ssl {
|
||||
|
||||
#[pyclass(with(Constructor, Representable), flags(BASETYPE))]
|
||||
impl PySSLSocket {
|
||||
// Call only for transport/protocol failures. User message callbacks
|
||||
// may raise SSLEOFError without closing the connection.
|
||||
fn remember_eof(
|
||||
&self,
|
||||
error: PyBaseExceptionRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyBaseExceptionRef {
|
||||
if error.fast_isinstance(PySSLEOFError::class(&vm.ctx)) {
|
||||
self.got_eof_error.store(true, Ordering::Relaxed);
|
||||
}
|
||||
error
|
||||
}
|
||||
|
||||
// Check if this is BIO mode
|
||||
pub(crate) fn is_bio_mode(&self) -> bool {
|
||||
matches!(self.io, SocketOrBio::Bio { .. })
|
||||
@@ -2275,13 +2467,7 @@ mod _ssl {
|
||||
|
||||
// Try to get session data from context's session cache
|
||||
// IMPORTANT: Acquire and release locks quickly to avoid deadlock
|
||||
let (context_identity, session_cache_arc) = {
|
||||
let context = self.context.read();
|
||||
(
|
||||
context.context_identity.clone(),
|
||||
context.client_session_cache.clone(),
|
||||
)
|
||||
};
|
||||
let session_cache_arc = self.context.read().client_session_cache.clone();
|
||||
|
||||
let cached_session_data = if let Some(ref name) = server_name {
|
||||
let key = name.as_bytes().to_vec();
|
||||
@@ -2327,7 +2513,7 @@ mod _ssl {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let Some(client_config) = self.client_config.write().take() else {
|
||||
let Some(config_snapshot) = self.client_config.write().take() else {
|
||||
return;
|
||||
};
|
||||
let Some(session_store) = self.client_session_store.write().take() else {
|
||||
@@ -2335,8 +2521,9 @@ mod _ssl {
|
||||
};
|
||||
|
||||
let session = PySSLSession {
|
||||
context_identity,
|
||||
client_config,
|
||||
context_identity: config_snapshot.context_identity,
|
||||
policy_identity: config_snapshot.policy_identity,
|
||||
client_config: config_snapshot.config,
|
||||
chain_builder: self
|
||||
.chain_builder
|
||||
.read()
|
||||
@@ -2594,7 +2781,7 @@ mod _ssl {
|
||||
Ok(())
|
||||
})();
|
||||
*self.state.lock() = TlsState::WaitingForClientHello(acceptor);
|
||||
read?;
|
||||
read.map_err(|error| self.remember_eof(error, vm))?;
|
||||
}
|
||||
TlsState::SendingAlert { error } => {
|
||||
let result = self.flush_pending_tls_output(vm, None);
|
||||
@@ -2602,7 +2789,7 @@ mod _ssl {
|
||||
error: error.clone(),
|
||||
};
|
||||
result?;
|
||||
return Err(error);
|
||||
return Err(self.remember_eof(error, vm));
|
||||
}
|
||||
state @ (TlsState::Handshaking
|
||||
| TlsState::Connected
|
||||
@@ -2784,41 +2971,36 @@ mod _ssl {
|
||||
}
|
||||
|
||||
/// Helper: Prepare KX groups (ECDH curve) from context settings
|
||||
fn prepare_kx_groups(
|
||||
&self,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<Option<Vec<&'static dyn SupportedKxGroup>>> {
|
||||
fn prepare_kx_groups(&self) -> Option<Vec<&'static dyn SupportedKxGroup>> {
|
||||
let ctx = self.context.read();
|
||||
let ecdh_curve = ctx.ecdh_curve.read().clone();
|
||||
let kx_groups = ctx.kx_groups.read().clone();
|
||||
let suite_b_kx_groups = ctx.suite_b_kx_groups.read().clone();
|
||||
drop(ctx);
|
||||
|
||||
if let Some(ref curve_name) = ecdh_curve {
|
||||
match curve_name_to_kx_group(curve_name) {
|
||||
Ok(groups) => Ok(Some(groups)),
|
||||
Err(e) => Err(vm.new_value_error(format!("Failed to set ECDH curve: {e}"))),
|
||||
}
|
||||
if kx_groups.is_some() {
|
||||
kx_groups
|
||||
} else {
|
||||
// A SUITEB* cipher string pins its own groups, and nothing
|
||||
// else asked for a curve.
|
||||
Ok(suite_b_kx_groups)
|
||||
suite_b_kx_groups
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Prepare all common protocol settings (versions, KX groups, ciphers, ALPN)
|
||||
fn prepare_protocol_settings(&self, vm: &VirtualMachine) -> PyResult<ProtocolSettings> {
|
||||
fn prepare_protocol_settings(&self) -> ProtocolSettings {
|
||||
let ctx = self.context.read();
|
||||
let versions = self.prepare_tls_versions();
|
||||
let kx_groups = self.prepare_kx_groups(vm)?;
|
||||
let kx_groups = self.prepare_kx_groups();
|
||||
let cipher_suites = ctx.selected_ciphers.read().clone();
|
||||
let alpn_protocols = ctx.alpn_protocols.read().clone();
|
||||
|
||||
Ok(ProtocolSettings {
|
||||
ProtocolSettings {
|
||||
versions,
|
||||
kx_groups,
|
||||
cipher_suites,
|
||||
server_sigalgs: ctx.server_sigalgs.read().clone(),
|
||||
alpn_protocols,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize server-side TLS connection with configuration
|
||||
@@ -2838,6 +3020,7 @@ mod _ssl {
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let ctx = self.context.read();
|
||||
let policy_identity = ctx.policy_identity.read().clone();
|
||||
let cert_keys = ctx.cert_keys.read();
|
||||
|
||||
if cert_keys.is_empty() {
|
||||
@@ -2855,7 +3038,7 @@ mod _ssl {
|
||||
drop(cert_keys);
|
||||
|
||||
// Prepare common protocol settings (TLS versions, ECDH curve, cipher suites, ALPN)
|
||||
let protocol_settings = self.prepare_protocol_settings(vm)?;
|
||||
let protocol_settings = self.prepare_protocol_settings();
|
||||
let min_ver = *ctx.minimum_version.read();
|
||||
|
||||
// Check if client certificate verification is required
|
||||
@@ -2888,8 +3071,9 @@ mod _ssl {
|
||||
// Certificate selection uses the actual ClientHello after SNI has
|
||||
// selected the context, including multi-certificate configurations.
|
||||
let cert_keys_only = cert_keys_clone.iter().map(|(ck, _)| ck.clone()).collect();
|
||||
let cert_resolver: Option<Arc<dyn ResolvesServerCert>> =
|
||||
Some(Arc::new(MultiCertResolver::new(cert_keys_only)));
|
||||
let cert_resolver: Option<Arc<dyn ResolvesServerCert>> = Some(Arc::new(
|
||||
MultiCertResolver::new(cert_keys_only, protocol_settings.server_sigalgs.as_deref()),
|
||||
));
|
||||
|
||||
// Extract cert_chain and private_key from first cert_key
|
||||
//
|
||||
@@ -2935,7 +3119,11 @@ mod _ssl {
|
||||
let cache_server_config =
|
||||
!use_deferred_validation && ctx.capath_state.read().directories.is_empty();
|
||||
let cached_config_arc = if cache_server_config {
|
||||
ctx.server_config.read().clone()
|
||||
ctx.server_config
|
||||
.read()
|
||||
.as_ref()
|
||||
.filter(|(policy, _)| Arc::ptr_eq(policy, &policy_identity))
|
||||
.map(|(_, config)| config.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -2944,12 +3132,14 @@ mod _ssl {
|
||||
let (mut config_arc, chain_builder) = if let Some(cached) = cached_config_arc {
|
||||
cached
|
||||
} else {
|
||||
let config =
|
||||
create_server_config(config_options).map_err(|e| vm.new_value_error(e))?;
|
||||
let config = create_server_config(config_options).map_err(|e| {
|
||||
vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), None, e)
|
||||
.upcast()
|
||||
})?;
|
||||
|
||||
if cache_server_config {
|
||||
let ctx = self.context.read();
|
||||
*ctx.server_config.write() = Some(config.clone());
|
||||
*ctx.server_config.write() = Some((policy_identity, config.clone()));
|
||||
}
|
||||
|
||||
config
|
||||
@@ -2965,11 +3155,10 @@ mod _ssl {
|
||||
*conn_guard = Some(TlsConnection::new(Connection::Server(conn)));
|
||||
*self.state.lock() = TlsState::Handshaking;
|
||||
}
|
||||
Err((error, mut alert)) => {
|
||||
let mut bytes = Vec::new();
|
||||
alert
|
||||
.write_all(&mut bytes)
|
||||
.map_err(|e| e.into_pyexception(vm))?;
|
||||
Err((error, alert)) => {
|
||||
let bytes =
|
||||
rustpython_host_env::ssl::handshake::initial_handshake_alert(&error, alert)
|
||||
.map_err(|e| e.into_pyexception(vm))?;
|
||||
self.reject_connection(SslError::from_rustls(error).into_py_err(vm), bytes, vm);
|
||||
self.accept_client_hello(vm)?;
|
||||
}
|
||||
@@ -2980,6 +3169,9 @@ mod _ssl {
|
||||
|
||||
#[pymethod]
|
||||
fn do_handshake(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
if zelf.got_eof_error.load(Ordering::Relaxed) {
|
||||
return Err(create_ssl_eof_error(vm).upcast());
|
||||
}
|
||||
// Check if handshake already done
|
||||
if zelf.handshake_completed() {
|
||||
return Ok(());
|
||||
@@ -3005,11 +3197,14 @@ mod _ssl {
|
||||
return zelf.accept_client_hello(vm).map(|_| ());
|
||||
}
|
||||
} else {
|
||||
// Client-side connection
|
||||
// Client-side connection. Capture policy before reading any
|
||||
// settings; mutations during construction invalidate this snapshot.
|
||||
let ctx = zelf.context.read();
|
||||
let policy_identity = ctx.policy_identity.read().clone();
|
||||
let has_capath = !ctx.capath_state.read().directories.is_empty();
|
||||
|
||||
// Prepare common protocol settings (TLS versions, ECDH curve, cipher suites, ALPN)
|
||||
let protocol_settings = zelf.prepare_protocol_settings(vm)?;
|
||||
let protocol_settings = zelf.prepare_protocol_settings();
|
||||
|
||||
// Clone values we need before building config
|
||||
let verify_mode = *ctx.verify_mode.read();
|
||||
@@ -3060,23 +3255,32 @@ mod _ssl {
|
||||
};
|
||||
|
||||
let explicit_session = zelf.session.read().clone();
|
||||
let explicit_session = explicit_session
|
||||
.as_ref()
|
||||
.map(|session| {
|
||||
let session = session
|
||||
.downcast_ref::<PySSLSession>()
|
||||
.ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?;
|
||||
if !Arc::ptr_eq(&session.context_identity, &context_identity) {
|
||||
return Err(
|
||||
vm.new_value_error("Session refers to a different SSLContext.")
|
||||
);
|
||||
}
|
||||
Ok(session)
|
||||
})
|
||||
.transpose()?;
|
||||
let reusable_session = explicit_session.filter(|session| {
|
||||
!has_capath
|
||||
&& Arc::ptr_eq(&session.policy_identity, &policy_identity)
|
||||
&& session.server_name.as_ref() == Some(&server_name)
|
||||
});
|
||||
let session_store = Arc::new(CapturingClientSessionStore::new(session_cache));
|
||||
let (mut config, chain_builder) = if let Some(session) = explicit_session {
|
||||
let session = session
|
||||
.downcast_ref::<PySSLSession>()
|
||||
.ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?;
|
||||
if !Arc::ptr_eq(&session.context_identity, &context_identity) {
|
||||
return Err(
|
||||
vm.new_value_error("Session refers to a different SSLContext.")
|
||||
);
|
||||
}
|
||||
if session.server_name.as_ref() == Some(&server_name) {
|
||||
session.session_store.transfer_session(
|
||||
&session_store,
|
||||
&server_name,
|
||||
session.kind,
|
||||
);
|
||||
}
|
||||
let (mut config, chain_builder) = if let Some(session) = reusable_session {
|
||||
session.session_store.transfer_session(
|
||||
&session_store,
|
||||
&server_name,
|
||||
session.kind,
|
||||
);
|
||||
let mut config = (*session.client_config).clone();
|
||||
config.resumption =
|
||||
rustls::client::Resumption::store(session_store.clone());
|
||||
@@ -3099,12 +3303,19 @@ mod _ssl {
|
||||
session_store: Some(session_store.clone()),
|
||||
crls: crls_clone,
|
||||
};
|
||||
create_client_config(config_options).map_err(|e| vm.new_value_error(e))?
|
||||
create_client_config(config_options).map_err(|e| {
|
||||
vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), None, e)
|
||||
.upcast()
|
||||
})?
|
||||
};
|
||||
Arc::make_mut(&mut config).key_log = zelf.key_log.clone();
|
||||
|
||||
*zelf.chain_builder.write() = Some(chain_builder);
|
||||
*zelf.client_config.write() = Some(config.clone());
|
||||
*zelf.client_config.write() = Some(ClientConfigSnapshot {
|
||||
config: config.clone(),
|
||||
context_identity,
|
||||
policy_identity,
|
||||
});
|
||||
*zelf.client_session_store.write() = Some(session_store);
|
||||
|
||||
let mut conn = ClientConnection::new(config, server_name).map_err(|e| {
|
||||
@@ -3131,7 +3342,7 @@ mod _ssl {
|
||||
zelf.reject_connection(error.into_py_err(vm), bytes, vm);
|
||||
return zelf.accept_client_hello(vm).map(|_| ());
|
||||
}
|
||||
handshake_result.map_err(|e| e.into_py_err(vm))?;
|
||||
handshake_result.map_err(|e| zelf.remember_eof(e.into_py_err(vm), vm))?;
|
||||
if let Some(exc) = zelf.take_msg_exc() {
|
||||
return Err(exc);
|
||||
}
|
||||
@@ -3171,6 +3382,10 @@ mod _ssl {
|
||||
}
|
||||
}
|
||||
|
||||
if zelf.got_eof_error.load(Ordering::Relaxed) {
|
||||
return Err(create_ssl_eof_error(vm).upcast());
|
||||
}
|
||||
|
||||
// return empty bytes immediately for len=0
|
||||
if len == 0 {
|
||||
return match buffer {
|
||||
@@ -3243,7 +3458,11 @@ mod _ssl {
|
||||
let mut conn_guard = zelf.connection.lock();
|
||||
let conn = match conn_guard.as_mut() {
|
||||
Some(conn) => conn,
|
||||
None => return Err(create_ssl_eof_error(vm).upcast()),
|
||||
None => {
|
||||
return Err(
|
||||
zelf.remember_eof(create_ssl_eof_error(vm).upcast(), vm)
|
||||
);
|
||||
}
|
||||
};
|
||||
conn.pending_plaintext()
|
||||
};
|
||||
@@ -3256,7 +3475,7 @@ mod _ssl {
|
||||
}
|
||||
}
|
||||
// EOF occurred in violation of protocol (unexpected closure)
|
||||
Err(create_ssl_eof_error(vm).upcast())
|
||||
Err(zelf.remember_eof(create_ssl_eof_error(vm).upcast(), vm))
|
||||
}
|
||||
Err(error) if error.is_zero_return() => {
|
||||
// If plaintext is still buffered, return it before clean EOF.
|
||||
@@ -3302,12 +3521,12 @@ mod _ssl {
|
||||
Err(timeout_error_msg(vm, msg).upcast())
|
||||
}
|
||||
Err(crate::ssl::compat::SslError::Py(e)) => {
|
||||
// Python exception - pass through
|
||||
Err(e)
|
||||
// Exception from a transport operation.
|
||||
Err(zelf.remember_eof(e, vm))
|
||||
}
|
||||
Err(e) => {
|
||||
// Other SSL errors
|
||||
Err(e.into_py_err(vm))
|
||||
Err(zelf.remember_eof(e.into_py_err(vm), vm))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3326,6 +3545,9 @@ mod _ssl {
|
||||
|
||||
#[pymethod]
|
||||
fn write(zelf: &Py<Self>, b: ArgBytesLike, vm: &VirtualMachine) -> PyResult<usize> {
|
||||
if zelf.got_eof_error.load(Ordering::Relaxed) {
|
||||
return Err(create_ssl_eof_error(vm).upcast());
|
||||
}
|
||||
let data_bytes = b.borrow_buf();
|
||||
let data_len = data_bytes.len();
|
||||
|
||||
@@ -3372,7 +3594,7 @@ mod _ssl {
|
||||
Err(crate::ssl::compat::SslError::Timeout(msg)) => {
|
||||
Err(timeout_error_msg(vm, msg).upcast())
|
||||
}
|
||||
Err(e) => Err(e.into_py_err(vm)),
|
||||
Err(e) => Err(zelf.remember_eof(e.into_py_err(vm), vm)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3441,6 +3663,55 @@ mod _ssl {
|
||||
))
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn group(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<Option<String>> {
|
||||
let group = {
|
||||
let conn_guard = zelf.connection.lock();
|
||||
let Some(conn) = conn_guard.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(group) = conn.inner().negotiated_key_exchange_group() else {
|
||||
return Ok(None);
|
||||
};
|
||||
group.name()
|
||||
};
|
||||
group
|
||||
.as_str()
|
||||
.map(|name| Some(name.to_owned()))
|
||||
.ok_or_else(|| {
|
||||
vm.new_not_implemented_error(format!(
|
||||
"The TLS provider has no name for negotiated group {:#06x}",
|
||||
u16::from(group)
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn client_sigalg(_zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<Option<String>> {
|
||||
Err(vm.new_not_implemented_error(
|
||||
"Rustls does not expose the selected client authentication signature algorithm",
|
||||
))
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn server_sigalg(_zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<Option<String>> {
|
||||
Err(vm.new_not_implemented_error(
|
||||
"Rustls does not expose the selected server handshake signature algorithm",
|
||||
))
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn uses_ktls_for_send(_zelf: &Py<Self>) -> bool {
|
||||
// This transport encrypts records in rustls; it never enables kernel TLS.
|
||||
false
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn uses_ktls_for_recv(_zelf: &Py<Self>) -> bool {
|
||||
// This transport decrypts records in rustls; it never enables kernel TLS.
|
||||
false
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn version(zelf: &Py<Self>) -> Option<String> {
|
||||
// Extract cipher suite, releasing lock quickly
|
||||
@@ -4019,6 +4290,7 @@ mod _ssl {
|
||||
#[derive(Debug, PyPayload)]
|
||||
struct PySSLSession {
|
||||
context_identity: Arc<()>,
|
||||
policy_identity: Arc<()>,
|
||||
client_config: Arc<rustls::ClientConfig>,
|
||||
chain_builder: Arc<VerifiedChainBuilder>,
|
||||
session_store: Arc<CapturingClientSessionStore>,
|
||||
@@ -4185,6 +4457,28 @@ mod _ssl {
|
||||
tuple.into()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn get_sigalgs(vm: &VirtualMachine) -> PyResult<PyListRef> {
|
||||
// These are the TLS schemes advertised by the installed provider, in its
|
||||
// preference order, rather than certificate signature algorithms.
|
||||
let names = CryptoExt::get_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
.into_iter()
|
||||
.map(|scheme| {
|
||||
let name =
|
||||
rustpython_host_env::ssl::sigalg::scheme_name(scheme).ok_or_else(|| {
|
||||
vm.new_not_implemented_error(format!(
|
||||
"The TLS provider uses an unrecognized signature scheme {:#06x}",
|
||||
u16::from(scheme)
|
||||
))
|
||||
})?;
|
||||
Ok(vm.ctx.new_str(name).into())
|
||||
})
|
||||
.collect::<PyResult<Vec<_>>>()?;
|
||||
Ok(vm.ctx.new_list(names))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn RAND_status() -> i32 {
|
||||
1 // The configured rustls provider supplies cryptographic randomness.
|
||||
|
||||
@@ -140,6 +140,7 @@ impl SslError {
|
||||
match self {
|
||||
Self::Rustls(error) => TlsError::from_rustls(error.clone()).is_eof(),
|
||||
Self::Eof => true,
|
||||
Self::Io(error) => error.kind() == std::io::ErrorKind::UnexpectedEof,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cspell: ignore sigalgs
|
||||
|
||||
//! Wasm `_ssl` on the rustls-free `rustpython_host_env::ssl` surface.
|
||||
//!
|
||||
//! MemoryBIO, protocol constants, OID lookup, hostname checks, and ALPN
|
||||
@@ -137,6 +139,8 @@ mod _ssl {
|
||||
#[pyattr]
|
||||
const HAS_PSK: bool = false;
|
||||
#[pyattr]
|
||||
const HAS_PSK_TLS13: bool = false;
|
||||
#[pyattr]
|
||||
const HAS_SSLv2: bool = false;
|
||||
#[pyattr]
|
||||
const HAS_SSLv3: bool = false;
|
||||
@@ -886,6 +890,11 @@ mod _ssl {
|
||||
.into()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn get_sigalgs(vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
Err(vm.new_not_implemented_error(UNAVAILABLE))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn RAND_status() -> i32 {
|
||||
1
|
||||
|
||||
Reference in new issue
Block a user