From b0ec93850dec2f957760416f111f496217665098 Mon Sep 17 00:00:00 2001 From: Mike McDougall Date: Sat, 8 Aug 2026 17:30:07 -1000 Subject: [PATCH 1/3] feat: verify governed staging binding --- .github/workflows/staging-integration.yml | 3 + scripts/_smoke_harness.py | 185 +++++++++++++++++++++- tests/test_staging_binding_contract.py | 141 +++++++++++++++++ 3 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 tests/test_staging_binding_contract.py diff --git a/.github/workflows/staging-integration.yml b/.github/workflows/staging-integration.yml index 6ca63fb..d1354c4 100644 --- a/.github/workflows/staging-integration.yml +++ b/.github/workflows/staging-integration.yml @@ -47,6 +47,7 @@ jobs: HONUA_OGC_PROCESS_PAYLOAD_JSON: ${{ vars.HONUA_OGC_PROCESS_PAYLOAD_JSON }} HONUA_PROTOCOL_BBOX: ${{ vars.HONUA_PROTOCOL_BBOX }} HONUA_API_KEY: ${{ secrets.HONUA_API_KEY }} + HONUA_CLIENT_COMPAT_BINDING_JSON: ${{ vars.HONUA_CLIENT_COMPAT_BINDING_JSON }} HONUA_LOCAL_SERVER_IMAGE: ${{ vars.HONUA_LOCAL_SERVER_IMAGE || 'ghcr.io/honua-io/honua-server:latest-aot' }} steps: - uses: actions/checkout@v7 @@ -70,6 +71,7 @@ jobs: if [[ -n "${HONUA_BASE_URL}" ]]; then echo "configured=true" >> "${GITHUB_OUTPUT}" echo "local_stack=false" >> "${GITHUB_OUTPUT}" + echo "HONUA_LOCAL_STACK=false" >> "${GITHUB_ENV}" exit 0 fi @@ -82,6 +84,7 @@ jobs: echo "configured=true" >> "${GITHUB_OUTPUT}" echo "local_stack=true" >> "${GITHUB_OUTPUT}" + echo "HONUA_LOCAL_STACK=true" >> "${GITHUB_ENV}" - name: Checkout Honua Server seed fixtures if: ${{ steps.smoke-config.outputs.local_stack == 'true' }} diff --git a/scripts/_smoke_harness.py b/scripts/_smoke_harness.py index 888f142..5034470 100644 --- a/scripts/_smoke_harness.py +++ b/scripts/_smoke_harness.py @@ -4,13 +4,16 @@ from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone +from hashlib import sha256 import json from math import isclose import os from pathlib import Path +import re from typing import Any, Literal from urllib.parse import quote +from urllib.request import Request, urlopen from uuid import uuid4 from honua_sdk import HonuaClient, HonuaHttpError, __version__ as HONUA_SDK_VERSION @@ -30,6 +33,18 @@ POINT_GEOMETRY_ABS_TOLERANCE = 1e-6 OPTIONAL_PROTOCOL_SKIP_HTTP_STATUSES = frozenset({400, 404, 405, 501}) MAX_BODY_SUMMARY_LENGTH = 1200 +MAX_DESCRIPTOR_BYTES = 1_048_576 +CLIENT_COMPAT_BINDING_FORMAT = "honua.demo.client-compat-deployment.v1" +CLIENT_COMPAT_DESCRIPTOR_FORMAT = "honua.demo.client-compat.v1" +CLIENT_COMPAT_BINDING_ENV = "HONUA_CLIENT_COMPAT_BINDING_JSON" +CLIENT_COMPAT_OWNER_REPOSITORY = "honua-io/honua-demo-infra" +CLIENT_COMPAT_DESCRIPTOR_URL_PATTERN = re.compile( + r"^https://raw\.githubusercontent\.com/honua-io/honua-demo-infra/" + r"[0-9a-f]{40}/manifest/client-compat\.v1\.json$" +) +FULL_COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$") +IMMUTABLE_IMAGE_PATTERN = re.compile(r"^.+@sha256:[0-9a-f]{64}$") +SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") class SmokeConfigError(ValueError): @@ -52,6 +67,8 @@ class SmokeConfig: server_commit: str | None = None server_image: str | None = None seed_profile: str | None = None + local_stack: bool | None = None + governed_binding: Mapping[str, Any] | None = None ogc_collection_id: str | None = None stac_collection_id: str | None = None ogc_process_id: str | None = None @@ -67,6 +84,8 @@ def target_dict(self) -> dict[str, Any]: "server_commit": self.server_commit, "server_image": self.server_image, "seed_profile": self.seed_profile, + "local_stack": self.local_stack, + "governed_binding": dict(self.governed_binding) if self.governed_binding else None, "write_smoke_enabled": self.enable_write_smoke, "uid_prefix": self.uid_prefix, "ogc_collection_id": self.ogc_collection_id, @@ -170,17 +189,48 @@ def load_smoke_config_from_env(*, require_base_url: bool = True) -> SmokeConfig: except ValueError as exc: raise SmokeConfigError("HONUA_LAYER_ID must be an integer.") from exc + service_id = _read_env("HONUA_SERVICE_ID") or DEFAULT_SERVICE_ID + server_commit = _read_env("HONUA_SERVER_COMMIT") + server_image = _read_env("HONUA_SERVER_IMAGE") + seed_profile = _read_env("HONUA_SEED_PROFILE") + local_stack_text = _read_env("HONUA_LOCAL_STACK") + local_stack: bool | None = None + if local_stack_text: + normalized = local_stack_text.lower() + if normalized not in {"true", "false"}: + raise SmokeConfigError("HONUA_LOCAL_STACK must be true or false when set.") + local_stack = normalized == "true" + + governed_binding = None + binding_json = _read_env(CLIENT_COMPAT_BINDING_ENV) + if local_stack is False: + if not binding_json: + raise SmokeConfigError( + f"{CLIENT_COMPAT_BINDING_ENV} is required when HONUA_LOCAL_STACK=false." + ) + governed_binding = validate_client_compat_binding( + binding_json, + base_url=base_url, + service_id=service_id, + layer_id=layer_id, + server_commit=server_commit, + server_image=server_image, + seed_profile=seed_profile, + ) + return SmokeConfig( base_url=base_url, - service_id=_read_env("HONUA_SERVICE_ID") or DEFAULT_SERVICE_ID, + service_id=service_id, layer_id=layer_id, api_key=_read_env("HONUA_API_KEY"), enable_write_smoke=_read_bool_env("HONUA_ENABLE_WRITE_SMOKE", default=False), uid_prefix=_read_env("HONUA_SMOKE_UID_PREFIX") or DEFAULT_UID_PREFIX, results_path=Path(_read_env("HONUA_SMOKE_RESULTS_PATH") or DEFAULT_RESULTS_PATH), - server_commit=_read_env("HONUA_SERVER_COMMIT"), - server_image=_read_env("HONUA_SERVER_IMAGE"), - seed_profile=_read_env("HONUA_SEED_PROFILE"), + server_commit=server_commit, + server_image=server_image, + seed_profile=seed_profile, + local_stack=local_stack, + governed_binding=governed_binding, ogc_collection_id=_read_env("HONUA_OGC_COLLECTION_ID"), stac_collection_id=_read_env("HONUA_STAC_COLLECTION_ID"), ogc_process_id=_read_env("HONUA_OGC_PROCESS_ID"), @@ -189,6 +239,131 @@ def load_smoke_config_from_env(*, require_base_url: bool = True) -> SmokeConfig: ) +def validate_client_compat_binding( + binding_json: str, + *, + base_url: str, + service_id: str, + layer_id: int, + server_commit: str | None, + server_image: str | None, + seed_profile: str | None, + fetch_descriptor: Callable[[str], bytes] | None = None, + now: datetime | None = None, +) -> dict[str, Any]: + """Validate one atomic, non-secret remote deployment binding.""" + + try: + binding = json.loads(binding_json) + except json.JSONDecodeError as exc: + raise SmokeConfigError(f"{CLIENT_COMPAT_BINDING_ENV} must contain valid JSON.") from exc + if not isinstance(binding, dict): + raise SmokeConfigError(f"{CLIENT_COMPAT_BINDING_ENV} must contain a JSON object.") + if binding.get("format") != CLIENT_COMPAT_BINDING_FORMAT or binding.get("schemaVersion") != "1.0.0": + raise SmokeConfigError("Client-compat binding format/schemaVersion is unsupported.") + + generated_at = _parse_binding_timestamp(binding.get("generatedAt"), "generatedAt") + expires_at = _parse_binding_timestamp(binding.get("expiresAt"), "expiresAt") + checked_at = now or datetime.now(timezone.utc) + if generated_at > checked_at + timedelta(minutes=5): + raise SmokeConfigError("Client-compat binding generatedAt is in the future.") + if expires_at <= checked_at: + raise SmokeConfigError("Client-compat binding is stale: expiresAt has passed.") + if expires_at <= generated_at or expires_at - generated_at > timedelta(days=30): + raise SmokeConfigError("Client-compat binding expiry window must be positive and at most 30 days.") + + owner = binding.get("owner") + descriptor_ref = binding.get("descriptor") + target = binding.get("target") + access = binding.get("access") + if not all(isinstance(value, dict) for value in (owner, descriptor_ref, target, access)): + raise SmokeConfigError("Client-compat binding owner/descriptor/target/access must be objects.") + if owner.get("repository") != CLIENT_COMPAT_OWNER_REPOSITORY: + raise SmokeConfigError("Client-compat binding owner repository is not governed.") + if access.get("allowAnonymous") is not False or access.get("credentialRecorded") is not False: + raise SmokeConfigError("Client-compat binding must prove protected, credential-free evidence.") + + descriptor_url = descriptor_ref.get("url") + descriptor_digest = descriptor_ref.get("sha256") + if not isinstance(descriptor_url, str) or not CLIENT_COMPAT_DESCRIPTOR_URL_PATTERN.fullmatch(descriptor_url): + raise SmokeConfigError("Client-compat descriptor URL must be commit-pinned to honua-demo-infra.") + if not isinstance(descriptor_digest, str) or not SHA256_PATTERN.fullmatch(descriptor_digest): + raise SmokeConfigError("Client-compat descriptor SHA-256 is invalid.") + + if not server_commit or not FULL_COMMIT_PATTERN.fullmatch(server_commit): + raise SmokeConfigError("HONUA_SERVER_COMMIT must be a full commit for remote certification.") + if not server_image or not IMMUTABLE_IMAGE_PATTERN.fullmatch(server_image): + raise SmokeConfigError("HONUA_SERVER_IMAGE must be pinned by @sha256 for remote certification.") + expected_target = { + "baseUrl": base_url.rstrip("/"), + "serviceName": service_id, + "layerId": layer_id, + "seedProfile": seed_profile, + "server": {"commit": server_commit, "image": server_image}, + } + if target != expected_target: + raise SmokeConfigError("Client-compat binding target disagrees with staging variables.") + + descriptor_bytes = (fetch_descriptor or _fetch_descriptor_bytes)(descriptor_url) + if len(descriptor_bytes) > MAX_DESCRIPTOR_BYTES: + raise SmokeConfigError("Client-compat descriptor exceeds the maximum governed size.") + actual_descriptor_digest = sha256(descriptor_bytes).hexdigest() + if actual_descriptor_digest != descriptor_digest: + raise SmokeConfigError("Client-compat descriptor digest mismatch.") + try: + descriptor = json.loads(descriptor_bytes) + except json.JSONDecodeError as exc: + raise SmokeConfigError("Client-compat descriptor is not valid JSON.") from exc + if descriptor.get("format") != CLIENT_COMPAT_DESCRIPTOR_FORMAT or descriptor.get("schemaVersion") != "1.0.0": + raise SmokeConfigError("Client-compat descriptor format/schemaVersion is unsupported.") + descriptor_target = { + "baseUrl": str(descriptor.get("baseUrl", "")).rstrip("/"), + "serviceName": descriptor.get("service", {}).get("name"), + "layerId": descriptor.get("service", {}).get("layerId"), + "seedProfile": descriptor.get("fixture", {}).get("profile"), + } + if descriptor_target != {key: value for key, value in expected_target.items() if key != "server"}: + raise SmokeConfigError("Client-compat descriptor target disagrees with staging variables.") + if descriptor.get("access", {}).get("allowAnonymous") is not False: + raise SmokeConfigError("Client-compat descriptor does not require protected access.") + + canonical_binding = json.dumps( + binding, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + b"\n" + return { + "format": binding["format"], + "schema_version": binding["schemaVersion"], + "generated_at": binding["generatedAt"], + "expires_at": binding["expiresAt"], + "owner": owner, + "descriptor_url": descriptor_url, + "descriptor_sha256": descriptor_digest, + "deployment_evidence_sha256": sha256(canonical_binding).hexdigest(), + } + + +def _parse_binding_timestamp(value: Any, field_name: str) -> datetime: + if not isinstance(value, str): + raise SmokeConfigError(f"Client-compat binding {field_name} must be an ISO-8601 timestamp.") + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise SmokeConfigError(f"Client-compat binding {field_name} must be an ISO-8601 timestamp.") from exc + if parsed.tzinfo is None: + raise SmokeConfigError(f"Client-compat binding {field_name} must include a UTC offset.") + return parsed.astimezone(timezone.utc) + + +def _fetch_descriptor_bytes(url: str) -> bytes: + request = Request(url, headers={"Accept": "application/json", "User-Agent": "honua-sdk-python-staging-smoke"}) + with urlopen(request, timeout=10) as response: # noqa: S310 - URL is restricted by a full-match allowlist above. + body = response.read(MAX_DESCRIPTOR_BYTES + 1) + return body + + def _serialize_probe_exception(exc: Exception) -> dict[str, Any]: message = exc.message if isinstance(exc, HonuaHttpError) else str(exc) payload = { diff --git a/tests/test_staging_binding_contract.py b/tests/test_staging_binding_contract.py new file mode 100644 index 0000000..968b299 --- /dev/null +++ b/tests/test_staging_binding_contract.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from hashlib import sha256 +import json + +import pytest + +from scripts import _smoke_harness as smoke + + +NOW = datetime(2026, 8, 9, 3, 30, tzinfo=timezone.utc) +SERVER_COMMIT = "e" * 40 +SERVER_IMAGE = f"ghcr.io/honua-io/honua-server@sha256:{'f' * 64}" +DESCRIPTOR_URL = ( + f"https://raw.githubusercontent.com/honua-io/honua-demo-infra/{'a' * 40}" + "/manifest/client-compat.v1.json" +) + + +def descriptor_bytes() -> bytes: + return (json.dumps( + { + "format": "honua.demo.client-compat.v1", + "schemaVersion": "1.0.0", + "baseUrl": "https://demo.honua.io", + "service": {"name": "test_service", "layerId": 68823}, + "access": {"allowAnonymous": False}, + "fixture": {"profile": "client-compat"}, + }, + indent=2, + ) + "\n").encode() + + +def binding(*, expires_at: str = "2026-08-16T03:30:00Z") -> dict[str, object]: + descriptor = descriptor_bytes() + return { + "format": "honua.demo.client-compat-deployment.v1", + "schemaVersion": "1.0.0", + "generatedAt": "2026-08-09T03:30:00Z", + "expiresAt": expires_at, + "owner": { + "repository": "honua-io/honua-demo-infra", + "issue": "https://github.com/honua-io/honua-demo-infra/issues/28", + }, + "descriptor": { + "format": "honua.demo.client-compat.v1", + "schemaVersion": "1.0.0", + "url": DESCRIPTOR_URL, + "sha256": sha256(descriptor).hexdigest(), + }, + "target": { + "baseUrl": "https://demo.honua.io", + "serviceName": "test_service", + "layerId": 68823, + "seedProfile": "client-compat", + "server": {"commit": SERVER_COMMIT, "image": SERVER_IMAGE}, + }, + "access": { + "allowAnonymous": False, + "apiKeyHeader": "X-API-Key", + "credentialRecorded": False, + }, + "source": { + "repository": "honua-io/honua-demo-infra", + "commit": "a" * 40, + "workflowRunUrl": "https://github.com/honua-io/honua-demo-infra/actions/runs/1", + }, + } + + +def validate(payload: dict[str, object]) -> dict[str, object]: + return smoke.validate_client_compat_binding( + json.dumps(payload), + base_url="https://demo.honua.io", + service_id="test_service", + layer_id=68823, + server_commit=SERVER_COMMIT, + server_image=SERVER_IMAGE, + seed_profile="client-compat", + fetch_descriptor=lambda _url: descriptor_bytes(), + now=NOW, + ) + + +def test_valid_binding_records_descriptor_and_canonical_evidence_digest() -> None: + payload = binding() + actual = validate(payload) + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + b"\n" + + assert actual["descriptor_url"] == DESCRIPTOR_URL + assert actual["descriptor_sha256"] == sha256(descriptor_bytes()).hexdigest() + assert actual["deployment_evidence_sha256"] == sha256(canonical).hexdigest() + assert actual["expires_at"] == "2026-08-16T03:30:00Z" + + +def test_binding_fails_closed_when_expired() -> None: + with pytest.raises(smoke.SmokeConfigError, match="stale"): + validate(binding(expires_at="2026-08-09T03:29:59Z")) + + +def test_binding_fails_closed_on_descriptor_digest_mismatch() -> None: + payload = binding() + payload["descriptor"]["sha256"] = "0" * 64 # type: ignore[index] + with pytest.raises(smoke.SmokeConfigError, match="digest mismatch"): + validate(payload) + + +def test_binding_fails_closed_on_staging_target_mismatch() -> None: + payload = binding() + payload["target"]["layerId"] = 0 # type: ignore[index] + with pytest.raises(smoke.SmokeConfigError, match="disagrees with staging variables"): + validate(payload) + + +def test_remote_config_requires_atomic_binding(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("HONUA_BASE_URL", "https://demo.honua.io") + monkeypatch.setenv("HONUA_LOCAL_STACK", "false") + monkeypatch.delenv("HONUA_CLIENT_COMPAT_BINDING_JSON", raising=False) + + with pytest.raises(smoke.SmokeConfigError, match="is required"): + smoke.load_smoke_config_from_env() + + +def test_receipt_target_records_explicit_remote_mode_and_binding() -> None: + governed = validate(binding()) + config = smoke.SmokeConfig( + base_url="https://demo.honua.io", + service_id="test_service", + layer_id=68823, + server_commit=SERVER_COMMIT, + server_image=SERVER_IMAGE, + seed_profile="client-compat", + local_stack=False, + governed_binding=governed, + ) + + target = config.target_dict() + assert target["local_stack"] is False + assert target["governed_binding"]["descriptor_url"] == DESCRIPTOR_URL + assert target["governed_binding"]["deployment_evidence_sha256"] From 4f9726338f88ab4583675ef6fb2a41a89f5872b3 Mon Sep 17 00:00:00 2001 From: Mike McDougall Date: Sat, 8 Aug 2026 18:13:12 -1000 Subject: [PATCH 2/3] fix: scope descriptor URL lint suppression --- scripts/_smoke_harness.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/_smoke_harness.py b/scripts/_smoke_harness.py index 5034470..040390d 100644 --- a/scripts/_smoke_harness.py +++ b/scripts/_smoke_harness.py @@ -358,8 +358,11 @@ def _parse_binding_timestamp(value: Any, field_name: str) -> datetime: def _fetch_descriptor_bytes(url: str) -> bytes: - request = Request(url, headers={"Accept": "application/json", "User-Agent": "honua-sdk-python-staging-smoke"}) - with urlopen(request, timeout=10) as response: # noqa: S310 - URL is restricted by a full-match allowlist above. + request = Request( # noqa: S310 - URL is restricted by a full-match allowlist above. + url, + headers={"Accept": "application/json", "User-Agent": "honua-sdk-python-staging-smoke"}, + ) + with urlopen(request, timeout=10) as response: body = response.read(MAX_DESCRIPTOR_BYTES + 1) return body From d9d7fed9e3003a3cb443a3fd62c6246c547bb960 Mon Sep 17 00:00:00 2001 From: Mike McDougall Date: Sat, 8 Aug 2026 18:17:05 -1000 Subject: [PATCH 3/3] fix: allowlist descriptor fetch lint sites --- scripts/_smoke_harness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/_smoke_harness.py b/scripts/_smoke_harness.py index 040390d..49366b2 100644 --- a/scripts/_smoke_harness.py +++ b/scripts/_smoke_harness.py @@ -362,7 +362,7 @@ def _fetch_descriptor_bytes(url: str) -> bytes: url, headers={"Accept": "application/json", "User-Agent": "honua-sdk-python-staging-smoke"}, ) - with urlopen(request, timeout=10) as response: + with urlopen(request, timeout=10) as response: # noqa: S310 - Request URL is allowlisted above. body = response.read(MAX_DESCRIPTOR_BYTES + 1) return body