445 lines
18 KiB
Python
445 lines
18 KiB
Python
"""Governed release evidence producer contract tests (V2R2C2 P0).
|
|
|
|
Proves that ``governed.release.evidence.produce`` (the producer half) writes a
|
|
self-consistent evidence closure from real build artifacts, and that the closure
|
|
can be re-validated and projected byte-for-byte by ``TrustedBuilderEvidenceMaterializer``
|
|
(the projection half). Also proves fail-closed behavior on tampered/inconsistent
|
|
input and idempotency/conflict handling, plus operation-registry registration.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
import sys
|
|
|
|
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
|
|
|
from aios_trusted_candidate_builder import ( # noqa: E402
|
|
load_builder_policy,
|
|
TrustedCandidateBuilderConfig,
|
|
)
|
|
from governed_release_evidence_producer import ( # noqa: E402
|
|
EvidenceProduceError,
|
|
GovernedBuildArtifacts,
|
|
producer_operation_definition,
|
|
producer_registry,
|
|
produce_evidence_closure,
|
|
)
|
|
from release_evidence_materializer import ( # noqa: E402
|
|
TrustedBuilderEvidenceMaterializer,
|
|
)
|
|
|
|
|
|
POLICY_PATH = ROOT / "config" / "gitops" / "trusted-candidate-builder-policy.json"
|
|
|
|
|
|
def canonical(value: dict) -> bytes:
|
|
return json.dumps(
|
|
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
|
).encode()
|
|
|
|
|
|
def digest(label: str) -> str:
|
|
return hashlib.sha256(label.encode()).hexdigest()
|
|
|
|
|
|
def make_provider_receipt(provider: str, body: dict, source_commit: str,
|
|
tree_oid: str, issued_at: str) -> dict:
|
|
"""Bind a trusted-provider receipt to an evidence body (content-digest bound)."""
|
|
return {
|
|
"schema": "aios.trusted-provider.evidence.v1",
|
|
"provider_identity": provider,
|
|
"operation_run_id": f"{provider}-run-{digest('run-' + str(body.get('passed', '')))[:12]}",
|
|
"result_reference": f"{provider}/{digest('result')[:16]}/result.json",
|
|
"content_sha256": hashlib.sha256(canonical(dict(body))).hexdigest(),
|
|
"source_oid": source_commit,
|
|
"tree_oid": tree_oid,
|
|
"environment": "trusted-builder-gitops-live",
|
|
"issued_at": issued_at,
|
|
}
|
|
|
|
|
|
def bind_receipt(evidence: dict, provider: str, source_commit: str,
|
|
tree_oid: str, issued_at: str) -> dict:
|
|
return {**dict(evidence), "receipt": make_provider_receipt(
|
|
provider, dict(evidence), source_commit, tree_oid, issued_at)}
|
|
|
|
|
|
def build_input(tmp_path: Path, *, tamper_artifact: bool = False,
|
|
bad_secret_scan: bool = False) -> tuple[GovernedBuildArtifacts, str, str]:
|
|
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
|
|
recipe = config.fixed_recipe("V2R2C2")
|
|
version = recipe["version"]
|
|
tree_oid = "a" * 40
|
|
artifact_bytes = b"artifact-bytes"
|
|
if tamper_artifact:
|
|
artifact_bytes = b"artifact-tampered"
|
|
real_artifact_sha = hashlib.sha256(artifact_bytes).hexdigest()
|
|
|
|
closure_bytes = b"checksums-v1\n"
|
|
closure_sha = hashlib.sha256(closure_bytes).hexdigest()
|
|
manifest_dict = {
|
|
"artifact_sha256": real_artifact_sha,
|
|
"package_closure_sha256": closure_sha,
|
|
}
|
|
# manifest_sha256 is the digest of the manifest evidence file; the producer
|
|
# derives it from the manifest content (never self-referential).
|
|
manifest_sha = hashlib.sha256(canonical(manifest_dict)).hexdigest()
|
|
|
|
policy_dict = {"trusted": "release-policy"}
|
|
# policy_sha256 is the digest of the policy evidence file; derived by the
|
|
# producer from policy content (not a self-referential field).
|
|
policy_sha = hashlib.sha256(canonical(policy_dict)).hexdigest()
|
|
|
|
issued_at = "2026-09-09T00:00:00+00:00"
|
|
src = recipe["source_commit"]
|
|
|
|
tests = bind_receipt({"passed": True}, "aios.tests-runner", src, tree_oid, issued_at)
|
|
canary = bind_receipt({"passed": True}, "aios.canary-gate", src, tree_oid, issued_at)
|
|
gitea = bind_receipt(
|
|
{
|
|
"consistent": True,
|
|
"remote_ref_oid": recipe["source_commit"],
|
|
"repository": config.repository,
|
|
"source_commit": recipe["source_commit"],
|
|
"tree_oid": tree_oid,
|
|
},
|
|
"aios.gitea-integrity", src, tree_oid, issued_at,
|
|
)
|
|
rollback_sha = digest("rollback-contract")
|
|
rollback = bind_receipt(
|
|
{
|
|
"ready": True,
|
|
"rollback_sha256": rollback_sha,
|
|
"source_commit": recipe["source_commit"],
|
|
},
|
|
"aios.rollback-controller", src, tree_oid, issued_at,
|
|
)
|
|
if bad_secret_scan:
|
|
secret_scan = bind_receipt(
|
|
{"artifact_sha256": digest("wrong"), "passed": True},
|
|
"aios.secret-scanner", src, tree_oid, issued_at,
|
|
)
|
|
else:
|
|
secret_scan = bind_receipt(
|
|
{"artifact_sha256": real_artifact_sha, "passed": True},
|
|
"aios.secret-scanner", src, tree_oid, issued_at,
|
|
)
|
|
|
|
deployment = {
|
|
"service": "aios-silent-release-controller",
|
|
"release_path": "/srv/release/r1",
|
|
"release_root": "/srv/release",
|
|
"cutover_link": "/srv/release/current",
|
|
"dropin_path": "/srv/release/r1/deploy/systemd/controller.service",
|
|
"dropin_sha256": digest("dropin"),
|
|
"systemd_unit_sha256": digest("unit"),
|
|
"rollback_path": "/srv/release/rollback-r1",
|
|
"rollback_sha256": rollback_sha,
|
|
"authoritative_git_dir": "/data/aios-gitops/repo.git",
|
|
"repository_identity": config.repository,
|
|
"baseline_before_sha256": digest("baseline"),
|
|
"registry_before_sha256": digest("registry"),
|
|
"production_tag_refset_before_sha256": digest("tagset"),
|
|
"health_contract_version": "trusted-builder/v1",
|
|
"timeout_policy": {"seconds": 10},
|
|
"retry_policy": {"attempts": 1},
|
|
"health_contract": {"checked": True},
|
|
}
|
|
tag = {
|
|
"version": "trusted-v2r2c2-001",
|
|
"tag_type": "annotated",
|
|
"expected_tag_oid": "b" * 40,
|
|
"expected_target_oid": recipe["source_commit"],
|
|
"evidence_closure_oid": "c" * 40,
|
|
"approval_plan_id": "owner-plan-trusted-v2r2c2",
|
|
"repository_identity": config.repository,
|
|
"gitea_owner": "aios",
|
|
"gitea_repository": "aios",
|
|
"current_remote_tag_oid": None,
|
|
"remote_absent_evidence_sha256": digest("tag-absence"),
|
|
"verification_contract": "trusted-builder/v1",
|
|
}
|
|
finalization = {
|
|
"version": "trusted-v2r2c2-001",
|
|
"expected_tag_oid": "b" * 40,
|
|
"expected_target_oid": recipe["source_commit"],
|
|
"expected_previous_baseline_sha256": digest("baseline"),
|
|
"registry_expected_record": {"base": recipe["source_commit"]},
|
|
"baseline_expected_record": {"base": recipe["source_commit"]},
|
|
"final_consistency_contract": "trusted-builder/v1",
|
|
}
|
|
|
|
request = GovernedBuildArtifacts(
|
|
version=version,
|
|
candidate_id=recipe["candidate_id"],
|
|
source_commit=recipe["source_commit"],
|
|
tree_oid=tree_oid,
|
|
artifact=artifact_bytes,
|
|
manifest=manifest_dict,
|
|
package_closure=closure_bytes,
|
|
policy=policy_dict,
|
|
tests=tests,
|
|
canary=canary,
|
|
gitea=gitea,
|
|
rollback=rollback,
|
|
secret_scan=secret_scan,
|
|
deployment=deployment,
|
|
tag=tag,
|
|
finalization=finalization,
|
|
rollback_sha256=rollback_sha,
|
|
issued_at="2026-09-09T00:00:00+00:00",
|
|
)
|
|
return request, recipe["candidate_id"], tree_oid
|
|
|
|
|
|
def fake_git(tree_oid: str):
|
|
def runner(argv, **_kwargs):
|
|
if "rev-parse" in argv:
|
|
return SimpleNamespace(stdout=tree_oid + "\n", returncode=0)
|
|
return SimpleNamespace(stdout="", returncode=0)
|
|
return runner
|
|
|
|
|
|
def make_materializer(tmp_path: Path, source_root: Path, output_root: Path, tree_oid: str):
|
|
authority = tmp_path / "repo.git"
|
|
authority.mkdir(mode=0o700, exist_ok=True)
|
|
output_root.mkdir(mode=0o700, exist_ok=True)
|
|
return TrustedBuilderEvidenceMaterializer(
|
|
builder_policy=POLICY_PATH,
|
|
authority_git_dir=authority,
|
|
source_root=source_root,
|
|
output_root=output_root,
|
|
expected_owner_uid=None,
|
|
git_runner=fake_git(tree_oid),
|
|
)
|
|
|
|
|
|
def test_produce_writes_closure_that_revalidates_under_builder_verification(tmp_path: Path) -> None:
|
|
"""Producer closure re-validates and is projected byte-for-byte by the materializer.
|
|
|
|
The producer must write a self-consistent closure that the deterministic builder
|
|
verification re-validates, and that TrustedBuilderEvidenceMaterializer projects
|
|
with destination bytes exactly equal to the source (byte-exact, including a
|
|
package-closure fixture containing an LF byte).
|
|
"""
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
output_root = tmp_path / "output"
|
|
request, candidate_id, tree_oid = build_input(tmp_path)
|
|
version = request.version
|
|
|
|
result = produce_evidence_closure(
|
|
builder_policy=POLICY_PATH,
|
|
source_root=source_root,
|
|
request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
assert result.status == "EVIDENCE_PRODUCED"
|
|
assert result.idempotent is False
|
|
assert (source_root / version / "receipt.json").is_file()
|
|
for name in ("artifact.tar.gz", "manifest.json", "checksums.sha256",
|
|
"trusted-release-policy.json", "tests.json", "canary.json",
|
|
"gitea.json", "rollback.json", "secret-scan.json"):
|
|
assert (source_root / version / name).is_file(), f"missing {name}"
|
|
|
|
# Re-validate the produced closure with the deterministic builder verification
|
|
# (the exact collector the materializer uses); it must collect cleanly and
|
|
# reproduce the producer's evidence digests.
|
|
from aios_trusted_candidate_builder import ( # noqa: F811
|
|
DeterministicEvidenceCollector,
|
|
ServerOwnedEvidenceSource,
|
|
)
|
|
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
|
|
authority = tmp_path / "repo.git"
|
|
authority.mkdir(mode=0o700)
|
|
source = ServerOwnedEvidenceSource(
|
|
config, authority, source_root.resolve(),
|
|
expected_owner_uid=None, git_runner=fake_git(tree_oid),
|
|
)
|
|
collector = DeterministicEvidenceCollector(config, evidence_source=source)
|
|
recipe = config.fixed_recipe(version)
|
|
bundle = collector.collect(version, recipe)
|
|
assert set(bundle["evidence_digests"]) == set(result.evidence_digests)
|
|
for name, digest in result.evidence_digests.items():
|
|
assert bundle["evidence_digests"][name] == digest, f"digest mismatch {name}"
|
|
|
|
# Byte-exact projection: source closure -> materializer -> destination closure,
|
|
# with destination bytes exactly equal to source (incl. the LF-bearing
|
|
# package-closure file) and a destination collector PASS.
|
|
materializer = make_materializer(tmp_path, source_root, output_root, tree_oid)
|
|
outcome = materializer.materialize(
|
|
version=version,
|
|
candidate_id=candidate_id,
|
|
source_commit=request.source_commit,
|
|
)
|
|
assert outcome["status"] == "EVIDENCE_MATERIALIZED"
|
|
for name in ("receipt.json", "artifact.tar.gz", "manifest.json",
|
|
"checksums.sha256", "trusted-release-policy.json",
|
|
"tests.json", "canary.json", "gitea.json",
|
|
"rollback.json", "secret-scan.json"):
|
|
src = (source_root / version / name).read_bytes()
|
|
dst = (output_root / version / name).read_bytes()
|
|
assert src == dst, f"byte-exact projection failed for {name}"
|
|
assert hashlib.sha256(src).hexdigest() == hashlib.sha256(dst).hexdigest()
|
|
# Destination closure passes the collector too.
|
|
dst_source = ServerOwnedEvidenceSource(
|
|
config, authority, output_root.resolve(),
|
|
expected_owner_uid=None, git_runner=fake_git(tree_oid),
|
|
)
|
|
dst_collector = DeterministicEvidenceCollector(config, evidence_source=dst_source)
|
|
dst_bundle = dst_collector.collect(version, recipe)
|
|
assert set(dst_bundle["evidence_digests"]) == set(result.evidence_digests)
|
|
|
|
|
|
def test_produce_is_idempotent_on_identical_closure(tmp_path: Path) -> None:
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
request, _, _ = build_input(tmp_path)
|
|
first = produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
second = produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
assert first.idempotent is False
|
|
assert second.idempotent is True
|
|
assert second.status == "EVIDENCE_PRODUCED"
|
|
|
|
|
|
def test_produce_conflicts_on_tampered_artifact(tmp_path: Path) -> None:
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
request, _, _ = build_input(tmp_path)
|
|
produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
tampered, _, _ = build_input(tmp_path, tamper_artifact=True)
|
|
with pytest.raises(EvidenceProduceError):
|
|
produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=tampered,
|
|
expected_owner_uid=None,
|
|
)
|
|
|
|
|
|
def test_produce_fails_closed_on_secret_scan_artifact_mismatch(tmp_path: Path) -> None:
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
bad, _, _ = build_input(tmp_path, bad_secret_scan=True)
|
|
with pytest.raises(EvidenceProduceError):
|
|
produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=bad,
|
|
expected_owner_uid=None,
|
|
)
|
|
assert not (source_root / bad.version).exists()
|
|
|
|
|
|
def test_produce_registers_governed_operation() -> None:
|
|
definition = producer_operation_definition()
|
|
assert definition.operation_id == "governed.release.evidence.produce"
|
|
registry = producer_registry()
|
|
assert (
|
|
registry.get("governed.release.evidence.produce").operation_id
|
|
== "governed.release.evidence.produce"
|
|
)
|
|
assert "governed.release.evidence.produce" in definition.required_permissions
|
|
|
|
|
|
def _clone_with(request: GovernedBuildArtifacts, **overrides) -> GovernedBuildArtifacts:
|
|
data = {k: getattr(request, k) for k in request.__dataclass_fields__}
|
|
data.update(overrides)
|
|
return GovernedBuildArtifacts(**data)
|
|
|
|
|
|
def test_produce_requires_trusted_provider_receipt_on_every_evidence(tmp_path: Path) -> None:
|
|
# A bare {"passed": true} without a trusted-provider receipt must fail closed
|
|
# for EVERY governed evidence type (tests/canary/gitea/rollback/secret_scan).
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
good, _, _ = build_input(tmp_path)
|
|
for etype in ("tests", "canary", "gitea", "rollback", "secret_scan"):
|
|
bad_evidence = dict(getattr(good, etype))
|
|
bad_evidence.pop("receipt", None)
|
|
request = _clone_with(good, **{etype: bad_evidence})
|
|
with pytest.raises(EvidenceProduceError, match="EVIDENCE_RECEIPT_MISSING"):
|
|
produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
assert not (source_root / good.version).exists()
|
|
|
|
|
|
def test_produce_rejects_receipt_content_digest_mismatch(tmp_path: Path) -> None:
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
good, _, _ = build_input(tmp_path)
|
|
bad_evidence = dict(getattr(good, "tests"))
|
|
bad_evidence["receipt"] = {**bad_evidence["receipt"], "content_sha256": "0" * 64}
|
|
request = _clone_with(good, tests=bad_evidence)
|
|
with pytest.raises(EvidenceProduceError, match="EVIDENCE_RECEIPT_CONTENT_MISMATCH"):
|
|
produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
|
|
|
|
def test_produce_rejects_missing_receipt_fields(tmp_path: Path) -> None:
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
good, _, _ = build_input(tmp_path)
|
|
bad_evidence = dict(getattr(good, "canary"))
|
|
bad_evidence["receipt"] = {k: v for k, v in bad_evidence["receipt"].items()
|
|
if k != "provider_identity"}
|
|
request = _clone_with(good, canary=bad_evidence)
|
|
with pytest.raises(EvidenceProduceError, match="EVIDENCE_RECEIPT_INVALID"):
|
|
produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
|
|
|
|
def test_produce_enforces_source_tree_oid_binding_in_receipt(tmp_path: Path) -> None:
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
good, _, _ = build_input(tmp_path)
|
|
bad_evidence = dict(getattr(good, "rollback"))
|
|
bad_evidence["receipt"] = {**bad_evidence["receipt"], "source_oid": "f" * 40}
|
|
request = _clone_with(good, rollback=bad_evidence)
|
|
with pytest.raises(EvidenceProduceError, match="EVIDENCE_RECEIPT_SOURCE_OID_MISMATCH"):
|
|
produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
|
|
|
|
def test_produce_embeds_trusted_provider_binding_in_closure(tmp_path: Path) -> None:
|
|
# Positive: with valid receipts the closure is produced and the receipt.json
|
|
# carries the provider binding for each governed evidence type.
|
|
source_root = tmp_path / "source"
|
|
source_root.mkdir(mode=0o700)
|
|
request, candidate_id, tree_oid = build_input(tmp_path)
|
|
result = produce_evidence_closure(
|
|
builder_policy=POLICY_PATH, source_root=source_root, request=request,
|
|
expected_owner_uid=None,
|
|
)
|
|
assert result.status == "EVIDENCE_PRODUCED"
|
|
receipt = json.loads(
|
|
(source_root / request.version / "receipt.json").read_text(encoding="utf-8")
|
|
)
|
|
candidate = receipt["candidate"]
|
|
assert candidate["source_commit"] == request.source_commit
|
|
assert candidate["tree_oid"] == tree_oid
|
|
for etype in ("tests", "canary", "gitea", "rollback", "secret_scan"):
|
|
ev = candidate.get(etype)
|
|
if isinstance(ev, dict):
|
|
assert ev.get("passed") is True or ev.get("consistent") is True
|