Skip to content
16 changes: 11 additions & 5 deletions docs/decision-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ resource is missing. Callers can validate `evidence.as_dict()` without adding a
JSON Schema runtime dependency to EgressWeave.

The v1 contract requires every emitted field, including the non-empty
`address_count` total and its IPv4/IPv6 family counts. `allowed_methods` may be
an intentionally empty deny-all set, but any method token must already be an
uppercase normalized token and `CONNECT` is never accepted. These checks
`address_count` total and its IPv4/IPv6 family counts. Its `authority` field is a
bounded canonical lowercase ASCII hostname plus TCP port, matching the runtime
comparison form after IDNA processing. The schema rejects URL syntax,
credentials, paths, IP literals and legacy numeric spellings—including dotted
hexadecimal forms—non-canonical hostname spellings, DNS names beyond the runtime
length ceiling, and ports outside `1..65535` rather than accepting sensitive or
impossible authority shapes that runtime evidence cannot emit. `allowed_methods`
may be an intentionally empty deny-all set, but any method token must already be
an uppercase normalized token and `CONNECT` is never accepted. These checks
describe evidence for a decision that was already authorized; the artifact is
subject to purpose limitation and does not authorize a request, path,
credential, tenant, or destination by itself.
subject to purpose limitation and does not authorize a request, path, credential,
tenant, or destination by itself.

## Data minimization

Expand Down
4 changes: 3 additions & 1 deletion src/egressweave/schemas/decision-evidence-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
},
"authority": {
"type": "string",
"minLength": 1
"minLength": 3,
"maxLength": 259,
"pattern": "^(?=.{1,253}:)(?![0-9.]+:)(?!0(?:\\.)*x[a-z0-9.-]*:)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*:(?:[1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"
},
"allowed_methods": {
"type": "array",
Expand Down
79 changes: 79 additions & 0 deletions tests/test_decision_evidence_authority_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Regressions for the privacy-minimized decision-evidence authority schema."""

from __future__ import annotations

import re

import egressweave


def _authority_schema() -> dict[str, object]:
"""Return the versioned authority field schema from the public loader."""
schema = egressweave.get_decision_evidence_json_schema()
properties = schema["properties"]
assert isinstance(properties, dict)
authority = properties["authority"]
assert isinstance(authority, dict)
return authority


def test_authority_schema_matches_runtime_canonical_shape() -> None:
"""Accept only bounded lowercase hostname-and-port evidence authorities."""
authority = _authority_schema()
pattern = authority["pattern"]

assert isinstance(pattern, str)
assert authority["type"] == "string"
assert authority["minLength"] == 3
assert authority["maxLength"] == 259

for value in (
"api.example.com:443",
"service-1:80",
"xn--bcher-kva.example:8443",
"a:1",
"api.example.com:65535",
):
assert re.fullmatch(pattern, value) is not None


def test_authority_schema_rejects_non_runtime_and_sensitive_shapes() -> None:
"""Keep URL syntax, credentials, paths, literals, and invalid ports out."""
pattern = _authority_schema()["pattern"]
assert isinstance(pattern, str)

for value in (
"https://api.example.com:443/private",
"https://user:secret@example.com/private?token=value",
"api.example.com/private:443",
"api.example.com",
"API.EXAMPLE.COM:443",
"api.example.com.:443",
"api..example.com:443",
"-api.example.com:443",
"api.example.com-:443",
"127.0.0.1:443",
"0x7f000001:443",
"0x7f.0.0.1:443",
"0.x7f:443",
"[2001:db8::1]:443",
"api.example.com:0",
"api.example.com:65536",
"api.example.com:99999",
):
assert re.fullmatch(pattern, value) is None


def test_authority_schema_bounds_hostname_length_independent_of_port_width() -> None:
"""Reject evidence hostnames that exceed the runtime DNS-name ceiling."""
pattern = _authority_schema()["pattern"]
assert isinstance(pattern, str)

label = "a" * 63
maximum_hostname = f"{label}.{label}.{label}.{'b' * 61}"
oversized_hostname = f"{label}.{label}.{label}.{'b' * 62}"

assert len(maximum_hostname) == 253
assert len(oversized_hostname) == 254
assert re.fullmatch(pattern, f"{maximum_hostname}:1") is not None
assert re.fullmatch(pattern, f"{oversized_hostname}:1") is None
16 changes: 14 additions & 2 deletions tests/test_decision_evidence_schema_current_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
from egressweave.validation import _make_validated_egress_url

_REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
_AUTHORITY_SCHEMA = {
"type": "string",
"minLength": 3,
"maxLength": 259,
"pattern": (
r"^(?=.{1,253}:)(?![0-9.]+:)(?!0(?:\.)*x[a-z0-9.-]*:)"
r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)"
r"(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*:"
r"(?:[1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|"
r"65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"
),
}
_METHOD_ITEM_SCHEMA = {
"type": "string",
"pattern": "^[!#$%&'*+.^_`|~0-9A-Z-]+$",
Expand Down Expand Up @@ -58,7 +70,7 @@ def test_packaged_schema_matches_protected_main_runtime_shape() -> None:
assert properties["schema_version"] == {
"const": egressweave.DECISION_EVIDENCE_SCHEMA_VERSION
}
assert properties["authority"] == {"type": "string", "minLength": 1}
assert properties["authority"] == _AUTHORITY_SCHEMA
assert properties["allowed_methods"] == {
"type": "array",
"uniqueItems": True,
Expand Down Expand Up @@ -137,7 +149,7 @@ def test_schema_loader_returns_detached_data_on_every_call() -> None:
second = _load_schema()
second_properties = second["properties"]
assert isinstance(second_properties, dict)
assert second_properties["authority"] == {"type": "string", "minLength": 1}
assert second_properties["authority"] == _AUTHORITY_SCHEMA


def test_schema_is_a_packaged_utf8_json_resource() -> None:
Expand Down
Loading