Source code for steamloop.certs
"""Embedded client certificates for thermostat mTLS authentication."""
from __future__ import annotations
import base64
import ssl
import tempfile
import zlib
from contextlib import contextmanager
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Generator
# Primary client cert + key (zlib compressed, base64 encoded)
_PRIMARY_CHAIN = (
"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"
"XPHJPg=="
)
# Secondary client cert + key (zlib compressed, base64 encoded)
_SECONDARY_CHAIN = (
"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"
"BNI9+NefT9H/fG7+DWdR0uU="
)
# Secondary root CA (zlib compressed, base64 encoded)
_SECONDARY_ROOT_CA = (
"eNrNlMvOs0YMhvdcRffRLw6BJCy6mIEBBkLIhDM7jkNCDiRAJnD1zfdXqlT1Burla8uW/dj+9etr"
"EJn48IeGTgE2sAYC9CP+4lyMdWnRNDB4FDAMAcXQCcAB0u7ZdmdTZQIEZDCADieXDEwjqR4RYiJm"
"R+GCEs6FoQnEEGmUeX6s3LPEXgrpM+wjuy1u5GMsIIL0EEEwuHp3nTIpmktJHQpN9NNY6TjXF5jD"
"fmfVde0UljdjynwY5mY0F7G6VKYhYMN+F2tCifTpi9gQvn4Yoo4SATHOasuDG3TM04H8LSF4AZbj"
"H20hP5rwj3b537ZAKTq7QDA1/2n6uFjrBH2nHgIgY6gzwL5+Bzy+dIgO2NDdxfyYlPReN9ZzvQtw"
"zDGwKsrj0TfTcnTZzUwrs/BdNeW3YiQr95zkt4rm6SHWAbG2ebwUwLkZm9p6CO1Fxhmn70PVX/BB"
"f79PTmgFr9Yva3dbduqudlb0Plb26J3f/G6vZiPKN+3rljF+LI4vCXrbZs2FsrXSdnFKHHxqL5d6"
"yg/F1gbVXXPn9TSm1XvcSCdwGZbZDrocN5in0d7F5LzKcK9AjuycdVm8n8/FYDgfz/w5LFev90V1"
"SNzW3XZ1jRam61NMn14yekm9LxL4SHpVgcLlc9C5x+lF0nLlH92yYH1Zq4eMP2iSR061gbtq90jX"
"mjYakyOW+yKeGnNvqUOvrj61gm9lI3KAuhAA80JTl9EUMB18kZ6E4DsyHgLMgA68nz2wyA6CZvdF"
"5Gogl5lFfsd5HIQpMpxlhpmQkhZ4n/6x+M+NQHD8nroq+Yb/LFplf/HyjMQuJKamDeYXtQGZq3GQ"
"0hekyICk1EEAmt/VfBeZOogp9IcdjSsTYaIPee4Kz2lEmvHa9la0/H2x3L9OFngaIAiMSoysNZV2"
"9S29b9DTleYALnFW1HqTjK3brjZdbqK01rcFJ3T+mXyUtG5mkvc7JD/obKeJHHinsLO7l5qAVMqV"
"8oaDl8zSc08bOGvLjMSLRjcPi8uW/npltB688Vwppf+SHtK43F+z1Lp8pYrdUJiW5b6WeWmGZq+/"
"k5C/e7UMlWg/OFjkNITkVxvuvY+6zvJxZ68Emk/2rdtG/eqaNbaFa3QN9X133IpJcjF9oVGGq8/W"
"PK8G3YMbrWiwPuDdoJMzzIelraJH7g3l4AzYFNPR3B2qzewBQ6vvtb20eXv2qks18ad9r03ym7sc"
"r/HIW+sgPA4xUGKtu7KtNAR22d43lShO5y3/fJe9Rf/8k/v9RtFB/+9r/QuNPsOH"
)
def _decode(data: str) -> bytes:
"""Decode a zlib-compressed, base64-encoded PEM blob."""
return zlib.decompress(base64.b64decode(data))
@contextmanager
def _pem_path(data: bytes) -> Generator[str, None, None]:
"""
Yield a file path containing PEM data, cleaned up on exit.
ssl.SSLContext.load_cert_chain() only accepts file paths — there is no
in-memory (cadata) variant for client certs in Python's ssl module.
"""
with tempfile.NamedTemporaryFile(suffix=".pem") as f:
f.write(data)
f.flush()
yield f.name
[docs]
@dataclass(frozen=True)
class CertSet:
"""A set of client certificates for mTLS authentication."""
name: str
chain_data: str
root_ca_data: str | None = None
[docs]
def create_ssl_context(cert_set: CertSet) -> ssl.SSLContext:
"""
Create an SSL context configured for thermostat mTLS.
Decodes the embedded cert data and loads it via load_cert_chain().
A brief temp file is used because ssl.SSLContext.load_cert_chain()
only accepts file paths.
"""
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# Thermostat only supports TLSv1.2 — TLS 1.3 ClientHello causes it to
# drop the connection with EOF during handshake.
ctx.maximum_version = ssl.TLSVersion.TLSv1_2
# Lower security level to allow older ciphers the thermostat requires
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
# Enable legacy server connect for older TLS implementations
ctx.options |= getattr(ssl, "OP_LEGACY_SERVER_CONNECT", 0x4)
chain_pem = _decode(cert_set.chain_data)
with _pem_path(chain_pem) as chain_path:
ctx.load_cert_chain(certfile=chain_path)
if cert_set.root_ca_data is not None:
root_pem = _decode(cert_set.root_ca_data)
ctx.load_verify_locations(cadata=root_pem.decode("ascii"))
return ctx
CERT_SETS = [
CertSet(
name="primary",
chain_data=_PRIMARY_CHAIN,
),
CertSet(
name="secondary",
chain_data=_SECONDARY_CHAIN,
root_ca_data=_SECONDARY_ROOT_CA,
),
]