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
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user