fix(gitops): repair trusted-builder receipt replay framing
autonomy-smoke-tests focused receipt/replay and Task lifecycle smoke: PASS
autonomy-unit-tests full Linux regression: 834 passed, 3 skipped, 154 subtests passed
campaign-approval-gate standing Owner V15 exact Repair Task authorization and formal Task verification: PASS
git-diff-check exact two-file governed diff and whitespace check: PASS
py-compile active-runtime and tests compileall: PASS
reconciliation-contract WriterLease completed replay and governed branch convergence: PASS
release-state-machine full Linux regression includes release state-machine contract: PASS; no release mutation
secret-scan exact changed-file high-signal credential/private-key scan: PASS
signer-validator-contract receipt framing/replay/adoption contract: 18 focused tests PASS; private key non-exportable

This commit was merged in pull request #72.
This commit is contained in:
AIOS Governed Continuation
2026-08-30 00:05:00 +08:00
parent f8ad8889a4
commit 91f104ef62
2 changed files with 429 additions and 26 deletions
@@ -428,7 +428,7 @@ def _append(path: Path, row: Mapping[str, Any]) -> None:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
os.write(fd, pretty_json(dict(row)))
os.write(fd, canonical_json(dict(row)) + b"\n")
os.fsync(fd)
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
@@ -437,17 +437,45 @@ def _append(path: Path, row: Mapping[str, Any]) -> None:
def _receipts(path: Path) -> list[dict[str, Any]]:
if not path.is_file() or path.is_symlink():
if not path.exists() and not path.is_symlink():
return []
if path.is_symlink() or not path.is_file():
raise ProvisioningError("RECEIPT_LOG_INVALID")
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
raise ProvisioningError("RECEIPT_LOG_INVALID") from exc
if not text:
return []
# New receipts are canonical JSONL: exactly one object per physical line.
# If that framing does not parse, accept exactly one complete legacy object
# so the original pretty-printed receipt remains immutable and replayable.
lines = text.splitlines()
result: list[dict[str, Any]] = []
for line in path.read_text(encoding="utf-8").splitlines():
jsonl_valid = bool(lines)
for line in lines:
if not line:
jsonl_valid = False
break
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
result.append(value)
return result
jsonl_valid = False
break
if not isinstance(value, dict):
jsonl_valid = False
break
result.append(value)
if jsonl_valid:
return result
try:
legacy = json.loads(text)
except json.JSONDecodeError as exc:
raise ProvisioningError("RECEIPT_LOG_INVALID") from exc
if not isinstance(legacy, dict):
raise ProvisioningError("RECEIPT_LOG_INVALID")
return [legacy]
def _run_signature_verify(config: ProvisionerConfig, payload: bytes, signature: Path) -> None:
@@ -465,6 +493,26 @@ def _run_signature_verify(config: ProvisionerConfig, payload: bytes, signature:
raise ProvisioningError("OWNER_SIGNATURE_VERIFY_FAILED")
def _verify_service_policy(config: ProvisionerConfig) -> None:
if config.service_unit.is_symlink() or not config.service_unit.is_file():
raise ProvisioningError("BUILDER_SERVICE_UNIT_MISSING")
text = config.service_unit.read_text(encoding="utf-8")
required = (
"User=root", "LoadCredentialEncrypted=signing_private_key:",
"LoadCredentialEncrypted=builder_hmac:", "NoNewPrivileges=true",
"PrivateNetwork=true", "RestrictAddressFamilies=AF_UNIX",
)
if any(item not in text for item in required):
raise ProvisioningError("BUILDER_SERVICE_POLICY_INVALID")
def _git(config: ProvisionerConfig, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
["git", "--git-dir", str(config.authority_git_dir), *args],
capture_output=True, check=False, timeout=30,
)
def _verify_source(config: ProvisionerConfig, source: Mapping[str, Any]) -> None:
live_main = subprocess.run(
["git", "--git-dir", str(config.authority_git_dir), "rev-parse", "refs/heads/main"],
@@ -495,16 +543,38 @@ def _verify_source(config: ProvisionerConfig, source: Mapping[str, Any]) -> None
actual = _digest_file(config.source_root / relative)
if actual != expected:
raise ProvisioningError("SOURCE_EVIDENCE_DIGEST_MISMATCH")
if config.service_unit.is_symlink() or not config.service_unit.is_file():
raise ProvisioningError("BUILDER_SERVICE_UNIT_MISSING")
text = config.service_unit.read_text(encoding="utf-8")
required = (
"User=root", "LoadCredentialEncrypted=signing_private_key:",
"LoadCredentialEncrypted=builder_hmac:", "NoNewPrivileges=true",
"PrivateNetwork=true", "RestrictAddressFamilies=AF_UNIX",
)
if any(item not in text for item in required):
raise ProvisioningError("BUILDER_SERVICE_POLICY_INVALID")
_verify_service_policy(config)
def _verify_replay_source(config: ProvisionerConfig, source: Mapping[str, Any]) -> None:
"""Verify the historical f8-bound source after authority main advances."""
commit = source["protected_merge_commit"]
live_main = _git(config, "rev-parse", "refs/heads/main")
tree = _git(config, "rev-parse", f"{commit}^{{tree}}")
parents = _git(config, "rev-list", "--parents", "-n", "1", commit)
source_ancestry = _git(config, "merge-base", "--is-ancestor", commit, live_main.stdout.strip().decode("ascii", "ignore"))
pr_ancestry = _git(config, "merge-base", "--is-ancestor", source["pr_head_oid"], commit)
expected_parents = [commit, *source["merge_parent_oids"]]
if (
live_main.returncode != 0
or tree.returncode != 0
or tree.stdout.strip().decode("ascii", "ignore").lower() != source["gitea_main_tree_oid"]
or parents.returncode != 0
or parents.stdout.decode("utf-8", "replace").strip().split() != expected_parents
or source_ancestry.returncode != 0
or pr_ancestry.returncode != 0
):
raise ProvisioningError("AUTHORITY_HISTORICAL_SOURCE_DRIFT")
if source["merge_parent_oids"][1] != source["pr_head_oid"]:
raise ProvisioningError("SOURCE_PR_HEAD_SECOND_PARENT_INVALID")
for relative, expected in source["source_evidence_sha256"].items():
historical = _git(config, "show", f"{commit}:{relative}")
if historical.returncode != 0 or sha256_bytes(historical.stdout) != expected:
raise ProvisioningError("SOURCE_EVIDENCE_DIGEST_MISMATCH")
# The installed repair package must still satisfy the governed package and
# service policy, but its bytes are expected to differ from historical f8.
verify_package_contract(config.source_root)
_verify_service_policy(config)
def _keyring(identity: Mapping[str, Any], public: bytes, now: dt.datetime) -> dict[str, Any]:
@@ -537,14 +607,158 @@ def _encrypt(config: ProvisionerConfig, plain: Path, target: Path, name: str) ->
os.chmod(target, 0o600)
def _require_private_regular_file(path: Path, code: str) -> None:
if path.is_symlink() or not path.is_file():
raise ProvisioningError(code)
metadata = path.stat()
if stat.S_IMODE(metadata.st_mode) != 0o600 or metadata.st_size <= 0:
raise ProvisioningError(code)
def _read_state_json(path: Path, code: str) -> dict[str, Any]:
_require_private_regular_file(path, code)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ProvisioningError(code) from exc
if not isinstance(value, dict):
raise ProvisioningError(code)
return value
def _parse_active_window(record: Mapping[str, Any]) -> None:
try:
not_before = dt.datetime.fromisoformat(str(record["not_before"]).replace("Z", "+00:00"))
expires_at = dt.datetime.fromisoformat(str(record["expires_at"]).replace("Z", "+00:00"))
except (KeyError, ValueError) as exc:
raise ProvisioningError("KEYRING_BINDING_INVALID") from exc
now = utc_now()
if not_before.tzinfo is None or expires_at.tzinfo is None or not (not_before <= now < expires_at):
raise ProvisioningError("KEYRING_BINDING_INVALID")
def _verify_existing_state(
config: ProvisionerConfig,
payload: Mapping[str, Any],
raw: bytes,
signature_bytes: bytes,
row: Mapping[str, Any],
) -> None:
receipt_keys = {
"schema", "status", "created_at", "operation_key", "payload_sha256",
"signature_sha256", "source_commit", "source_tree", "key_id",
"generation", "algorithm", "purpose", "service_principal",
"service_principal_bound", "public_key_sha256",
"private_key_exportable", "credential_projection",
"negative_capability_tests", "production_mutation",
"protected_ref_mutation", "tag_mutation", "baseline_mutation",
}
if set(row) != receipt_keys:
raise ProvisioningError("RECEIPT_CONFLICT")
try:
created_at = dt.datetime.fromisoformat(str(row["created_at"]).replace("Z", "+00:00"))
except ValueError as exc:
raise ProvisioningError("RECEIPT_CONFLICT") from exc
identity = payload["signing_identity"]
source = payload["source"]
expected = {
"schema": "aios.trusted-builder-signer-provisioning-receipt.v1",
"status": "COMPLETED",
"operation_key": "payload:" + sha256_bytes(raw) + ":signature:" + sha256_bytes(signature_bytes),
"payload_sha256": sha256_bytes(raw),
"signature_sha256": sha256_bytes(signature_bytes),
"source_commit": source["gitea_main_oid"],
"source_tree": source["gitea_main_tree_oid"],
"key_id": identity["key_id"],
"generation": identity["key_generation"],
"algorithm": "Ed25519",
"purpose": PURPOSE,
"service_principal": identity["service_principal"],
"service_principal_bound": True,
"private_key_exportable": False,
"credential_projection": "systemd-encrypted-host-credential",
"negative_capability_tests": {name: True for name in sorted(DENIED_CAPABILITIES)},
"production_mutation": False,
"protected_ref_mutation": False,
"tag_mutation": False,
"baseline_mutation": False,
}
if created_at.tzinfo is None or any(row.get(key) != value for key, value in expected.items()):
raise ProvisioningError("RECEIPT_CONFLICT")
public_keyring = _read_state_json(config.public_keyring, "KEYRING_BINDING_INVALID")
if set(public_keyring) != {"algorithm", "purpose", "keys", "keyring_sha256"}:
raise ProvisioningError("KEYRING_BINDING_INVALID")
keys = public_keyring.get("keys")
if not isinstance(keys, list) or len(keys) != 1 or not isinstance(keys[0], Mapping):
raise ProvisioningError("KEYRING_BINDING_INVALID")
record = dict(keys[0])
if set(record) != {
"algorithm", "purpose", "key_id", "generation", "not_before",
"expires_at", "status", "public_key", "public_key_sha256",
}:
raise ProvisioningError("KEYRING_BINDING_INVALID")
try:
public_key = base64.b64decode(str(record["public_key"]), validate=True)
except (ValueError, TypeError) as exc:
raise ProvisioningError("KEYRING_BINDING_INVALID") from exc
public_sha = sha256_bytes(public_key)
keyring_body = {key: public_keyring[key] for key in ("algorithm", "purpose", "keys")}
if (
len(public_key) != 32
or public_keyring["algorithm"] != "Ed25519"
or public_keyring["purpose"] != PURPOSE
or record.get("algorithm") != "Ed25519"
or record.get("purpose") != PURPOSE
or record.get("key_id") != identity["key_id"]
or record.get("generation") != identity["key_generation"]
or record.get("status") != "ACTIVE"
or record.get("public_key_sha256") != public_sha
or row.get("public_key_sha256") != public_sha
or public_keyring.get("keyring_sha256") != sha256_bytes(canonical_json(keyring_body))
):
raise ProvisioningError("KEYRING_BINDING_INVALID")
_parse_active_window(record)
binding = _read_state_json(config.binding_file, "SERVICE_PRINCIPAL_BINDING_INVALID")
expected_binding = {
"schema": "aios.trusted-builder.service-principal-binding.v1",
"purpose": PURPOSE,
"service_principal": identity["service_principal"],
"key_id": identity["key_id"],
"generation": identity["key_generation"],
"algorithm": "Ed25519",
"private_key_export": False,
"public_key_sha256": public_sha,
"source_commit": source["gitea_main_oid"],
"source_tree": source["gitea_main_tree_oid"],
"payload_sha256": sha256_bytes(raw),
}
if binding != expected_binding:
raise ProvisioningError("SERVICE_PRINCIPAL_BINDING_INVALID")
if (
config.credential_store.is_symlink()
or not config.credential_store.is_dir()
or stat.S_IMODE(config.credential_store.stat().st_mode) != 0o700
):
raise ProvisioningError("CREDENTIAL_STORE_INVALID")
_require_private_regular_file(
config.credential_store / "signing_private_key", "RECEIPT_OUTPUT_MISSING"
)
_require_private_regular_file(
config.credential_store / "builder_hmac", "RECEIPT_OUTPUT_MISSING"
)
def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Path) -> dict[str, Any]:
if os.name != "nt" and os.geteuid() != 0:
raise ProvisioningError("ROOT_REQUIRED")
raw, payload = _read_json(payload_path)
_validate_payload(payload)
_run_signature_verify(config, raw, signature_path)
_verify_source(config, payload["source"])
operation_key = "payload:" + sha256_bytes(raw) + ":signature:" + sha256_bytes(signature_path.read_bytes())
signature_bytes = signature_path.read_bytes()
operation_key = "payload:" + sha256_bytes(raw) + ":signature:" + sha256_bytes(signature_bytes)
lock_path = config.receipt_log.with_suffix(".lock")
_safe_parent(lock_path)
with lock_path.open("a+b") as lock:
@@ -553,12 +767,24 @@ def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Pat
previous = [row for row in rows if row.get("operation_key") == operation_key]
if previous:
row = previous[-1]
if row.get("status") != "COMPLETED" or row.get("purpose") != PURPOSE or row.get("key_id") != payload["signing_identity"]["key_id"]:
if any(canonical_json(item) != canonical_json(row) for item in previous):
raise ProvisioningError("RECEIPT_CONFLICT")
for required in (config.public_keyring, config.binding_file, config.credential_store / "signing_private_key", config.credential_store / "builder_hmac"):
if required.is_symlink() or not required.is_file():
raise ProvisioningError("RECEIPT_OUTPUT_MISSING")
return {"status": "IDEMPOTENT_REPLAY_PASS", "idempotent": True, **{key: row[key] for key in ("operation_key", "payload_sha256", "signature_sha256", "key_id", "generation", "public_key_sha256", "purpose", "service_principal_bound", "private_key_exportable")}}
_verify_replay_source(config, payload["source"])
_verify_existing_state(config, payload, raw, signature_bytes, row)
return {
"status": "IDEMPOTENT_REPLAY_PASS",
"idempotent": True,
"mutation_executed": False,
"existing_signer_state_adopted": True,
**{key: row[key] for key in (
"operation_key", "payload_sha256", "signature_sha256",
"source_commit", "source_tree", "key_id", "generation",
"public_key_sha256", "purpose", "service_principal_bound",
"private_key_exportable",
)},
"negative_capability_tests": row["negative_capability_tests"],
}
_verify_source(config, payload["source"])
outputs = [config.public_keyring, config.binding_file, config.credential_store / "signing_private_key", config.credential_store / "builder_hmac"]
if any(path.exists() or path.is_symlink() for path in outputs):
raise ProvisioningError("PREEXISTING_STATE_CONFLICT")
@@ -612,7 +838,7 @@ def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Pat
"created_at": iso(now),
"operation_key": operation_key,
"payload_sha256": sha256_bytes(raw),
"signature_sha256": sha256_bytes(signature_path.read_bytes()),
"signature_sha256": sha256_bytes(signature_bytes),
"source_commit": payload["source"]["gitea_main_oid"],
"source_tree": payload["source"]["gitea_main_tree_oid"],
"key_id": payload["signing_identity"]["key_id"],
@@ -631,7 +857,7 @@ def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Pat
"baseline_mutation": False,
}
_append(config.receipt_log, receipt)
return {"status": "PROVISIONED", "idempotent": False, **{key: receipt[key] for key in ("operation_key", "payload_sha256", "signature_sha256", "source_commit", "source_tree", "key_id", "generation", "purpose", "service_principal_bound", "public_key_sha256", "private_key_exportable")}, "negative_capability_tests": receipt["negative_capability_tests"]}
return {"status": "PROVISIONED", "idempotent": False, "mutation_executed": True, "existing_signer_state_adopted": False, **{key: receipt[key] for key in ("operation_key", "payload_sha256", "signature_sha256", "source_commit", "source_tree", "key_id", "generation", "purpose", "service_principal_bound", "public_key_sha256", "private_key_exportable")}, "negative_capability_tests": receipt["negative_capability_tests"]}
finally:
shutil.rmtree(temp_root, ignore_errors=True)
@@ -112,3 +112,180 @@ def test_package_contract_declares_socket_activation_no_credentials_and_0755_lau
def test_provisioner_self_test_has_no_mutation(capsys):
assert provisioning.main(["--self-test"]) == 0
assert capsys.readouterr().out.strip() == "PASS"
def _replay_fixture(tmp_path: Path):
_source_root, payload_path, payload = _payload(tmp_path)
raw = payload_path.read_bytes()
signature = tmp_path / "owner.sig"
signature.write_bytes(b"owner-signature-test")
os.chmod(signature, 0o600)
config = provisioning.ProvisionerConfig()
config.credential_store = tmp_path / "credstore"
config.public_keyring = tmp_path / "state" / "public-keyring.json"
config.binding_file = tmp_path / "state" / "service-principal-binding.json"
config.receipt_log = tmp_path / "state" / "provisioning-receipts.jsonl"
config.credential_store.mkdir(mode=0o700)
os.chmod(config.credential_store, 0o700)
config.public_keyring.parent.mkdir(mode=0o700)
public = b"p" * 32
now = provisioning.utc_now()
keyring = provisioning._keyring(payload["signing_identity"], public, now)
config.public_keyring.write_bytes(provisioning.pretty_json(keyring))
os.chmod(config.public_keyring, 0o600)
binding = {
"schema": "aios.trusted-builder.service-principal-binding.v1",
"purpose": provisioning.PURPOSE,
"service_principal": payload["signing_identity"]["service_principal"],
"key_id": payload["signing_identity"]["key_id"],
"generation": payload["signing_identity"]["key_generation"],
"algorithm": "Ed25519",
"private_key_export": False,
"public_key_sha256": provisioning.sha256_bytes(public),
"source_commit": payload["source"]["gitea_main_oid"],
"source_tree": payload["source"]["gitea_main_tree_oid"],
"payload_sha256": provisioning.sha256_bytes(raw),
}
config.binding_file.write_bytes(provisioning.pretty_json(binding))
os.chmod(config.binding_file, 0o600)
for name in ("signing_private_key", "builder_hmac"):
target = config.credential_store / name
target.write_bytes(("encrypted-" + name).encode())
os.chmod(target, 0o600)
signature_bytes = signature.read_bytes()
receipt = {
"schema": "aios.trusted-builder-signer-provisioning-receipt.v1",
"status": "COMPLETED",
"created_at": provisioning.iso(now),
"operation_key": "payload:" + provisioning.sha256_bytes(raw) + ":signature:" + provisioning.sha256_bytes(signature_bytes),
"payload_sha256": provisioning.sha256_bytes(raw),
"signature_sha256": provisioning.sha256_bytes(signature_bytes),
"source_commit": payload["source"]["gitea_main_oid"],
"source_tree": payload["source"]["gitea_main_tree_oid"],
"key_id": payload["signing_identity"]["key_id"],
"generation": payload["signing_identity"]["key_generation"],
"algorithm": "Ed25519",
"purpose": provisioning.PURPOSE,
"service_principal": payload["signing_identity"]["service_principal"],
"service_principal_bound": True,
"public_key_sha256": provisioning.sha256_bytes(public),
"private_key_exportable": False,
"credential_projection": "systemd-encrypted-host-credential",
"negative_capability_tests": {name: True for name in sorted(provisioning.DENIED_CAPABILITIES)},
"production_mutation": False,
"protected_ref_mutation": False,
"tag_mutation": False,
"baseline_mutation": False,
}
return config, payload_path, payload, signature, receipt
def test_legacy_multiline_receipt_parse(tmp_path: Path):
path = tmp_path / "receipts.jsonl"
receipt = {"schema": "legacy", "status": "COMPLETED", "nested": {"value": True}}
path.write_bytes(provisioning.pretty_json(receipt))
assert provisioning._receipts(path) == [receipt]
def test_new_canonical_jsonl_receipt_parse(tmp_path: Path):
path = tmp_path / "receipts.jsonl"
rows = [{"id": 1}, {"id": 2}]
for row in rows:
provisioning._append(path, row)
assert path.read_bytes() == b"".join(provisioning.canonical_json(row) + b"\n" for row in rows)
assert provisioning._receipts(path) == rows
@pytest.mark.parametrize(
"content",
[b'{"status":"COMPLETED"}\nnot-json\n', b'{"status":"COMPLETED"'],
ids=["malformed", "truncated"],
)
def test_malformed_or_truncated_receipt_fails_closed(tmp_path: Path, content: bytes):
path = tmp_path / "receipts.jsonl"
path.write_bytes(content)
with pytest.raises(provisioning.ProvisioningError, match="RECEIPT_LOG_INVALID"):
provisioning._receipts(path)
def test_preexisting_valid_state_is_adopted_without_mutation(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
os.chmod(config.receipt_log, 0o600)
before = {
path: path.read_bytes()
for path in (
config.receipt_log, config.public_keyring, config.binding_file,
config.credential_store / "signing_private_key",
config.credential_store / "builder_hmac",
)
}
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_replay_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
result = provisioning.provision(config, payload_path, signature)
assert result["status"] == "IDEMPOTENT_REPLAY_PASS"
assert result["mutation_executed"] is False
assert result["existing_signer_state_adopted"] is True
assert result["private_key_exportable"] is False
assert before == {path: path.read_bytes() for path in before}
def test_duplicate_replay_is_idempotent(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
os.chmod(config.receipt_log, 0o600)
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_replay_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
first = provisioning.provision(config, payload_path, signature)
receipt_bytes = config.receipt_log.read_bytes()
second = provisioning.provision(config, payload_path, signature)
assert first == second
assert config.receipt_log.read_bytes() == receipt_bytes
def test_wrong_payload_is_rejected_against_preexisting_state(tmp_path: Path, monkeypatch):
config, payload_path, payload, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
os.chmod(config.receipt_log, 0o600)
payload["nonce"] = "wrong-payload-nonce-0002"
payload_path.write_bytes(provisioning.pretty_json(payload))
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
with pytest.raises(provisioning.ProvisioningError, match="PREEXISTING_STATE_CONFLICT"):
provisioning.provision(config, payload_path, signature)
def test_wrong_signature_is_rejected(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
monkeypatch.setattr(
provisioning,
"_run_signature_verify",
lambda *_args: (_ for _ in ()).throw(provisioning.ProvisioningError("OWNER_SIGNATURE_VERIFY_FAILED")),
)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
with pytest.raises(provisioning.ProvisioningError, match="OWNER_SIGNATURE_VERIFY_FAILED"):
provisioning.provision(config, payload_path, signature)
def test_wrong_purpose_is_rejected(tmp_path: Path):
_source_root, _payload_path, payload = _payload(tmp_path)
payload["signing_identity"]["purpose"] = "WRONG_PURPOSE"
with pytest.raises(provisioning.ProvisioningError, match="SIGNING_IDENTITY_INVALID"):
provisioning._validate_payload(payload)
def test_replay_rejects_state_binding_tampering(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
receipt["service_principal"] = "wrong.service"
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_replay_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
with pytest.raises(provisioning.ProvisioningError, match="RECEIPT_CONFLICT"):
provisioning.provision(config, payload_path, signature)