From a3e387eb89e4a210bac1b3c8212250ec4cd5c607 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:01:24 +0900 Subject: [PATCH 1/5] test: reproduce TLS configuration subclass dispatch --- tests/test_tls_configuration_exact_type.py | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/test_tls_configuration_exact_type.py diff --git a/tests/test_tls_configuration_exact_type.py b/tests/test_tls_configuration_exact_type.py new file mode 100644 index 00000000..3daa619f --- /dev/null +++ b/tests/test_tls_configuration_exact_type.py @@ -0,0 +1,28 @@ +"""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): + """Return an insecure context if subclass-controlled dispatch is allowed.""" + + def create_ssl_context(self) -> ssl.SSLContext: + """Build a context that deliberately violates the EgressWeave TLS contract.""" + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + return context + + +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) From 7b04d33c6177f62529e7473b133e9f597fd1c63e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:02:49 +0900 Subject: [PATCH 2/5] fix: seal TLS configuration dispatch to exact type --- src/egressweave/tls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/egressweave/tls.py b/src/egressweave/tls.py index 556de828..e242b37c 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() From 02334e78316e3d8f4b3080f673b8fdff12f3f627 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:05:03 +0900 Subject: [PATCH 3/5] test(docs): require exact TLS dispatch guidance --- ..._configuration_exact_type_documentation.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/test_tls_configuration_exact_type_documentation.py 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 00000000..c11ce508 --- /dev/null +++ b/tests/test_tls_configuration_exact_type_documentation.py @@ -0,0 +1,24 @@ +"""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" + + +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 "exact `TLSConfiguration` type" in guide + assert "subclass" in guide + assert "before" in guide and "create_ssl_context" in guide + assert "hostname verification" in guide + assert "certificate verification" in guide From da35f3171d2a7295d7294b33ef6a08640bbf536a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:05:28 +0900 Subject: [PATCH 4/5] docs(security): document sealed TLS configuration dispatch --- docs/research/tls-configuration.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/research/tls-configuration.md b/docs/research/tls-configuration.md index 7ee06bf1..e9cd100c 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/ From 1db2a43cc367f15fb0eab6b3dfd682ca5ae44919 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:08:37 +0900 Subject: [PATCH 5/5] docs(security): preserve exact TLS boundary release history --- CHANGELOG.md | 5 +++++ tests/test_tls_configuration_exact_type_documentation.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e9dc0c..a916e69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). without changing the centrally managed review-agent credential contract. ### 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. - Canonicalize the public manifest writer's optional `forbidden_root` before any output-parent creation or output-path access. Missing, non-directory, symlinked, unresolvable, or otherwise noncanonical roots now fail with one diff --git a/tests/test_tls_configuration_exact_type_documentation.py b/tests/test_tls_configuration_exact_type_documentation.py index c11ce508..6d2f01d5 100644 --- a/tests/test_tls_configuration_exact_type_documentation.py +++ b/tests/test_tls_configuration_exact_type_documentation.py @@ -6,6 +6,7 @@ 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: @@ -22,3 +23,11 @@ def test_tls_guide_requires_exact_configuration_type_before_dispatch() -> None: assert "before" in guide and "create_ssl_context" in guide assert "hostname verification" in guide assert "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 "exact `TLSConfiguration` type" in changelog + assert "subclass" in changelog