From 8a6e10d0ef5508100b9979b6caad21461f61dd1a Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 05:22:43 -0700 Subject: [PATCH 01/10] feat: require signed production e2e evidence --- CHANGELOG.md | 7 + artifacts/p1_acceptance_report.json | 27 +- contracts/schemas/certification-run.v1.json | 21 +- deploy/deploy_forge.sh | 2 +- docs/OPERATIONS.md | 8 + docs/PROJECT_CONTRACT.md | 16 + policies/mandatory-rules.v1.json | 8 + policies/mandatory-rules.v2.json | 8 + pyproject.toml | 2 +- scripts/certification_journey.py | 2 +- scripts/p1_acceptance.py | 29 +- src/echo_certification_forge/__init__.py | 2 +- src/echo_certification_forge/executor.py | 40 +- src/echo_certification_forge/intake.py | 66 +++- src/echo_certification_forge/models.py | 6 + .../production_e2e.py | 351 ++++++++++++++++++ src/echo_certification_forge/run_worker.py | 45 ++- src/echo_certification_forge/verdict.py | 45 +++ tests/conftest.py | 5 +- tests/production_e2e_support.py | 46 +++ tests/test_additional_contracts.py | 9 +- tests/test_evidence_and_verdict.py | 58 ++- tests/test_p5_executor_adapter_gate.py | 5 +- tests/test_p6_deployment_enforcement.py | 6 +- tests/test_p6_platform_integration.py | 32 +- tests/test_p7_subscriber_governance.py | 3 + tests/test_production_e2e.py | 214 +++++++++++ tests/test_runner.py | 7 +- tests/test_t4_live_run.py | 4 +- tests/test_t4p5_executor.py | 6 +- tests/test_t4p7_e2e.py | 2 + tests/test_t4p8_sandbox.py | 2 + 32 files changed, 1027 insertions(+), 57 deletions(-) create mode 100644 src/echo_certification_forge/production_e2e.py create mode 100644 tests/production_e2e_support.py create mode 100644 tests/test_production_e2e.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8b23b..4d2f4c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.2.0] - 2026-08-30 + +### Added + +- A pinned-key-signed production E2E gate required before any `PRODUCTION_READY` verdict. +- Exact E2E bindings for source, deployment, target, environment, accounts, and clients. + All notable changes to Echo Certification Forge are documented here. Versions follow Semantic Versioning; dates use ISO 8601. diff --git a/artifacts/p1_acceptance_report.json b/artifacts/p1_acceptance_report.json index 771a206..1e54769 100644 --- a/artifacts/p1_acceptance_report.json +++ b/artifacts/p1_acceptance_report.json @@ -1,10 +1,10 @@ { - "manifest_digest": "0ca325c3e6d39b7f22cdd3d01068c8f99dcd976c4422200b5b4d4cfe452f29f7", + "manifest_digest": "9e9f963377374e827ccc9c8b07ea82cc7966e25a2392838997d3c1371f5a4e5c", "manifest_id": "certforge.release-strict.v1", "passed": true, "phase": "P1", - "platform": "Windows-11-10.0.22631-SP0", - "python": "3.13.0 (tags/v3.13.0:60403a5, Oct 7 2024, 09:38:07) [MSC v.1941 64 bit (AMD64)]", + "platform": "Windows-11-10.0.26200-SP0", + "python": "3.12.10 (tags/v3.12.10:0cc8128, Apr 8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)]", "scenarios": { "default_block": { "passed": true, @@ -17,27 +17,30 @@ "mandatory_rule_missing:environment_identity", "mandatory_rule_missing:evidence_integrity", "mandatory_rule_missing:immutable_target_identity", + "mandatory_rule_missing:production_e2e", "mandatory_rule_missing:runner_control_channel", "mandatory_rule_missing:signing_authority_separation", "mandatory_rule_missing:tenant_isolation", + "production_e2e_attestation_missing", "run_outcome_inconclusive" ], "verdict": "NOT_READY" }, + "source_only_blocks": { + "deploy_gate_allowed": false, + "evidence_merkle_root": "41a4fc282a478767dc16f6c64128c93ab2535866e620a3f5b0ae922220ffb449", + "passed": true, + "payload_sha256": "a7255f5bae53fcd49b1e049b3edd1feb6c9b03f4520b0e2abb0e44f0da7138e7", + "signing_key_id": "ed25519:e222f1c31e85f1b8a6d8cfed30fd2300", + "verdict": "NOT_READY" + }, "tamper_blocks": { "deploy_gate_allowed": false, "passed": true, "reasons": [ - "current_evidence_invalid" + "current_evidence_invalid", + "verdict_not_production_ready" ] - }, - "verified_ready": { - "deploy_gate_allowed": true, - "evidence_merkle_root": "438efda29a515669d0db0472f6527c201c55d0d80afebf2963123d11c77a7d73", - "passed": true, - "payload_sha256": "db3fe179c7c950a9ffaf90cfef6d475fc915af87f0d1b10a85071c7d8411b975", - "signing_key_id": "ed25519:4592da038060823a957506d477e49114", - "verdict": "PRODUCTION_READY" } }, "schema_version": "1.0.0" diff --git a/contracts/schemas/certification-run.v1.json b/contracts/schemas/certification-run.v1.json index 3e4f4b0..4797ffd 100644 --- a/contracts/schemas/certification-run.v1.json +++ b/contracts/schemas/certification-run.v1.json @@ -3,7 +3,7 @@ "$id": "https://cert.echoforge.com/contracts/certification-run.v1.json", "title": "CertificationRunV1", "type": "object", - "required": ["run_id", "tenant_id", "state", "run_outcome", "release_verdict", "target_identity_digest", "environment_identity_digest", "policy_version"], + "required": ["run_id", "tenant_id", "state", "run_outcome", "release_verdict", "target_identity_digest", "environment_identity_digest", "policy_version", "production_e2e"], "properties": { "run_id": {"type": "string", "minLength": 1, "maxLength": 128}, "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, @@ -14,6 +14,25 @@ "environment_identity_digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "policy_version": {"type": "string", "minLength": 1, "maxLength": 128}, "evidence_merkle_root": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, + "production_e2e": { + "type": "object", + "required": ["verified", "profile", "attestation_id", "source_commit", "deployment_sha", "target_identity_digest", "environment_identity_digest", "canonical_target", "tool_count", "signature_verified", "collector_key_id", "attestation_envelope_sha256"], + "properties": { + "verified": {"type": "boolean"}, + "profile": {"type": ["string", "null"]}, + "attestation_id": {"type": ["string", "null"]}, + "source_commit": {"type": ["string", "null"], "pattern": "^[0-9a-f]{40}(?:[0-9a-f]{24})?$"}, + "deployment_sha": {"type": ["string", "null"], "pattern": "^[0-9a-f]{40}(?:[0-9a-f]{24})?$"}, + "target_identity_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, + "environment_identity_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, + "canonical_target": {"type": ["string", "null"]}, + "tool_count": {"type": ["integer", "null"], "minimum": 0}, + "signature_verified": {"type": "boolean"}, + "collector_key_id": {"type": ["string", "null"], "pattern": "^ed25519:[0-9a-f]{32}$"}, + "attestation_envelope_sha256": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"} + }, + "additionalProperties": false + }, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"} }, diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index c280297..2e649b5 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -16,7 +16,7 @@ CURRENT_LINK="${CERTFORGE_CURRENT_LINK:-/home/forge/echo-certification-forge-cur STATE_ROOT="${CERTFORGE_STATE_ROOT:-/home/forge/echo-certification-forge/var}" ADAPTER_DIR="${ECHO_CERTFORGE_PROD_ADAPTER_DIR:-$STATE_ROOT/p5}" ADAPTER_MODE="${CERTFORGE_ADAPTER_MODE:-required}" -TRUSTED_MANIFEST_SHA256="${ECHO_CERTFORGE_TRUSTED_MANIFEST_SHA256:-7dc98e0e95e6dd2c000ec069a8c46c4d1d49a4fe869ad4eae25e059d103644f4}" +TRUSTED_MANIFEST_SHA256="${ECHO_CERTFORGE_TRUSTED_MANIFEST_SHA256:-965106b00917268d556b325719f26f5096e6c3746551658ffecb9fd4a95ec342}" UNIT_PATH="/etc/systemd/system/$SERVICE.service" DISPATCH_UNIT_PATH="/etc/systemd/system/$DISPATCH_SERVICE.service" RELEASE_DROPIN="/etc/systemd/system/$SERVICE.service.d/10-release.conf" diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 66e4b6f..7e47fee 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -10,6 +10,14 @@ ## Verification sequence +Before a worker may issue `PRODUCTION_READY`, mount a collector envelope through +`--production-e2e-attestation` (or +`ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION`) and a directory containing only the +pinned collector public keys through `--trusted-production-e2e-keys` (or +`ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS`). The private collector key must not +be present on the worker. Absence, signature failure, expiration, or any exact +identity/E2E mismatch is a normal `NOT_READY` result, never a bypass. + 1. Confirm hosted CI succeeded for the exact source SHA. 2. Execute the declared production-shaped journey with the pinned runner image. 3. Verify evidence custody, receipt chains, signatures, expiry, and revocation state. diff --git a/docs/PROJECT_CONTRACT.md b/docs/PROJECT_CONTRACT.md index eb191dd..e452ded 100644 --- a/docs/PROJECT_CONTRACT.md +++ b/docs/PROJECT_CONTRACT.md @@ -41,3 +41,19 @@ GS343 may propose discovery, classification, and bounded harness repairs. Determ ## Completion rule Implementation, source validation, contracts, real runtime, failure and recovery, evidence package, scoped Git record, and Commander acceptance are distinct gates. A registered capability is not presumed visible, healthy, schema-complete, authorized, or runnable. + +## Production E2E verdict gate + +`PRODUCTION_READY` requires a current Ed25519-signed `certforge.production-e2e.v1` +attestation from an independently pinned collector. The attestation must bind the +exact target and environment identities, exact deployed source revision, real +critical journeys, negative controls, repeated stability probes, and external +acceptance. Source checks, local journeys, HTTP 200, unsigned status fields, and +target-authored evidence cannot satisfy this gate. Missing, expired, untrusted, +partially passing, or identity-mismatched attestations remain `NOT_READY`. + +For Echo GitHub Autonomy the target-specific profile additionally requires the +canonical MCP and OAuth surface, 30-tool schema, repeated discovery plus +invocation without registry loss, reconciled public/private inventory and +read/write/certify authority for all four exact GitHub account IDs, and matching +private-repository fingerprints from ChatGPT, Claude, Codex, and Grok. diff --git a/policies/mandatory-rules.v1.json b/policies/mandatory-rules.v1.json index ae17648..25e8723 100644 --- a/policies/mandatory-rules.v1.json +++ b/policies/mandatory-rules.v1.json @@ -83,6 +83,14 @@ "conditional_allowed": false, "minimum_evidence": 1, "description": "Deployment authorization checks the exact certified digest, policy, environment, and lifecycle." + }, + { + "id": "production_e2e", + "severity": "BLOCKER", + "mandatory": true, + "conditional_allowed": false, + "minimum_evidence": 1, + "description": "A trusted independent collector proves the exact deployed identity, real critical journeys, negative controls, stability, and external client acceptance end to end." } ] } diff --git a/policies/mandatory-rules.v2.json b/policies/mandatory-rules.v2.json index 45c4ee9..30a31dc 100644 --- a/policies/mandatory-rules.v2.json +++ b/policies/mandatory-rules.v2.json @@ -91,6 +91,14 @@ "conditional_allowed": false, "minimum_evidence": 1, "description": "Deployment authorization checks the exact certified digest, policy, environment, and lifecycle." + }, + { + "id": "production_e2e", + "severity": "BLOCKER", + "mandatory": true, + "conditional_allowed": false, + "minimum_evidence": 1, + "description": "A trusted independent collector proves the exact deployed identity, real critical journeys, negative controls, stability, and external client acceptance end to end." } ] } diff --git a/pyproject.toml b/pyproject.toml index 754c58c..3f1a10e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "echo-certification-forge" -version = "1.1.0" +version = "1.2.0" description = "Deterministic, evidence-backed release authority for EchoForge" readme = "README.md" requires-python = ">=3.12" diff --git a/scripts/certification_journey.py b/scripts/certification_journey.py index 712b631..38ca793 100644 --- a/scripts/certification_journey.py +++ b/scripts/certification_journey.py @@ -45,7 +45,7 @@ def require(condition: bool, message: str) -> None: def main() -> int: - require(__version__ == "1.1.0", "package version is not synchronized") + require(__version__ == "1.2.0", "package version is not synchronized") python_files = sorted(SRC.rglob("*.py")) require(bool(python_files), "source tree contains no Python modules") for path in python_files: diff --git a/scripts/p1_acceptance.py b/scripts/p1_acceptance.py index 77c43e4..163a603 100644 --- a/scripts/p1_acceptance.py +++ b/scripts/p1_acceptance.py @@ -70,30 +70,37 @@ def main() -> int: "reasons": list(default.reasons), } - store.register_run("cert-verified-ready", target, environment, manifest.manifest_id, manifest.digest) + # P1 is source/control-plane acceptance, not an external production E2E run. Even with + # every local rule row marked true, it must remain NOT_READY without the independently + # signed production_e2e attestation. + store.register_run("cert-source-only", target, environment, manifest.manifest_id, manifest.digest) for index, rule in enumerate(manifest.rules, start=1): artifact_id = f"proof-{index:02d}" content = json.dumps({"rule": rule.id, "passed": True}, sort_keys=True).encode() store.append_artifact( - "cert-verified-ready", target.tenant_id, artifact_id, content, + "cert-source-only", target.tenant_id, artifact_id, content, "application/json", "p1-independent-harness", RedactionStatus.COMPLETE, ) store.record_rule_result( - "cert-verified-ready", target.tenant_id, + "cert-source-only", target.tenant_id, RuleResult(rule.id, True, (artifact_id,), {"fixture": "known-good"}), ) - store.set_run_outcome("cert-verified-ready", target.tenant_id, RunOutcome.COMPLETE) - ready = engine.evaluate(store, "cert-verified-ready", target.tenant_id, manifest, signer.key_id) + store.set_run_outcome("cert-source-only", target.tenant_id, RunOutcome.COMPLETE) + ready = engine.evaluate(store, "cert-source-only", target.tenant_id, manifest, signer.key_id) envelope = signer.sign(ready) - store.save_signed_verdict("cert-verified-ready", target.tenant_id, envelope) + store.save_signed_verdict("cert-source-only", target.tenant_id, envelope) trusted = TrustedPublicKeyRegistry.empty() trusted.add_pem(signer.public_key_pem) gate = DeployGate(store, trusted).evaluate( - target.tenant_id, "cert-verified-ready", target.identity_digest, + target.tenant_id, "cert-source-only", target.identity_digest, environment.identity_digest, manifest.digest, ) - report["scenarios"]["verified_ready"] = { - "passed": ready.release_verdict.value == "PRODUCTION_READY" and gate.allowed, + report["scenarios"]["source_only_blocks"] = { + "passed": ( + ready.release_verdict.value == "NOT_READY" + and not gate.allowed + and "production_e2e_schema_invalid" in ready.reasons + ), "verdict": ready.release_verdict.value, "deploy_gate_allowed": gate.allowed, "evidence_merkle_root": ready.evidence_merkle_root, @@ -101,10 +108,10 @@ def main() -> int: "signing_key_id": envelope.key_id, } - tampered_path = workspace / "evidence" / target.tenant_id / "cert-verified-ready" / "artifacts" / "proof-01.bin" + tampered_path = workspace / "evidence" / target.tenant_id / "cert-source-only" / "artifacts" / "proof-01.bin" tampered_path.write_bytes(b"tampered") tampered_gate = DeployGate(store, trusted).evaluate( - target.tenant_id, "cert-verified-ready", target.identity_digest, + target.tenant_id, "cert-source-only", target.identity_digest, environment.identity_digest, manifest.digest, ) report["scenarios"]["tamper_blocks"] = { diff --git a/src/echo_certification_forge/__init__.py b/src/echo_certification_forge/__init__.py index c8c894b..ddc7799 100644 --- a/src/echo_certification_forge/__init__.py +++ b/src/echo_certification_forge/__init__.py @@ -3,4 +3,4 @@ from .models import ReleaseVerdict, RunOutcome, RunState __all__ = ["ReleaseVerdict", "RunOutcome", "RunState"] -__version__ = "1.1.0" +__version__ = "1.2.0" diff --git a/src/echo_certification_forge/executor.py b/src/echo_certification_forge/executor.py index 4105c7e..e909dc3 100644 --- a/src/echo_certification_forge/executor.py +++ b/src/echo_certification_forge/executor.py @@ -29,8 +29,11 @@ from .canonical import to_utc_iso, utc_now from .evidence import EvidenceStore from .hostile import scan_target_source -from .models import RedactionStatus, RunOutcome, RuleResult, RunState +from .models import EnvironmentIdentity, RedactionStatus, RuleResult, RunOutcome, RunState, TargetIdentity from .policy import RuleManifest +from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID +from .production_e2e import VerifiedProductionE2E +from .production_e2e import validate_production_e2e from .signing import Ed25519VerdictSigner from .supply_chain import scan_dockerfile from .verdict import DeterministicVerdictEngine @@ -193,6 +196,7 @@ def execute( control_attestations: dict[str, bool] | None = None, adapter_records: tuple[AdapterExecutionRecord, ...] | None = None, adapter_policy: AdapterAcceptancePolicy | None = None, + production_e2e_attestation: VerifiedProductionE2E | None = None, execution_guard: Callable[[], None] | None = None, completion_callback: Callable[[Any], None] | None = None, ) -> ExecutionResult: @@ -393,6 +397,32 @@ def bound(digest_key: str) -> bool: # Includes adapter records/assessment when supplied. evidence_chain_ok = self.store.verify_evidence(run_id, tenant_id).valid + production_e2e_passed = False + production_e2e_detail: dict[str, Any] = { + "validation": "production_e2e_attestation_missing" + } + try: + target_identity = TargetIdentity(**json.loads(run["target_identity_json"])) + environment_identity = EnvironmentIdentity(**json.loads(run["environment_identity_json"])) + production_e2e_payload = ( + production_e2e_attestation.payload + if isinstance(production_e2e_attestation, VerifiedProductionE2E) + else None + ) + production_e2e_passed, validation = validate_production_e2e( + production_e2e_payload, + target_identity, + environment_identity, + now=utc_now(), + ) + production_e2e_detail = ( + production_e2e_attestation.result_details() + if production_e2e_passed and production_e2e_attestation is not None + else {"validation": validation} + ) + except (TypeError, ValueError): + production_e2e_detail = {"validation": "production_e2e_identity_invalid"} + checks: dict[str, tuple[bool, dict]] = { "immutable_target_identity": ( bound("target_identity_digest"), @@ -429,6 +459,7 @@ def bound(digest_key: str) -> bool: bool(control_attestations.get("signing_authority_separation", False)), {"check": "trusted_caller_attestation"}, ), + PRODUCTION_E2E_RULE_ID: (production_e2e_passed, production_e2e_detail), } for rule in self.manifest.rules: if execution_guard is not None: @@ -444,7 +475,12 @@ def bound(digest_key: str) -> bool: self.store.record_rule_result( run_id, tenant_id, - RuleResult(rule.id, passed, (artifact_id,), {"source": "executor"}), + RuleResult( + rule.id, + passed, + (artifact_id,), + detail if rule.id == PRODUCTION_E2E_RULE_ID else {"source": "executor"}, + ), ) self._t(run_id, tenant_id, RunState.CLASSIFYING_FINDINGS, "classify") diff --git a/src/echo_certification_forge/intake.py b/src/echo_certification_forge/intake.py index 2842cf5..67cf2af 100644 --- a/src/echo_certification_forge/intake.py +++ b/src/echo_certification_forge/intake.py @@ -12,9 +12,17 @@ from pydantic import BaseModel, ConfigDict, Field -from .canonical import sha256_json +from .canonical import sha256_json, utc_now from .evidence import EvidenceStore -from .models import ReleaseVerdict, RunState, declared_target_identity_digest +from .models import ( + EnvironmentIdentity, + ReleaseVerdict, + RunState, + TargetIdentity, + declared_target_identity_digest, +) +from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID +from .production_e2e import validate_production_e2e from .policy import RuleManifest # Internal fine-grained RunState -> coarse public state (contracts/schemas/certification-run.v1.json). @@ -152,6 +160,59 @@ def project_run(store: EvidenceStore, row: dict[str, Any]) -> dict[str, Any]: payload = json.loads(verdict_row["payload_json"]) release_verdict = payload.get("release_verdict", release_verdict) evidence_merkle_root = payload.get("evidence_merkle_root") + production_e2e = { + "verified": False, + "profile": None, + "attestation_id": None, + "source_commit": None, + "deployment_sha": None, + "target_identity_digest": None, + "environment_identity_digest": None, + "canonical_target": None, + "tool_count": None, + "signature_verified": False, + "collector_key_id": None, + "attestation_envelope_sha256": None, + } + result = store.list_rule_results(run_id, tenant_id).get(PRODUCTION_E2E_RULE_ID) + if result is not None: + try: + target = TargetIdentity(**json.loads(row["target_identity_json"])) + environment = EnvironmentIdentity(**json.loads(row["environment_identity_json"])) + verified, _reason = validate_production_e2e( + result.details, + target, + environment, + now=utc_now(), + ) + production_e2e = { + "verified": verified, + "profile": result.details.get("profile") if verified else None, + "attestation_id": result.details.get("attestation_id") if verified else None, + "source_commit": result.details.get("source_commit") if verified else None, + "deployment_sha": result.details.get("deployment_sha") if verified else None, + "target_identity_digest": ( + result.details.get("target_identity_digest") if verified else None + ), + "environment_identity_digest": ( + result.details.get("environment_identity_digest") if verified else None + ), + "canonical_target": ( + result.details.get("canonical_target") if verified else None + ), + "tool_count": result.details.get("tool_count") if verified else None, + "signature_verified": ( + result.details.get("signature_verified") is True if verified else False + ), + "collector_key_id": ( + result.details.get("collector_key_id") if verified else None + ), + "attestation_envelope_sha256": ( + result.details.get("attestation_envelope_sha256") if verified else None + ), + } + except (TypeError, ValueError): + pass projected = { "run_id": run_id, "tenant_id": tenant_id, @@ -162,6 +223,7 @@ def project_run(store: EvidenceStore, row: dict[str, Any]) -> dict[str, Any]: "environment_identity_digest": row["environment_identity_digest"], "policy_version": row["policy_version"] or row["rule_manifest_id"], "evidence_merkle_root": evidence_merkle_root, + "production_e2e": production_e2e, "created_at": row["created_at"], "updated_at": row["updated_at"], } diff --git a/src/echo_certification_forge/models.py b/src/echo_certification_forge/models.py index 546faa7..74602d6 100644 --- a/src/echo_certification_forge/models.py +++ b/src/echo_certification_forge/models.py @@ -182,6 +182,9 @@ class VerdictDecision: rule_manifest_id: str rule_manifest_digest: str evidence_merkle_root: str + production_e2e_attestation_id: str | None + production_e2e_profile: str | None + production_e2e_envelope_sha256: str | None signing_key_id: str issued_at: datetime expires_at: datetime @@ -199,6 +202,9 @@ def to_dict(self) -> dict[str, Any]: "rule_manifest_id": self.rule_manifest_id, "rule_manifest_digest": self.rule_manifest_digest, "evidence_merkle_root": self.evidence_merkle_root, + "production_e2e_attestation_id": self.production_e2e_attestation_id, + "production_e2e_profile": self.production_e2e_profile, + "production_e2e_envelope_sha256": self.production_e2e_envelope_sha256, "signing_key_id": self.signing_key_id, "issued_at": to_utc_iso(self.issued_at), "expires_at": to_utc_iso(self.expires_at), diff --git a/src/echo_certification_forge/production_e2e.py b/src/echo_certification_forge/production_e2e.py new file mode 100644 index 0000000..b35e4eb --- /dev/null +++ b/src/echo_certification_forge/production_e2e.py @@ -0,0 +1,351 @@ +"""Fail-closed validation for signed production end-to-end attestations. + +Source checks, a successful process exit, and a reachable URL are not production +evidence. This contract is evaluated by the deterministic verdict authority +after a trusted collector has signed the attestation. Target-controlled code +cannot manufacture the attestation or select its trust key. +""" +from __future__ import annotations + +import json +import re +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from .canonical import sha256_json +from .models import EnvironmentIdentity, SignedVerdictEnvelope, TargetIdentity +from .signing import TrustedPublicKeyRegistry + +RULE_ID = "production_e2e" +SCHEMA_VERSION = "certforge.production-e2e.v1" +GENERIC_PROFILE = "generic-production-v1" +ECHO_GITHUB_AUTONOMY_PROFILE = "echo-github-autonomy-remote-mcp-v1" +ECHO_GITHUB_AUTONOMY_REPOSITORY = "echoomegaprime/echo-github-autonomy" +ECHO_GITHUB_AUTONOMY_CANONICAL_MCP = "https://echo-ghub.grok.me/api/plugin/mcp" + +BASE_CHECKS = frozenset( + { + "runtime_or_artifact_executed", + "exact_identity_readback", + "critical_journeys_complete", + "negative_controls_pass", + "stability_verified", + "external_acceptance_verified", + } +) +ECHO_GITHUB_AUTONOMY_CHECKS = BASE_CHECKS | frozenset( + { + "canonical_mcp_health", + "oauth_discovery", + "mcp_initialize", + "tool_schema", + "repeated_tool_invocation", + "registry_persistence", + "four_account_reconciliation", + "private_public_visibility", + "read_write_certify", + "cross_client_consistency", + } +) +ECHO_GITHUB_ACCOUNTS = { + "echoomegaprime": 314902331, + "ECHO-OMEGA-PRIME": 264607697, + "Bmcbob76": 203470412, + "bobmcwilliams4": 235318155, +} +ECHO_CLIENTS = frozenset({"chatgpt", "claude", "codex", "grok"}) + +_SHA = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$") +_SHA256 = re.compile(r"^[0-9a-f]{64}$") +_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$") +_SIGNING_KEY_ID = re.compile(r"^ed25519:[0-9a-f]{32}$") +_SECRET_VALUE = re.compile( + r"(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|" + r"-----BEGIN [A-Z ]*PRIVATE KEY-----|Bearer\s+[A-Za-z0-9._~-]{16,})" +) +_RESTRICTED_KEYS = { + "password", + "passwd", + "secret", + "token", + "private_key", + "authorization", + "cookie", + "api_key", +} +_VERIFIED_MARKER = object() + + +@dataclass(frozen=True, slots=True) +class VerifiedProductionE2E: + """An attestation whose envelope was verified by an independently pinned key registry.""" + + payload: dict[str, Any] + collector_key_id: str + envelope_sha256: str + _marker: object + + def __post_init__(self) -> None: + if self._marker is not _VERIFIED_MARKER: + raise ValueError("VerifiedProductionE2E must come from signature verification") + + def result_details(self) -> dict[str, Any]: + return { + **self.payload, + "signature_verified": True, + "collector_key_id": self.collector_key_id, + "attestation_envelope_sha256": self.envelope_sha256, + } + + +def _parse_time(value: object) -> datetime | None: + if not isinstance(value, str): + return None + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + return parsed.astimezone(UTC) if parsed.tzinfo is not None else None + + +def _contains_restricted_value(value: Any, *, key_name: str = "") -> bool: + normalized = key_name.casefold().replace("-", "_") + if normalized in _RESTRICTED_KEYS: + return True + if isinstance(value, Mapping): + return any( + _contains_restricted_value(item, key_name=str(key)) for key, item in value.items() + ) + if isinstance(value, (list, tuple, set)): + return any(_contains_restricted_value(item) for item in value) + return isinstance(value, str) and bool(_SECRET_VALUE.search(value)) + + +def _source_repository(target: TargetIdentity) -> str | None: + reference = target.canonical_ref.casefold() + marker = "github.com/" + if marker not in reference: + return None + suffix = reference.split(marker, 1)[1].split("@", 1)[0] + if suffix.endswith(".git"): + suffix = suffix[:-4] + return suffix + + +def _validate_common( + payload: Mapping[str, Any], + target: TargetIdentity, + environment: EnvironmentIdentity, + *, + now: datetime, +) -> str | None: + if _contains_restricted_value(payload): + return "production_e2e_contains_restricted_value" + if payload.get("schema_version") != SCHEMA_VERSION: + return "production_e2e_schema_invalid" + if not _IDENTIFIER.fullmatch(str(payload.get("attestation_id") or "")): + return "production_e2e_attestation_id_invalid" + if payload.get("target_identity_digest") != target.identity_digest: + return "production_e2e_target_identity_mismatch" + if payload.get("environment_identity_digest") != environment.identity_digest: + return "production_e2e_environment_identity_mismatch" + if not _SIGNING_KEY_ID.fullmatch(str(payload.get("signing_key_id") or "")): + return "production_e2e_signing_key_id_invalid" + + observed_at = _parse_time(payload.get("observed_at")) + expires_at = _parse_time(payload.get("expires_at")) + current = now.astimezone(UTC) if now.tzinfo is not None else None + if current is None or observed_at is None or expires_at is None: + return "production_e2e_time_invalid" + if expires_at <= observed_at or (expires_at - observed_at).total_seconds() > 3600: + return "production_e2e_lifetime_invalid" + if observed_at > current or current >= expires_at: + return "production_e2e_attestation_not_current" + + source_commit = payload.get("source_commit") + deployment_sha = payload.get("deployment_sha") + if target.source_commit is not None: + expected = target.source_commit.casefold() + if not _SHA.fullmatch(expected): + return "production_e2e_target_source_commit_not_exact" + if source_commit != expected or deployment_sha != expected: + return "production_e2e_deployment_sha_mismatch" + elif source_commit is not None or deployment_sha is not None: + if source_commit != deployment_sha or not _SHA.fullmatch(str(source_commit)): + return "production_e2e_deployment_sha_invalid" + + required_checks = payload.get("required_checks") + checks = payload.get("checks") + if not isinstance(required_checks, list) or not isinstance(checks, Mapping): + return "production_e2e_checks_invalid" + if len(required_checks) != len(set(required_checks)): + return "production_e2e_checks_duplicated" + check_set = frozenset(required_checks) + if set(checks) != set(check_set) or any(checks.get(name) is not True for name in check_set): + return "production_e2e_checks_incomplete" + if not BASE_CHECKS <= check_set: + return "production_e2e_baseline_checks_missing" + if not isinstance(payload.get("stability_probe_count"), int) or payload["stability_probe_count"] < 3: + return "production_e2e_stability_probe_count_insufficient" + return None + + +def _validate_echo_github_autonomy(payload: Mapping[str, Any]) -> str | None: + if payload.get("canonical_target") != ECHO_GITHUB_AUTONOMY_CANONICAL_MCP: + return "production_e2e_canonical_mcp_mismatch" + if frozenset(payload.get("required_checks") or ()) != ECHO_GITHUB_AUTONOMY_CHECKS: + return "production_e2e_github_autonomy_checks_incomplete" + if payload.get("tool_count") != 30: + return "production_e2e_tool_count_mismatch" + + repositories = payload.get("sample_private_repositories") + if not isinstance(repositories, Mapping) or set(repositories) != set(ECHO_GITHUB_ACCOUNTS): + return "production_e2e_sample_repositories_incomplete" + sample_digests: dict[str, str] = {} + for login, sample in repositories.items(): + if not isinstance(sample, Mapping): + return "production_e2e_sample_repository_invalid" + if ( + not isinstance(sample.get("repository_id"), int) + or isinstance(sample["repository_id"], bool) + or sample["repository_id"] <= 0 + ): + return "production_e2e_sample_repository_id_invalid" + if not str(sample.get("node_id") or "").strip(): + return "production_e2e_sample_node_id_invalid" + if not str(sample.get("default_branch") or "").strip(): + return "production_e2e_sample_default_branch_invalid" + if not _SHA.fullmatch(str(sample.get("head_sha") or "")): + return "production_e2e_sample_head_sha_invalid" + digest = str(sample.get("fingerprint_sha256") or "") + if not _SHA256.fullmatch(digest): + return "production_e2e_sample_fingerprint_invalid" + sample_digests[login] = digest + + accounts = payload.get("accounts") + if not isinstance(accounts, Mapping) or set(accounts) != set(ECHO_GITHUB_ACCOUNTS): + return "production_e2e_accounts_incomplete" + for login, account_id in ECHO_GITHUB_ACCOUNTS.items(): + account = accounts.get(login) + if not isinstance(account, Mapping) or account.get("account_id") != account_id: + return "production_e2e_account_identity_mismatch" + counts = ( + account.get("enumerated_count"), + account.get("upstream_total_count"), + account.get("public_count"), + account.get("private_count"), + ) + if any(not isinstance(value, int) or isinstance(value, bool) or value < 0 for value in counts): + return "production_e2e_account_counts_invalid" + if counts[0] != counts[1] or counts[2] <= 0 or counts[3] <= 0: + return "production_e2e_account_reconciliation_failed" + if any(account.get(name) is not True for name in ("read", "write", "certify")): + return "production_e2e_account_authority_incomplete" + if account.get("credential_source") not in { + "github_app_installation", + "vault_user_token_fallback", + }: + return "production_e2e_credential_source_invalid" + if account.get("secret_exposed") is not False: + return "production_e2e_secret_boundary_failed" + + clients = payload.get("clients") + if not isinstance(clients, Mapping) or set(clients) != set(ECHO_CLIENTS): + return "production_e2e_clients_incomplete" + for client in clients.values(): + if not isinstance(client, Mapping) or client.get("accepted") is not True: + return "production_e2e_client_not_accepted" + fingerprints = client.get("repository_fingerprints") + if not isinstance(fingerprints, Mapping) or dict(fingerprints) != sample_digests: + return "production_e2e_cross_client_repository_mismatch" + return None + + +def validate_production_e2e( + payload: Mapping[str, Any] | None, + target: TargetIdentity, + environment: EnvironmentIdentity, + *, + now: datetime, +) -> tuple[bool, str]: + """Validate one trusted-collector payload against the exact run identities.""" + + if not isinstance(payload, Mapping): + return False, "production_e2e_attestation_missing" + error = _validate_common(payload, target, environment, now=now) + if error is not None: + return False, error + repository = _source_repository(target) + profile = payload.get("profile") + if repository == ECHO_GITHUB_AUTONOMY_REPOSITORY: + if profile != ECHO_GITHUB_AUTONOMY_PROFILE: + return False, "production_e2e_profile_mismatch" + error = _validate_echo_github_autonomy(payload) + return (error is None, error or "production_e2e_verified") + if profile != GENERIC_PROFILE: + return False, "production_e2e_profile_mismatch" + if frozenset(payload.get("required_checks") or ()) != BASE_CHECKS: + return False, "production_e2e_generic_checks_incomplete" + return True, "production_e2e_verified" + + +def validate_attestation_trust_metadata(payload: Mapping[str, Any]) -> tuple[bool, str]: + """Require the signature-verification metadata added by ``VerifiedProductionE2E``.""" + + if payload.get("signature_verified") is not True: + return False, "production_e2e_signature_not_verified" + if payload.get("collector_key_id") != payload.get("signing_key_id"): + return False, "production_e2e_collector_key_mismatch" + if not _SIGNING_KEY_ID.fullmatch(str(payload.get("collector_key_id") or "")): + return False, "production_e2e_collector_key_invalid" + if not _SHA256.fullmatch(str(payload.get("attestation_envelope_sha256") or "")): + return False, "production_e2e_envelope_sha256_invalid" + return True, "production_e2e_signature_verified" + + +def verify_signed_attestation( + envelope: SignedVerdictEnvelope, + trusted_keys: TrustedPublicKeyRegistry, +) -> VerifiedProductionE2E: + """Verify one collector envelope against an independently selected key registry.""" + + verified, reason = trusted_keys.verify(envelope) + if not verified: + raise ValueError(f"production E2E attestation is not trusted: {reason}") + if envelope.payload.get("schema_version") != SCHEMA_VERSION: + raise ValueError("production E2E attestation schema is unsupported") + if envelope.payload.get("signing_key_id") != envelope.key_id: + raise ValueError("production E2E attestation key binding is invalid") + envelope_sha256 = sha256_json( + { + "payload": envelope.payload, + "signature_b64": envelope.signature_b64, + "key_id": envelope.key_id, + } + ) + return VerifiedProductionE2E( + payload=dict(envelope.payload), + collector_key_id=envelope.key_id, + envelope_sha256=envelope_sha256, + _marker=_VERIFIED_MARKER, + ) + + +def load_signed_attestation(path: Path, trusted_key_directory: Path) -> VerifiedProductionE2E: + """Load and independently verify a collector envelope using pinned public keys.""" + + try: + document = json.loads(path.read_text(encoding="utf-8")) + envelope = SignedVerdictEnvelope( + payload=dict(document["payload"]), + signature_b64=str(document["signature_b64"]), + key_id=str(document["key_id"]), + public_key_pem=str(document.get("public_key_pem") or ""), + ) + except (OSError, ValueError, TypeError, KeyError, json.JSONDecodeError) as exc: + raise ValueError("production E2E attestation envelope is invalid") from exc + registry = TrustedPublicKeyRegistry.from_directory(trusted_key_directory) + return verify_signed_attestation(envelope, registry) diff --git a/src/echo_certification_forge/run_worker.py b/src/echo_certification_forge/run_worker.py index 3774b5d..e2fc1a6 100644 --- a/src/echo_certification_forge/run_worker.py +++ b/src/echo_certification_forge/run_worker.py @@ -17,6 +17,7 @@ import sqlite3 import tempfile import threading +from collections.abc import Callable from pathlib import Path from cryptography.hazmat.primitives import serialization @@ -38,6 +39,7 @@ from .executor import RetentionPolicy, RunExecutor, StaticEntitlement from .models import EnvironmentIdentity, RunOutcome, RunState, TargetIdentity from .policy import RuleManifest +from .production_e2e import VerifiedProductionE2E, load_signed_attestation from .runner import RunnerResponse from .sandbox import DEFAULT_IMAGE, DockerSandbox, sandboxed_journey_runner from .signing import Ed25519VerdictSigner @@ -47,7 +49,7 @@ _ADAPTER_RULE = "adapter_identity_and_quality" _PRODUCTION_MANIFEST_ID = "certforge.release-strict.v2" _PRODUCTION_MANIFEST_SHA256 = ( - "7dc98e0e95e6dd2c000ec069a8c46c4d1d49a4fe869ad4eae25e059d103644f4" + "08ba068ceb3e14bfed2690337edbb94c546e3e0a1a89b1321f7657653d8eea43" ) class _ClaimHeartbeat: def __init__(self, governance: SubscriberGovernance, claim) -> None: @@ -160,6 +162,10 @@ def run( adapter_records: tuple[AdapterExecutionRecord, ...] | None = None, adapter_policy: AdapterAcceptancePolicy | None = None, adapter_bundle_response: RunnerResponse | None = None, + production_e2e_attestation: VerifiedProductionE2E | None = None, + production_e2e_provider: ( + Callable[[TargetIdentity, EnvironmentIdentity], VerifiedProductionE2E] | None + ) = None, worker_id: str | None = None, worker_attestation_sha256: str | None = None, execution_location: str = "local", @@ -348,6 +354,8 @@ def run( else None ) environment = _worker_environment(adapter_digest, adapter_execution_profile_sha256) + if production_e2e_attestation is None and production_e2e_provider is not None: + production_e2e_attestation = production_e2e_provider(target, environment) if subscribers is not None: if existing is None or existing["state"] != RunState.QUEUED.value: if claim is not None: @@ -530,6 +538,7 @@ def complete_subscriber_execution(envelope): }, adapter_records=adapter_records, adapter_policy=adapter_policy, + production_e2e_attestation=production_e2e_attestation, execution_guard=execution_guard, completion_callback=completion_callback, ) @@ -669,6 +678,26 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--tenant", required=True) parser.add_argument("--target-json", required=True, help='e.g. {"type":"local","path":"/abs/dir"}') parser.add_argument("--journey-json", default=None, help='e.g. ["python3","hello.py"]') + parser.add_argument( + "--production-e2e-attestation", + type=Path, + default=( + Path(os.environ["ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION"]) + if os.environ.get("ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION") + else None + ), + help="trusted-collector signed production E2E envelope", + ) + parser.add_argument( + "--trusted-production-e2e-keys", + type=Path, + default=( + Path(os.environ["ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS"]) + if os.environ.get("ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS") + else None + ), + help="directory of pinned Ed25519 collector public keys", + ) parser.add_argument("--policy-version", default=None) parser.add_argument( "--db", @@ -875,6 +904,19 @@ def main(argv: list[str] | None = None) -> int: ) target_spec = json.loads(args.target_json) journey = json.loads(args.journey_json) if args.journey_json else None + production_e2e_attestation = None + if args.production_e2e_attestation is not None: + if args.trusted_production_e2e_keys is None: + print(json.dumps({"error": "trusted_production_e2e_keys_missing"})) + return 2 + try: + production_e2e_attestation = load_signed_attestation( + args.production_e2e_attestation, + args.trusted_production_e2e_keys, + ) + except ValueError as exc: + print(json.dumps({"error": "production_e2e_attestation_invalid", "detail": str(exc)})) + return 2 sandbox = None if args.sandbox: sandbox = DockerSandbox( @@ -896,6 +938,7 @@ def main(argv: list[str] | None = None) -> int: adapter_records=adapter_records, adapter_policy=adapter_policy, adapter_bundle_response=adapter_rebound_response, + production_e2e_attestation=production_e2e_attestation, worker_id=args.worker_id, worker_attestation_sha256=args.worker_attestation_sha256, execution_location=args.execution_location, diff --git a/src/echo_certification_forge/verdict.py b/src/echo_certification_forge/verdict.py index c739357..457751c 100644 --- a/src/echo_certification_forge/verdict.py +++ b/src/echo_certification_forge/verdict.py @@ -9,6 +9,9 @@ from .evidence import EvidenceStore from .models import EnvironmentIdentity, ReleaseVerdict, RunOutcome, TargetIdentity, VerdictDecision from .policy import RuleManifest +from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID +from .production_e2e import validate_attestation_trust_metadata +from .production_e2e import validate_production_e2e class DeterministicVerdictEngine: @@ -34,6 +37,8 @@ def evaluate( # ({tenant_id, target_type, declared_identity_digest, reference}), not the full canonical # TargetIdentity. Such a run has not been reconciled to an acquired artifact, so it cannot be # certified: fail-closed with an explicit reason instead of crashing on TargetIdentity(**...). + target: TargetIdentity | None = None + environment: EnvironmentIdentity | None = None if "declared_identity_digest" in target_data or "artifact_sha256" not in target_data: reasons.append("target_identity_not_reconciled") else: @@ -72,6 +77,35 @@ def evaluate( if invalid_references: reasons.append(f"invalid_rule_evidence:{rule.id}") + # PRODUCTION_READY is impossible without a current, signed, exact-identity E2E + # attestation. This is an engine invariant, not an optional policy convention; + # legacy/custom manifests that omit the rule fail closed as well. + if not any(rule.id == PRODUCTION_E2E_RULE_ID for rule in manifest.rules): + reasons.append("production_e2e_rule_missing_from_policy") + e2e_result = results.get(PRODUCTION_E2E_RULE_ID) + verified_e2e: dict[str, Any] | None = None + if target is None or environment is None: + reasons.append("production_e2e_identity_unavailable") + elif e2e_result is None: + reasons.append("production_e2e_attestation_missing") + else: + e2e_valid, e2e_reason = validate_production_e2e( + e2e_result.details, + target, + environment, + now=utc_now(), + ) + if not e2e_valid: + reasons.append(e2e_reason) + else: + trust_valid, trust_reason = validate_attestation_trust_metadata( + e2e_result.details + ) + if not trust_valid: + reasons.append(trust_reason) + else: + verified_e2e = e2e_result.details + if store.blocking_findings(run_id, tenant_id): reasons.append("blocking_findings_present") @@ -97,6 +131,17 @@ def evaluate( rule_manifest_id=manifest.manifest_id, rule_manifest_digest=manifest.digest, evidence_merkle_root=verification.merkle_root, + production_e2e_attestation_id=( + str(verified_e2e["attestation_id"]) if verified_e2e is not None else None + ), + production_e2e_profile=( + str(verified_e2e["profile"]) if verified_e2e is not None else None + ), + production_e2e_envelope_sha256=( + str(verified_e2e["attestation_envelope_sha256"]) + if verified_e2e is not None + else None + ), signing_key_id=signing_key_id, issued_at=issued_at, expires_at=issued_at + timedelta(seconds=manifest.verdict_ttl_seconds), diff --git a/tests/conftest.py b/tests/conftest.py index f674a9a..138dac5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,12 +26,13 @@ def store(tmp_path: Path) -> EvidenceStore: @pytest.fixture def target() -> TargetIdentity: + source_commit = "a" * 40 return TargetIdentity( tenant_id="tenant-alpha", target_type="git", - canonical_ref="https://github.com/example/project@abc123", + canonical_ref=f"https://github.com/example/project@{source_commit}", artifact_sha256=digest("artifact"), - source_commit="abc123", + source_commit=source_commit, dependency_sha256=digest("dependencies"), configuration_sha256=digest("configuration"), ) diff --git a/tests/production_e2e_support.py b/tests/production_e2e_support.py new file mode 100644 index 0000000..8c3d2c7 --- /dev/null +++ b/tests/production_e2e_support.py @@ -0,0 +1,46 @@ +"""Trusted production-E2E attestations used by tests that exercise ready paths.""" + +from __future__ import annotations + +from datetime import timedelta + +from echo_certification_forge.canonical import to_utc_iso, utc_now +from echo_certification_forge.models import EnvironmentIdentity, TargetIdentity +from echo_certification_forge.production_e2e import ( + BASE_CHECKS, + GENERIC_PROFILE, + SCHEMA_VERSION, + VerifiedProductionE2E, + verify_signed_attestation, +) +from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry + + +def trusted_generic_production_e2e( + target: TargetIdentity, + environment: EnvironmentIdentity, +) -> VerifiedProductionE2E: + """Create a fresh signed generic attestation through the real trust verifier.""" + + observed_at = utc_now() + signer = Ed25519VerdictSigner.generate() + payload = { + "schema_version": SCHEMA_VERSION, + "attestation_id": f"test-e2e-{target.identity_digest[:16]}", + "profile": GENERIC_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": target.source_commit, + "deployment_sha": target.source_commit, + "canonical_target": target.canonical_ref, + "required_checks": sorted(BASE_CHECKS), + "checks": {name: True for name in sorted(BASE_CHECKS)}, + "stability_probe_count": 3, + "observed_at": to_utc_iso(observed_at), + "expires_at": to_utc_iso(observed_at + timedelta(minutes=30)), + "signing_key_id": signer.key_id, + } + envelope = signer.sign_payload(payload) + trusted = TrustedPublicKeyRegistry.empty() + trusted.add_pem(signer.public_key_pem) + return verify_signed_attestation(envelope, trusted) diff --git a/tests/test_additional_contracts.py b/tests/test_additional_contracts.py index 7687852..362b3f7 100644 --- a/tests/test_additional_contracts.py +++ b/tests/test_additional_contracts.py @@ -22,10 +22,12 @@ from echo_certification_forge.service import ServiceContext, create_app from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry from echo_certification_forge.verdict import DeterministicVerdictEngine +from production_e2e_support import trusted_generic_production_e2e def complete_run(store, manifest, target, environment, run_id="cert-extra"): store.register_run(run_id, target, environment, manifest.manifest_id, manifest.digest) + production_e2e = trusted_generic_production_e2e(target, environment).result_details() for index, rule in enumerate(manifest.rules, start=1): artifact_id = f"extra-{index:02d}" store.append_artifact( @@ -34,7 +36,12 @@ def complete_run(store, manifest, target, environment, run_id="cert-extra"): ) store.record_rule_result( run_id, target.tenant_id, - RuleResult(rule.id, True, (artifact_id,), {"verified": True}), + RuleResult( + rule.id, + True, + (artifact_id,), + production_e2e if rule.id == "production_e2e" else {"verified": True}, + ), ) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) return run_id diff --git a/tests/test_evidence_and_verdict.py b/tests/test_evidence_and_verdict.py index 716f488..2265075 100644 --- a/tests/test_evidence_and_verdict.py +++ b/tests/test_evidence_and_verdict.py @@ -3,6 +3,7 @@ import json import sqlite3 from contextlib import closing +from datetime import timedelta from pathlib import Path import pytest @@ -13,10 +14,13 @@ ReleaseVerdict, RuleResult, RunOutcome, + EnvironmentIdentity, TargetIdentity, VerdictLifecycleEvent, ) from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from echo_certification_forge.canonical import to_utc_iso, utc_now +from echo_certification_forge.production_e2e import BASE_CHECKS, GENERIC_PROFILE, SCHEMA_VERSION from echo_certification_forge.verdict import DeterministicVerdictEngine @@ -25,8 +29,33 @@ def register(store, manifest, target, environment, run_id="cert-001"): return run_id -def satisfy_all_rules(store, manifest, run_id, tenant_id): +def satisfy_all_rules(store, manifest, run_id, tenant_id, *, include_production_e2e=True): + run = store.get_run(run_id, tenant_id) + target = TargetIdentity(**json.loads(run["target_identity_json"])) + environment = EnvironmentIdentity(**json.loads(run["environment_identity_json"])) + observed_at = utc_now() + production_e2e = { + "schema_version": SCHEMA_VERSION, + "attestation_id": f"e2e-{run_id}", + "profile": GENERIC_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": target.source_commit, + "deployment_sha": target.source_commit, + "canonical_target": "https://example.test/runtime", + "required_checks": sorted(BASE_CHECKS), + "checks": {name: True for name in sorted(BASE_CHECKS)}, + "stability_probe_count": 3, + "observed_at": to_utc_iso(observed_at), + "expires_at": to_utc_iso(observed_at + timedelta(minutes=30)), + "signing_key_id": "ed25519:" + "a" * 32, + "signature_verified": True, + "collector_key_id": "ed25519:" + "a" * 32, + "attestation_envelope_sha256": "f" * 64, + } for index, rule in enumerate(manifest.rules, start=1): + if rule.id == "production_e2e" and not include_production_e2e: + continue artifact_id = f"evidence-{index:02d}" store.append_artifact( run_id, @@ -40,7 +69,12 @@ def satisfy_all_rules(store, manifest, run_id, tenant_id): store.record_rule_result( run_id, tenant_id, - RuleResult(rule.id, True, (artifact_id,), {"source": "acceptance"}), + RuleResult( + rule.id, + True, + (artifact_id,), + production_e2e if rule.id == "production_e2e" else {"source": "acceptance"}, + ), ) @@ -81,6 +115,26 @@ def test_complete_run_with_verified_evidence_gets_signed_ready(store, manifest, assert gate.allowed +def test_source_and_rule_rows_without_production_e2e_never_get_ready( + store, manifest, target, environment +): + run_id = register(store, manifest, target, environment) + satisfy_all_rules( + store, + manifest, + run_id, + target.tenant_id, + include_production_e2e=False, + ) + store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) + signer = Ed25519VerdictSigner.generate() + decision = DeterministicVerdictEngine().evaluate( + store, run_id, target.tenant_id, manifest, signer.key_id + ) + assert decision.release_verdict is ReleaseVerdict.NOT_READY + assert "production_e2e_attestation_missing" in decision.reasons + + def test_bare_database_rows_cannot_manufacture_ready(store, manifest, target, environment): run_id = register(store, manifest, target, environment) now = "2026-07-16T00:00:00Z" diff --git a/tests/test_p5_executor_adapter_gate.py b/tests/test_p5_executor_adapter_gate.py index 9f1b9fd..8607e44 100644 --- a/tests/test_p5_executor_adapter_gate.py +++ b/tests/test_p5_executor_adapter_gate.py @@ -17,6 +17,7 @@ from echo_certification_forge.models import EnvironmentIdentity from echo_certification_forge.policy import RuleManifest from echo_certification_forge.signing import Ed25519VerdictSigner +from production_e2e_support import trusted_generic_production_e2e RUN = "cert-p5-executor" @@ -89,7 +90,8 @@ def manifest_v2() -> RuleManifest: def execute(store, target, tmp_path: Path, *, records, adapter_policy, adapter_digest: str): manifest = manifest_v2() - store.register_run(RUN, target, environment(adapter_digest), manifest.manifest_id, manifest.digest) + run_environment = environment(adapter_digest) + store.register_run(RUN, target, run_environment, manifest.manifest_id, manifest.digest) executor = RunExecutor(store, manifest, Ed25519VerdictSigner.generate()) return executor.execute( RUN, @@ -103,6 +105,7 @@ def execute(store, target, tmp_path: Path, *, records, adapter_policy, adapter_d }, adapter_records=records, adapter_policy=adapter_policy, + production_e2e_attestation=trusted_generic_production_e2e(target, run_environment), ) diff --git a/tests/test_p6_deployment_enforcement.py b/tests/test_p6_deployment_enforcement.py index 5c4496a..937915b 100644 --- a/tests/test_p6_deployment_enforcement.py +++ b/tests/test_p6_deployment_enforcement.py @@ -41,6 +41,7 @@ VerdictLifecycleEvent, ) from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e TENANT = "tenant-alpha" OTHER_TENANT = "tenant-beta" @@ -57,7 +58,7 @@ def _target(artifact_label: str, tenant_id: str = TENANT) -> TargetIdentity: target_type="container", canonical_ref=f"registry.echo/app@{artifact_label}", artifact_sha256=_digest(artifact_label), - source_commit="abc123def456", + source_commit="abc123abc123abc123abc123abc123abc123abcd", dependency_sha256=_digest("dependencies"), configuration_sha256=_digest("configuration"), ) @@ -107,6 +108,7 @@ def _certify( entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings @@ -1120,4 +1122,4 @@ def test_delayed_stale_binding_cannot_shadow_active_certification( _admission(target.artifact_sha256, "staging", environment, rolled_manifest, "deploy-shadow-s2"), ACTOR, ) - assert stale_decision.run_id == "cert-shadow-new" \ No newline at end of file + assert stale_decision.run_id == "cert-shadow-new" diff --git a/tests/test_p6_platform_integration.py b/tests/test_p6_platform_integration.py index e5406f3..267bd0a 100644 --- a/tests/test_p6_platform_integration.py +++ b/tests/test_p6_platform_integration.py @@ -51,12 +51,14 @@ from echo_certification_forge.service import ServiceContext, create_app from echo_certification_forge.sandbox import DockerSandbox from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e TENANT = "tenant-alpha" OTHER_TENANT = "tenant-beta" SECRET = "p6-webhook-secret-0123456789abcdef" DEPLOY_SECRET = "p6-deploy-credential-alpha-0001" OTHER_DEPLOY_SECRET = "p6-deploy-credential-beta-0002" +SOURCE_COMMIT = "abc123abc123abc123abc123abc123abc123abcd" def _digest(label: str) -> str: @@ -101,6 +103,7 @@ def _certify(store, manifest, signer, environment, tmp_path, run_id, target) -> entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings @@ -112,7 +115,7 @@ def certified_target(store, manifest, signer, environment, tmp_path) -> TargetId target_type="container", canonical_ref="registry.echo/app@v1", artifact_sha256=_digest("http-app-v1"), - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, dependency_sha256=_digest("dependencies"), configuration_sha256=_digest("configuration"), ) @@ -401,7 +404,7 @@ def test_http_binding_refuses_unsigned_run_and_foreign_tenant( target_type="container", canonical_ref="registry.echo/app@u1", artifact_sha256=_digest("unsigned-app"), - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) store.register_run("cert-unsigned", target, environment, manifest.manifest_id, manifest.digest) response = _post_signed(client, "/v1/certifications/cert-unsigned/bindings") @@ -442,7 +445,7 @@ def _build_event(event_id: str = "evt-00000001") -> dict: "event_type": "build.artifact.published", "tenant_id": TENANT, "artifact_sha256": _digest("built-artifact"), - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "repository": "https://github.com/echo/app", "environment_identity_digest": _digest("build-env"), "policy_version": "certforge.release-strict.v1", @@ -488,7 +491,7 @@ def test_registry_webhook_maps_container_target(client): "tenant_id": TENANT, "image_digest": f"sha256:{_digest('pushed-image')}", "image_repository": "registry.echo/app", - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "environment_identity_digest": _digest("registry-env"), "policy_version": "certforge.release-strict.v1", } @@ -557,7 +560,7 @@ def _declared_event(artifact: str, environment, manifest, event_id: str = "evt-r "event_type": "build.artifact.published", "tenant_id": TENANT, "artifact_sha256": artifact, - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "repository": "https://github.com/echo/app", "environment_identity_digest": environment.identity_digest, "policy_version": manifest.manifest_id, @@ -582,9 +585,9 @@ def test_webhook_declared_run_reconciles_certifies_and_deploys_end_to_end( acquired = TargetIdentity( tenant_id=TENANT, target_type="container", - canonical_ref="registry.echo/app@abc123def456", + canonical_ref=f"registry.echo/app@{SOURCE_COMMIT}", artifact_sha256=artifact, - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) store.reconcile_declared_target(run_id, TENANT, acquired, environment) row = store.get_run(run_id, TENANT) @@ -602,6 +605,7 @@ def test_webhook_declared_run_reconciles_certifies_and_deploys_end_to_end( entitlement=StaticEntitlement(frozenset({TENANT})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(acquired, environment), ) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings @@ -646,9 +650,9 @@ def test_reconciliation_refuses_mismatched_artifact_and_run_stays_undeployable( tampered = TargetIdentity( tenant_id=TENANT, target_type="container", - canonical_ref="registry.echo/app@abc123def456", + canonical_ref=f"registry.echo/app@{SOURCE_COMMIT}", artifact_sha256=_digest("DIFFERENT-acquired-artifact"), - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) with pytest.raises(ValueError, match="target_declared_artifact_mismatch"): store.reconcile_declared_target(run_id, TENANT, tampered, environment) @@ -732,9 +736,9 @@ def test_reconciliation_window_closes_at_acquisition(client, store, manifest, en acquired = TargetIdentity( tenant_id=TENANT, target_type="container", - canonical_ref="registry.echo/app@abc123def456", + canonical_ref=f"registry.echo/app@{SOURCE_COMMIT}", artifact_sha256=artifact, - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) with pytest.raises(ValueError, match="target_reconciliation_window_closed"): store.reconcile_declared_target(run_id, TENANT, acquired, environment) @@ -772,6 +776,7 @@ def test_run_worker_executes_webhook_declared_run_end_to_end( signer=signer, entitled=frozenset({TENANT}), journey=[sys.executable, "hello.py"], + production_e2e_provider=trusted_generic_production_e2e, ) assert outcome.get("error") is None, outcome assert outcome["release_verdict"] == "PRODUCTION_READY" @@ -1837,7 +1842,7 @@ def test_registry_webhook_oci_run_certifies_and_deploys_end_to_end( "tenant_id": TENANT, "image_digest": manifest_digest, "image_repository": repo, - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, # the platform declares the WORKER's environment commitment for the run "environment_identity_digest": run_worker._worker_environment().identity_digest, "policy_version": manifest.manifest_id, @@ -1862,6 +1867,7 @@ def test_registry_webhook_oci_run_certifies_and_deploys_end_to_end( entitled=frozenset({TENANT}), journey=["python3", "app/hello.py"], sandbox=DockerSandbox(docker=(sys.executable, str(stub))), + production_e2e_provider=trusted_generic_production_e2e, ) finally: os.environ.pop("CERTFORGE_TEST_DOCKER_LOG", None) @@ -2025,7 +2031,7 @@ def test_registry_webhook_oci_journey_without_sandbox_fails_closed( "tenant_id": TENANT, "image_digest": manifest_digest, "image_repository": repo, - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "environment_identity_digest": run_worker._worker_environment().identity_digest, "policy_version": manifest.manifest_id, } diff --git a/tests/test_p7_subscriber_governance.py b/tests/test_p7_subscriber_governance.py index ce9ba38..a112bba 100644 --- a/tests/test_p7_subscriber_governance.py +++ b/tests/test_p7_subscriber_governance.py @@ -40,6 +40,7 @@ SubscriberGovernance, SubscriberPolicy, ) +from production_e2e_support import trusted_generic_production_e2e PEPPER = b"p7-acceptance-pepper-material-32-bytes-minimum" @@ -1166,6 +1167,7 @@ def test_run_worker_enforces_live_subscriber_entitlement( entitled=frozenset({org.organization_id}), subscribers=governance, journey=[sys.executable, "journey.py"], + production_e2e_provider=trusted_generic_production_e2e, ) assert result["error"] == "subscriber_worker_claim_denied" @@ -3578,6 +3580,7 @@ def test_legal_hold_lifecycle_and_public_verification_use_hardened_subscriber_au entitled=frozenset({owner.organization_id}), subscribers=governance, journey=[sys.executable, "journey.py"], + production_e2e_provider=trusted_generic_production_e2e, ) assert result["state"] == RunState.COMPLETED.value headers = _headers(owner.organization_id, owner.bootstrap_api_key) diff --git a/tests/test_production_e2e.py b/tests/test_production_e2e.py new file mode 100644 index 0000000..81460f5 --- /dev/null +++ b/tests/test_production_e2e.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import json +from datetime import UTC, datetime, timedelta + +import pytest + +from echo_certification_forge.models import EnvironmentIdentity, TargetIdentity +from echo_certification_forge.production_e2e import ( + ECHO_CLIENTS, + ECHO_GITHUB_ACCOUNTS, + ECHO_GITHUB_AUTONOMY_CANONICAL_MCP, + ECHO_GITHUB_AUTONOMY_CHECKS, + ECHO_GITHUB_AUTONOMY_PROFILE, + SCHEMA_VERSION, + load_signed_attestation, + validate_production_e2e, +) +from echo_certification_forge.signing import Ed25519VerdictSigner + +NOW = datetime(2026, 8, 30, 12, 0, tzinfo=UTC) +SOURCE_SHA = "5" * 40 + + +def _target() -> TargetIdentity: + return TargetIdentity( + tenant_id="echo-github-apps", + target_type="git", + canonical_ref=( + "https://github.com/echoomegaprime/echo-github-autonomy.git@" + SOURCE_SHA + ), + artifact_sha256="a" * 64, + source_commit=SOURCE_SHA, + dependency_sha256="b" * 64, + configuration_sha256="c" * 64, + ) + + +def _environment() -> EnvironmentIdentity: + return EnvironmentIdentity( + **{ + name: str(index) * 64 + for index, name in enumerate(EnvironmentIdentity.__dataclass_fields__, start=1) + } + ) + + +def _payload(*, signing_key_id: str = "ed25519:" + "d" * 32) -> dict: + target = _target() + environment = _environment() + samples = { + login: { + "repository_id": index, + "node_id": f"R_repo_{index}", + "default_branch": "main", + "head_sha": str(index) * 40, + "fingerprint_sha256": str(index) * 64, + } + for index, login in enumerate(ECHO_GITHUB_ACCOUNTS, start=1) + } + accounts = { + login: { + "account_id": account_id, + "enumerated_count": 10 + index, + "upstream_total_count": 10 + index, + "public_count": 5, + "private_count": 5 + index, + "read": True, + "write": True, + "certify": True, + "credential_source": ( + "github_app_installation" + if index == 1 + else "vault_user_token_fallback" + ), + "secret_exposed": False, + } + for index, (login, account_id) in enumerate(ECHO_GITHUB_ACCOUNTS.items(), start=1) + } + fingerprints = { + login: sample["fingerprint_sha256"] for login, sample in samples.items() + } + return { + "schema_version": SCHEMA_VERSION, + "attestation_id": "echo-github-autonomy-live-001", + "profile": ECHO_GITHUB_AUTONOMY_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": SOURCE_SHA, + "deployment_sha": SOURCE_SHA, + "canonical_target": ECHO_GITHUB_AUTONOMY_CANONICAL_MCP, + "required_checks": sorted(ECHO_GITHUB_AUTONOMY_CHECKS), + "checks": {name: True for name in sorted(ECHO_GITHUB_AUTONOMY_CHECKS)}, + "stability_probe_count": 3, + "tool_count": 30, + "accounts": accounts, + "sample_private_repositories": samples, + "clients": { + name: {"accepted": True, "repository_fingerprints": fingerprints} + for name in sorted(ECHO_CLIENTS) + }, + "observed_at": NOW.isoformat(), + "expires_at": (NOW + timedelta(minutes=30)).isoformat(), + "signing_key_id": signing_key_id, + } + + +def test_echo_github_autonomy_requires_complete_exact_cross_client_e2e() -> None: + valid, reason = validate_production_e2e( + _payload(), _target(), _environment(), now=NOW + timedelta(minutes=1) + ) + assert valid, reason + + +@pytest.mark.parametrize( + ("mutation", "reason"), + [ + (lambda value: value.__setitem__("deployment_sha", "6" * 40), "production_e2e_deployment_sha_mismatch"), + (lambda value: value.__setitem__("tool_count", 27), "production_e2e_tool_count_mismatch"), + ( + lambda value: value["accounts"]["Bmcbob76"].__setitem__("private_count", 0), + "production_e2e_account_reconciliation_failed", + ), + ( + lambda value: value["clients"]["grok"].__setitem__("accepted", False), + "production_e2e_client_not_accepted", + ), + ( + lambda value: value["accounts"]["Bmcbob76"].__setitem__( + "credential_source", "model_config_pat" + ), + "production_e2e_credential_source_invalid", + ), + ( + lambda value: value["accounts"]["Bmcbob76"].__setitem__( + "secret_exposed", True + ), + "production_e2e_secret_boundary_failed", + ), + ( + lambda value: value["sample_private_repositories"]["Bmcbob76"].__setitem__( + "repository_id", True + ), + "production_e2e_sample_repository_id_invalid", + ), + ], +) +def test_incomplete_or_mismatched_e2e_fails_closed(mutation, reason: str) -> None: + payload = _payload() + mutation(payload) + valid, actual = validate_production_e2e( + payload, _target(), _environment(), now=NOW + timedelta(minutes=1) + ) + assert not valid + assert actual == reason + + +def test_stale_e2e_attestation_fails_closed() -> None: + valid, reason = validate_production_e2e( + _payload(), _target(), _environment(), now=NOW + timedelta(hours=2) + ) + assert not valid + assert reason == "production_e2e_attestation_not_current" + + +def test_attestation_loader_requires_a_pinned_collector_key(tmp_path) -> None: + signer = Ed25519VerdictSigner.generate() + payload = _payload(signing_key_id=signer.key_id) + envelope = signer.sign_payload(payload) + envelope_path = tmp_path / "attestation.json" + envelope_path.write_text( + json.dumps( + { + "payload": envelope.payload, + "signature_b64": envelope.signature_b64, + "key_id": envelope.key_id, + "public_key_pem": envelope.public_key_pem, + } + ), + encoding="utf-8", + ) + trusted = tmp_path / "trusted" + trusted.mkdir() + with pytest.raises(ValueError, match="untrusted_signing_key"): + load_signed_attestation(envelope_path, trusted) + (trusted / "collector.pem").write_text(signer.public_key_pem, encoding="ascii") + verified = load_signed_attestation(envelope_path, trusted) + assert verified.payload == payload + assert verified.collector_key_id == signer.key_id + assert len(verified.envelope_sha256) == 64 + + +def test_self_selected_public_key_cannot_replace_the_pinned_collector(tmp_path) -> None: + trusted_signer = Ed25519VerdictSigner.generate() + attacker = Ed25519VerdictSigner.generate() + payload = _payload(signing_key_id=attacker.key_id) + envelope = attacker.sign_payload(payload) + envelope_path = tmp_path / "attestation.json" + envelope_path.write_text( + json.dumps( + { + "payload": envelope.payload, + "signature_b64": envelope.signature_b64, + "key_id": envelope.key_id, + "public_key_pem": envelope.public_key_pem, + } + ), + encoding="utf-8", + ) + trusted = tmp_path / "trusted" + trusted.mkdir() + (trusted / "collector.pem").write_text(trusted_signer.public_key_pem, encoding="ascii") + with pytest.raises(ValueError, match="untrusted_signing_key"): + load_signed_attestation(envelope_path, trusted) diff --git a/tests/test_runner.py b/tests/test_runner.py index 7a16b63..0266497 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -634,7 +634,12 @@ def test_workspace_owner_marker_and_symlink_detection(tmp_path: Path) -> None: ) target = tmp_path / "outside.txt" target.write_text("outside") - os.symlink(target, workspace / "link") + try: + os.symlink(target, workspace / "link") + except OSError as exc: + if os.name == "nt" and getattr(exc, "winerror", None) == 1314: + pytest.skip("Windows symlink privilege is unavailable") + raise with pytest.raises(IsolationFailure, match="symlink_not_allowed"): assert_no_symlinks(workspace) diff --git a/tests/test_t4_live_run.py b/tests/test_t4_live_run.py index 3d7e6fa..02d6784 100644 --- a/tests/test_t4_live_run.py +++ b/tests/test_t4_live_run.py @@ -18,6 +18,7 @@ from echo_certification_forge.models import SignedVerdictEnvelope from echo_certification_forge.run_worker import run from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e def _benign(tmp_path: Path) -> Path: @@ -99,7 +100,8 @@ def _run_worker(store, manifest, tenant, source, journey): signer = Ed25519VerdictSigner.generate() result = run("cert-live", tenant, {"type": "local", "path": str(source)}, store=store, manifest=manifest, signer=signer, - entitled=frozenset({tenant}), journey=journey) + entitled=frozenset({tenant}), journey=journey, + production_e2e_provider=trusted_generic_production_e2e) return result, signer diff --git a/tests/test_t4p5_executor.py b/tests/test_t4p5_executor.py index f4cd76d..60dbfc8 100644 --- a/tests/test_t4p5_executor.py +++ b/tests/test_t4p5_executor.py @@ -11,6 +11,7 @@ from echo_certification_forge.canonical import to_utc_iso, utc_now from echo_certification_forge.executor import RunExecutor, StaticEntitlement from echo_certification_forge.signing import Ed25519VerdictSigner +from production_e2e_support import trusted_generic_production_e2e RUN = "cert-exec" @@ -140,7 +141,10 @@ def test_retention_purge_removes_expired_content_but_keeps_signed_verdict( entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, - "signing_authority_separation": True}) + "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e( + target, environment + )) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings verdict_before = store.latest_signed_verdict(RUN, target.tenant_id) diff --git a/tests/test_t4p7_e2e.py b/tests/test_t4p7_e2e.py index 025c9a8..e733c57 100644 --- a/tests/test_t4p7_e2e.py +++ b/tests/test_t4p7_e2e.py @@ -15,6 +15,7 @@ from echo_certification_forge.executor import RunExecutor, StaticEntitlement from echo_certification_forge.models import SignedVerdictEnvelope from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e RUN = "cert-e2e" @@ -46,6 +47,7 @@ def test_e2e_benign_target_certified_and_deploy_gate_allows(store, manifest, tar entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) # certified through the full path diff --git a/tests/test_t4p8_sandbox.py b/tests/test_t4p8_sandbox.py index b0d3d45..a1e9dc5 100644 --- a/tests/test_t4p8_sandbox.py +++ b/tests/test_t4p8_sandbox.py @@ -15,6 +15,7 @@ DEFAULT_IMAGE, DockerSandbox, SandboxError, sandboxed_journey_runner, ) from echo_certification_forge.signing import Ed25519VerdictSigner +from production_e2e_support import trusted_generic_production_e2e def test_build_command_has_all_hardening_flags(tmp_path): @@ -72,6 +73,7 @@ def fake_runner(argv, workdir): entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=["python3", "hello.py"], journey_runner=fake_runner, control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) assert calls and calls[0][0] == ["python3", "hello.py"] # the injected runner was used assert result.release_verdict == "PRODUCTION_READY" From 61cebbb7b8dbd84e1cb444df5e69acb6b12ebaa1 Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 05:43:38 -0700 Subject: [PATCH 02/10] Test the P6 gate with signed production E2E --- artifacts/p6_acceptance.json | 22 +++++++++--------- artifacts/p6_acceptance.summary.json | 2 +- scripts/p6_acceptance.py | 34 ++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/artifacts/p6_acceptance.json b/artifacts/p6_acceptance.json index cfbd348..13c22df 100644 --- a/artifacts/p6_acceptance.json +++ b/artifacts/p6_acceptance.json @@ -1,8 +1,8 @@ { "phase": "P6", "title": "deployment enforcement and platform integration \u2014 live local acceptance", - "generated_at": "2026-07-26T02:18:04.411111Z", - "python": "3.13.0 (tags/v3.13.0:60403a5, Oct 7 2024, 09:38:07) [MSC v.1941 64 bit (AMD64)]", + "generated_at": "2026-08-30T12:40:31.960114Z", + "python": "3.12.10 (tags/v3.12.10:0cc8128, Apr 8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)]", "passed": true, "checks": [ { @@ -11,7 +11,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-3413cd5f5334d3b106b9ce81f7c7a26b\", \"allowed\": false, \"artifact_sha256\": \"cc9a65d96c25c8f96ba720e01628b734d6df4f5a1057238506ab6d504fb998fd\", \"chain_hash\": \"ab88eb7a583dc68353ca39d53cd4e37d9737ac4b6fa0e6c9b0ab8c659325a64d\", \"created_at\": \"2026-07-26T02:17:59.537952Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a1\", \"reasons\": [\"artifact_not_certified\", \"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-39149ed9c3643c355bbe8f4f09eec260\", \"allowed\": false, \"artifact_sha256\": \"cc9a65d96c25c8f96ba720e01628b734d6df4f5a1057238506ab6d504fb998fd\", \"chain_hash\": \"ff5f335a43355fdc85e2b64c11e7429ad3aa9070dca78655a74bf0d929da28b0\", \"created_at\": \"2026-08-30T12:40:27.987414Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a1\", \"reasons\": [\"artifact_not_certified\", \"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" } }, { @@ -29,7 +29,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-f49980b48837ae58dec6fa3ffbc216e4\", \"allowed\": false, \"artifact_sha256\": \"11d7d626a19af7cf0b64c0862b3214a15fac77e3aa1400d70e6902f1a77fdb86\", \"chain_hash\": \"40c1d83374649745ee6fcd3c42c58ce5b6257e362aa6e22954f224c7284b61f5\", \"created_at\": \"2026-07-26T02:17:59.744469Z\", \"deployment_environment\": \"staging\", \"deployment_id\": \"deploy-a2\", \"reasons\": [\"artifact_not_certified\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-de226c922ca25e4fe770043fa5cce489\", \"allowed\": false, \"artifact_sha256\": \"11d7d626a19af7cf0b64c0862b3214a15fac77e3aa1400d70e6902f1a77fdb86\", \"chain_hash\": \"1b08dbf5ba6527bbb6b743ab658e726bb487454d6ab95bd8ec1ec7889764501a\", \"created_at\": \"2026-08-30T12:40:28.129813Z\", \"deployment_environment\": \"staging\", \"deployment_id\": \"deploy-a2\", \"reasons\": [\"artifact_not_certified\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" } }, { @@ -38,7 +38,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-8ac38cbd2ade07ff1bb5c59d63d2582c\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"5d31c6a43518753dc06259bee965f1e8fb91e4114f86c47bf61adde8a84ff4dd\", \"created_at\": \"2026-07-26T02:17:59.912909Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a4\", \"reasons\": [\"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-393b3f1c13aeaf883a659c67fe3a6062\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"4adc5c6ca6c45d5664c517904d6b86304b4ae573fa525cef6397e6a315d30422\", \"created_at\": \"2026-08-30T12:40:28.260911Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a4\", \"reasons\": [\"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" } }, { @@ -57,8 +57,8 @@ "passed": true, "detail": { "failure_candidate": { - "admission_id": "dep-c3f042d865f9fcd3c85e9d0a1c91484a", - "admitted_at": "2026-07-26T02:18:00.438507Z", + "admission_id": "dep-88177f711ac3ea76d71d3f5657753a8a", + "admitted_at": "2026-08-30T12:40:28.640308Z", "artifact_sha256": "67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0", "deployment_id": "deploy-a3-prd", "run_id": "cert-p6-v1" @@ -66,12 +66,12 @@ "rollback_target": { "artifact_sha256": "67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0", "run_id": "cert-p6-v1", - "admission_id": "dep-c3f042d865f9fcd3c85e9d0a1c91484a", + "admission_id": "dep-88177f711ac3ea76d71d3f5657753a8a", "deployment_id": "deploy-a3-prd", - "admitted_at": "2026-07-26T02:18:00.438507Z" + "admitted_at": "2026-08-30T12:40:28.640308Z" }, "rolled_back": { - "admission_id": "dep-d1f547a64a4ddb086ca7a521e0d1a4ce", + "admission_id": "dep-368a34d3be4daeb9870a2b93e41ebf45", "artifact_sha256": "6cf9eb2e75fbaf4037393188b98be82a05499dcfc0c45a6eeeb840bd7f775c20", "deployment_environment": "production", "detail": "restored v1", @@ -87,7 +87,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-4ec864d16bf5447abf4145975d62ed0a\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"928bee7494d599c95422e4d3f6c8db2a7e80c2ec1ff5837056aead46be211115\", \"created_at\": \"2026-07-26T02:18:01.842335Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a5\", \"reasons\": [\"verdict_revoked\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-1d051283684978484ff174d2f3cc5b7b\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"8cbb0ce94045d583aaaaa7e150f5a997759f188f1de824d89f973711ef9a5754\", \"created_at\": \"2026-08-30T12:40:29.545903Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a5\", \"reasons\": [\"verdict_revoked\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" } }, { diff --git a/artifacts/p6_acceptance.summary.json b/artifacts/p6_acceptance.summary.json index 2c77777..ac231fd 100644 --- a/artifacts/p6_acceptance.summary.json +++ b/artifacts/p6_acceptance.summary.json @@ -3,5 +3,5 @@ "passed": true, "checks_total": 12, "checks_passed": 12, - "generated_at": "2026-07-26T02:18:04.411111Z" + "generated_at": "2026-08-30T12:40:31.960114Z" } diff --git a/scripts/p6_acceptance.py b/scripts/p6_acceptance.py index 05c4429..15490fe 100644 --- a/scripts/p6_acceptance.py +++ b/scripts/p6_acceptance.py @@ -41,7 +41,7 @@ import time import urllib.parse import urllib.request -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] @@ -64,6 +64,12 @@ VerdictLifecycleEvent, ) from echo_certification_forge.policy import RuleManifest # noqa: E402 +from echo_certification_forge.production_e2e import ( # noqa: E402 + BASE_CHECKS, + GENERIC_PROFILE, + SCHEMA_VERSION, + verify_signed_attestation, +) from echo_certification_forge.release_hooks import ( # noqa: E402 SIGNATURE_HEADER, TIMESTAMP_HEADER, @@ -187,6 +193,9 @@ def check(check_id: str, description: str, passed: bool, detail: object) -> None signer = Ed25519VerdictSigner.generate() trusted = TrustedPublicKeyRegistry.empty() trusted.add_pem(signer.public_key_pem) + e2e_signer = Ed25519VerdictSigner.generate() + e2e_trusted = TrustedPublicKeyRegistry.empty() + e2e_trusted.add_pem(e2e_signer.public_key_pem) environment = EnvironmentIdentity( runner_image_sha256=digest("runner"), @@ -205,6 +214,26 @@ def certify(run_id: str, target: TargetIdentity) -> None: workdir = tmp_path / f"src-{run_id}" workdir.mkdir() (workdir / "hello.py").write_text("print('service ok')\n", encoding="utf-8") + observed_at = utc_now() + e2e_payload = { + "schema_version": SCHEMA_VERSION, + "attestation_id": f"p6-e2e-{target.identity_digest[:16]}", + "profile": GENERIC_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": target.source_commit, + "deployment_sha": target.source_commit, + "canonical_target": target.canonical_ref, + "required_checks": sorted(BASE_CHECKS), + "checks": {name: True for name in sorted(BASE_CHECKS)}, + "stability_probe_count": 3, + "observed_at": to_utc_iso(observed_at), + "expires_at": to_utc_iso(observed_at + timedelta(minutes=30)), + "signing_key_id": e2e_signer.key_id, + } + production_e2e = verify_signed_attestation( + e2e_signer.sign_payload(e2e_payload), e2e_trusted + ) result = RunExecutor(store, manifest, signer).execute( run_id, target.tenant_id, @@ -215,6 +244,7 @@ def certify(run_id: str, target: TargetIdentity) -> None: "runner_control_channel": True, "signing_authority_separation": True, }, + production_e2e_attestation=production_e2e, ) if result.release_verdict != "PRODUCTION_READY": raise RuntimeError(f"certification failed: {result.blocking_findings}") @@ -225,7 +255,7 @@ def make_target(label: str) -> TargetIdentity: target_type="container", canonical_ref=f"registry.echo/app@{label}", artifact_sha256=digest(label), - source_commit="abc123def456", + source_commit=digest("source-commit")[:40], dependency_sha256=digest("dependencies"), configuration_sha256=digest("configuration"), ) From 7a7a1c92b9ee4978bf27693620f4c503e524f51e Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 05:57:39 -0700 Subject: [PATCH 03/10] Bind deployment checks to the effective database --- deploy/deploy_forge.sh | 11 ++++++----- tests/test_deploy_gate.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index 2e649b5..72ee784 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -173,6 +173,7 @@ set -a set +a PROD_PEPPER="${ECHO_CERTFORGE_API_KEY_PEPPER:-}" STAGING_PEPPER="${ECHO_CERTFORGE_STAGING_API_KEY_PEPPER:-}" +PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}" test "${#PROD_PEPPER}" -ge 32 || { echo "!! production ECHO_CERTFORGE_API_KEY_PEPPER must be at least 32 bytes" exit 1 @@ -305,7 +306,7 @@ if sudo test -f "$DISPATCH_RELEASE_DROPIN"; then sudo cat "$DISPATCH_RELEASE_DROPIN" >"$DISPATCH_RELEASE_DROPIN_BACKUP" HAD_DISPATCH_RELEASE_DROPIN=1 fi -DB_PATH="$STATE_ROOT/certforge.sqlite3" +DB_PATH="$PROD_DB_PATH" DB_BACKUP="$STATE_ROOT/deploy-scratch/echo-certforge-db.$RELEASE_ID" HAD_DB=0 DB_SNAPSHOT_READY=0 @@ -503,7 +504,7 @@ Type=simple User=forge WorkingDirectory=$CURRENT_LINK Environment=PYTHONUNBUFFERED=1 -Environment=ECHO_CERTFORGE_DB=$STATE_ROOT/certforge.sqlite3 +Environment=ECHO_CERTFORGE_DB=$DB_PATH Environment=ECHO_CERTFORGE_EVIDENCE_ROOT=$STATE_ROOT/evidence Environment=ECHO_CERTFORGE_POLICY=$CURRENT_LINK/policies/mandatory-rules.v2.json Environment=ECHO_CERTFORGE_SUBSCRIBER_POLICY=$CURRENT_LINK/policies/subscriber-governance.v1.json @@ -546,7 +547,7 @@ Type=simple User=forge WorkingDirectory=$CURRENT_LINK Environment=PYTHONUNBUFFERED=1 -Environment=ECHO_CERTFORGE_DB=$STATE_ROOT/certforge.sqlite3 +Environment=ECHO_CERTFORGE_DB=$DB_PATH Environment=ECHO_CERTFORGE_EVIDENCE_ROOT=$STATE_ROOT/evidence Environment=ECHO_CERTFORGE_POLICY=$CURRENT_LINK/policies/mandatory-rules.v2.json Environment=ECHO_CERTFORGE_SUBSCRIBER_POLICY=$CURRENT_LINK/policies/subscriber-governance.v1.json @@ -593,7 +594,7 @@ for _ in $(seq 1 40); do } sleep 0.5 done -if [ "$ready" != 1 ] || ! ECHO_CERTFORGE_DB="$STATE_ROOT/certforge.sqlite3" \ +if [ "$ready" != 1 ] || ! ECHO_CERTFORGE_DB="$DB_PATH" \ ECHO_CERTFORGE_SUBSCRIBER_POLICY="$RELEASE_DIR/policies/subscriber-governance.v1.json" \ ECHO_CERTFORGE_API_KEY_PEPPER="$PROD_PEPPER" \ ECHO_CERTFORGE_EXPECT_PRODUCT_READY=1 \ @@ -621,7 +622,7 @@ if [ "$dispatcher_ready" != 1 ]; then exit 1 fi if [ "$ADAPTER_MODE" = required ]; then - ECHO_CERTFORGE_DB="$STATE_ROOT/certforge.sqlite3" \ + ECHO_CERTFORGE_DB="$DB_PATH" \ ECHO_CERTFORGE_SUBSCRIBER_POLICY="$RELEASE_DIR/policies/subscriber-governance.v1.json" \ ECHO_CERTFORGE_API_KEY_PEPPER="$PROD_PEPPER" \ "$RELEASE_DIR/.venv/bin/python" "$RELEASE_DIR/deploy/smoke_dispatch.py" \ diff --git a/tests/test_deploy_gate.py b/tests/test_deploy_gate.py index 8a0a1ed..67f7b9c 100644 --- a/tests/test_deploy_gate.py +++ b/tests/test_deploy_gate.py @@ -96,6 +96,17 @@ def test_deploy_gate_snapshots_and_restores_persistent_database() -> None: assert 'DB_SNAPSHOT_READY=1' in DEPLOY_SCRIPT +def test_deploy_gate_uses_the_effective_production_database_everywhere() -> None: + assert ( + 'PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}"' + in DEPLOY_SCRIPT + ) + assert 'DB_PATH="$PROD_DB_PATH"' in DEPLOY_SCRIPT + assert DEPLOY_SCRIPT.count("Environment=ECHO_CERTFORGE_DB=$DB_PATH") == 2 + assert DEPLOY_SCRIPT.count('ECHO_CERTFORGE_DB="$DB_PATH" \\') == 2 + assert 'ECHO_CERTFORGE_DB="$STATE_ROOT/certforge.sqlite3"' not in DEPLOY_SCRIPT + + def test_deploy_gate_restores_prior_service_lifecycle() -> None: assert ( 'PREV_ENABLED="$(systemctl is-enabled "$SERVICE.service"' From 917ffe933b2c6b36bbbaab5f621b1c5889b2fd25 Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:08:08 -0700 Subject: [PATCH 04/10] Inherit the live CertForge database during deploy --- deploy/deploy_forge.sh | 23 ++++++++++++++++++++++- tests/test_deploy_gate.py | 10 ++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index 72ee784..c9c3b34 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -173,7 +173,28 @@ set -a set +a PROD_PEPPER="${ECHO_CERTFORGE_API_KEY_PEPPER:-}" STAGING_PEPPER="${ECHO_CERTFORGE_STAGING_API_KEY_PEPPER:-}" -PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}" +PROD_DB_PATH="${ECHO_CERTFORGE_DB:-}" +if [ -z "$PROD_DB_PATH" ]; then + CURRENT_SERVICE_PID="$(systemctl show "$SERVICE.service" --property MainPID --value 2>/dev/null || true)" + if [ "${CURRENT_SERVICE_PID:-0}" -gt 1 ] 2>/dev/null && \ + [ -r "/proc/$CURRENT_SERVICE_PID/environ" ]; then + PROD_DB_PATH="$(python3 - "$CURRENT_SERVICE_PID" <<'PY' +from pathlib import Path +import sys + +for entry in Path(f"/proc/{sys.argv[1]}/environ").read_bytes().split(b"\0"): + if entry.startswith(b"ECHO_CERTFORGE_DB="): + print(entry.split(b"=", 1)[1].decode("utf-8")) + break +PY +)" + fi +fi +PROD_DB_PATH="${PROD_DB_PATH:-$STATE_ROOT/certforge.sqlite3}" +case "$(realpath -m "$PROD_DB_PATH")" in + "$STATE_ROOT"/*|/mnt/documentation/echo/certforge/*) ;; + *) echo "!! effective production database is outside an approved state root"; exit 1 ;; +esac test "${#PROD_PEPPER}" -ge 32 || { echo "!! production ECHO_CERTFORGE_API_KEY_PEPPER must be at least 32 bytes" exit 1 diff --git a/tests/test_deploy_gate.py b/tests/test_deploy_gate.py index 67f7b9c..9f83072 100644 --- a/tests/test_deploy_gate.py +++ b/tests/test_deploy_gate.py @@ -97,10 +97,12 @@ def test_deploy_gate_snapshots_and_restores_persistent_database() -> None: def test_deploy_gate_uses_the_effective_production_database_everywhere() -> None: - assert ( - 'PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}"' - in DEPLOY_SCRIPT - ) + assert 'PROD_DB_PATH="${ECHO_CERTFORGE_DB:-}"' in DEPLOY_SCRIPT + assert 'systemctl show "$SERVICE.service" --property MainPID --value' in DEPLOY_SCRIPT + assert 'Path(f"/proc/{sys.argv[1]}/environ")' in DEPLOY_SCRIPT + assert 'entry.startswith(b"ECHO_CERTFORGE_DB=")' in DEPLOY_SCRIPT + assert 'PROD_DB_PATH="${PROD_DB_PATH:-$STATE_ROOT/certforge.sqlite3}"' in DEPLOY_SCRIPT + assert '/mnt/documentation/echo/certforge/*' in DEPLOY_SCRIPT assert 'DB_PATH="$PROD_DB_PATH"' in DEPLOY_SCRIPT assert DEPLOY_SCRIPT.count("Environment=ECHO_CERTFORGE_DB=$DB_PATH") == 2 assert DEPLOY_SCRIPT.count('ECHO_CERTFORGE_DB="$DB_PATH" \\') == 2 From fba53a4bd3a251d939d8a71f174a80c5f47b786e Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:21:24 -0700 Subject: [PATCH 05/10] Make production deploy smoke prove fail-closed E2E gate --- deploy/smoke_dispatch.py | 39 +++++++++++++----- tests/test_deploy_dispatch_smoke_contract.py | 43 ++++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 tests/test_deploy_dispatch_smoke_contract.py diff --git a/deploy/smoke_dispatch.py b/deploy/smoke_dispatch.py index b0410c1..1661749 100644 --- a/deploy/smoke_dispatch.py +++ b/deploy/smoke_dispatch.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -"""Production subscriber-to-dispatcher certification smoke. +"""Production subscriber-to-dispatcher fail-closed certification smoke. This test runs only after the real API and dispatcher are active. It submits an exact-identity -local target through the customer HTTP contract, waits for the durable dispatcher, and verifies -the signed PRODUCTION_READY verdict and append-only evidence through the public API. +local target through the customer HTTP contract without a trusted production-E2E attestation, +waits for the durable dispatcher, and verifies that the service signs ``NOT_READY`` while keeping +the append-only evidence and public signature-verification paths healthy. A deploy smoke must not +manufacture the external evidence that the production gate is intended to require. """ from __future__ import annotations @@ -65,6 +67,22 @@ def _require(condition: bool, message: str, detail: object | None = None) -> Non print(f" [ok ] {message}") +def _require_attestation_gate_closed(terminal: dict) -> None: + """Prove an unattested run cannot be promoted by the production dispatcher.""" + + _require( + terminal.get("release_verdict") == "NOT_READY", + "unattested customer run is NOT_READY", + terminal.get("release_verdict"), + ) + production_e2e = terminal.get("production_e2e") + _require( + isinstance(production_e2e, dict) and production_e2e.get("verified") is False, + "unattested customer run has no verified production E2E", + production_e2e, + ) + + def main() -> int: base = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8309" db_path = os.environ.get("ECHO_CERTFORGE_DB") @@ -177,11 +195,7 @@ def main() -> int: time.sleep(1) _require(terminal.get("state") == "COMPLETE", "dispatcher completed the customer run", terminal) _require(terminal.get("run_outcome") == "COMPLETE", "customer run outcome is COMPLETE") - _require( - terminal.get("release_verdict") == "PRODUCTION_READY", - "customer received PRODUCTION_READY", - terminal.get("release_verdict"), - ) + _require_attestation_gate_closed(terminal) _require( terminal.get("environment_identity_digest") == environment_digest, "verdict retained the published environment identity", @@ -198,7 +212,12 @@ def main() -> int: ) _require(verdict_status == 200, "signed verdict is retrievable", verdict_status) payload = verdict.get("payload", {}) - _require(payload.get("release_verdict") == "PRODUCTION_READY", "signed verdict is PRODUCTION_READY") + _require(payload.get("release_verdict") == "NOT_READY", "signed verdict is NOT_READY") + _require( + "production_e2e_attestation_missing" in payload.get("reasons", []), + "signed verdict records the missing production E2E attestation", + payload.get("reasons"), + ) _require(payload.get("environment_identity_digest") == environment_digest, "signed verdict binds environment") verify_status, verified = _request( @@ -209,7 +228,7 @@ def main() -> int: "public verdict verification succeeds", verified, ) - print(f"PRODUCTION DISPATCH SMOKE GREEN - run_id={run_id}") + print(f"PRODUCTION DISPATCH FAIL-CLOSED SMOKE GREEN - run_id={run_id}") return 0 finally: shutil.rmtree(target_root, ignore_errors=True) diff --git a/tests/test_deploy_dispatch_smoke_contract.py b/tests/test_deploy_dispatch_smoke_contract.py new file mode 100644 index 0000000..2118fa4 --- /dev/null +++ b/tests/test_deploy_dispatch_smoke_contract.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "certforge_deploy_smoke_dispatch", ROOT / "deploy" / "smoke_dispatch.py" +) +assert SPEC is not None and SPEC.loader is not None +SMOKE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(SMOKE) + + +def test_production_dispatch_smoke_requires_fail_closed_unattested_verdict() -> None: + SMOKE._require_attestation_gate_closed( # noqa: SLF001 + { + "release_verdict": "NOT_READY", + "production_e2e": {"verified": False}, + } + ) + + +@pytest.mark.parametrize( + "terminal", + [ + { + "release_verdict": "PRODUCTION_READY", + "production_e2e": {"verified": False}, + }, + { + "release_verdict": "NOT_READY", + "production_e2e": {"verified": True}, + }, + {"release_verdict": "NOT_READY"}, + ], +) +def test_production_dispatch_smoke_rejects_open_or_ambiguous_gate(terminal: dict) -> None: + with pytest.raises(RuntimeError): + SMOKE._require_attestation_gate_closed(terminal) # noqa: SLF001 From c7982ee3de2ccd9e8b84da86b3021652d7e2ccab Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:27:47 -0700 Subject: [PATCH 06/10] Preserve missing production E2E failure reason --- src/echo_certification_forge/verdict.py | 9 ++++++ tests/test_evidence_and_verdict.py | 42 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/echo_certification_forge/verdict.py b/src/echo_certification_forge/verdict.py index 457751c..82c1e12 100644 --- a/src/echo_certification_forge/verdict.py +++ b/src/echo_certification_forge/verdict.py @@ -88,6 +88,15 @@ def evaluate( reasons.append("production_e2e_identity_unavailable") elif e2e_result is None: reasons.append("production_e2e_attestation_missing") + elif ( + not e2e_result.passed + and e2e_result.details.get("validation") + == "production_e2e_attestation_missing" + ): + # The executor records a fail-closed rule row even when no attestation was supplied. + # Preserve that explicit cause instead of re-validating the diagnostic placeholder as + # though it were a malformed attestation payload. + reasons.append("production_e2e_attestation_missing") else: e2e_valid, e2e_reason = validate_production_e2e( e2e_result.details, diff --git a/tests/test_evidence_and_verdict.py b/tests/test_evidence_and_verdict.py index 2265075..c616c7c 100644 --- a/tests/test_evidence_and_verdict.py +++ b/tests/test_evidence_and_verdict.py @@ -135,6 +135,48 @@ def test_source_and_rule_rows_without_production_e2e_never_get_ready( assert "production_e2e_attestation_missing" in decision.reasons +def test_failed_executor_rule_preserves_missing_attestation_reason( + store, manifest, target, environment +): + run_id = register(store, manifest, target, environment) + for index, rule in enumerate(manifest.rules, start=1): + artifact_id = f"executor-evidence-{index:02d}" + store.append_artifact( + run_id, + target.tenant_id, + artifact_id, + json.dumps({"rule": rule.id}, sort_keys=True).encode(), + "application/json", + "executor-regression", + RedactionStatus.COMPLETE, + ) + is_production_e2e = rule.id == "production_e2e" + store.record_rule_result( + run_id, + target.tenant_id, + RuleResult( + rule.id, + not is_production_e2e, + (artifact_id,), + ( + {"validation": "production_e2e_attestation_missing"} + if is_production_e2e + else {"source": "executor-regression"} + ), + ), + ) + store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) + + signer = Ed25519VerdictSigner.generate() + decision = DeterministicVerdictEngine().evaluate( + store, run_id, target.tenant_id, manifest, signer.key_id + ) + + assert decision.release_verdict is ReleaseVerdict.NOT_READY + assert "production_e2e_attestation_missing" in decision.reasons + assert "production_e2e_schema_invalid" not in decision.reasons + + def test_bare_database_rows_cannot_manufacture_ready(store, manifest, target, environment): run_id = register(store, manifest, target, environment) now = "2026-07-16T00:00:00Z" From 3bfe40b486520be31e4bc94cb83740dbc8b224be Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:38:58 -0700 Subject: [PATCH 07/10] Canonicalize the GitHub App publish capability --- .echo/sdk.json | 1 + contracts/certforge-sdk-capabilities.v1.json | 29 +++++++++++++++++++- scripts/certification_journey.py | 4 +-- scripts/generate_certforge_sdk_contract.py | 10 +++++++ scripts/master_acceptance.py | 6 ++-- scripts/register_certforge_caps.sql | 6 ++++ scripts/register_certforge_sdk_schemas.sql | 5 ++-- tests/test_echo_sdk_manifest.py | 2 +- tests/test_sdk_capability_contract.py | 10 ++++++- 9 files changed, 63 insertions(+), 10 deletions(-) diff --git a/.echo/sdk.json b/.echo/sdk.json index 2d1fe37..f856e69 100644 --- a/.echo/sdk.json +++ b/.echo/sdk.json @@ -46,6 +46,7 @@ "echo.certforge.projects.create", "echo.certforge.projects.list", "echo.certforge.public_verification", + "echo.certforge.publish", "echo.certforge.r5.status", "echo.certforge.r5.submit_async", "echo.certforge.run", diff --git a/contracts/certforge-sdk-capabilities.v1.json b/contracts/certforge-sdk-capabilities.v1.json index e3522ea..66e2950 100644 --- a/contracts/certforge-sdk-capabilities.v1.json +++ b/contracts/certforge-sdk-capabilities.v1.json @@ -1412,6 +1412,33 @@ "type": "object" } }, + "echo.certforge.publish": { + "input_schema": { + "additionalProperties": false, + "properties": { + "command": { + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "run_id": { + "type": "string" + } + }, + "required": [ + "command", + "run_id" + ], + "type": "object" + }, + "operation": "POST /v1/subscriber/certifications/{run_id}/publish", + "output_schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, "echo.certforge.r5.status": { "input_schema": { "additionalProperties": false, @@ -2295,7 +2322,7 @@ } } }, - "capability_count": 60, + "capability_count": 61, "policy": "Every Certification Forge SDK action has a command-bearing strict input schema and an output schema derived from its production route contract.", "schema_version": "1.0.0" } diff --git a/scripts/certification_journey.py b/scripts/certification_journey.py index 38ca793..bf48bc6 100644 --- a/scripts/certification_journey.py +++ b/scripts/certification_journey.py @@ -62,8 +62,8 @@ def main() -> int: sdk = json.loads((ROOT / ".echo" / "sdk.json").read_text(encoding="utf-8")) capabilities = sdk.get("capabilities", []) - require(len(capabilities) == 60, "SDK capability declaration must contain exactly 60 entries") - require(len(set(capabilities)) == 60, "SDK capability declaration contains duplicates") + require(len(capabilities) == 61, "SDK capability declaration must contain exactly 61 entries") + require(len(set(capabilities)) == 61, "SDK capability declaration contains duplicates") require(all(isinstance(item, str) and item.startswith("echo.") for item in capabilities), "SDK capability declaration contains an invalid identifier") diff --git a/scripts/generate_certforge_sdk_contract.py b/scripts/generate_certforge_sdk_contract.py index 1537784..b061f51 100644 --- a/scripts/generate_certforge_sdk_contract.py +++ b/scripts/generate_certforge_sdk_contract.py @@ -216,6 +216,16 @@ def build_contract() -> dict[str, Any]: "input_schema": _operation_input(operation, components), "output_schema": _operation_output(operation, components), } + # Keep the least-privileged publishing capability used by the Certification Forge GitHub + # App as a first-class compatibility alias. It targets the same fail-closed route as the + # tier-2 administrative capability, but the service still re-evaluates the exact signed + # PRODUCTION_READY verdict before publishing public verification material. + publish = capabilities["echo.certforge.admin.publish"] + capabilities["echo.certforge.publish"] = { + "operation": publish["operation"], + "input_schema": publish["input_schema"], + "output_schema": publish["output_schema"], + } capabilities.update(_manual_capabilities()) return { "schema_version": "1.0.0", diff --git a/scripts/master_acceptance.py b/scripts/master_acceptance.py index e9f4a2e..4048db6 100644 --- a/scripts/master_acceptance.py +++ b/scripts/master_acceptance.py @@ -72,10 +72,10 @@ def _p7(value: dict[str, Any]) -> None: def _sdk(value: dict[str, Any]) -> None: capabilities = value.get("capabilities") - _require(value.get("capability_count") == 60, "SDK contract must contain exactly 60 capabilities") - _require(isinstance(capabilities, dict) and len(capabilities) == 60, "SDK capability rows are incomplete") + _require(value.get("capability_count") == 61, "SDK contract must contain exactly 61 capabilities") + _require(isinstance(capabilities, dict) and len(capabilities) == 61, "SDK capability rows are incomplete") names = list(capabilities) - _require(len(set(names)) == 60, "SDK capability IDs are not unique") + _require(len(set(names)) == 61, "SDK capability IDs are not unique") _require(all(isinstance(name, str) and name.startswith("echo.cert") for name in names), "SDK capability namespace drift") _require( all( diff --git a/scripts/register_certforge_caps.sql b/scripts/register_certforge_caps.sql index 80dbd57..6969503 100644 --- a/scripts/register_certforge_caps.sql +++ b/scripts/register_certforge_caps.sql @@ -136,6 +136,12 @@ VALUES '{"type":"object","required":["run_id"],"properties":{"run_id":{"type":"string","minLength":1,"maxLength":128}},"additionalProperties":false}'::jsonb, 'certforge.admin.mutate', 2, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 15, 'active', 'unknown'), + ('echo.certforge.publish', + 'Certification Forge: publish public-only verification material for one already-current, signed, production-ready run. The service re-evaluates the deployment gate; this capability cannot change evidence, policy, verdicts, or lifecycle state.', + 'http', 'http://127.0.0.1:8309/v1/subscriber/certifications/{run_id}/publish', 'POST', 'path', 'forge', + '{"type":"object","required":["run_id"],"properties":{"run_id":{"type":"string","minLength":1,"maxLength":128}},"additionalProperties":false}'::jsonb, + 'certforge.read', 1, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 15, 'active', 'unknown'), + ('echo.certforge.admin.quarantine', 'Certification Forge administration: quarantine one authenticated runner or adapter, removing it from eligible capacity/execution without deleting telemetry. Tier-2 HMAC and Desktop reauthentication are required.', 'http', 'http://127.0.0.1:8309/v1/subscriber/operational-quarantines', 'POST', 'json_body', 'forge', diff --git a/scripts/register_certforge_sdk_schemas.sql b/scripts/register_certforge_sdk_schemas.sql index fbfaae4..78f876b 100644 --- a/scripts/register_certforge_sdk_schemas.sql +++ b/scripts/register_certforge_sdk_schemas.sql @@ -50,6 +50,7 @@ INSERT INTO certforge_sdk_contract(id, input_schema_json, output_schema_json) VA ('echo.certforge.projects.create', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"name":{"maxLength":160,"minLength":1,"type":"string"},"slug":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"},"target_reference":{"maxLength":2048,"minLength":1,"type":"string"}},"required":["command","name","slug","target_reference"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb), ('echo.certforge.projects.list', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"}},"required":["command"],"type":"object"}'::jsonb, '{"items":{"additionalProperties":true,"type":"object"},"type":"array"}'::jsonb), ('echo.certforge.public_verification', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"verification_id":{"type":"string"}},"required":["command","verification_id"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb), + ('echo.certforge.publish', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"type":"string"}},"required":["command","run_id"],"type":"object"}'::jsonb, '{"additionalProperties":{"type":"string"},"type":"object"}'::jsonb), ('echo.certforge.r5.status', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"pattern":"^[A-Za-z0-9._-]{1,64}$","type":"string"}},"required":["command","run_id"],"type":"object"}'::jsonb, '{"additionalProperties":false,"properties":{"note":{"type":"string"},"result":{"anyOf":[{"additionalProperties":false,"properties":{"completion_marker":{"type":["string","null"]},"deployment_authorized":{"type":"boolean"},"evidence_run_id":{"type":"string"},"forge_verification":{"additionalProperties":true,"type":"object"},"mode":{"enum":["preflight","full"],"type":"string"},"operator_exit_code":{"type":"integer"},"operator_report":{"additionalProperties":true,"type":"object"},"r5_gate":{"enum":["PASS","BLOCK"],"type":"string"},"release_verdict":{"enum":["NOT_READY"],"type":"string"},"run_outcome":{"type":"string"},"schema":{"type":"string"},"stderr_tail":{"type":"string"}},"required":["schema","mode","evidence_run_id","run_outcome","release_verdict","r5_gate","deployment_authorized","completion_marker","operator_report","forge_verification","operator_exit_code"],"type":"object"},{"type":"null"}]},"run_id":{"type":"string"},"status":{"enum":["RUNNING","COMPLETE","FAILED"],"type":"string"}},"required":["run_id","status"],"type":"object"}'::jsonb), ('echo.certforge.r5.submit_async', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"dry_run":{"type":"boolean"},"evidence_run_id":{"pattern":"^[A-Za-z0-9._-]{1,64}$","type":"string"},"evidence_run_nonce":{"maxLength":128,"minLength":16,"type":"string"},"expected_base_model_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_base_model_revision":{"maxLength":128,"minLength":6,"type":"string"},"expected_gs343_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_r2d2_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_registry_revision":{"maxLength":128,"minLength":6,"type":"string"},"expected_registry_snapshot_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_server_build_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_signing_key_id":{"maxLength":128,"minLength":8,"type":"string"},"target_family":{"enum":["gs343","r2d2"],"type":"string"}},"required":["command","target_family","expected_server_build_digest","expected_registry_snapshot_digest","expected_registry_revision","expected_signing_key_id","expected_base_model_digest","expected_base_model_revision","expected_gs343_digest","expected_r2d2_digest","evidence_run_id","evidence_run_nonce"],"type":"object"}'::jsonb, '{"additionalProperties":false,"properties":{"idempotent":{"type":"boolean"},"mode":{"enum":["preflight","full"],"type":"string"},"poll_capability":{"enum":["echo.certforge.r5.status"],"type":"string"},"run_id":{"type":"string"},"status":{"enum":["RESERVED","RUNNING"],"type":"string"}},"required":["run_id","status","mode"],"type":"object"}'::jsonb), ('echo.certforge.run', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"pattern":"^[A-Za-z0-9._-]{1,128}$","type":"string"},"tenant":{"pattern":"^[A-Za-z0-9._-]{1,128}$","type":"string"}},"required":["command","run_id","tenant"],"type":"object"}'::jsonb, '{"additionalProperties":false,"properties":{"poll_capability":{"enum":["echo.certforge.status"],"type":"string"},"run_id":{"type":"string"},"sandboxed":{"type":"boolean"},"status":{"enum":["PENDING","CLAIMED"],"type":"string"},"tenant":{"type":"string"}},"required":["run_id","status","tenant","sandboxed","poll_capability"],"type":"object"}'::jsonb), @@ -74,8 +75,8 @@ BEGIN FROM arcanum_sdk.sdk_capabilities WHERE id LIKE 'echo.certforge.%' OR id = 'echo.certification_forge.r5.run_negative_controls'; - IF live_count <> 60 THEN - RAISE EXCEPTION 'Certification Forge SDK surface drift: live %, contract 60', live_count; + IF live_count <> 61 THEN + RAISE EXCEPTION 'Certification Forge SDK surface drift: live %, contract 61', live_count; END IF; IF EXISTS ( SELECT 1 FROM certforge_sdk_contract c diff --git a/tests/test_echo_sdk_manifest.py b/tests/test_echo_sdk_manifest.py index d3dfe13..9848804 100644 --- a/tests/test_echo_sdk_manifest.py +++ b/tests/test_echo_sdk_manifest.py @@ -13,7 +13,7 @@ def test_echo_sdk_manifest_matches_the_authoritative_contract() -> None: contract = json.loads(contract_path.read_text(encoding="utf-8")) assert manifest["version"] == 1 - assert len(manifest["capabilities"]) == contract["capability_count"] == 60 + assert len(manifest["capabilities"]) == contract["capability_count"] == 61 assert set(manifest["capabilities"]) == set(contract["capabilities"]) assert len(manifest["capabilities"]) == len(set(manifest["capabilities"])) assert all(capability.startswith("echo.") for capability in manifest["capabilities"]) diff --git a/tests/test_sdk_capability_contract.py b/tests/test_sdk_capability_contract.py index adcb6ef..a0afa0e 100644 --- a/tests/test_sdk_capability_contract.py +++ b/tests/test_sdk_capability_contract.py @@ -32,7 +32,7 @@ def test_generated_sdk_contract_is_current() -> None: def test_every_certforge_capability_has_command_and_output_schema() -> None: contract = json.loads(CONTRACT.read_text(encoding="utf-8")) capabilities = contract["capabilities"] - assert contract["capability_count"] == 60 == len(capabilities) + assert contract["capability_count"] == 61 == len(capabilities) for capability, item in capabilities.items(): input_schema = item["input_schema"] output_schema = item["output_schema"] @@ -48,6 +48,14 @@ def test_every_certforge_capability_has_command_and_output_schema() -> None: assert "type" in output_schema or "anyOf" in output_schema, capability +def test_github_app_publish_alias_is_exactly_schema_compatible() -> None: + contract = json.loads(CONTRACT.read_text(encoding="utf-8")) + capabilities = contract["capabilities"] + least_privileged = capabilities["echo.certforge.publish"] + administrative = capabilities["echo.certforge.admin.publish"] + assert least_privileged == administrative + + def test_registered_capability_set_equals_sdk_contract() -> None: contract = json.loads(CONTRACT.read_text(encoding="utf-8")) registered: set[str] = set() From f4eb95a5173ab505e4ea631e6a938fdaebb66f1d Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 21:27:04 -0700 Subject: [PATCH 08/10] feat(sandbox): add bounded memory profiles --- CHANGELOG.md | 7 +++ docs/OPERATIONS.md | 5 ++ .../dispatch_worker.py | 9 +++- src/echo_certification_forge/run_worker.py | 15 +++++- src/echo_certification_forge/sandbox.py | 36 +++++++++++-- tests/test_t4p8_sandbox.py | 53 ++++++++++++++++++- 6 files changed, 119 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d2f4c6..056e61e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ All notable changes to Echo Certification Forge are documented here. Versions follow Semantic Versioning; dates use ISO 8601. +## [Unreleased] + +### Changed + +- Allow operators to select a validated 128 MiB to 4 GiB journey cgroup while preserving no-swap + containment, and record the effective memory, CPU, PID, and scratch limits in journey evidence. + ## [1.1.0] - 2026-08-09 ### Added diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 7e47fee..8a70d52 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -26,6 +26,11 @@ identity/E2E mismatch is a normal `NOT_READY` result, never a bypass. 6. Promote atomically, health-check production, and roll back on any mismatch. 7. Publish immutable machine certificates and the repository certificate graphic. +The execution cgroup remains mandatory. `ECHO_CERTFORGE_SANDBOX_MEMORY` (or +`--sandbox-memory`) may select a whole-MiB/GiB limit from `128m` through `4g`; the default is +`512m`. CertForge rejects malformed, lower, higher, or unbounded values, applies the same value to +memory and memory-swap, and records the effective resource profile in critical-journey evidence. + ## Incident triage Capture the run ID, target SHA, environment digest, policy version, service health, dispatcher state, diff --git a/src/echo_certification_forge/dispatch_worker.py b/src/echo_certification_forge/dispatch_worker.py index 2bc2b88..17b48ae 100644 --- a/src/echo_certification_forge/dispatch_worker.py +++ b/src/echo_certification_forge/dispatch_worker.py @@ -12,7 +12,7 @@ from .intake import SubmitRequest from .policy import RuleManifest from .run_worker import _load_adapter_inputs, _load_signer, run -from .sandbox import DEFAULT_IMAGE, DockerSandbox +from .sandbox import DEFAULT_IMAGE, DEFAULT_MEMORY, DockerSandbox, normalize_memory_limit from .subscriber import SubscriberDispatch, SubscriberError, SubscriberGovernance, SubscriberPolicy _REPO = Path(__file__).resolve().parents[2] @@ -187,6 +187,12 @@ def main(argv: list[str] | None = None) -> int: "--sandbox-image", default=os.environ.get("ECHO_CERTFORGE_SANDBOX_IMAGE", DEFAULT_IMAGE), ) + parser.add_argument( + "--sandbox-memory", + type=normalize_memory_limit, + default=os.environ.get("ECHO_CERTFORGE_SANDBOX_MEMORY", DEFAULT_MEMORY), + help="bounded container memory limit (128m through 4g)", + ) parser.add_argument( "--sandbox-docker", default=os.environ.get("ECHO_CERTFORGE_SANDBOX_DOCKER", "docker"), @@ -273,6 +279,7 @@ def main(argv: list[str] | None = None) -> int: sandbox = ( DockerSandbox( image=args.sandbox_image, + memory=args.sandbox_memory, docker=tuple(args.sandbox_docker.split()), ) if args.sandbox diff --git a/src/echo_certification_forge/run_worker.py b/src/echo_certification_forge/run_worker.py index e2fc1a6..8bc804c 100644 --- a/src/echo_certification_forge/run_worker.py +++ b/src/echo_certification_forge/run_worker.py @@ -41,7 +41,13 @@ from .policy import RuleManifest from .production_e2e import VerifiedProductionE2E, load_signed_attestation from .runner import RunnerResponse -from .sandbox import DEFAULT_IMAGE, DockerSandbox, sandboxed_journey_runner +from .sandbox import ( + DEFAULT_IMAGE, + DEFAULT_MEMORY, + DockerSandbox, + normalize_memory_limit, + sandboxed_journey_runner, +) from .signing import Ed25519VerdictSigner from .subscriber import SubscriberError, SubscriberGovernance, SubscriberPolicy @@ -749,6 +755,12 @@ def main(argv: list[str] | None = None) -> int: "--sandbox-image", default=os.environ.get("ECHO_CERTFORGE_SANDBOX_IMAGE", DEFAULT_IMAGE), ) + parser.add_argument( + "--sandbox-memory", + type=normalize_memory_limit, + default=os.environ.get("ECHO_CERTFORGE_SANDBOX_MEMORY", DEFAULT_MEMORY), + help="bounded container memory limit (128m through 4g)", + ) parser.add_argument( "--sandbox-docker", default=os.environ.get("ECHO_CERTFORGE_SANDBOX_DOCKER", "docker"), @@ -921,6 +933,7 @@ def main(argv: list[str] | None = None) -> int: if args.sandbox: sandbox = DockerSandbox( image=args.sandbox_image, + memory=args.sandbox_memory, docker=tuple(args.sandbox_docker.split()), ) diff --git a/src/echo_certification_forge/sandbox.py b/src/echo_certification_forge/sandbox.py index a6ed098..f4df2a4 100644 --- a/src/echo_certification_forge/sandbox.py +++ b/src/echo_certification_forge/sandbox.py @@ -18,6 +18,7 @@ """ from __future__ import annotations +import re import subprocess import time from dataclasses import dataclass, field @@ -26,12 +27,31 @@ # Pinned minimal Python base (same digest the P4 supply-chain pipeline pins). Override per policy. DEFAULT_IMAGE = "python:3.12-alpine@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df" +DEFAULT_MEMORY = "512m" +MIN_MEMORY_MIB = 128 +MAX_MEMORY_MIB = 4096 +_MEMORY_LIMIT = re.compile(r"^([1-9][0-9]*)([mMgG])$") class SandboxError(RuntimeError): """The sandbox runtime is unavailable or failed to launch (distinct from a target-code failure).""" +def normalize_memory_limit(value: str) -> str: + """Validate and normalize a bounded Docker memory limit.""" + match = _MEMORY_LIMIT.fullmatch(value.strip()) + if match is None: + raise ValueError("sandbox memory must be a whole number followed by m or g") + amount = int(match.group(1)) + unit = match.group(2).lower() + memory_mib = amount if unit == "m" else amount * 1024 + if not MIN_MEMORY_MIB <= memory_mib <= MAX_MEMORY_MIB: + raise ValueError( + f"sandbox memory must be between {MIN_MEMORY_MIB}m and {MAX_MEMORY_MIB // 1024}g" + ) + return f"{amount}{unit}" + + @dataclass(frozen=True, slots=True) class SandboxResult: returncode: int @@ -43,7 +63,7 @@ class SandboxResult: @dataclass(frozen=True, slots=True) class DockerSandbox: image: str = DEFAULT_IMAGE - memory: str = "512m" + memory: str = DEFAULT_MEMORY cpus: str = "1.0" pids_limit: int = 128 tmpfs_size: str = "64m" @@ -56,11 +76,15 @@ def build_command(self, argv: list[str], workdir: Path) -> list[str]: """Construct the fully-hardened `docker run` argv. Pure — no side effects, unit-testable.""" if not argv: raise SandboxError("empty journey argv") + try: + memory = normalize_memory_limit(self.memory) + except ValueError as exc: + raise SandboxError(str(exc)) from exc cmd: list[str] = [ *self.docker, "run", "--rm", "--network", "none", - "--memory", self.memory, - "--memory-swap", self.memory, # no swap escape past the memory cap + "--memory", memory, + "--memory-swap", memory, # no swap escape past the memory cap "--cpus", self.cpus, "--pids-limit", str(self.pids_limit), "--read-only", @@ -139,6 +163,12 @@ def _run(argv: list[str], workdir: Path) -> tuple[bool, dict]: return False, {"executed": True, "isolation": "docker", "error": f"sandbox_unavailable:{exc}"} return result.returncode == 0, { "executed": True, "isolation": "docker", "image": sandbox.image, + "resource_limits": { + "memory": normalize_memory_limit(sandbox.memory), + "cpus": sandbox.cpus, + "pids": sandbox.pids_limit, + "tmpfs": sandbox.tmpfs_size, + }, "argv": argv, "returncode": result.returncode, "timed_out": result.timed_out, "stdout_tail": result.stdout[-2000:], "stderr_tail": result.stderr[-2000:], } diff --git a/tests/test_t4p8_sandbox.py b/tests/test_t4p8_sandbox.py index a1e9dc5..3bcdc34 100644 --- a/tests/test_t4p8_sandbox.py +++ b/tests/test_t4p8_sandbox.py @@ -12,7 +12,12 @@ from echo_certification_forge.executor import RunExecutor, StaticEntitlement from echo_certification_forge.sandbox import ( - DEFAULT_IMAGE, DockerSandbox, SandboxError, sandboxed_journey_runner, + DEFAULT_IMAGE, + DockerSandbox, + SandboxError, + SandboxResult, + normalize_memory_limit, + sandboxed_journey_runner, ) from echo_certification_forge.signing import Ed25519VerdictSigner from production_e2e_support import trusted_generic_production_e2e @@ -42,6 +47,52 @@ def test_empty_argv_rejected(tmp_path): DockerSandbox().build_command([], tmp_path) +@pytest.mark.parametrize( + ("raw", "normalized"), + (("128m", "128m"), ("1024M", "1024m"), ("1G", "1g"), ("4g", "4g")), +) +def test_memory_limit_is_bounded_and_normalized(raw, normalized): + assert normalize_memory_limit(raw) == normalized + + +@pytest.mark.parametrize("raw", ("", "0m", "127m", "5g", "512", "1.5g", "unlimited")) +def test_memory_limit_rejects_unbounded_or_malformed_values(raw): + with pytest.raises(ValueError, match="sandbox memory"): + normalize_memory_limit(raw) + + +def test_custom_memory_limit_stays_cgroup_bounded(tmp_path): + cmd = DockerSandbox(memory="1G").build_command(["python3", "hello.py"], tmp_path) + joined = " ".join(cmd) + assert "--memory 1g" in joined + assert "--memory-swap 1g" in joined + + +def test_effective_resource_limits_are_recorded_in_journey_evidence(tmp_path): + class StubSandbox: + image = "example.invalid/runtime@sha256:" + ("a" * 64) + memory = "1G" + cpus = "1.5" + pids_limit = 96 + tmpfs_size = "80m" + + @staticmethod + def run(argv, workdir, execution_guard=None): + return SandboxResult(0, "ok", "", False) + + passed, detail = sandboxed_journey_runner(StubSandbox())( + ["python3", "hello.py"], + tmp_path, + ) + assert passed is True + assert detail["resource_limits"] == { + "memory": "1g", + "cpus": "1.5", + "pids": 96, + "tmpfs": "80m", + } + + def test_unavailable_runtime_is_a_harness_failure_not_a_pass(tmp_path): # a bogus docker binary -> SandboxError -> runner reports passed=False (never a silent pass) runner = sandboxed_journey_runner(DockerSandbox(docker=("definitely-not-docker-xyz",))) From c0d4b3eee187441b047eacb71fb32fc709acd4bc Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 21:38:25 -0700 Subject: [PATCH 09/10] fix(identity): bind sandbox runtime to verdict --- CHANGELOG.md | 1 + docs/OPERATIONS.md | 2 ++ src/echo_certification_forge/app.py | 7 ++++- src/echo_certification_forge/run_worker.py | 20 ++++++++++++-- src/echo_certification_forge/sandbox.py | 32 +++++++++++++++------- tests/test_p5_adapter_execution.py | 2 ++ tests/test_p6_platform_integration.py | 5 +++- tests/test_t4p8_sandbox.py | 25 +++++++++++++++++ 8 files changed, 79 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056e61e..b8fcd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Versioning; dates use ISO 8601. - Allow operators to select a validated 128 MiB to 4 GiB journey cgroup while preserving no-swap containment, and record the effective memory, CPU, PID, and scratch limits in journey evidence. +- Bind the exact sandbox image digest and resource profile into the certification environment identity. ## [1.1.0] - 2026-08-09 diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 8a70d52..7780dde 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -30,6 +30,8 @@ The execution cgroup remains mandatory. `ECHO_CERTFORGE_SANDBOX_MEMORY` (or `--sandbox-memory`) may select a whole-MiB/GiB limit from `128m` through `4g`; the default is `512m`. CertForge rejects malformed, lower, higher, or unbounded values, applies the same value to memory and memory-swap, and records the effective resource profile in critical-journey evidence. +The pinned sandbox image digest and resource profile are also incorporated into the authoritative +certification environment identity; changing either invalidates reuse of the prior environment digest. ## Incident triage diff --git a/src/echo_certification_forge/app.py b/src/echo_certification_forge/app.py index 3f3f7dc..1ea3226 100644 --- a/src/echo_certification_forge/app.py +++ b/src/echo_certification_forge/app.py @@ -10,6 +10,7 @@ from .runner import TrustedTransportRegistry from .adapters import adapter_set_digest from .run_worker import _worker_environment, load_adapter_execution_profile +from .sandbox import DEFAULT_IMAGE, DEFAULT_MEMORY, DockerSandbox from .service import ServiceContext, create_app from .signing import TrustedPublicKeyRegistry from .subscriber import SubscriberGovernance, SubscriberPolicy @@ -82,7 +83,11 @@ def _load_certification_environment() -> dict[str, str] | None: **{name: Path(value) for name, value in required.items() if value is not None} ) adapter_sha256 = adapter_set_digest(records) - environment = _worker_environment(adapter_sha256, profile_sha256) + sandbox = DockerSandbox( + image=os.environ.get("ECHO_CERTFORGE_SANDBOX_IMAGE", DEFAULT_IMAGE), + memory=os.environ.get("ECHO_CERTFORGE_SANDBOX_MEMORY", DEFAULT_MEMORY), + ) + environment = _worker_environment(adapter_sha256, profile_sha256, sandbox) return { "certification_environment_identity_digest": environment.identity_digest, "runner_image_digest": "sha256:" + environment.runner_image_sha256, diff --git a/src/echo_certification_forge/run_worker.py b/src/echo_certification_forge/run_worker.py index 8bc804c..26276ed 100644 --- a/src/echo_certification_forge/run_worker.py +++ b/src/echo_certification_forge/run_worker.py @@ -107,18 +107,28 @@ def _env_digest(component: str) -> str: def _worker_environment( adapter_set_sha256: str | None = None, adapter_execution_profile_sha256: str | None = None, + sandbox: DockerSandbox | None = None, ) -> EnvironmentIdentity: """Declared certification environment. The legacy v1 path retains its historical environment commitment. P5/v2 callers pass the exact digest derived from the verified signed adapter execution records. """ + runner_image_sha256 = sandbox.image_sha256() if sandbox is not None else _env_digest("runner-image") + harness_sha256 = _env_digest("harness") + if sandbox is not None: + harness_sha256 = sha256_json( + { + "base_harness_sha256": harness_sha256, + "sandbox_resource_limits": sandbox.resource_limits(), + } + ) return EnvironmentIdentity( - runner_image_sha256=_env_digest("runner-image"), + runner_image_sha256=runner_image_sha256, adapter_set_sha256=adapter_set_sha256 or _env_digest("adapter-set"), test_plan_sha256=_env_digest("test-plan"), policy_sha256=_env_digest("policy"), - harness_sha256=_env_digest("harness"), + harness_sha256=harness_sha256, prompt_set_sha256=_env_digest("prompt-set"), model_route_sha256=( sha256_json( @@ -359,7 +369,11 @@ def run( if adapter_bundle_response is not None else None ) - environment = _worker_environment(adapter_digest, adapter_execution_profile_sha256) + environment = _worker_environment( + adapter_digest, + adapter_execution_profile_sha256, + sandbox_effective, + ) if production_e2e_attestation is None and production_e2e_provider is not None: production_e2e_attestation = production_e2e_provider(target, environment) if subscribers is not None: diff --git a/src/echo_certification_forge/sandbox.py b/src/echo_certification_forge/sandbox.py index f4df2a4..8bf9c92 100644 --- a/src/echo_certification_forge/sandbox.py +++ b/src/echo_certification_forge/sandbox.py @@ -31,6 +31,7 @@ MIN_MEMORY_MIB = 128 MAX_MEMORY_MIB = 4096 _MEMORY_LIMIT = re.compile(r"^([1-9][0-9]*)([mMgG])$") +_PINNED_IMAGE = re.compile(r"(?:^|@)sha256:([0-9a-f]{64})$") class SandboxError(RuntimeError): @@ -72,14 +73,30 @@ class DockerSandbox: docker: tuple[str, ...] = ("docker",) extra_env: dict[str, str] = field(default_factory=dict) - def build_command(self, argv: list[str], workdir: Path) -> list[str]: - """Construct the fully-hardened `docker run` argv. Pure — no side effects, unit-testable.""" - if not argv: - raise SandboxError("empty journey argv") + def image_sha256(self) -> str: + match = _PINNED_IMAGE.search(self.image) + if match is None: + raise SandboxError("sandbox image must be pinned by sha256 digest") + return match.group(1) + + def resource_limits(self) -> dict[str, str | int]: try: memory = normalize_memory_limit(self.memory) except ValueError as exc: raise SandboxError(str(exc)) from exc + return { + "memory": memory, + "cpus": self.cpus, + "pids": self.pids_limit, + "tmpfs": self.tmpfs_size, + } + + def build_command(self, argv: list[str], workdir: Path) -> list[str]: + """Construct the fully-hardened `docker run` argv. Pure — no side effects, unit-testable.""" + if not argv: + raise SandboxError("empty journey argv") + self.image_sha256() + memory = str(self.resource_limits()["memory"]) cmd: list[str] = [ *self.docker, "run", "--rm", "--network", "none", @@ -163,12 +180,7 @@ def _run(argv: list[str], workdir: Path) -> tuple[bool, dict]: return False, {"executed": True, "isolation": "docker", "error": f"sandbox_unavailable:{exc}"} return result.returncode == 0, { "executed": True, "isolation": "docker", "image": sandbox.image, - "resource_limits": { - "memory": normalize_memory_limit(sandbox.memory), - "cpus": sandbox.cpus, - "pids": sandbox.pids_limit, - "tmpfs": sandbox.tmpfs_size, - }, + "resource_limits": sandbox.resource_limits(), "argv": argv, "returncode": result.returncode, "timed_out": result.timed_out, "stdout_tail": result.stdout[-2000:], "stderr_tail": result.stderr[-2000:], } diff --git a/tests/test_p5_adapter_execution.py b/tests/test_p5_adapter_execution.py index 36f9fef..eb69ffe 100644 --- a/tests/test_p5_adapter_execution.py +++ b/tests/test_p5_adapter_execution.py @@ -39,6 +39,7 @@ from echo_certification_forge.family_r5 import execute as execute_r5 from echo_certification_forge.evidence import merkle_root from echo_certification_forge.runner import RunnerEphemeralIdentity +from echo_certification_forge.sandbox import DockerSandbox from echo_certification_forge.run_worker import ( _load_adapter_inputs, load_adapter_execution_profile, @@ -720,6 +721,7 @@ def test_production_router_arguments_rebind_bundle_and_reach_worker_execution( expected_environment = _worker_environment( adapter_set_digest(records), result["adapter_execution_profile_sha256"], + DockerSandbox(), ) assert result["environment_identity_digest"] == expected_environment.identity_digest store = EvidenceStore(db_path, evidence_root) diff --git a/tests/test_p6_platform_integration.py b/tests/test_p6_platform_integration.py index 267bd0a..debe07c 100644 --- a/tests/test_p6_platform_integration.py +++ b/tests/test_p6_platform_integration.py @@ -1836,6 +1836,7 @@ def test_registry_webhook_oci_run_certifies_and_deploys_end_to_end( try: manifest_digest = _push_test_image(registry) repo = f"http://127.0.0.1:{registry.port}/testapp" + exact_image = f"127.0.0.1:{registry.port}/testapp@{manifest_digest}" event = { "event_id": "evt-oci-0001", "event_type": "registry.image.pushed", @@ -1844,7 +1845,9 @@ def test_registry_webhook_oci_run_certifies_and_deploys_end_to_end( "image_repository": repo, "source_commit": SOURCE_COMMIT, # the platform declares the WORKER's environment commitment for the run - "environment_identity_digest": run_worker._worker_environment().identity_digest, + "environment_identity_digest": run_worker._worker_environment( + sandbox=DockerSandbox(image=exact_image) + ).identity_digest, "policy_version": manifest.manifest_id, } body = json.dumps(event).encode("utf-8") diff --git a/tests/test_t4p8_sandbox.py b/tests/test_t4p8_sandbox.py index 3bcdc34..cda218b 100644 --- a/tests/test_t4p8_sandbox.py +++ b/tests/test_t4p8_sandbox.py @@ -11,6 +11,7 @@ import pytest from echo_certification_forge.executor import RunExecutor, StaticEntitlement +from echo_certification_forge.run_worker import _worker_environment from echo_certification_forge.sandbox import ( DEFAULT_IMAGE, DockerSandbox, @@ -68,6 +69,26 @@ def test_custom_memory_limit_stays_cgroup_bounded(tmp_path): assert "--memory-swap 1g" in joined +def test_environment_identity_binds_pinned_image_and_resource_profile(): + image_a = "example.invalid/runtime@sha256:" + ("a" * 64) + image_b = "example.invalid/runtime@sha256:" + ("b" * 64) + baseline = _worker_environment(sandbox=DockerSandbox(image=image_a, memory="1g")) + different_image = _worker_environment( + sandbox=DockerSandbox(image=image_b, memory="1g") + ) + different_memory = _worker_environment( + sandbox=DockerSandbox(image=image_a, memory="2g") + ) + assert baseline.runner_image_sha256 == "a" * 64 + assert baseline.identity_digest != different_image.identity_digest + assert baseline.identity_digest != different_memory.identity_digest + + +def test_unpinned_image_is_rejected_before_execution(tmp_path): + with pytest.raises(SandboxError, match="pinned by sha256"): + DockerSandbox(image="python:latest").build_command(["python3", "hello.py"], tmp_path) + + def test_effective_resource_limits_are_recorded_in_journey_evidence(tmp_path): class StubSandbox: image = "example.invalid/runtime@sha256:" + ("a" * 64) @@ -80,6 +101,10 @@ class StubSandbox: def run(argv, workdir, execution_guard=None): return SandboxResult(0, "ok", "", False) + @staticmethod + def resource_limits(): + return {"memory": "1g", "cpus": "1.5", "pids": 96, "tmpfs": "80m"} + passed, detail = sandboxed_journey_runner(StubSandbox())( ["python3", "hello.py"], tmp_path, From 2c95413650b4f333d84e50ba1c3b2672d15e6564 Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Mon, 31 Aug 2026 17:18:13 -0700 Subject: [PATCH 10/10] feat: publish signed public verification proof --- CHANGELOG.md | 11 + README.md | 2 +- docs/PUBLIC_VERIFICATION_CONTRACT.md | 74 +++ scripts/p6_acceptance.py | 459 +++++++++++++----- src/echo_certification_forge/acquisition.py | 22 + src/echo_certification_forge/models.py | 4 + .../production_e2e.py | 200 +++++++- .../public_verification.py | 144 ++++++ src/echo_certification_forge/service.py | 41 +- src/echo_certification_forge/verdict.py | 14 + tests/production_e2e_support.py | 7 +- tests/test_evidence_and_verdict.py | 120 ++++- tests/test_p7_subscriber_governance.py | 75 ++- tests/test_production_e2e.py | 248 +++++++++- tests/test_public_verification.py | 205 ++++++++ tests/test_t4_live_run.py | 25 + 16 files changed, 1465 insertions(+), 186 deletions(-) create mode 100644 docs/PUBLIC_VERIFICATION_CONTRACT.md create mode 100644 src/echo_certification_forge/public_verification.py create mode 100644 tests/test_public_verification.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b8fcd5d..ccca010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,17 @@ Versioning; dates use ISO 8601. ## [Unreleased] +### Added + +- Return secret-safe public target and production-E2E projections plus the canonical environment + identity from the public verification endpoint, bind each projection to a dedicated digest in + the signed verdict, and fail the public result closed on any serialization mismatch. +- Publish only signed aggregate production-E2E outcomes; raw accounts, private repository samples, + credential routes, and client fingerprints remain in the private evidence record. +- Require a bounded, credential-free HTTPS canonical target for generic production-E2E proofs. +- Reject credential-bearing Git source URLs before acquisition, and add exact three-account + Autonomy plus four-client Continuity production-E2E profiles. + ### Changed - Allow operators to select a validated 128 MiB to 4 GiB journey cgroup while preserving no-swap diff --git a/README.md b/README.md index a16aad9..0f1e84d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Release gate](https://img.shields.io/badge/release%20gate-fail--closed-caa85e) ![License](https://img.shields.io/badge/license-proprietary-59636e) -[Architecture](docs/ARCHITECTURE.md) · [Operations](docs/OPERATIONS.md) · [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) +[Architecture](docs/ARCHITECTURE.md) · [Public verification](docs/PUBLIC_VERIFICATION_CONTRACT.md) · [Operations](docs/OPERATIONS.md) · [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) Echo Certification Forge is a deterministic, evidence-backed release authority for EchoForge. Every run begins at `NOT_READY`. Exact target, environment, policy, evidence, anchor, key, and lifecycle identities must verify before any signed verdict can be trusted. diff --git a/docs/PUBLIC_VERIFICATION_CONTRACT.md b/docs/PUBLIC_VERIFICATION_CONTRACT.md new file mode 100644 index 0000000..27c6cc2 --- /dev/null +++ b/docs/PUBLIC_VERIFICATION_CONTRACT.md @@ -0,0 +1,74 @@ +# Public verification contract + +`GET /v1/public/verifications/{verification_id}` is the secret-free, +machine-readable surface for independently checking a published Certification +Forge verdict. Publication is allowed only for a current signed +`PRODUCTION_READY` run. Every later read re-evaluates the deploy gate, so an +expired, revoked, superseded, compromised, or otherwise invalid verdict returns +`valid: false` with fail-closed reasons. + +## Response material + +The response contains: + +- `verification_id`, `valid`, and deterministic `reasons`; +- the exact signed verdict `payload`; +- a secret-safe public `target_identity` projection, the canonical + `environment_identity`, and a secret-safe signed `production_e2e` projection; +- the Ed25519 `signature_b64`, `key_id`, and public verification key. + +These identities are public verification material, not authority by themselves. +The private target record remains bound to `target_identity_digest` but is never +returned. The public target projection deliberately omits local paths and raw +Git remote URLs; for a credential-free HTTPS GitHub source it exposes only the +normalized `owner/repository`. Its hash is committed separately as +`public_target_identity_sha256`. The environment and production-E2E projections +are bound by `environment_identity_digest` and +`production_e2e_identity_sha256`. A serialization or digest mismatch forces +`valid: false` with a specific fail-closed reason. + +The public production-E2E projection contains signed aggregate results only: +tool/account/client counts and boolean reconciliation, visibility, authority, +OAuth, persistence, ledger, sharing, import, and continuity outcomes. Raw +account names, per-account repository counts, credential routes, private sample +repository IDs/node IDs/branches/HEAD SHAs, and per-client fingerprints remain +inside the private evidence record and are never copied to the public response. +Every generic production-E2E proof must include a bounded, credential-free +HTTPS canonical target without user information, query data, fragments, or +port zero. Hosts are normalized before policy evaluation and must be either an +unscoped global IP literal or a strict ASCII LDH name with an alphabetic TLD. +Special-use names, punycode A-labels, noncanonical numeric hosts, and +non-global, multicast, site-local, or scoped address literals fail closed. The +same canonicalizer governs signed-proof validation and public projection so a +consumer never receives a target the producer would reject. + +## Independent verifier procedure + +An independent consumer must: + +1. Canonicalize the verdict payload and verify `signature_b64` with the returned + Ed25519 public key, while enforcing the expected trusted key identity and + lifecycle policy. +2. Canonicalize `target_identity`, `environment_identity`, and + `production_e2e`; hash each with SHA-256; and compare them with + `public_target_identity_sha256`, `environment_identity_digest`, and + `production_e2e_identity_sha256` in the signed payload. +3. Confirm the target identity names the intended repository/artifact and exact + source revision. +4. Confirm the signed production-E2E identity, environment, rule-manifest + digest, evidence root, and release verdict satisfy the consumer's named + profile. Independently pin the accepted CertForge key IDs and rule-manifest + digests; response-provided values cannot expand either trust set. +5. Treat network errors, missing fields, key-policy failures, digest mismatches, + and `valid: false` as rejection. Cached green state is not authority. + +The public response never contains a private signing key, GitHub credential, +Vault secret, subscriber token, OAuth secret, customer-private evidence body, +or Commander signing key. Certificate presentation and Commander approval are +owned by Echo GitHub Autonomy; Certification Forge independently owns the +signed source/environment/E2E verdict that the App must verify. + +Git acquisition also rejects HTTP(S) user information, URL query/fragment +credentials, and nonstandard credential-like SCP usernames before invoking Git, +so those values cannot enter source identity, subprocess errors, or public +verification data. diff --git a/scripts/p6_acceptance.py b/scripts/p6_acceptance.py index 15490fe..fb6eb3d 100644 --- a/scripts/p6_acceptance.py +++ b/scripts/p6_acceptance.py @@ -27,6 +27,7 @@ Writes artifacts/p6_acceptance.json (+ .summary.json). Exit 0 only if every check passed. """ + from __future__ import annotations import hashlib @@ -98,8 +99,13 @@ def free_port() -> int: return sock.getsockname()[1] -def http(method: str, url: str, body: dict | bytes | None = None, headers: dict | None = None, - sign: bool = False): +def http( + method: str, + url: str, + body: dict | bytes | None = None, + headers: dict | None = None, + sign: bool = False, +): data = None all_headers = {"X-Tenant-ID": TENANT, "Content-Type": "application/json"} if headers: @@ -114,7 +120,13 @@ def http(method: str, url: str, body: dict | bytes | None = None, headers: dict all_headers[DEPLOY_TIMESTAMP_HEADER] = ts all_headers[DEPLOY_NONCE_HEADER] = nonce all_headers[DEPLOY_SIGNATURE_HEADER] = sign_deployment_request( - DEPLOY_SECRET, TENANT, method, urllib.parse.urlsplit(url).path, ts, nonce, data or b"" + DEPLOY_SECRET, + TENANT, + method, + urllib.parse.urlsplit(url).path, + ts, + nonce, + data or b"", ) request = urllib.request.Request(url, data=data, method=method, headers=all_headers) try: @@ -124,20 +136,37 @@ def http(method: str, url: str, body: dict | bytes | None = None, headers: dict return exc.code, json.loads(exc.read().decode("utf-8")) -def run_hook(base_url: str, artifact: str, env: str, env_digest: str, rule_digest: str, - deployment_id: str) -> tuple[int, str]: +def run_hook( + base_url: str, + artifact: str, + env: str, + env_digest: str, + rule_digest: str, + deployment_id: str, +) -> tuple[int, str]: proc = subprocess.run( [ - sys.executable, str(HOOK), "admit", - "--forge-url", base_url, - "--tenant", TENANT, - "--artifact-digest", artifact, - "--environment", env, - "--environment-identity-digest", env_digest, - "--rule-manifest-digest", rule_digest, - "--deployment-id", deployment_id, - "--requested-by", "p6.acceptance", - "--timeout", "15", + sys.executable, + str(HOOK), + "admit", + "--forge-url", + base_url, + "--tenant", + TENANT, + "--artifact-digest", + artifact, + "--environment", + env, + "--environment-identity-digest", + env_digest, + "--rule-manifest-digest", + rule_digest, + "--deployment-id", + deployment_id, + "--requested-by", + "p6.acceptance", + "--timeout", + "15", ], capture_output=True, text=True, @@ -147,17 +176,30 @@ def run_hook(base_url: str, artifact: str, env: str, env_digest: str, rule_diges return proc.returncode, proc.stdout.strip() -def run_hook_outcome(base_url: str, admission_id: str, status: str, detail: str, - rollback_to: str | None = None) -> tuple[int, str]: +def run_hook_outcome( + base_url: str, + admission_id: str, + status: str, + detail: str, + rollback_to: str | None = None, +) -> tuple[int, str]: """Record a REAL deployment outcome through the supplied pipeline integration.""" command = [ - sys.executable, str(HOOK), "outcome", - "--forge-url", base_url, - "--tenant", TENANT, - "--admission-id", admission_id, - "--status", status, - "--detail", detail, - "--timeout", "15", + sys.executable, + str(HOOK), + "outcome", + "--forge-url", + base_url, + "--tenant", + TENANT, + "--admission-id", + admission_id, + "--status", + status, + "--detail", + detail, + "--timeout", + "15", ] if rollback_to: command += ["--rollback-to", rollback_to] @@ -178,7 +220,12 @@ def main() -> int: def check(check_id: str, description: str, passed: bool, detail: object) -> None: checks.append( - {"id": check_id, "description": description, "passed": bool(passed), "detail": detail} + { + "id": check_id, + "description": description, + "passed": bool(passed), + "detail": detail, + } ) marker = "PASS" if passed else "FAIL" print(f"[{marker}] {check_id} — {description}") @@ -210,7 +257,9 @@ def check(check_id: str, description: str, passed: bool, detail: object) -> None ) def certify(run_id: str, target: TargetIdentity) -> None: - store.register_run(run_id, target, environment, manifest.manifest_id, manifest.digest) + store.register_run( + run_id, target, environment, manifest.manifest_id, manifest.digest + ) workdir = tmp_path / f"src-{run_id}" workdir.mkdir() (workdir / "hello.py").write_text("print('service ok')\n", encoding="utf-8") @@ -223,7 +272,9 @@ def certify(run_id: str, target: TargetIdentity) -> None: "environment_identity_digest": environment.identity_digest, "source_commit": target.source_commit, "deployment_sha": target.source_commit, - "canonical_target": target.canonical_ref, + "canonical_target": ( + f"https://api.github.com/certforge/{target.identity_digest}" + ), "required_checks": sorted(BASE_CHECKS), "checks": {name: True for name in sorted(BASE_CHECKS)}, "stability_probe_count": 3, @@ -271,7 +322,9 @@ def make_target(label: str) -> TargetIdentity: trusted_keys=trusted, deployment_ledger_path=tmp_path / "deployments.sqlite3", webhook_secrets=WebhookSecretRegistry(secrets={TENANT: WEBHOOK_SECRET}), - deployment_credentials=WebhookSecretRegistry(secrets={TENANT: DEPLOY_SECRET}), + deployment_credentials=WebhookSecretRegistry( + secrets={TENANT: DEPLOY_SECRET} + ), ) app = create_app(context) port = free_port() @@ -296,36 +349,80 @@ def make_target(label: str) -> TargetIdentity: try: # A1 — uncertified artifact must fail (through the REAL pipeline hook subprocess) - code, out = run_hook(base, digest("never-certified"), "production", - env_digest, rule_digest, "deploy-a1") - check("A1", "uncertified artifact production deployment fails via hook", - code == 2 and "artifact_not_certified" in out, {"exit": code, "stdout": out}) + code, out = run_hook( + base, + digest("never-certified"), + "production", + env_digest, + rule_digest, + "deploy-a1", + ) + check( + "A1", + "uncertified artifact production deployment fails via hook", + code == 2 and "artifact_not_certified" in out, + {"exit": code, "stdout": out}, + ) # bind both certifications over live HTTP (signed with the deployment credential) - status, body = http("POST", f"{base}/v1/certifications/cert-p6-v1/bindings", sign=True) + status, body = http( + "POST", f"{base}/v1/certifications/cert-p6-v1/bindings", sign=True + ) bound_v1 = status == 201 and body["artifact_sha256"] == v1.artifact_sha256 - status, body = http("POST", f"{base}/v1/certifications/cert-p6-v2/bindings", sign=True) + status, body = http( + "POST", f"{base}/v1/certifications/cert-p6-v2/bindings", sign=True + ) bound_v2 = status == 201 and body["artifact_sha256"] == v2.artifact_sha256 - check("A0", "certifications bind to exact artifact digests over live HTTP", - bound_v1 and bound_v2, {"v1": bound_v1, "v2": bound_v2}) + check( + "A0", + "certifications bind to exact artifact digests over live HTTP", + bound_v1 and bound_v2, + {"v1": bound_v1, "v2": bound_v2}, + ) # A2 — different digest from the certified artifact must fail - code, out = run_hook(base, digest("p6-app-v1-TAMPERED"), "staging", - env_digest, rule_digest, "deploy-a2") - check("A2", "different digest from certified artifact fails via hook", - code == 2 and "artifact_not_certified" in out, {"exit": code, "stdout": out}) + code, out = run_hook( + base, + digest("p6-app-v1-TAMPERED"), + "staging", + env_digest, + rule_digest, + "deploy-a2", + ) + check( + "A2", + "different digest from certified artifact fails via hook", + code == 2 and "artifact_not_certified" in out, + {"exit": code, "stdout": out}, + ) # A4 — production before staging acceptance fails (staging-first) - code, out = run_hook(base, v1.artifact_sha256, "production", - env_digest, rule_digest, "deploy-a4") - check("A4", "production before staging acceptance fails (staging-first)", - code == 2 and "staging_acceptance_missing" in out, {"exit": code, "stdout": out}) + code, out = run_hook( + base, + v1.artifact_sha256, + "production", + env_digest, + rule_digest, + "deploy-a4", + ) + check( + "A4", + "production before staging acceptance fails (staging-first)", + code == 2 and "staging_acceptance_missing" in out, + {"exit": code, "stdout": out}, + ) # A3 — valid unexpired READY artifact under the required policy passes; the # staging SUCCESS is recorded through the REAL pipeline integration (hook # outcome subcommand), which is what unlocks production. - code, out = run_hook(base, f"sha256:{v1.artifact_sha256}", "staging", - env_digest, rule_digest, "deploy-a3-stg") + code, out = run_hook( + base, + f"sha256:{v1.artifact_sha256}", + "staging", + env_digest, + rule_digest, + "deploy-a3-stg", + ) staging_ok = code == 0 staging_admission = json.loads(out)["admission_id"] if staging_ok else None outcome_ok = False @@ -333,67 +430,133 @@ def make_target(label: str) -> TargetIdentity: prod_admission: str | None = None if staging_ok: outcome_code, outcome_out = run_hook_outcome( - base, staging_admission, "SUCCEEDED", "staging smoke green") - outcome_ok = (outcome_code == 0 - and json.loads(outcome_out).get("recorded") is True) - code, out = run_hook(base, v1.artifact_sha256, "production", - env_digest, rule_digest, "deploy-a3-prd") + base, staging_admission, "SUCCEEDED", "staging smoke green" + ) + outcome_ok = ( + outcome_code == 0 + and json.loads(outcome_out).get("recorded") is True + ) + code, out = run_hook( + base, + v1.artifact_sha256, + "production", + env_digest, + rule_digest, + "deploy-a3-prd", + ) production_ok = code == 0 - prod_admission = json.loads(out)["admission_id"] if production_ok else None - check("A3", "READY artifact passes staging->production, outcomes via pipeline hook", - staging_ok and outcome_ok and production_ok, - {"staging_exit0": staging_ok, "outcome_recorded": outcome_ok, - "production_exit0": production_ok}) + prod_admission = ( + json.loads(out)["admission_id"] if production_ok else None + ) + check( + "A3", + "READY artifact passes staging->production, outcomes via pipeline hook", + staging_ok and outcome_ok and production_ok, + { + "staging_exit0": staging_ok, + "outcome_recorded": outcome_ok, + "production_exit0": production_ok, + }, + ) # A6 — failed v2 production deployment yields rollback evidence to v1; every # outcome (success, failure, rollback) is recorded through the pipeline hook. if prod_admission is None: - raise RuntimeError("A3 did not produce a production admission; cannot continue") + raise RuntimeError( + "A3 did not produce a production admission; cannot continue" + ) run_hook_outcome(base, prod_admission, "SUCCEEDED", "v1 live in production") - code, out = run_hook(base, v2.artifact_sha256, "staging", - env_digest, rule_digest, "deploy-a6-stg") + code, out = run_hook( + base, + v2.artifact_sha256, + "staging", + env_digest, + rule_digest, + "deploy-a6-stg", + ) v2_staging = json.loads(out)["admission_id"] run_hook_outcome(base, v2_staging, "SUCCEEDED", "v2 staging green") - code, out = run_hook(base, v2.artifact_sha256, "production", - env_digest, rule_digest, "deploy-a6-prd") + code, out = run_hook( + base, + v2.artifact_sha256, + "production", + env_digest, + rule_digest, + "deploy-a6-prd", + ) v2_production = json.loads(out)["admission_id"] failure_code, failure_out = run_hook_outcome( - base, v2_production, "FAILED", "v2 production smoke red") + base, v2_production, "FAILED", "v2 production smoke red" + ) failure = json.loads(failure_out) if failure_out else {} candidate = (failure.get("payload") or {}).get("rollback_candidate") or {} status, rollback = http("GET", f"{base}/v1/deployments/rollback-target") target_info = rollback.get("rollback_target") or {} rolled_code, rolled_out = run_hook_outcome( - base, v2_production, "ROLLED_BACK", "restored v1", - rollback_to=v1.artifact_sha256) + base, + v2_production, + "ROLLED_BACK", + "restored v1", + rollback_to=v1.artifact_sha256, + ) rolled = json.loads(rolled_out) if rolled_out else {} - check("A6", "failed production deployment produces rollback evidence to last-known-good", - failure_code == 0 - and candidate.get("artifact_sha256") == v1.artifact_sha256 - and target_info.get("artifact_sha256") == v1.artifact_sha256 - and rolled_code == 0 - and (rolled.get("payload") or {}).get("rollback_to") == v1.artifact_sha256, - {"failure_candidate": candidate, "rollback_target": target_info, - "rolled_back": rolled.get("payload")}) + check( + "A6", + "failed production deployment produces rollback evidence to last-known-good", + failure_code == 0 + and candidate.get("artifact_sha256") == v1.artifact_sha256 + and target_info.get("artifact_sha256") == v1.artifact_sha256 + and rolled_code == 0 + and (rolled.get("payload") or {}).get("rollback_to") + == v1.artifact_sha256, + { + "failure_candidate": candidate, + "rollback_target": target_info, + "rolled_back": rolled.get("payload"), + }, + ) # A5 — revoked certification fails afterwards store.append_lifecycle_event( - "cert-p6-v1", TENANT, VerdictLifecycleEvent.REVOKED, - "p6.acceptance", "critical vulnerability discovered", + "cert-p6-v1", + TENANT, + VerdictLifecycleEvent.REVOKED, + "p6.acceptance", + "critical vulnerability discovered", + ) + code, out = run_hook( + base, + v1.artifact_sha256, + "production", + env_digest, + rule_digest, + "deploy-a5", + ) + check( + "A5", + "revoked certification is denied afterwards (fail-closed lifecycle)", + code == 2 and "verdict_revoked" in out, + {"exit": code, "stdout": out}, ) - code, out = run_hook(base, v1.artifact_sha256, "production", - env_digest, rule_digest, "deploy-a5") - check("A5", "revoked certification is denied afterwards (fail-closed lifecycle)", - code == 2 and "verdict_revoked" in out, {"exit": code, "stdout": out}) # A7 — the deployment audit chain verifies end-to-end status, audit = http("GET", f"{base}/v1/deployments/audit") - admissions = [r for r in audit["records"] if r["record_type"] == "ADMISSION"] + admissions = [ + r for r in audit["records"] if r["record_type"] == "ADMISSION" + ] outcomes = [r for r in audit["records"] if r["record_type"] == "OUTCOME"] - check("A7", "append-only deployment audit chain verifies with full decision history", - audit["chain_valid"] is True and len(admissions) >= 8 and len(outcomes) >= 5, - {"chain_valid": audit["chain_valid"], "admissions": len(admissions), - "outcomes": len(outcomes)}) + check( + "A7", + "append-only deployment audit chain verifies with full decision history", + audit["chain_valid"] is True + and len(admissions) >= 8 + and len(outcomes) >= 5, + { + "chain_valid": audit["chain_valid"], + "admissions": len(admissions), + "outcomes": len(outcomes), + }, + ) # A8 — signed build webhook creates a run, replay deduplicates, bad signature 401 event = { @@ -412,70 +575,110 @@ def make_target(label: str) -> TargetIdentity: TIMESTAMP_HEADER: ts, SIGNATURE_HEADER: sign_webhook(WEBHOOK_SECRET, ts, payload), } - status1, first = http("POST", f"{base}/v1/hooks/build", payload, signed_headers) - status2, second = http("POST", f"{base}/v1/hooks/build", payload, signed_headers) + status1, first = http( + "POST", f"{base}/v1/hooks/build", payload, signed_headers + ) + status2, second = http( + "POST", f"{base}/v1/hooks/build", payload, signed_headers + ) bad_headers = { TIMESTAMP_HEADER: ts, SIGNATURE_HEADER: sign_webhook("wrong-secret", ts, payload), } - status3, third = http("POST", f"{base}/v1/hooks/build", payload, bad_headers) - check("A8", "signed webhook creates run, replay deduplicates, bad signature is 401", - status1 == 201 and status2 == 200 - and second["run"]["run_id"] == first["run"]["run_id"] - and status3 == 401, - {"first": status1, "replay": status2, "bad_signature": status3}) + status3, third = http( + "POST", f"{base}/v1/hooks/build", payload, bad_headers + ) + check( + "A8", + "signed webhook creates run, replay deduplicates, bad signature is 401", + status1 == 201 + and status2 == 200 + and second["run"]["run_id"] == first["run"]["run_id"] + and status3 == 401, + {"first": status1, "replay": status2, "bad_signature": status3}, + ) # A10 — tenant header alone is NOT authorization for mutations (fail-closed 401) status_unsigned, unsigned_body = http( "POST", f"{base}/v1/deployments/admissions", - {"artifact_sha256": v2.artifact_sha256, - "deployment_environment": "staging", - "environment_identity_digest": env_digest, - "rule_manifest_digest": rule_digest, - "deployment_id": "deploy-a10", - "requested_by": "p6.acceptance"}, - ) - check("A10", "unsigned mutation with tenant header only is rejected 401", - status_unsigned == 401 - and unsigned_body.get("detail") == "deployment_credential_signature_missing", - {"status": status_unsigned, "body": unsigned_body}) + { + "artifact_sha256": v2.artifact_sha256, + "deployment_environment": "staging", + "environment_identity_digest": env_digest, + "rule_manifest_digest": rule_digest, + "deployment_id": "deploy-a10", + "requested_by": "p6.acceptance", + }, + ) + check( + "A10", + "unsigned mutation with tenant header only is rejected 401", + status_unsigned == 401 + and unsigned_body.get("detail") + == "deployment_credential_signature_missing", + {"status": status_unsigned, "body": unsigned_body}, + ) # A11 — an EXACT byte-for-byte replay of an accepted signed mutation is rejected - replay_body = json.dumps({ - "artifact_sha256": digest("replay-check"), - "deployment_environment": "staging", - "environment_identity_digest": env_digest, - "rule_manifest_digest": rule_digest, - "deployment_id": "deploy-a11", - "requested_by": "p6.acceptance", - }).encode("utf-8") + replay_body = json.dumps( + { + "artifact_sha256": digest("replay-check"), + "deployment_environment": "staging", + "environment_identity_digest": env_digest, + "rule_manifest_digest": rule_digest, + "deployment_id": "deploy-a11", + "requested_by": "p6.acceptance", + } + ).encode("utf-8") replay_ts = to_utc_iso(utc_now()) replay_nonce = secrets.token_hex(16) replay_headers = { DEPLOY_TIMESTAMP_HEADER: replay_ts, DEPLOY_NONCE_HEADER: replay_nonce, DEPLOY_SIGNATURE_HEADER: sign_deployment_request( - DEPLOY_SECRET, TENANT, "POST", "/v1/deployments/admissions", - replay_ts, replay_nonce, replay_body), + DEPLOY_SECRET, + TENANT, + "POST", + "/v1/deployments/admissions", + replay_ts, + replay_nonce, + replay_body, + ), } - first_status, _ = http("POST", f"{base}/v1/deployments/admissions", - replay_body, replay_headers) - replay_status, replay_response = http("POST", f"{base}/v1/deployments/admissions", - replay_body, replay_headers) - check("A11", "exact replay of a signed mutation is rejected (nonce consumed)", - first_status == 200 and replay_status == 401 - and replay_response.get("detail") == "deployment_credential_nonce_reused", - {"first": first_status, "replay": replay_status, "body": replay_response}) + first_status, _ = http( + "POST", f"{base}/v1/deployments/admissions", replay_body, replay_headers + ) + replay_status, replay_response = http( + "POST", f"{base}/v1/deployments/admissions", replay_body, replay_headers + ) + check( + "A11", + "exact replay of a signed mutation is rejected (nonce consumed)", + first_status == 200 + and replay_status == 401 + and replay_response.get("detail") + == "deployment_credential_nonce_reused", + { + "first": first_status, + "replay": replay_status, + "body": replay_response, + }, + ) finally: server.should_exit = True thread.join(timeout=10) # A9 — with the forge DOWN, the hook fails CLOSED - code, out = run_hook(base, v1.artifact_sha256, "production", - env_digest, rule_digest, "deploy-a9") - check("A9", "forge unreachable -> hook fails closed (exit 3), deployment blocked", - code == 3 and "admission_unavailable" in out, {"exit": code, "stdout": out}) + code, out = run_hook( + base, v1.artifact_sha256, "production", env_digest, rule_digest, "deploy-a9" + ) + check( + "A9", + "forge unreachable -> hook fails closed (exit 3), deployment blocked", + code == 3 and "admission_unavailable" in out, + {"exit": code, "stdout": out}, + ) passed = all(item["passed"] for item in checks) report = { @@ -506,9 +709,11 @@ def make_target(label: str) -> TargetIdentity: encoding="utf-8", newline="\n", ) - print(f"\nP6 acceptance: {'PASSED' if passed else 'FAILED'} " - f"({sum(1 for item in checks if item['passed'])}/{len(checks)}) " - f"-> artifacts/p6_acceptance.json") + print( + f"\nP6 acceptance: {'PASSED' if passed else 'FAILED'} " + f"({sum(1 for item in checks if item['passed'])}/{len(checks)}) " + f"-> artifacts/p6_acceptance.json" + ) return 0 if passed else 1 diff --git a/src/echo_certification_forge/acquisition.py b/src/echo_certification_forge/acquisition.py index c11ef29..1dc7af2 100644 --- a/src/echo_certification_forge/acquisition.py +++ b/src/echo_certification_forge/acquisition.py @@ -20,6 +20,7 @@ import subprocess import tarfile import urllib.error +import urllib.parse import urllib.request from dataclasses import dataclass from pathlib import Path, PurePosixPath @@ -63,6 +64,26 @@ class AcquiredTarget: ) +def _validate_secret_safe_git_url(url: str) -> None: + """Reject remote references that could persist credentials in run identity or errors.""" + + if any(character in url for character in ("\r", "\n", "\x00")): + raise AcquisitionError("git target URL contains an invalid control character") + parsed = urllib.parse.urlsplit(url) + if parsed.scheme.casefold() in {"http", "https"}: + if parsed.username is not None or parsed.password is not None: + raise AcquisitionError("git target URL must not contain embedded credentials") + if parsed.query or parsed.fragment: + raise AcquisitionError("git target URL must not contain query or fragment credentials") + return + if "://" not in url: + authority = url.split(":", 1)[0] + if "@" in authority and authority.split("@", 1)[0] != "git": + raise AcquisitionError( + "git SCP-style target URL must use the non-secret 'git' SSH account" + ) + + def _tree_digest(root: Path) -> str: """Deterministic content digest of a source tree: sha256 over sorted (relpath, sha256(bytes)).""" entries: list[str] = [] @@ -372,6 +393,7 @@ def acquire_target(spec: dict, dest: Path, *, clone_timeout_s: float = 120.0) -> url = str(spec.get("url", "")).strip() if not url: raise AcquisitionError("git target requires 'url'") + _validate_secret_safe_git_url(url) ref = str(spec.get("ref", "")).strip() dest = dest.resolve(strict=False) if dest.exists(): diff --git a/src/echo_certification_forge/models.py b/src/echo_certification_forge/models.py index 74602d6..8983540 100644 --- a/src/echo_certification_forge/models.py +++ b/src/echo_certification_forge/models.py @@ -185,6 +185,8 @@ class VerdictDecision: production_e2e_attestation_id: str | None production_e2e_profile: str | None production_e2e_envelope_sha256: str | None + public_target_identity_sha256: str | None + production_e2e_identity_sha256: str | None signing_key_id: str issued_at: datetime expires_at: datetime @@ -205,6 +207,8 @@ def to_dict(self) -> dict[str, Any]: "production_e2e_attestation_id": self.production_e2e_attestation_id, "production_e2e_profile": self.production_e2e_profile, "production_e2e_envelope_sha256": self.production_e2e_envelope_sha256, + "public_target_identity_sha256": self.public_target_identity_sha256, + "production_e2e_identity_sha256": self.production_e2e_identity_sha256, "signing_key_id": self.signing_key_id, "issued_at": to_utc_iso(self.issued_at), "expires_at": to_utc_iso(self.expires_at), diff --git a/src/echo_certification_forge/production_e2e.py b/src/echo_certification_forge/production_e2e.py index b35e4eb..3eb4ea4 100644 --- a/src/echo_certification_forge/production_e2e.py +++ b/src/echo_certification_forge/production_e2e.py @@ -5,8 +5,10 @@ after a trusted collector has signed the attestation. Target-controlled code cannot manufacture the attestation or select its trust key. """ + from __future__ import annotations +import ipaddress import json import re from collections.abc import Mapping @@ -14,6 +16,7 @@ from datetime import UTC, datetime from pathlib import Path from typing import Any +from urllib.parse import urlsplit, urlunsplit from .canonical import sha256_json from .models import EnvironmentIdentity, SignedVerdictEnvelope, TargetIdentity @@ -22,9 +25,27 @@ RULE_ID = "production_e2e" SCHEMA_VERSION = "certforge.production-e2e.v1" GENERIC_PROFILE = "generic-production-v1" -ECHO_GITHUB_AUTONOMY_PROFILE = "echo-github-autonomy-remote-mcp-v1" +ECHO_GITHUB_AUTONOMY_PROFILE = "echo-github-autonomy-remote-mcp-v2" ECHO_GITHUB_AUTONOMY_REPOSITORY = "echoomegaprime/echo-github-autonomy" ECHO_GITHUB_AUTONOMY_CANONICAL_MCP = "https://echo-ghub.grok.me/api/plugin/mcp" +ECHO_CONTINUITY_PROFILE = "echo-continuity-fabric-remote-mcp-v1" +ECHO_CONTINUITY_REPOSITORY = "echoomegaprime/echo-continuity-fabric" +ECHO_CONTINUITY_CANONICAL_MCP = "https://ecf.echo-op.com/mcp" +_NONPUBLIC_HOST_SUFFIXES = ( + "alt", + "arpa", + "example", + "example.com", + "example.net", + "example.org", + "invalid", + "local", + "localhost", + "onion", + "test", +) +_NAT64_WELL_KNOWN = ipaddress.IPv6Network("64:ff9b::/96") +_NAT64_LOCAL_USE = ipaddress.IPv6Network("64:ff9b:1::/48") BASE_CHECKS = frozenset( { @@ -44,7 +65,7 @@ "tool_schema", "repeated_tool_invocation", "registry_persistence", - "four_account_reconciliation", + "three_account_reconciliation", "private_public_visibility", "read_write_certify", "cross_client_consistency", @@ -53,10 +74,24 @@ ECHO_GITHUB_ACCOUNTS = { "echoomegaprime": 314902331, "ECHO-OMEGA-PRIME": 264607697, - "Bmcbob76": 203470412, "bobmcwilliams4": 235318155, } ECHO_CLIENTS = frozenset({"chatgpt", "claude", "codex", "grok"}) +ECHO_CONTINUITY_CHECKS = BASE_CHECKS | frozenset( + { + "canonical_mcp_health", + "oauth_discovery", + "mcp_initialize", + "tool_schema", + "repeated_tool_invocation", + "registry_persistence", + "ledger_integrity", + "sharing", + "import", + "read_write_continuity", + "cross_client_consistency", + } +) _SHA = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$") _SHA256 = re.compile(r"^[0-9a-f]{64}$") @@ -90,7 +125,9 @@ class VerifiedProductionE2E: def __post_init__(self) -> None: if self._marker is not _VERIFIED_MARKER: - raise ValueError("VerifiedProductionE2E must come from signature verification") + raise ValueError( + "VerifiedProductionE2E must come from signature verification" + ) def result_details(self) -> dict[str, Any]: return { @@ -105,7 +142,7 @@ def _parse_time(value: object) -> datetime | None: if not isinstance(value, str): return None try: - parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + parsed = datetime.fromisoformat(value) except ValueError: return None return parsed.astimezone(UTC) if parsed.tzinfo is not None else None @@ -117,7 +154,8 @@ def _contains_restricted_value(value: Any, *, key_name: str = "") -> bool: return True if isinstance(value, Mapping): return any( - _contains_restricted_value(item, key_name=str(key)) for key, item in value.items() + _contains_restricted_value(item, key_name=str(key)) + for key, item in value.items() ) if isinstance(value, (list, tuple, set)): return any(_contains_restricted_value(item) for item in value) @@ -130,9 +168,96 @@ def _source_repository(target: TargetIdentity) -> str | None: if marker not in reference: return None suffix = reference.split(marker, 1)[1].split("@", 1)[0] - if suffix.endswith(".git"): - suffix = suffix[:-4] - return suffix + return suffix.removesuffix(".git") + + +def canonical_public_https_target(value: object) -> str | None: + """Return a normalized public HTTPS target or fail closed with ``None``.""" + + if not isinstance(value, str) or not value or len(value) > 2048: + return None + if any( + character.isspace() or ord(character) < 0x20 or ord(character) == 0x7F + for character in value + ): + return None + try: + parsed = urlsplit(value) + hostname = parsed.hostname + port = parsed.port + except (ValueError, UnicodeError): + return None + if ( + parsed.scheme != "https" + or not hostname + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or parsed.netloc.endswith(":") + or port == 0 + ): + return None + hostname = hostname.rstrip(".").casefold() + if not hostname: + return None + try: + address = ipaddress.ip_address(hostname) + except ValueError: + try: + ascii_hostname = hostname.encode("idna").decode("ascii") + except UnicodeError: + return None + if any( + ascii_hostname == suffix or ascii_hostname.endswith(f".{suffix}") + for suffix in _NONPUBLIC_HOST_SUFFIXES + ): + return None + labels = ascii_hostname.split(".") + if ( + len(ascii_hostname) > 253 + or len(labels) < 2 + or any(label.startswith("xn--") for label in labels) + or not re.fullmatch(r"[a-z]{2,63}", labels[-1]) + or all(re.fullmatch(r"(?:0x[0-9a-f]*|[0-9]+)", label) for label in labels) + or any( + not re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", label) + for label in labels + ) + ): + return None + normalized_host = ascii_hostname + else: + if isinstance(address, ipaddress.IPv6Address): + if address in _NAT64_LOCAL_USE: + return None + embedded_ipv4: list[ipaddress.IPv4Address] = [] + if address in _NAT64_WELL_KNOWN: + embedded_ipv4.append(ipaddress.IPv4Address(int(address) & 0xFFFFFFFF)) + if address.ipv4_mapped is not None: + embedded_ipv4.append(address.ipv4_mapped) + if address.sixtofour is not None: + embedded_ipv4.append(address.sixtofour) + if address.teredo is not None: + embedded_ipv4.extend(address.teredo) + if any(not item.is_global or item.is_multicast for item in embedded_ipv4): + return None + if ( + not address.is_global + or address.is_multicast + or getattr(address, "is_site_local", False) + or getattr(address, "scope_id", None) is not None + ): + return None + normalized_host = ( + f"[{address.compressed}]" if address.version == 6 else address.compressed + ) + normalized_netloc = normalized_host if port is None else f"{normalized_host}:{port}" + return urlunsplit(("https", normalized_netloc, parsed.path, "", "")) + + +def _safe_canonical_https_target(value: object) -> bool: + return canonical_public_https_target(value) is not None def _validate_common( @@ -184,11 +309,16 @@ def _validate_common( if len(required_checks) != len(set(required_checks)): return "production_e2e_checks_duplicated" check_set = frozenset(required_checks) - if set(checks) != set(check_set) or any(checks.get(name) is not True for name in check_set): + if set(checks) != set(check_set) or any( + checks.get(name) is not True for name in check_set + ): return "production_e2e_checks_incomplete" if not BASE_CHECKS <= check_set: return "production_e2e_baseline_checks_missing" - if not isinstance(payload.get("stability_probe_count"), int) or payload["stability_probe_count"] < 3: + if ( + not isinstance(payload.get("stability_probe_count"), int) + or payload["stability_probe_count"] < 3 + ): return "production_e2e_stability_probe_count_insufficient" return None @@ -202,7 +332,9 @@ def _validate_echo_github_autonomy(payload: Mapping[str, Any]) -> str | None: return "production_e2e_tool_count_mismatch" repositories = payload.get("sample_private_repositories") - if not isinstance(repositories, Mapping) or set(repositories) != set(ECHO_GITHUB_ACCOUNTS): + if not isinstance(repositories, Mapping) or set(repositories) != set( + ECHO_GITHUB_ACCOUNTS + ): return "production_e2e_sample_repositories_incomplete" sample_digests: dict[str, str] = {} for login, sample in repositories.items(): @@ -238,7 +370,10 @@ def _validate_echo_github_autonomy(payload: Mapping[str, Any]) -> str | None: account.get("public_count"), account.get("private_count"), ) - if any(not isinstance(value, int) or isinstance(value, bool) or value < 0 for value in counts): + if any( + not isinstance(value, int) or isinstance(value, bool) or value < 0 + for value in counts + ): return "production_e2e_account_counts_invalid" if counts[0] != counts[1] or counts[2] <= 0 or counts[3] <= 0: return "production_e2e_account_reconciliation_failed" @@ -259,11 +394,37 @@ def _validate_echo_github_autonomy(payload: Mapping[str, Any]) -> str | None: if not isinstance(client, Mapping) or client.get("accepted") is not True: return "production_e2e_client_not_accepted" fingerprints = client.get("repository_fingerprints") - if not isinstance(fingerprints, Mapping) or dict(fingerprints) != sample_digests: + if ( + not isinstance(fingerprints, Mapping) + or dict(fingerprints) != sample_digests + ): return "production_e2e_cross_client_repository_mismatch" return None +def _validate_echo_continuity(payload: Mapping[str, Any]) -> str | None: + if payload.get("canonical_target") != ECHO_CONTINUITY_CANONICAL_MCP: + return "production_e2e_canonical_mcp_mismatch" + if frozenset(payload.get("required_checks") or ()) != ECHO_CONTINUITY_CHECKS: + return "production_e2e_continuity_checks_incomplete" + if payload.get("tool_count") != 26: + return "production_e2e_tool_count_mismatch" + clients = payload.get("clients") + if not isinstance(clients, Mapping) or set(clients) != set(ECHO_CLIENTS): + return "production_e2e_clients_incomplete" + schema_digests: set[str] = set() + for client in clients.values(): + if not isinstance(client, Mapping) or client.get("accepted") is not True: + return "production_e2e_client_not_accepted" + digest = str(client.get("tool_schema_sha256") or "") + if not _SHA256.fullmatch(digest): + return "production_e2e_client_schema_digest_invalid" + schema_digests.add(digest) + if len(schema_digests) != 1: + return "production_e2e_cross_client_schema_mismatch" + return None + + def validate_production_e2e( payload: Mapping[str, Any] | None, target: TargetIdentity, @@ -285,8 +446,15 @@ def validate_production_e2e( return False, "production_e2e_profile_mismatch" error = _validate_echo_github_autonomy(payload) return (error is None, error or "production_e2e_verified") + if repository == ECHO_CONTINUITY_REPOSITORY: + if profile != ECHO_CONTINUITY_PROFILE: + return False, "production_e2e_profile_mismatch" + error = _validate_echo_continuity(payload) + return (error is None, error or "production_e2e_verified") if profile != GENERIC_PROFILE: return False, "production_e2e_profile_mismatch" + if not _safe_canonical_https_target(payload.get("canonical_target")): + return False, "production_e2e_canonical_target_invalid" if frozenset(payload.get("required_checks") or ()) != BASE_CHECKS: return False, "production_e2e_generic_checks_incomplete" return True, "production_e2e_verified" @@ -334,7 +502,9 @@ def verify_signed_attestation( ) -def load_signed_attestation(path: Path, trusted_key_directory: Path) -> VerifiedProductionE2E: +def load_signed_attestation( + path: Path, trusted_key_directory: Path +) -> VerifiedProductionE2E: """Load and independently verify a collector envelope using pinned public keys.""" try: diff --git a/src/echo_certification_forge/public_verification.py b/src/echo_certification_forge/public_verification.py new file mode 100644 index 0000000..235a62d --- /dev/null +++ b/src/echo_certification_forge/public_verification.py @@ -0,0 +1,144 @@ +"""Secret-safe public identity projections committed by signed verdicts.""" + +from __future__ import annotations + +import json +from collections.abc import Mapping +from typing import Any +from urllib.parse import urlsplit + +from .production_e2e import canonical_public_https_target + +_PUBLIC_E2E_FIELDS = frozenset( + { + "schema_version", + "attestation_id", + "profile", + "target_identity_digest", + "environment_identity_digest", + "source_commit", + "deployment_sha", + "canonical_target", + "required_checks", + "checks", + "stability_probe_count", + "tool_count", + "account_count", + "client_count", + "upstream_reconciled", + "private_public_visible", + "read_write_certify", + "registry_persistent", + "oauth_verified", + "ledger_integrity", + "sharing_verified", + "import_verified", + "read_write_continuity", + "observed_at", + "expires_at", + "signing_key_id", + "signature_verified", + "collector_key_id", + "attestation_envelope_sha256", + } +) + + +def _json_copy(value: Any) -> Any: + return json.loads(json.dumps(value, sort_keys=True, separators=(",", ":"))) + + +def _safe_github_repository(canonical_ref: object) -> str | None: + if not isinstance(canonical_ref, str) or len(canonical_ref) > 2048: + return None + try: + parsed = urlsplit(canonical_ref) + port = parsed.port + except (ValueError, UnicodeError): + return None + if ( + parsed.scheme != "https" + or (parsed.hostname or "").casefold() != "github.com" + or port not in (None, 443) + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + ): + return None + repository = parsed.path.strip("/").split("@", 1)[0].removesuffix(".git") + parts = repository.split("/") + if len(parts) != 2 or not all(parts): + return None + return f"{parts[0]}/{parts[1]}" + + +def public_target_identity(target: Mapping[str, Any]) -> dict[str, Any]: + """Return only source facts safe for an unauthenticated public verifier.""" + + projection: dict[str, Any] = { + "target_type": str(target.get("target_type") or "")[:128], + "artifact_sha256": target.get("artifact_sha256"), + "source_commit": target.get("source_commit"), + } + repository = _safe_github_repository(target.get("canonical_ref")) + if repository is not None: + projection["repository"] = repository + return projection + + +def public_production_e2e_identity(details: Mapping[str, Any]) -> dict[str, Any]: + """Expose signed aggregate proof without repository or credential details.""" + + projection = { + key: _json_copy(value) + for key, value in details.items() + if key in _PUBLIC_E2E_FIELDS + } + checks = details.get("checks") + safe_checks = checks if isinstance(checks, Mapping) else {} + accounts = details.get("accounts") + account_values = list(accounts.values()) if isinstance(accounts, Mapping) else [] + clients = details.get("clients") + projection.update( + { + "account_count": len(accounts) if isinstance(accounts, Mapping) else None, + "client_count": len(clients) if isinstance(clients, Mapping) else None, + "upstream_reconciled": bool(account_values) + and all( + isinstance(item, Mapping) + and item.get("enumerated_count") == item.get("upstream_total_count") + for item in account_values + ), + "private_public_visible": bool(account_values) + and all( + isinstance(item, Mapping) + and isinstance(item.get("public_count"), int) + and not isinstance(item.get("public_count"), bool) + and item["public_count"] > 0 + and isinstance(item.get("private_count"), int) + and not isinstance(item.get("private_count"), bool) + and item["private_count"] > 0 + for item in account_values + ), + "read_write_certify": bool(account_values) + and all( + isinstance(item, Mapping) + and all(item.get(name) is True for name in ("read", "write", "certify")) + for item in account_values + ), + "registry_persistent": safe_checks.get("registry_persistence") is True, + "oauth_verified": safe_checks.get("oauth_discovery") is True, + "ledger_integrity": safe_checks.get("ledger_integrity") is True, + "sharing_verified": safe_checks.get("sharing") is True, + "import_verified": safe_checks.get("import") is True, + "read_write_continuity": safe_checks.get("read_write_continuity") is True, + } + ) + canonical_target = projection.get("canonical_target") + normalized_target = canonical_public_https_target(canonical_target) + if normalized_target is None: + projection.pop("canonical_target", None) + else: + projection["canonical_target"] = normalized_target + return projection diff --git a/src/echo_certification_forge/service.py b/src/echo_certification_forge/service.py index 422cdf3..4a3f489 100644 --- a/src/echo_certification_forge/service.py +++ b/src/echo_certification_forge/service.py @@ -38,6 +38,11 @@ from .openapi_auth import install_openapi_auth from .policy import RuleManifest from .product_readiness import verify_product_readiness +from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID +from .public_verification import ( + public_production_e2e_identity, + public_target_identity, +) from .release_hooks import WebhookSecretRegistry from .operational_telemetry import ( OperationalTelemetryError, @@ -1096,12 +1101,26 @@ def publish_verification( def public_verification(verification_id: str) -> dict[str, Any]: try: published = context.subscribers.public_verification(verification_id) + run = context.store.get_run( + published["run_id"], published["organization_id"] + ) row = context.store.latest_signed_verdict( published["run_id"], published["organization_id"] ) if row is None: raise SubscriberError(404, "verification_not_found") payload = json.loads(row["payload_json"]) + private_target_identity = json.loads(str(run["target_identity_json"])) + target_identity = public_target_identity(private_target_identity) + environment_identity = json.loads(str(run["environment_identity_json"])) + production_result = context.store.list_rule_results( + published["run_id"], published["organization_id"] + ).get(PRODUCTION_E2E_RULE_ID) + production_e2e = ( + public_production_e2e_identity(production_result.details) + if production_result is not None and production_result.passed + else {} + ) gate = DeployGate(context.store, context.trusted_keys).evaluate( tenant_id=published["organization_id"], run_id=published["run_id"], @@ -1111,11 +1130,29 @@ def public_verification(verification_id: str) -> dict[str, Any]: evidence_merkle_root=payload["evidence_merkle_root"], signing_key_id=payload["signing_key_id"], ) + identity_reasons: list[str] = [] + if sha256_json(private_target_identity) != payload["target_identity_digest"]: + identity_reasons.append("private_target_identity_serialization_mismatch") + if sha256_json(target_identity) != payload.get("public_target_identity_sha256"): + identity_reasons.append("public_target_identity_digest_mismatch") + if sha256_json(environment_identity) != payload["environment_identity_digest"]: + identity_reasons.append("environment_identity_serialization_mismatch") + if sha256_json(production_e2e) != payload.get( + "production_e2e_identity_sha256" + ): + identity_reasons.append("production_e2e_identity_digest_mismatch") + reasons = sorted(set(gate.reasons).union(identity_reasons)) return { "verification_id": verification_id, - "valid": gate.allowed, - "reasons": list(gate.reasons), + "valid": gate.allowed and not identity_reasons, + "reasons": reasons, "payload": payload, + # These identities are safe public verification material. They are not + # trusted merely because they are returned here: their canonical SHA-256 + # digests are committed inside the independently signed verdict above. + "target_identity": target_identity, + "environment_identity": environment_identity, + "production_e2e": production_e2e, "signature_b64": row["signature_b64"], "key_id": row["key_id"], "public_key_pem": row["public_key_pem"], diff --git a/src/echo_certification_forge/verdict.py b/src/echo_certification_forge/verdict.py index 82c1e12..e5464c3 100644 --- a/src/echo_certification_forge/verdict.py +++ b/src/echo_certification_forge/verdict.py @@ -12,6 +12,10 @@ from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID from .production_e2e import validate_attestation_trust_metadata from .production_e2e import validate_production_e2e +from .public_verification import ( + public_production_e2e_identity, + public_target_identity, +) class DeterministicVerdictEngine: @@ -151,6 +155,16 @@ def evaluate( if verified_e2e is not None else None ), + public_target_identity_sha256=( + sha256_json(public_target_identity(target_data)) + if target is not None + else None + ), + production_e2e_identity_sha256=( + sha256_json(public_production_e2e_identity(verified_e2e)) + if verified_e2e is not None + else None + ), signing_key_id=signing_key_id, issued_at=issued_at, expires_at=issued_at + timedelta(seconds=manifest.verdict_ttl_seconds), diff --git a/tests/production_e2e_support.py b/tests/production_e2e_support.py index 8c3d2c7..c4f20ae 100644 --- a/tests/production_e2e_support.py +++ b/tests/production_e2e_support.py @@ -13,7 +13,10 @@ VerifiedProductionE2E, verify_signed_attestation, ) -from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from echo_certification_forge.signing import ( + Ed25519VerdictSigner, + TrustedPublicKeyRegistry, +) def trusted_generic_production_e2e( @@ -32,7 +35,7 @@ def trusted_generic_production_e2e( "environment_identity_digest": environment.identity_digest, "source_commit": target.source_commit, "deployment_sha": target.source_commit, - "canonical_target": target.canonical_ref, + "canonical_target": f"https://api.github.com/certforge/{target.identity_digest}", "required_checks": sorted(BASE_CHECKS), "checks": {name: True for name in sorted(BASE_CHECKS)}, "stability_probe_count": 3, diff --git a/tests/test_evidence_and_verdict.py b/tests/test_evidence_and_verdict.py index c616c7c..17eb044 100644 --- a/tests/test_evidence_and_verdict.py +++ b/tests/test_evidence_and_verdict.py @@ -18,18 +18,29 @@ TargetIdentity, VerdictLifecycleEvent, ) -from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from echo_certification_forge.signing import ( + Ed25519VerdictSigner, + TrustedPublicKeyRegistry, +) from echo_certification_forge.canonical import to_utc_iso, utc_now -from echo_certification_forge.production_e2e import BASE_CHECKS, GENERIC_PROFILE, SCHEMA_VERSION +from echo_certification_forge.production_e2e import ( + BASE_CHECKS, + GENERIC_PROFILE, + SCHEMA_VERSION, +) from echo_certification_forge.verdict import DeterministicVerdictEngine def register(store, manifest, target, environment, run_id="cert-001"): - store.register_run(run_id, target, environment, manifest.manifest_id, manifest.digest) + store.register_run( + run_id, target, environment, manifest.manifest_id, manifest.digest + ) return run_id -def satisfy_all_rules(store, manifest, run_id, tenant_id, *, include_production_e2e=True): +def satisfy_all_rules( + store, manifest, run_id, tenant_id, *, include_production_e2e=True +): run = store.get_run(run_id, tenant_id) target = TargetIdentity(**json.loads(run["target_identity_json"])) environment = EnvironmentIdentity(**json.loads(run["environment_identity_json"])) @@ -42,7 +53,7 @@ def satisfy_all_rules(store, manifest, run_id, tenant_id, *, include_production_ "environment_identity_digest": environment.identity_digest, "source_commit": target.source_commit, "deployment_sha": target.source_commit, - "canonical_target": "https://example.test/runtime", + "canonical_target": "https://api.github.com/runtime", "required_checks": sorted(BASE_CHECKS), "checks": {name: True for name in sorted(BASE_CHECKS)}, "stability_probe_count": 3, @@ -73,7 +84,9 @@ def satisfy_all_rules(store, manifest, run_id, tenant_id, *, include_production_ rule.id, True, (artifact_id,), - production_e2e if rule.id == "production_e2e" else {"source": "acceptance"}, + production_e2e + if rule.id == "production_e2e" + else {"source": "acceptance"}, ), ) @@ -87,10 +100,14 @@ def test_run_defaults_to_not_ready(store, manifest, target, environment): assert decision.run_outcome is RunOutcome.INCONCLUSIVE assert decision.release_verdict is ReleaseVerdict.NOT_READY assert "evidence_integrity_failed" in decision.reasons - assert any(reason.startswith("mandatory_rule_missing:") for reason in decision.reasons) + assert any( + reason.startswith("mandatory_rule_missing:") for reason in decision.reasons + ) -def test_complete_run_with_verified_evidence_gets_signed_ready(store, manifest, target, environment): +def test_complete_run_with_verified_evidence_gets_signed_ready( + store, manifest, target, environment +): run_id = register(store, manifest, target, environment) satisfy_all_rules(store, manifest, run_id, target.tenant_id) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) @@ -177,7 +194,9 @@ def test_failed_executor_rule_preserves_missing_attestation_reason( assert "production_e2e_schema_invalid" not in decision.reasons -def test_bare_database_rows_cannot_manufacture_ready(store, manifest, target, environment): +def test_bare_database_rows_cannot_manufacture_ready( + store, manifest, target, environment +): run_id = register(store, manifest, target, environment) now = "2026-07-16T00:00:00Z" with closing(sqlite3.connect(store.db_path)) as connection: @@ -199,15 +218,27 @@ def test_bare_database_rows_cannot_manufacture_ready(store, manifest, target, en } from echo_certification_forge.canonical import sha256_json from echo_certification_forge.evidence import _ZERO_HASH + record_hash = sha256_json(descriptor) previous = _ZERO_HASH if index == 1 else "1" * 64 chain_hash = "1" * 64 connection.execute( "INSERT INTO evidence_artifacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( - artifact_id, run_id, target.tenant_id, descriptor["relative_path"], "0" * 64, 1, - "application/json", "forged-row", "COMPLETE", index, now, - record_hash, previous, chain_hash, + artifact_id, + run_id, + target.tenant_id, + descriptor["relative_path"], + "0" * 64, + 1, + "application/json", + "forged-row", + "COMPLETE", + index, + now, + record_hash, + previous, + chain_hash, ), ) connection.execute( @@ -223,7 +254,9 @@ def test_bare_database_rows_cannot_manufacture_ready(store, manifest, target, en ) assert decision.release_verdict is ReleaseVerdict.NOT_READY assert "evidence_integrity_failed" in decision.reasons - assert any(reason.startswith("invalid_rule_evidence:") for reason in decision.reasons) + assert any( + reason.startswith("invalid_rule_evidence:") for reason in decision.reasons + ) def test_altered_artifact_forces_not_ready(store, manifest, target, environment): @@ -232,7 +265,13 @@ def test_altered_artifact_forces_not_ready(store, manifest, target, environment) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) row = store.verify_evidence(run_id, target.tenant_id) assert row.valid - artifact = store.evidence_root / target.tenant_id / run_id / "artifacts" / "evidence-01.bin" + artifact = ( + store.evidence_root + / target.tenant_id + / run_id + / "artifacts" + / "evidence-01.bin" + ) artifact.write_bytes(b"tampered") signer = Ed25519VerdictSigner.generate() decision = DeterministicVerdictEngine().evaluate( @@ -246,7 +285,13 @@ def test_missing_artifact_forces_not_ready(store, manifest, target, environment) run_id = register(store, manifest, target, environment) satisfy_all_rules(store, manifest, run_id, target.tenant_id) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) - artifact = store.evidence_root / target.tenant_id / run_id / "artifacts" / "evidence-02.bin" + artifact = ( + store.evidence_root + / target.tenant_id + / run_id + / "artifacts" + / "evidence-02.bin" + ) artifact.unlink() signer = Ed25519VerdictSigner.generate() decision = DeterministicVerdictEngine().evaluate( @@ -260,7 +305,9 @@ def test_digest_mismatch_blocks_deployment(store, manifest, target, environment) satisfy_all_rules(store, manifest, run_id, target.tenant_id) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) signer = Ed25519VerdictSigner.generate() - decision = DeterministicVerdictEngine().evaluate(store, run_id, target.tenant_id, manifest, signer.key_id) + decision = DeterministicVerdictEngine().evaluate( + store, run_id, target.tenant_id, manifest, signer.key_id + ) envelope = signer.sign(decision) store.save_signed_verdict(run_id, target.tenant_id, envelope) trusted = TrustedPublicKeyRegistry.empty() @@ -272,34 +319,54 @@ def test_digest_mismatch_blocks_deployment(store, manifest, target, environment) assert "target_identity_mismatch" in gate.reasons -def test_untrusted_self_signed_verdict_is_rejected(store, manifest, target, environment): +def test_untrusted_self_signed_verdict_is_rejected( + store, manifest, target, environment +): run_id = register(store, manifest, target, environment) satisfy_all_rules(store, manifest, run_id, target.tenant_id) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) attacker = Ed25519VerdictSigner.generate() - decision = DeterministicVerdictEngine().evaluate(store, run_id, target.tenant_id, manifest, attacker.key_id) + decision = DeterministicVerdictEngine().evaluate( + store, run_id, target.tenant_id, manifest, attacker.key_id + ) store.save_signed_verdict(run_id, target.tenant_id, attacker.sign(decision)) gate = DeployGate(store, TrustedPublicKeyRegistry.empty()).evaluate( - target.tenant_id, run_id, target.identity_digest, environment.identity_digest, manifest.digest + target.tenant_id, + run_id, + target.identity_digest, + environment.identity_digest, + manifest.digest, ) assert not gate.allowed assert "untrusted_signing_key" in gate.reasons -def test_revocation_blocks_previously_valid_verdict(store, manifest, target, environment): +def test_revocation_blocks_previously_valid_verdict( + store, manifest, target, environment +): run_id = register(store, manifest, target, environment) satisfy_all_rules(store, manifest, run_id, target.tenant_id) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) signer = Ed25519VerdictSigner.generate() - decision = DeterministicVerdictEngine().evaluate(store, run_id, target.tenant_id, manifest, signer.key_id) + decision = DeterministicVerdictEngine().evaluate( + store, run_id, target.tenant_id, manifest, signer.key_id + ) store.save_signed_verdict(run_id, target.tenant_id, signer.sign(decision)) trusted = TrustedPublicKeyRegistry.empty() trusted.add_pem(signer.public_key_pem) store.append_lifecycle_event( - run_id, target.tenant_id, VerdictLifecycleEvent.REVOKED, "release-authority", "security advisory" + run_id, + target.tenant_id, + VerdictLifecycleEvent.REVOKED, + "release-authority", + "security advisory", ) gate = DeployGate(store, trusted).evaluate( - target.tenant_id, run_id, target.identity_digest, environment.identity_digest, manifest.digest + target.tenant_id, + run_id, + target.identity_digest, + environment.identity_digest, + manifest.digest, ) assert not gate.allowed assert "verdict_revoked" in gate.reasons @@ -308,7 +375,12 @@ def test_revocation_blocks_previously_valid_verdict(store, manifest, target, env def test_evidence_rows_are_append_only(store, manifest, target, environment): run_id = register(store, manifest, target, environment) store.append_artifact( - run_id, target.tenant_id, "immutable-evidence", b"proof", "text/plain", "unit-test" + run_id, + target.tenant_id, + "immutable-evidence", + b"proof", + "text/plain", + "unit-test", ) with closing(sqlite3.connect(store.db_path)) as connection: with pytest.raises(sqlite3.IntegrityError, match="append-only"): diff --git a/tests/test_p7_subscriber_governance.py b/tests/test_p7_subscriber_governance.py index a112bba..fef5b2a 100644 --- a/tests/test_p7_subscriber_governance.py +++ b/tests/test_p7_subscriber_governance.py @@ -20,7 +20,7 @@ from fastapi.testclient import TestClient from echo_certification_forge.acquisition import AcquiredTarget, acquire_target -from echo_certification_forge.canonical import parse_utc_iso +from echo_certification_forge.canonical import parse_utc_iso, sha256_json from echo_certification_forge.dispatch_worker import dispatch_once from echo_certification_forge.evidence import EvidenceStore from echo_certification_forge.intake import SubmitRequest @@ -3603,6 +3603,79 @@ def test_legal_hold_lifecycle_and_public_verification_use_hardened_subscriber_au public = client.get(published.json()["verification_url"]) assert public.status_code == 200 and public.json()["valid"] is True assert public.json()["payload"]["run_id"] == run_id + assert str(source) not in json.dumps(public.json()) + assert "canonical_ref" not in public.json()["target_identity"] + stored_target = json.loads( + store.get_run(run_id, owner.organization_id)["target_identity_json"] + ) + assert public.json()["target_identity"]["source_commit"] == stored_target["source_commit"] + assert sha256_json(public.json()["target_identity"]) == public.json()["payload"][ + "public_target_identity_sha256" + ] + assert sha256_json(public.json()["environment_identity"]) == public.json()["payload"][ + "environment_identity_digest" + ] + assert sha256_json(public.json()["production_e2e"]) == public.json()["payload"][ + "production_e2e_identity_sha256" + ] + public_e2e_text = json.dumps(public.json()["production_e2e"], sort_keys=True) + for private_field in ( + "accounts", + "sample_private_repositories", + "clients", + "credential_source", + "repository_fingerprints", + "tool_schema_sha256", + ): + assert private_field not in public_e2e_text + + original_target_json = store.get_run(run_id, owner.organization_id)[ + "target_identity_json" + ] + with store._connection() as connection: + connection.execute( + "UPDATE runs SET target_identity_json = ? WHERE run_id = ? AND tenant_id = ?", + ( + json.dumps(stored_target | {"source_commit": "0" * 40}, sort_keys=True), + run_id, + owner.organization_id, + ), + ) + tampered = client.get(published.json()["verification_url"]) + assert tampered.status_code == 200 and tampered.json()["valid"] is False + assert "public_target_identity_digest_mismatch" in tampered.json()["reasons"] + with store._connection() as connection: + connection.execute( + "UPDATE runs SET target_identity_json = ? WHERE run_id = ? AND tenant_id = ?", + (original_target_json, run_id, owner.organization_id), + ) + + original_environment_json = store.get_run(run_id, owner.organization_id)[ + "environment_identity_json" + ] + altered_environment = json.loads(original_environment_json) + altered_environment["runner_image_sha256"] = "0" * 64 + with store._connection() as connection: + connection.execute( + "UPDATE runs SET environment_identity_json = ? WHERE run_id = ? AND tenant_id = ?", + ( + json.dumps(altered_environment, sort_keys=True), + run_id, + owner.organization_id, + ), + ) + tampered_environment = client.get(published.json()["verification_url"]) + assert tampered_environment.status_code == 200 + assert tampered_environment.json()["valid"] is False + assert ( + "environment_identity_serialization_mismatch" + in tampered_environment.json()["reasons"] + ) + with store._connection() as connection: + connection.execute( + "UPDATE runs SET environment_identity_json = ? WHERE run_id = ? AND tenant_id = ?", + (original_environment_json, run_id, owner.organization_id), + ) lifecycle = client.post( f"/v1/subscriber/certifications/{run_id}/lifecycle", diff --git a/tests/test_production_e2e.py b/tests/test_production_e2e.py index 81460f5..b173974 100644 --- a/tests/test_production_e2e.py +++ b/tests/test_production_e2e.py @@ -7,12 +7,18 @@ from echo_certification_forge.models import EnvironmentIdentity, TargetIdentity from echo_certification_forge.production_e2e import ( + BASE_CHECKS, ECHO_CLIENTS, + ECHO_CONTINUITY_CANONICAL_MCP, + ECHO_CONTINUITY_CHECKS, + ECHO_CONTINUITY_PROFILE, ECHO_GITHUB_ACCOUNTS, ECHO_GITHUB_AUTONOMY_CANONICAL_MCP, ECHO_GITHUB_AUTONOMY_CHECKS, ECHO_GITHUB_AUTONOMY_PROFILE, + GENERIC_PROFILE, SCHEMA_VERSION, + canonical_public_https_target, load_signed_attestation, validate_production_e2e, ) @@ -40,7 +46,9 @@ def _environment() -> EnvironmentIdentity: return EnvironmentIdentity( **{ name: str(index) * 64 - for index, name in enumerate(EnvironmentIdentity.__dataclass_fields__, start=1) + for index, name in enumerate( + EnvironmentIdentity.__dataclass_fields__, start=1 + ) } ) @@ -69,13 +77,13 @@ def _payload(*, signing_key_id: str = "ed25519:" + "d" * 32) -> dict: "write": True, "certify": True, "credential_source": ( - "github_app_installation" - if index == 1 - else "vault_user_token_fallback" + "github_app_installation" if index == 1 else "vault_user_token_fallback" ), "secret_exposed": False, } - for index, (login, account_id) in enumerate(ECHO_GITHUB_ACCOUNTS.items(), start=1) + for index, (login, account_id) in enumerate( + ECHO_GITHUB_ACCOUNTS.items(), start=1 + ) } fingerprints = { login: sample["fingerprint_sha256"] for login, sample in samples.items() @@ -115,10 +123,18 @@ def test_echo_github_autonomy_requires_complete_exact_cross_client_e2e() -> None @pytest.mark.parametrize( ("mutation", "reason"), [ - (lambda value: value.__setitem__("deployment_sha", "6" * 40), "production_e2e_deployment_sha_mismatch"), - (lambda value: value.__setitem__("tool_count", 27), "production_e2e_tool_count_mismatch"), ( - lambda value: value["accounts"]["Bmcbob76"].__setitem__("private_count", 0), + lambda value: value.__setitem__("deployment_sha", "6" * 40), + "production_e2e_deployment_sha_mismatch", + ), + ( + lambda value: value.__setitem__("tool_count", 27), + "production_e2e_tool_count_mismatch", + ), + ( + lambda value: value["accounts"]["bobmcwilliams4"].__setitem__( + "private_count", 0 + ), "production_e2e_account_reconciliation_failed", ), ( @@ -126,21 +142,21 @@ def test_echo_github_autonomy_requires_complete_exact_cross_client_e2e() -> None "production_e2e_client_not_accepted", ), ( - lambda value: value["accounts"]["Bmcbob76"].__setitem__( + lambda value: value["accounts"]["bobmcwilliams4"].__setitem__( "credential_source", "model_config_pat" ), "production_e2e_credential_source_invalid", ), ( - lambda value: value["accounts"]["Bmcbob76"].__setitem__( + lambda value: value["accounts"]["bobmcwilliams4"].__setitem__( "secret_exposed", True ), "production_e2e_secret_boundary_failed", ), ( - lambda value: value["sample_private_repositories"]["Bmcbob76"].__setitem__( - "repository_id", True - ), + lambda value: value["sample_private_repositories"][ + "bobmcwilliams4" + ].__setitem__("repository_id", True), "production_e2e_sample_repository_id_invalid", ), ], @@ -163,6 +179,161 @@ def test_stale_e2e_attestation_fails_closed() -> None: assert reason == "production_e2e_attestation_not_current" +def _generic_target() -> TargetIdentity: + return TargetIdentity( + tenant_id="generic-service", + target_type="git", + canonical_ref="https://github.com/example/generic-service.git@" + SOURCE_SHA, + artifact_sha256="a" * 64, + source_commit=SOURCE_SHA, + dependency_sha256="b" * 64, + configuration_sha256="c" * 64, + ) + + +def _generic_payload(canonical_target: object) -> dict: + target = _generic_target() + environment = _environment() + return { + "schema_version": SCHEMA_VERSION, + "attestation_id": "generic-service-live-001", + "profile": GENERIC_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": SOURCE_SHA, + "deployment_sha": SOURCE_SHA, + "canonical_target": canonical_target, + "required_checks": sorted(BASE_CHECKS), + "checks": {name: True for name in sorted(BASE_CHECKS)}, + "stability_probe_count": 3, + "observed_at": NOW.isoformat(), + "expires_at": (NOW + timedelta(minutes=30)).isoformat(), + "signing_key_id": "ed25519:" + "d" * 32, + } + + +@pytest.mark.parametrize( + "canonical_target", + [ + None, + "", + "http://api.github.com/runtime", + "https://operator@api.github.com/runtime", + "https://api.github.com/runtime?token=value", + "https://api.github.com/runtime#private", + "https://[::1", + "https://api.github.com:bad/runtime", + "https://api.github.com:99999/runtime", + "https://api.github.com:0/runtime", + "https://api.github.com:00/runtime", + "https://[2606:4700:4700::1111]:0/runtime", + "https://a b.github.com/runtime", + "https://api.github.com/line\nbreak", + "https://localhost/runtime", + "https://api.localhost/runtime", + "https://service.local/runtime", + "https://service.home.arpa/runtime", + "https://service.invalid/runtime", + "https://service.test/runtime", + "https://service.example/runtime", + "https://service.example.com/runtime", + "https://service.example.net/runtime", + "https://service.example.org/runtime", + "https://service.onion/runtime", + "https://service.alt/runtime", + "https://resolver.arpa/runtime", + "https://service.arpa/runtime", + "https://api。localhost/runtime", + "https://service.onion/runtime", + "https://service。alt/runtime", + "https://service。example。com/runtime", + "https://resolver。arpa/runtime", + "https://service.example.com/runtime", + "https://service.onion/runtime", + "https://service.alt/runtime", + "https://service.arpa/runtime", + "https://127.0.0.1/runtime", + "https://10.0.0.1/runtime", + "https://169.254.1.1/runtime", + "https://100.64.0.1/runtime", + "https://100.127.255.254/runtime", + "https://0x7f.0.0.1/runtime", + "https://0x7f.1/runtime", + "https://127.0.0x0.1/runtime", + "https://0x7f.0x0.0x0.0x1/runtime", + "https://0177.0.0.1/runtime", + "https://0x.0.0.1/runtime", + "https://0x0.0x.0.1/runtime", + "https://0x7f.0x.0.1/runtime", + "https://xn--a.com/runtime", + "https://xn--0.com/runtime", + "https://xn--abc.com/runtime", + "https://xn--123.com/runtime", + "https://xn--a-ecp.com/runtime", + "https://foo.0x7f/runtime", + "https://foo.127/runtime", + "https://foo.0x/runtime", + "https://[2606:4700:4700::1111%25eth0]/runtime", + "https://[2606:4700:4700::1111%eth0]/runtime", + "https://[fec0::1]/runtime", + "https://[fedf:ffff:ffff:ffff:ffff:ffff:ffff:ffff]/runtime", + "https://[64:ff9b::7f00:1]/runtime", + "https://[64:ff9b::a00:1]/runtime", + "https://[64:ff9b:1::808:808]/runtime", + "https://0.0.0.0/runtime", + "https://224.0.0.1/runtime", + "https://" + "a" * 2048, + ], +) +def test_generic_profile_requires_safe_canonical_https_target( + canonical_target: object, +) -> None: + valid, reason = validate_production_e2e( + _generic_payload(canonical_target), + _generic_target(), + _environment(), + now=NOW + timedelta(minutes=1), + ) + + assert not valid + assert reason == "production_e2e_canonical_target_invalid" + + +def test_generic_profile_accepts_safe_canonical_https_target() -> None: + valid, reason = validate_production_e2e( + _generic_payload("https://api.github.com/runtime"), + _generic_target(), + _environment(), + now=NOW + timedelta(minutes=1), + ) + + assert valid, reason + + +def test_generic_profile_accepts_normalizable_public_https_target() -> None: + valid, reason = validate_production_e2e( + _generic_payload("https://API.GITHUB.COM:8443/runtime"), + _generic_target(), + _environment(), + now=NOW + timedelta(minutes=1), + ) + + assert valid, reason + + +def test_public_nat64_literal_requires_a_public_embedded_ipv4_address() -> None: + target = "https://[64:ff9b::808:808]/runtime" + assert canonical_public_https_target(target) == target + valid, reason = validate_production_e2e( + _generic_payload(target), + _generic_target(), + _environment(), + now=NOW + timedelta(minutes=1), + ) + + assert valid, reason + + def test_attestation_loader_requires_a_pinned_collector_key(tmp_path) -> None: signer = Ed25519VerdictSigner.generate() payload = _payload(signing_key_id=signer.key_id) @@ -209,6 +380,55 @@ def test_self_selected_public_key_cannot_replace_the_pinned_collector(tmp_path) ) trusted = tmp_path / "trusted" trusted.mkdir() - (trusted / "collector.pem").write_text(trusted_signer.public_key_pem, encoding="ascii") + (trusted / "collector.pem").write_text( + trusted_signer.public_key_pem, encoding="ascii" + ) with pytest.raises(ValueError, match="untrusted_signing_key"): load_signed_attestation(envelope_path, trusted) + + +def test_echo_continuity_requires_exact_profile_and_cross_client_schema() -> None: + target = TargetIdentity( + tenant_id="echo-continuity", + target_type="git", + canonical_ref=( + "https://github.com/echoomegaprime/echo-continuity-fabric.git@" + SOURCE_SHA + ), + artifact_sha256="a" * 64, + source_commit=SOURCE_SHA, + dependency_sha256="b" * 64, + configuration_sha256="c" * 64, + ) + environment = _environment() + schema_digest = "f" * 64 + payload = { + "schema_version": SCHEMA_VERSION, + "attestation_id": "echo-continuity-live-001", + "profile": ECHO_CONTINUITY_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": SOURCE_SHA, + "deployment_sha": SOURCE_SHA, + "canonical_target": ECHO_CONTINUITY_CANONICAL_MCP, + "required_checks": sorted(ECHO_CONTINUITY_CHECKS), + "checks": {name: True for name in sorted(ECHO_CONTINUITY_CHECKS)}, + "stability_probe_count": 3, + "tool_count": 26, + "clients": { + name: {"accepted": True, "tool_schema_sha256": schema_digest} + for name in sorted(ECHO_CLIENTS) + }, + "observed_at": NOW.isoformat(), + "expires_at": (NOW + timedelta(minutes=30)).isoformat(), + "signing_key_id": "ed25519:" + "d" * 32, + } + valid, reason = validate_production_e2e( + payload, target, environment, now=NOW + timedelta(minutes=1) + ) + assert valid, reason + payload["clients"]["grok"]["tool_schema_sha256"] = "e" * 64 + valid, reason = validate_production_e2e( + payload, target, environment, now=NOW + timedelta(minutes=1) + ) + assert not valid + assert reason == "production_e2e_cross_client_schema_mismatch" diff --git a/tests/test_public_verification.py b/tests/test_public_verification.py new file mode 100644 index 0000000..ecdb8fe --- /dev/null +++ b/tests/test_public_verification.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +from echo_certification_forge.public_verification import ( + public_production_e2e_identity, + public_target_identity, +) + + +def test_public_target_projection_never_returns_local_path_or_raw_remote() -> None: + local = public_target_identity( + { + "target_type": "local", + "canonical_ref": "C:/private/operator/worktree", + "artifact_sha256": "a" * 64, + "source_commit": None, + } + ) + credential_remote = public_target_identity( + { + "target_type": "git", + "canonical_ref": "https://token@github.com/example/project.git", + "artifact_sha256": "b" * 64, + "source_commit": "c" * 40, + } + ) + safe_remote = public_target_identity( + { + "target_type": "git", + "canonical_ref": "https://github.com/example/project.git@" + "c" * 40, + "artifact_sha256": "b" * 64, + "source_commit": "c" * 40, + } + ) + + assert "canonical_ref" not in local + assert "repository" not in local + assert "canonical_ref" not in credential_remote + assert "repository" not in credential_remote + assert safe_remote["repository"] == "example/project" + + +def test_public_e2e_projection_drops_credential_like_canonical_target() -> None: + details = { + "schema_version": "certforge.production-e2e.v1", + "profile": "generic-production-v1", + "canonical_target": "https://api.github.com/mcp?access_token=secret", + "checks": {"runtime_or_artifact_executed": True}, + "private_debug_path": "C:/private/evidence", + } + + projection = public_production_e2e_identity(details) + + assert "canonical_target" not in projection + assert "private_debug_path" not in projection + + +def test_public_e2e_projection_drops_malformed_or_nonpublic_targets() -> None: + for canonical_target in ( + "https://[::1", + "https://api.github.com:bad/runtime", + "https://api.github.com:99999/runtime", + "https://api.github.com:0/runtime", + "https://api.github.com:00/runtime", + "https://[2606:4700:4700::1111]:0/runtime", + "https://a b.github.com/runtime", + "https://localhost/runtime", + "https://service.local/runtime", + "https://service.home.arpa/runtime", + "https://service.invalid/runtime", + "https://service.test/runtime", + "https://service.example/runtime", + "https://service.example.com/runtime", + "https://service.example.net/runtime", + "https://service.example.org/runtime", + "https://service.onion/runtime", + "https://service.alt/runtime", + "https://resolver.arpa/runtime", + "https://service.arpa/runtime", + "https://api。localhost/runtime", + "https://service.onion/runtime", + "https://service。alt/runtime", + "https://service。example。com/runtime", + "https://resolver。arpa/runtime", + "https://service.example.com/runtime", + "https://service.onion/runtime", + "https://service.alt/runtime", + "https://service.arpa/runtime", + "https://127.0.0.1/runtime", + "https://10.0.0.1/runtime", + "https://169.254.1.1/runtime", + "https://100.64.0.1/runtime", + "https://100.127.255.254/runtime", + "https://0x7f.0.0.1/runtime", + "https://0x7f.1/runtime", + "https://127.0.0x0.1/runtime", + "https://0x7f.0x0.0x0.0x1/runtime", + "https://0177.0.0.1/runtime", + "https://0x.0.0.1/runtime", + "https://0x0.0x.0.1/runtime", + "https://0x7f.0x.0.1/runtime", + "https://xn--a.com/runtime", + "https://xn--0.com/runtime", + "https://xn--abc.com/runtime", + "https://xn--123.com/runtime", + "https://xn--a-ecp.com/runtime", + "https://foo.0x7f/runtime", + "https://foo.127/runtime", + "https://foo.0x/runtime", + "https://[2606:4700:4700::1111%25eth0]/runtime", + "https://[2606:4700:4700::1111%eth0]/runtime", + "https://[fec0::1]/runtime", + "https://[fedf:ffff:ffff:ffff:ffff:ffff:ffff:ffff]/runtime", + "https://[64:ff9b::7f00:1]/runtime", + "https://[64:ff9b::a00:1]/runtime", + "https://[64:ff9b:1::808:808]/runtime", + ): + projection = public_production_e2e_identity( + { + "profile": "generic-production-v1", + "canonical_target": canonical_target, + } + ) + assert "canonical_target" not in projection + + +def test_public_e2e_projection_normalizes_safe_public_target() -> None: + projection = public_production_e2e_identity( + { + "profile": "generic-production-v1", + "canonical_target": "https://API.GITHUB.COM.:8443/runtime", + } + ) + + assert projection["canonical_target"] == "https://api.github.com:8443/runtime" + + +def test_public_e2e_projection_preserves_public_nat64_literal() -> None: + target = "https://[64:ff9b::808:808]/runtime" + projection = public_production_e2e_identity( + {"profile": "generic-production-v1", "canonical_target": target} + ) + + assert projection["canonical_target"] == target + + +def test_public_e2e_projection_exposes_only_signed_aggregates() -> None: + details = { + "schema_version": "certforge.production-e2e.v1", + "profile": "echo-github-autonomy-remote-mcp-v2", + "checks": { + "registry_persistence": True, + "oauth_discovery": True, + }, + "tool_count": 30, + "accounts": { + "private-login": { + "enumerated_count": 2, + "upstream_total_count": 2, + "public_count": 1, + "private_count": 1, + "read": True, + "write": True, + "certify": True, + "credential_source": "vault_user_token_fallback", + "notes": "customer-private-repository", + } + }, + "sample_private_repositories": { + "private-login": { + "repository_id": 123, + "node_id": "R_private", + "default_branch": "main", + "head_sha": "a" * 40, + } + }, + "clients": { + "chatgpt": { + "accepted": True, + "repository_fingerprints": {"private-login": "b" * 64}, + "notes": "internal-client-detail", + } + }, + } + + projection = public_production_e2e_identity(details) + serialized = str(projection) + + assert projection["account_count"] == 1 + assert projection["client_count"] == 1 + assert projection["upstream_reconciled"] is True + assert projection["private_public_visible"] is True + assert projection["read_write_certify"] is True + assert projection["registry_persistent"] is True + assert projection["oauth_verified"] is True + assert "accounts" not in projection + assert "sample_private_repositories" not in projection + assert "clients" not in projection + for private_value in ( + "private-login", + "customer-private-repository", + "R_private", + "internal-client-detail", + "vault_user_token_fallback", + ): + assert private_value not in serialized diff --git a/tests/test_t4_live_run.py b/tests/test_t4_live_run.py index 02d6784..2297154 100644 --- a/tests/test_t4_live_run.py +++ b/tests/test_t4_live_run.py @@ -48,6 +48,31 @@ def test_acquire_rejects_unknown_type_and_bad_git(tmp_path): tmp_path / "d", clone_timeout_s=15.0) +@pytest.mark.parametrize( + "url", + [ + "https://user:password@github.com/example/project.git", + "https://token@github.com/example/project.git", + "https://github.com/example/project.git?access_token=secret", + "token@github.com:example/project.git", + ], +) +def test_acquire_git_rejects_credential_bearing_urls_before_execution( + tmp_path, monkeypatch, url +): + called = False + + def unexpected_run(*_args, **_kwargs): + nonlocal called + called = True + raise AssertionError("git must not execute for a credential-bearing URL") + + monkeypatch.setattr("echo_certification_forge.acquisition.subprocess.run", unexpected_run) + with pytest.raises(AcquisitionError, match="credentials|non-secret"): + acquire_target({"type": "git", "url": url}, tmp_path / "credential-target") + assert called is False + + def test_acquire_git_exact_commit_fetches_and_verifies_sha(tmp_path, monkeypatch): """A commit SHA is fetched as an object, never misused as a clone --branch value.""" source_commit = "a" * 40