Skip to content
Closed
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docs/research/tls-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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/

Expand Down
2 changes: 1 addition & 1 deletion src/egressweave/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
28 changes: 28 additions & 0 deletions tests/test_tls_configuration_exact_type.py
Original file line number Diff line number Diff line change
@@ -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)
33 changes: 33 additions & 0 deletions tests/test_tls_configuration_exact_type_documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""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 "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


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
Loading