Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions tests/test_irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,39 @@ def test_no_substring_of_the_line_survives(self):
out = _loggable_verb(line)
assert payload not in out
assert out in allowed


class TestTLSContext:
"""The Twitch bot sends its OAuth token via PASS, so the TLS floor matters.

`ssl.create_default_context()` leaves `minimum_version` at the
MINIMUM_SUPPORTED sentinel, which permits TLS 1.0/1.1 wherever the
platform still enables them (CodeQL alert #13).
"""

def test_context_pins_tls12_floor(self):
import ssl

from twitch import _make_tls_context

# Negative control: exhibit a context that FAILS the assertion below,
# so it cannot pass vacuously. Deliberately not asserting anything
# about create_default_context()'s own default — that varies by
# interpreter (3.12 reports the MINIMUM_SUPPORTED sentinel, 3.14
# already pins TLS 1.2), so a control built on it passes locally and
# fails in CI, which is exactly what happened here.
permissive = ssl.create_default_context()
permissive.minimum_version = ssl.TLSVersion.MINIMUM_SUPPORTED
assert permissive.minimum_version < ssl.TLSVersion.TLSv1_2

assert _make_tls_context().minimum_version >= ssl.TLSVersion.TLSv1_2

def test_context_still_verifies_certificates(self):
import ssl

from twitch import _make_tls_context

ctx = _make_tls_context()
# hardening the floor must not weaken verification
assert ctx.verify_mode == ssl.CERT_REQUIRED
assert ctx.check_hostname is True
16 changes: 15 additions & 1 deletion twitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ def _load_dotenv(dotenv_path: Path) -> None:
# =============================================================================


def _make_tls_context() -> ssl.SSLContext:
"""Return a TLS context with a TLS 1.2 floor.

`ssl.create_default_context()` leaves `minimum_version` at the
MINIMUM_SUPPORTED sentinel, so TLS 1.0/1.1 remain permitted wherever
the platform still enables them. This bot sends its OAuth token in the
IRC PASS command, so the negotiated floor is what protects that token
in transit.
"""
context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_2
return context


class BotConfig:
"""
Configuration class holding all bot settings.
Expand Down Expand Up @@ -235,7 +249,7 @@ def connect(self) -> None:
# the PASS command during authentication — travels encrypted rather than
# in plaintext. Twitch serves IRC over TLS on port 6697.
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
context = ssl.create_default_context()
context = _make_tls_context()
self.socket = context.wrap_socket(raw_sock, server_hostname=self.config.SERVER)
self.socket.connect((self.config.SERVER, self.config.PORT))

Expand Down
Loading