diff --git a/shelfmark/release_sources/irc/client.py b/shelfmark/release_sources/irc/client.py index dcea156e..5cc71249 100644 --- a/shelfmark/release_sources/irc/client.py +++ b/shelfmark/release_sources/irc/client.py @@ -123,6 +123,8 @@ class IRCClient: # Wrap with TLS if needed if self.use_tls: context = ssl.create_default_context() + # Python already defaults to TLS 1.2; pin it so an OpenSSL config cannot lower it + context.minimum_version = ssl.TLSVersion.TLSv1_2 # Skip verification for self-signed certs common on IRC servers context.check_hostname = False context.verify_mode = ssl.CERT_NONE diff --git a/tests/irc/test_client_tls.py b/tests/irc/test_client_tls.py new file mode 100644 index 00000000..fa102190 --- /dev/null +++ b/tests/irc/test_client_tls.py @@ -0,0 +1,33 @@ +"""The IRC TLS context must not allow anything older than TLS 1.2.""" + +import ssl + +import pytest + +from shelfmark.release_sources.irc.client import IRCClient, IRCConnectionError + + +def test_tls_context_refuses_tls_1_0_and_1_1(monkeypatch: pytest.MonkeyPatch) -> None: + contexts: list[ssl.SSLContext] = [] + real_create = ssl.create_default_context + + def capture(*args, **kwargs) -> ssl.SSLContext: + context = real_create(*args, **kwargs) + # Allow everything first, so the test fails if connect() does not raise the floor + context.minimum_version = ssl.TLSVersion.MINIMUM_SUPPORTED + + def refuse(*_args, **_kwargs): + raise OSError("no network in tests") + + context.wrap_socket = refuse # type: ignore[method-assign] + contexts.append(context) + return context + + monkeypatch.setattr(ssl, "create_default_context", capture) + client = IRCClient(nick="reader", server="irc.example.test", port=6697, use_tls=True) + + with pytest.raises(IRCConnectionError): + client.connect() + + assert len(contexts) == 1 + assert contexts[0].minimum_version == ssl.TLSVersion.TLSv1_2