diff --git a/CHANGELOG.md b/CHANGELOG.md index 56920e7..b439d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Add immutable provider-neutral `TLSConfiguration` dependency injection for private trust stores and mutual-TLS client identities across synchronous and asynchronous DNS-pinned builders. TLS 1.3 is the default; explicit TLS 1.2 - compatibility remains restricted to forward-secret ECDHE suites. + compatibility remains restricted to forward-secret ECDHE suites. Before + dispatch, builders require the exact `TLSConfiguration` type so a subclass + cannot replace context construction or inject subclass-controlled TLS policy. - Add explicit, deterministic `EgressDecisionEvidence` for successful egress decisions. Evidence revalidates signed state and records canonical authority, method policy, aggregate address-family counts, and correlation fingerprints @@ -86,6 +88,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). disable the recurring loop. ### Security +- Require the exact `TLSConfiguration` type before TLS context creation. A + subclass can no longer override `create_ssl_context()` to replace the reviewed + immutable policy with a context that disables hostname or certificate + verification; private trust, mTLS, and explicit TLS 1.2 compatibility remain + available through the documented declarative fields. - Reject non-exact integer subclasses in shared policy integer fields before retaining trusted configuration state. Exact built-in integers and existing ASCII decimal strings remain supported, preserving defaults and ranges while diff --git a/docs/research/tls-configuration.md b/docs/research/tls-configuration.md index 7ee06bf..e9cd100 100644 --- a/docs/research/tls-configuration.md +++ b/docs/research/tls-configuration.md @@ -50,6 +50,16 @@ trusted integration point for deferred secret retrieval. Every transport owns the fresh context that results, eliminating post-validation caller mutation as an authority channel. +The public context helper accepts only the exact `TLSConfiguration` type before +it invokes `create_ssl_context()`. Subclassing this security value object is not +an extension mechanism: a subclass could otherwise replace +`create_ssl_context()` with caller-controlled dispatch and return a context that +disables hostname verification or certificate verification while still passing +an `isinstance` check. Rejecting subclasses before that method is invoked keeps +the reviewed configuration fields, not polymorphic code, authoritative for TLS +policy. This is a pre-1.0 secure-default tightening and does not claim to sandbox +arbitrary trusted Python executing inside the embedding process. + ## Trust-store semantics The default preserves EgressWeave's existing HTTPX trust behavior while @@ -103,6 +113,11 @@ default. An existing endpoint that cannot yet negotiate TLS 1.3 can opt into `minimum_version=ssl.TLSVersion.TLSv1_2`; this is an explicit compatibility exception that should be inventoried and removed after the peer is upgraded. +Applications that previously subclassed `TLSConfiguration` must migrate to an +exact instance using the documented declarative fields. Private trust roots, +mutual-TLS identities, deferred private-key passwords, and the explicit TLS 1.2 +compatibility floor remain supported without subclassing. + The configuration is threaded through both public builders and both already-validated pinned-client builders. It changes only TLS trust and client identity; exact authority, DNS pinning, proxy isolation, request framing and @@ -114,6 +129,9 @@ independently enforced. Aviram, N. (2026). *Deprecating obsolete key exchange methods in TLS 1.2 and DTLS 1.2* (RFC 10015). RFC Editor. https://www.rfc-editor.org/rfc/rfc10015.html +MITRE Corporation. (2026). *CWE-295: Improper certificate validation* (CWE +Version 4.20). https://cwe.mitre.org/data/definitions/295.html + OpenSSL Project Authors. (2026). *openssl-ciphers*. OpenSSL 3.0 documentation. https://docs.openssl.org/3.0/man1/openssl-ciphers/ diff --git a/src/egressweave/tls.py b/src/egressweave/tls.py index 556de82..e242b37 100644 --- a/src/egressweave/tls.py +++ b/src/egressweave/tls.py @@ -212,7 +212,7 @@ def create_egress_ssl_context( """Create the default HTTPX context or a fresh configured enterprise context.""" if configuration is None: return _create_httpx_ssl_context(verify=True, trust_env=False) - if not isinstance(configuration, TLSConfiguration): + if type(configuration) is not TLSConfiguration: raise TypeError("tls_configuration must be TLSConfiguration or None") return configuration.create_ssl_context() diff --git a/tests/test_tls_configuration_exact_type.py b/tests/test_tls_configuration_exact_type.py new file mode 100644 index 0000000..055a9d5 --- /dev/null +++ b/tests/test_tls_configuration_exact_type.py @@ -0,0 +1,25 @@ +"""Exact-type security contracts for enterprise TLS configuration.""" + +from __future__ import annotations + +import ssl + +import pytest + +from egressweave.tls import TLSConfiguration, create_egress_ssl_context + + +class _VerificationDisablingTLSConfiguration(TLSConfiguration): + """Fail if subclass-controlled TLS context dispatch occurs.""" + + def create_ssl_context(self) -> ssl.SSLContext: + """Reject any attempt to execute subclass-controlled TLS policy code.""" + raise AssertionError("subclass dispatch must not occur") + + +def test_context_helper_rejects_tls_configuration_subclasses_before_dispatch() -> None: + """Reject polymorphic configuration before subclass code can replace TLS policy.""" + configuration = _VerificationDisablingTLSConfiguration() + + with pytest.raises(TypeError, match="TLSConfiguration or None"): + create_egress_ssl_context(configuration) diff --git a/tests/test_tls_configuration_exact_type_documentation.py b/tests/test_tls_configuration_exact_type_documentation.py new file mode 100644 index 0000000..d3ffc62 --- /dev/null +++ b/tests/test_tls_configuration_exact_type_documentation.py @@ -0,0 +1,43 @@ +"""Documentation contracts for the exact TLS configuration type boundary.""" + +from __future__ import annotations + +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +TLS_GUIDE = REPOSITORY_ROOT / "docs" / "research" / "tls-configuration.md" +CHANGELOG = REPOSITORY_ROOT / "CHANGELOG.md" + + +def _normalized(path: Path) -> str: + """Return repository documentation with insignificant whitespace collapsed.""" + return " ".join(path.read_text(encoding="utf-8").split()) + + +def test_tls_guide_requires_exact_configuration_type_before_dispatch() -> None: + """Explain why subclass dispatch cannot replace the reviewed TLS policy.""" + guide = _normalized(TLS_GUIDE) + + assert ( + "The public context helper accepts only the exact `TLSConfiguration` type " + "before it invokes `create_ssl_context()`." + ) in guide + assert ( + "a subclass could otherwise replace `create_ssl_context()` with " + "caller-controlled dispatch and return a context that disables hostname " + "verification or certificate verification" + ) in guide + + +def test_changelog_records_exact_tls_configuration_type_boundary() -> None: + """Keep the pre-1.0 TLS policy-integrity tightening visible to integrators.""" + changelog = _normalized(CHANGELOG) + + assert ( + "Require the exact `TLSConfiguration` type before TLS context creation." + ) in changelog + assert ( + "A subclass can no longer override `create_ssl_context()` to replace the " + "reviewed immutable policy with a context that disables hostname or " + "certificate verification" + ) in changelog