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:
youknowdot authored and GitHub committed 2026-10-17 11:34:23 +09:00
1 parent 19d2fe3af7
commit fb76135ec8
14 files changed
+2638 -854

No files matched your search

Vendored
+84 -27
View File
@@ -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
+580 -50
View File
@@ -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
+3 -3
View File
@@ -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"],
+37 -4
View File
@@ -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 }
}
}
+38
View File
@@ -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:?}"),
}
}
}
+347
View File
@@ -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);
}
}
+2
View File
@@ -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;
+190
View File
@@ -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()
}
}
+27 -2
View File
@@ -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 {
+2
View File
@@ -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 {
File diff suppressed because it is too large. Load diff
+382 -88
View File
@@ -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.
+1
View File
@@ -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,
}
}
+9
View File
@@ -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