autonomy-smoke-tests phase2-pr76 PASS evidence_sha256=c16c68f86ee0c4e8cfd0af3f76078be45441ee151395fbde5800145c08e189b5
autonomy-unit-tests phase2-pr76 PASS evidence_sha256=15bc76da47bdf8218f18fc8a7da65fafd841677d5f754b71a0c9f56e07d84c1b
campaign-approval-gate phase2-pr76 PASS evidence_sha256=ea7ed0516efe6736a8eaee426685e40a2393fbf3ae84104a743f394204c2ec33
git-diff-check phase2-pr76 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile phase2-pr76 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract phase2-pr76 PASS evidence_sha256=211444cb0c146f5d4a57e13b780cfe7ffb406e8a8bc2e11c02a3a4728f66b0b2
release-state-machine phase2-pr76 PASS evidence_sha256=6cea88752b3e58ae5cee56ff3b1b13eab34117918d6bad9661194c031e2dc157
secret-scan phase2-pr76 PASS evidence_sha256=7cb68dd604dc6cd4904246e16333072d512dba14c096301e08c5a2b78a45bd55
signer-validator-contract phase2-pr76 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
aios.owner-final-merge-authorization-v2 real v2 contract tests passed
328 lines
10 KiB
Python
328 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
|
from aios_owner_merge_authorization import (
|
|
GovernedOwnerMergeAdmission,
|
|
OwnerMergeApprovalAuthority,
|
|
OwnerMergeApprovalError,
|
|
OwnerMergeApprovalPolicy,
|
|
OwnerMergeApprovalVerifier,
|
|
OwnerMergeLiveState,
|
|
OwnerMergeOidReadback,
|
|
SQLiteOwnerMergeNonceLedger,
|
|
)
|
|
from aios_single_host_authority import (
|
|
RegistryOwnerApprovalVerifier,
|
|
ssh_fingerprint,
|
|
ssh_public_line,
|
|
)
|
|
|
|
|
|
NOW = 2_000_000_000
|
|
REPOSITORY = "aios/aios"
|
|
PR_NUMBER = 75
|
|
HEAD = "a" * 40
|
|
BASE = "b" * 40
|
|
MERGED = "c" * 40
|
|
CHECKS = ("contract", "security", "unit")
|
|
CHECK_CONTRACT = "d" * 64
|
|
LINEAGE = "e" * 64
|
|
|
|
|
|
def _write_key(path: Path, key: Ed25519PrivateKey) -> None:
|
|
path.write_bytes(
|
|
key.private_bytes(
|
|
serialization.Encoding.PEM,
|
|
serialization.PrivateFormat.PKCS8,
|
|
serialization.NoEncryption(),
|
|
)
|
|
)
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
class RecordingExecutor:
|
|
def __init__(self, result: str = MERGED) -> None:
|
|
self.result = result
|
|
self.calls: list[dict[str, object]] = []
|
|
|
|
def execute(self, approval: dict[str, object]) -> str:
|
|
self.calls.append(dict(approval))
|
|
return self.result
|
|
|
|
|
|
class RecordingReconciler:
|
|
def __init__(self, oid: str = MERGED) -> None:
|
|
self.oid = oid
|
|
self.calls: list[str] = []
|
|
|
|
def readback(self, merge_oid: str) -> OwnerMergeOidReadback:
|
|
self.calls.append(merge_oid)
|
|
return OwnerMergeOidReadback(
|
|
authority_main_oid=self.oid,
|
|
gitea_main_oid=self.oid,
|
|
api_main_oid=self.oid,
|
|
)
|
|
|
|
|
|
class UnavailableProofVerifier:
|
|
def verify(self, **_kwargs: object) -> bool:
|
|
raise RuntimeError("default signer/verifier unavailable")
|
|
|
|
|
|
@pytest.fixture
|
|
def approval_env(tmp_path: Path):
|
|
owner_key = Ed25519PrivateKey.generate()
|
|
owner_path = tmp_path / "owner.key"
|
|
_write_key(owner_path, owner_key)
|
|
registry_path = tmp_path / "registry.json"
|
|
registry_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"schema": "aios.single-host-signer-registry.v1",
|
|
"authority_generation": 1,
|
|
"signers": [
|
|
{
|
|
"signer_id": "local-owner-authority",
|
|
"role": "OWNER_AUTHORITY",
|
|
"provider": "local_unix_service",
|
|
"fingerprint": ssh_fingerprint(owner_key.public_key()),
|
|
"public_key": ssh_public_line(owner_key.public_key(), "owner"),
|
|
"socket": "/run/aios/owner-authority.sock",
|
|
"secret_ref": "aios-secret://owner",
|
|
"status": "ACTIVE",
|
|
"generation": 1,
|
|
"parent_signer_id": "",
|
|
"parent_fingerprint": "",
|
|
}
|
|
],
|
|
},
|
|
sort_keys=True,
|
|
)
|
|
+ "\n"
|
|
)
|
|
os.chmod(registry_path, 0o644)
|
|
policy = OwnerMergeApprovalPolicy(
|
|
repository=REPOSITORY,
|
|
pr_number=PR_NUMBER,
|
|
pr_head_oid=HEAD,
|
|
expected_main_oid=BASE,
|
|
operation="merge.pull-request",
|
|
required_check_contract_digest=CHECK_CONTRACT,
|
|
required_checks=CHECKS,
|
|
owner_identity="owner:aios-single-host",
|
|
audit_lineage_digest=LINEAGE,
|
|
maximum_ttl_seconds=300,
|
|
)
|
|
authority = OwnerMergeApprovalAuthority(
|
|
owner_path,
|
|
policy,
|
|
require_root_owner=False,
|
|
clock=lambda: NOW,
|
|
token_urlsafe=lambda _size: "fixed-nonce-material",
|
|
)
|
|
artifact = authority.issue()
|
|
registry_verifier = RegistryOwnerApprovalVerifier(
|
|
registry_path,
|
|
require_root_owner=False,
|
|
)
|
|
return {
|
|
"policy": policy,
|
|
"artifact": artifact,
|
|
"registry_verifier": registry_verifier,
|
|
"registry_path": registry_path,
|
|
"owner_path": owner_path,
|
|
"nonce_path": tmp_path / "nonces.sqlite3",
|
|
}
|
|
|
|
|
|
def _live_state(**updates: object) -> OwnerMergeLiveState:
|
|
raw = {
|
|
"repository": REPOSITORY,
|
|
"pr_number": PR_NUMBER,
|
|
"pr_head_oid": HEAD,
|
|
"main_oid": BASE,
|
|
"required_check_contract_digest": CHECK_CONTRACT,
|
|
"check_statuses": {name: True for name in CHECKS},
|
|
}
|
|
raw.update(updates)
|
|
return OwnerMergeLiveState(**raw)
|
|
|
|
|
|
def _admission(env, *, live: OwnerMergeLiveState | None = None, primary=None, recovery=None, reconciler=None):
|
|
verifier = OwnerMergeApprovalVerifier(
|
|
primary=primary or env["registry_verifier"],
|
|
recovery=recovery,
|
|
clock=lambda: NOW,
|
|
)
|
|
executor = RecordingExecutor()
|
|
reconciler = reconciler or RecordingReconciler()
|
|
admission = GovernedOwnerMergeAdmission(
|
|
policy=env["policy"],
|
|
verifier=verifier,
|
|
live_state_reader=lambda: live or _live_state(),
|
|
executor=executor,
|
|
reconciler=reconciler,
|
|
nonce_ledger=SQLiteOwnerMergeNonceLedger(env["nonce_path"]),
|
|
clock=lambda: NOW,
|
|
)
|
|
return admission, executor, reconciler
|
|
|
|
|
|
def test_valid_owner_approval_with_exact_live_binding_enters_governed_merge(approval_env):
|
|
admission, executor, reconciler = _admission(approval_env)
|
|
|
|
receipt = admission.admit(approval_env["artifact"])
|
|
|
|
assert receipt.merge_oid == MERGED
|
|
assert receipt.owner_identity == "owner:aios-single-host"
|
|
assert len(executor.calls) == 1
|
|
assert reconciler.calls == [MERGED]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("live", "code"),
|
|
[
|
|
(_live_state(pr_head_oid="f" * 40), "OWNER_MERGE_HEAD_DRIFT"),
|
|
(_live_state(main_oid="f" * 40), "OWNER_MERGE_BASE_DRIFT"),
|
|
],
|
|
)
|
|
def test_head_or_main_drift_invalidates_owner_approval_before_execution(approval_env, live, code):
|
|
admission, executor, _reconciler = _admission(approval_env, live=live)
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match=code):
|
|
admission.admit(approval_env["artifact"])
|
|
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_expired_owner_approval_is_rejected_before_execution(approval_env):
|
|
verifier = OwnerMergeApprovalVerifier(
|
|
primary=approval_env["registry_verifier"],
|
|
clock=lambda: NOW + 301,
|
|
)
|
|
executor = RecordingExecutor()
|
|
admission = GovernedOwnerMergeAdmission(
|
|
policy=approval_env["policy"],
|
|
verifier=verifier,
|
|
live_state_reader=_live_state,
|
|
executor=executor,
|
|
reconciler=RecordingReconciler(),
|
|
nonce_ledger=SQLiteOwnerMergeNonceLedger(approval_env["nonce_path"]),
|
|
clock=lambda: NOW + 301,
|
|
)
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_EXPIRED"):
|
|
admission.admit(approval_env["artifact"])
|
|
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_changed_policy_scope_requires_a_formal_owner_rebind(approval_env):
|
|
replacement = replace(approval_env["policy"], expected_main_oid="f" * 40)
|
|
verifier = OwnerMergeApprovalVerifier(
|
|
primary=approval_env["registry_verifier"],
|
|
clock=lambda: NOW,
|
|
)
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_BASE_BINDING_INVALID"):
|
|
verifier.verify(approval_env["artifact"], replacement)
|
|
|
|
|
|
def test_nonce_replay_cannot_create_a_second_merge_side_effect(approval_env):
|
|
admission, executor, _reconciler = _admission(approval_env)
|
|
admission.admit(approval_env["artifact"])
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_NONCE_REPLAY"):
|
|
admission.admit(approval_env["artifact"])
|
|
|
|
assert len(executor.calls) == 1
|
|
|
|
|
|
def test_non_owner_proof_is_rejected_even_when_payload_claims_owner_identity(approval_env):
|
|
attacker_path = approval_env["owner_path"].parent / "attacker.key"
|
|
_write_key(attacker_path, Ed25519PrivateKey.generate())
|
|
attacker = OwnerMergeApprovalAuthority(
|
|
attacker_path,
|
|
approval_env["policy"],
|
|
require_root_owner=False,
|
|
clock=lambda: NOW,
|
|
token_urlsafe=lambda _size: "attacker-nonce-material",
|
|
)
|
|
admission, executor, _reconciler = _admission(approval_env)
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_SIGNATURE_INVALID"):
|
|
admission.admit(attacker.issue())
|
|
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_recovery_verifier_can_prove_existing_owner_approval_when_primary_is_unavailable(approval_env):
|
|
recovery = approval_env["registry_verifier"]
|
|
admission, executor, _reconciler = _admission(
|
|
approval_env,
|
|
primary=UnavailableProofVerifier(),
|
|
recovery=recovery,
|
|
)
|
|
|
|
receipt = admission.admit(approval_env["artifact"])
|
|
|
|
assert receipt.proof_path == "recovery"
|
|
assert len(executor.calls) == 1
|
|
|
|
|
|
def test_missing_trusted_owner_proof_fails_closed(approval_env):
|
|
admission, executor, _reconciler = _admission(approval_env)
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_APPROVAL_ARTIFACT_INVALID"):
|
|
admission.admit({})
|
|
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_owner_approval_never_converts_failed_technical_checks_to_pass(approval_env):
|
|
statuses = {name: True for name in CHECKS}
|
|
statuses["security"] = False
|
|
admission, executor, _reconciler = _admission(
|
|
approval_env,
|
|
live=_live_state(check_statuses=statuses),
|
|
)
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_REQUIRED_CHECKS_FAILED"):
|
|
admission.admit(approval_env["artifact"])
|
|
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_post_merge_oid_disagreement_is_not_reported_as_success_and_is_not_retried(approval_env):
|
|
admission, executor, _reconciler = _admission(
|
|
approval_env,
|
|
reconciler=RecordingReconciler(oid="f" * 40),
|
|
)
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_OID_RECONCILIATION_REQUIRED"):
|
|
admission.admit(approval_env["artifact"])
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_NONCE_REPLAY"):
|
|
admission.admit(approval_env["artifact"])
|
|
|
|
assert len(executor.calls) == 1
|
|
|
|
|
|
def test_malformed_executor_result_is_unknown_and_cannot_be_retried(approval_env):
|
|
admission, executor, _reconciler = _admission(approval_env)
|
|
executor.result = "not-an-oid"
|
|
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_EXECUTOR_RESULT_INVALID"):
|
|
admission.admit(approval_env["artifact"])
|
|
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_NONCE_REPLAY"):
|
|
admission.admit(approval_env["artifact"])
|
|
|
|
assert len(executor.calls) == 1
|