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
906 lines
43 KiB
Python
906 lines
43 KiB
Python
#!/usr/bin/env python3
|
|
"""Root-only, purpose-bound trusted-builder signer provisioner.
|
|
|
|
The provisioner consumes one exact Owner-signed request after the normal
|
|
engineering review has prepared it. It creates the Ed25519 private material
|
|
and builder HMAC on the host, encrypts both for systemd, publishes only the
|
|
public verification key, and appends one replay-protected receipt. It never
|
|
accepts or writes main, tag, baseline, production, or Owner-approval keys.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import datetime as dt
|
|
try:
|
|
import fcntl
|
|
except ImportError: # pragma: no cover - Windows only affects local payload tooling.
|
|
class _NoopFcntl:
|
|
LOCK_EX = 0
|
|
|
|
@staticmethod
|
|
def flock(_file, _mode):
|
|
return None
|
|
|
|
fcntl = _NoopFcntl()
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import secrets
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import tempfile
|
|
from typing import Any, Mapping
|
|
|
|
|
|
PURPOSE = "AIOS_TRUSTED_RELEASE_BUILDER"
|
|
TRUST_DOMAIN = "aios.trusted-builder.attestation.v1"
|
|
OWNER_NAMESPACE = "aios-owner-approval-v1"
|
|
OWNER_PRINCIPAL = "aios-task-grant-signer"
|
|
SCHEMA = "aios.trusted-builder-signer-provisioning.v2"
|
|
ALLOWED_CAPABILITIES = frozenset(
|
|
{
|
|
"builder_receipt_signing",
|
|
"artifact_provenance_signing",
|
|
"manifest_provenance_signing",
|
|
"builder_owned_release_evidence_signing",
|
|
}
|
|
)
|
|
DENIED_CAPABILITIES = frozenset(
|
|
{
|
|
"owner_release_approval",
|
|
"protected_main_merge",
|
|
"arbitrary_ref_write",
|
|
"tag_publication",
|
|
"baseline_finalize",
|
|
"production_runtime_switch",
|
|
}
|
|
)
|
|
OID_FIELDS = {
|
|
"gitea_main_oid",
|
|
"authority_main_oid_at_capture",
|
|
"governed_main_oid",
|
|
"gitea_main_tree_oid",
|
|
"pr_head_oid",
|
|
"protected_merge_commit",
|
|
}
|
|
SHA_FIELDS = {"source_evidence_sha256"}
|
|
PACKAGE_MANIFEST_PATH = "deploy/trusted-builder/aios-trusted-builder-package-manifest.json"
|
|
PACKAGE_MANIFEST_SCHEMA = "aios.trusted-builder-package.v1"
|
|
SOURCE_EVIDENCE_PATHS = (
|
|
"active-runtime/app/trusted_builder_signer_provisioning.py",
|
|
"active-runtime/app/aios_trusted_candidate_builder.py",
|
|
"config/gitops/trusted-candidate-builder-policy.json",
|
|
"deploy/systemd/aios-trusted-candidate-builder.service",
|
|
"deploy/systemd/aios-trusted-candidate-builder.socket",
|
|
"deploy/systemd/aios-gitops-model-api-trusted-builder.conf.example",
|
|
"deploy/libexec/aios-trusted-builder-signer-provisioner",
|
|
PACKAGE_MANIFEST_PATH,
|
|
)
|
|
SOURCE_EVIDENCE_MODES = {path: frozenset({0o600, 0o640, 0o644, 0o664}) for path in SOURCE_EVIDENCE_PATHS}
|
|
|
|
|
|
def package_contract_document() -> dict[str, Any]:
|
|
"""The non-secret package/activation contract for a fresh B2 payload."""
|
|
files = []
|
|
for path in SOURCE_EVIDENCE_PATHS:
|
|
install_mode = "0755" if path == "deploy/libexec/aios-trusted-builder-signer-provisioner" else "0644"
|
|
files.append({"source_path": path, "source_mode": "0644", "install_mode": install_mode})
|
|
return {
|
|
"schema": PACKAGE_MANIFEST_SCHEMA,
|
|
"version": 1,
|
|
"package_name": "aios-trusted-candidate-builder",
|
|
"source_root": "/opt/aios/trusted-candidate-builder/current",
|
|
"files": files,
|
|
"activation": {
|
|
"daemon_reload": True,
|
|
"enable_units": ["aios-trusted-candidate-builder.socket"],
|
|
"service_start": "socket-activation-only",
|
|
"credential_installation": False,
|
|
"signer_invocation": False,
|
|
},
|
|
}
|
|
|
|
|
|
def _source_evidence(source_root: Path, source_files: list[str] | tuple[str, ...]) -> dict[str, str]:
|
|
if set(source_files) != set(SOURCE_EVIDENCE_PATHS) or len(source_files) != len(SOURCE_EVIDENCE_PATHS):
|
|
raise ProvisioningError("SOURCE_EVIDENCE_SET_INVALID")
|
|
evidence: dict[str, str] = {}
|
|
for path in SOURCE_EVIDENCE_PATHS:
|
|
candidate = source_root / path
|
|
if candidate.is_symlink() or not candidate.is_file():
|
|
raise ProvisioningError("SOURCE_EVIDENCE_MISSING")
|
|
if stat.S_IMODE(candidate.stat().st_mode) not in SOURCE_EVIDENCE_MODES[path]:
|
|
raise ProvisioningError("SOURCE_EVIDENCE_MODE_INVALID")
|
|
evidence[path] = _digest_file(candidate)
|
|
return evidence
|
|
|
|
|
|
def verify_package_contract(source_root: Path) -> None:
|
|
manifest = source_root / PACKAGE_MANIFEST_PATH
|
|
raw, value = _read_json(manifest)
|
|
if value != package_contract_document() or raw != pretty_json(package_contract_document()):
|
|
raise ProvisioningError("PACKAGE_CONTRACT_INVALID")
|
|
_source_evidence(source_root, list(SOURCE_EVIDENCE_PATHS))
|
|
|
|
|
|
class ProvisioningError(RuntimeError):
|
|
"""Stable fail-closed error code; never includes secret material."""
|
|
|
|
def __init__(self, code: str) -> None:
|
|
super().__init__(code)
|
|
self.code = code
|
|
|
|
|
|
def canonical_json(value: Any) -> bytes:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
|
|
|
|
def pretty_json(value: Any) -> bytes:
|
|
return (json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode("utf-8")
|
|
|
|
|
|
def sha256_bytes(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def utc_now() -> dt.datetime:
|
|
return dt.datetime.now(dt.timezone.utc)
|
|
|
|
|
|
def iso(value: dt.datetime) -> str:
|
|
return value.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def _require_oid(value: Any, code: str) -> str:
|
|
if not isinstance(value, str) or len(value) != 40 or any(c not in "0123456789abcdef" for c in value.lower()):
|
|
raise ProvisioningError(code)
|
|
return value.lower()
|
|
|
|
|
|
def _require_sha(value: Any, code: str) -> str:
|
|
if not isinstance(value, str) or len(value) != 64 or any(c not in "0123456789abcdef" for c in value.lower()):
|
|
raise ProvisioningError(code)
|
|
return value.lower()
|
|
|
|
|
|
def _read_json(path: Path) -> tuple[bytes, dict[str, Any]]:
|
|
if path.is_symlink() or not path.is_file():
|
|
raise ProvisioningError("PROVISIONING_INPUT_MISSING")
|
|
try:
|
|
raw = path.read_bytes()
|
|
value = json.loads(raw.decode("utf-8"))
|
|
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise ProvisioningError("PROVISIONING_INPUT_INVALID") from exc
|
|
if not isinstance(value, dict):
|
|
raise ProvisioningError("PROVISIONING_OBJECT_REQUIRED")
|
|
return raw, value
|
|
|
|
|
|
def _digest_file(path: Path) -> str:
|
|
if path.is_symlink() or not path.is_file():
|
|
raise ProvisioningError("SOURCE_EVIDENCE_MISSING")
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _validate_payload(payload: Mapping[str, Any]) -> None:
|
|
expected = {
|
|
"schema", "status", "operation", "repository", "source",
|
|
"signing_identity", "allowed_capabilities", "denied_capabilities",
|
|
"runtime_binding", "provisioning_constraints", "required_preconditions",
|
|
"provisioner_contract", "issued_at", "expires_in_seconds", "nonce",
|
|
}
|
|
if set(payload) != expected or payload.get("schema") != SCHEMA or payload.get("status") != "OWNER_SIGNED_PROVISIONING_REQUEST":
|
|
raise ProvisioningError("PROVISIONING_SCHEMA_INVALID")
|
|
if payload.get("operation") != "PROVISION_PURPOSE_BOUND_TRUSTED_RELEASE_BUILDER_SIGNER" or payload.get("repository") != "aios/aios":
|
|
raise ProvisioningError("PROVISIONING_SCOPE_INVALID")
|
|
source = payload.get("source")
|
|
identity = payload.get("signing_identity")
|
|
binding = payload.get("runtime_binding")
|
|
constraints = payload.get("provisioning_constraints")
|
|
contract = payload.get("provisioner_contract")
|
|
if not all(isinstance(item, Mapping) for item in (source, identity, binding, constraints, contract)):
|
|
raise ProvisioningError("PROVISIONING_BINDING_INVALID")
|
|
required_source = OID_FIELDS | {"authority_reconciliation_completed", "pr_number", "merge_parent_oids", "source_repository_identity", "source_evidence_sha256"}
|
|
if set(source) != required_source:
|
|
raise ProvisioningError("SOURCE_BINDING_INVALID")
|
|
for name in OID_FIELDS:
|
|
_require_oid(source.get(name), "SOURCE_OID_INVALID")
|
|
if not (
|
|
source["gitea_main_oid"] == source["authority_main_oid_at_capture"] == source["governed_main_oid"] == source["protected_merge_commit"]
|
|
) or source.get("authority_reconciliation_completed") is not True:
|
|
raise ProvisioningError("SOURCE_MAIN_NOT_CONVERGED")
|
|
if not isinstance(source.get("pr_number"), int) or isinstance(source["pr_number"], bool) or source["pr_number"] < 1 or source.get("source_repository_identity") != "aios/aios":
|
|
raise ProvisioningError("SOURCE_PR_BINDING_INVALID")
|
|
parents = source.get("merge_parent_oids")
|
|
if not isinstance(parents, list) or len(parents) != 2:
|
|
raise ProvisioningError("SOURCE_ANCESTRY_INVALID")
|
|
for item in parents:
|
|
_require_oid(item, "SOURCE_ANCESTRY_INVALID")
|
|
if parents[1] != source["pr_head_oid"] or parents[0] == parents[1]:
|
|
raise ProvisioningError("SOURCE_PR_HEAD_SECOND_PARENT_INVALID")
|
|
evidence = source.get("source_evidence_sha256")
|
|
if not isinstance(evidence, Mapping) or set(evidence) != set(SOURCE_EVIDENCE_PATHS):
|
|
raise ProvisioningError("SOURCE_EVIDENCE_SET_INVALID")
|
|
for path, digest in evidence.items():
|
|
if path not in SOURCE_EVIDENCE_MODES:
|
|
raise ProvisioningError("SOURCE_EVIDENCE_PATH_INVALID")
|
|
_require_sha(digest, "SOURCE_EVIDENCE_DIGEST_INVALID")
|
|
required_identity = {
|
|
"key_id", "key_generation", "algorithm", "purpose", "trust_domain",
|
|
"private_key_export", "private_key_delivery", "public_key_registration",
|
|
"service_principal_bound", "service_principal",
|
|
}
|
|
if set(identity) != required_identity or identity.get("algorithm") != "Ed25519" or identity.get("purpose") != PURPOSE or identity.get("trust_domain") != TRUST_DOMAIN:
|
|
raise ProvisioningError("SIGNING_IDENTITY_INVALID")
|
|
if identity.get("private_key_export") is not False or identity.get("service_principal_bound") is not True:
|
|
raise ProvisioningError("SIGNING_CAPABILITY_INVALID")
|
|
if not isinstance(identity.get("key_id"), str) or not identity["key_id"] or not isinstance(identity.get("key_generation"), int) or isinstance(identity["key_generation"], bool) or identity["key_generation"] < 1:
|
|
raise ProvisioningError("SIGNING_IDENTITY_INVALID")
|
|
if not isinstance(identity.get("service_principal"), str) or identity["service_principal"] != "aios-trusted-candidate-builder.service":
|
|
raise ProvisioningError("SERVICE_PRINCIPAL_INVALID")
|
|
if set(payload["allowed_capabilities"]) != ALLOWED_CAPABILITIES or set(payload["denied_capabilities"]) != DENIED_CAPABILITIES:
|
|
raise ProvisioningError("CAPABILITY_SCOPE_INVALID")
|
|
required_binding = {
|
|
"builder_service", "builder_socket", "credential_directory", "builder_policy",
|
|
"builder_public_keyring", "builder_hmac_credential", "signing_private_key_credential",
|
|
"hmac_credential_required", "peer_authentication_required", "execution_user",
|
|
}
|
|
if set(binding) != required_binding or binding.get("builder_service") != identity["service_principal"] or binding.get("hmac_credential_required") is not True or binding.get("peer_authentication_required") is not True or binding.get("execution_user") != "root":
|
|
raise ProvisioningError("RUNTIME_BINDING_INVALID")
|
|
required_constraints = {
|
|
"one_shot", "replay_protected", "append_only_receipt", "owner_approval_scope",
|
|
"production_mutation", "protected_ref_mutation", "tag_mutation", "baseline_mutation", "credential_material_in_payload",
|
|
}
|
|
if set(constraints) != required_constraints or any(constraints.get(name) is not False for name in ("production_mutation", "protected_ref_mutation", "tag_mutation", "baseline_mutation", "credential_material_in_payload")) or constraints.get("one_shot") is not True or constraints.get("replay_protected") is not True or constraints.get("append_only_receipt") is not True or constraints.get("owner_approval_scope") != "trusted-builder-signer-provisioning-only":
|
|
raise ProvisioningError("PROVISIONING_CONSTRAINTS_INVALID")
|
|
if not isinstance(contract.get("command"), str) or not contract["command"].startswith("/") or contract.get("credential_store") != "/etc/credstore.encrypted/aios-trusted-candidate-builder" or contract.get("receipt_log") != "/var/lib/aios-gitops/trusted-builder/provisioning-receipts.jsonl":
|
|
raise ProvisioningError("PROVISIONER_CONTRACT_INVALID")
|
|
if not isinstance(payload.get("required_preconditions"), list) or not payload["required_preconditions"]:
|
|
raise ProvisioningError("PROVISIONING_PRECONDITIONS_INVALID")
|
|
try:
|
|
issued = dt.datetime.fromisoformat(str(payload["issued_at"]).replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise ProvisioningError("PROVISIONING_TIME_INVALID") from exc
|
|
if issued.tzinfo is None or not isinstance(payload.get("expires_in_seconds"), int) or isinstance(payload["expires_in_seconds"], bool) or not 60 <= payload["expires_in_seconds"] <= 900:
|
|
raise ProvisioningError("PROVISIONING_TIME_INVALID")
|
|
if not isinstance(payload.get("nonce"), str) or len(payload["nonce"]) < 16:
|
|
raise ProvisioningError("PROVISIONING_NONCE_INVALID")
|
|
|
|
|
|
def build_payload(
|
|
source_root: Path,
|
|
output: Path,
|
|
*,
|
|
main_oid: str,
|
|
tree_oid: str,
|
|
pr_number: int,
|
|
pr_head_oid: str,
|
|
merge_parent_oids: list[str],
|
|
source_files: list[str],
|
|
nonce: str | None = None,
|
|
) -> dict[str, Any]:
|
|
main_oid = _require_oid(main_oid, "MAIN_OID_INVALID")
|
|
tree_oid = _require_oid(tree_oid, "TREE_OID_INVALID")
|
|
pr_head_oid = _require_oid(pr_head_oid, "PR_HEAD_OID_INVALID")
|
|
if not isinstance(pr_number, int) or isinstance(pr_number, bool) or pr_number < 1:
|
|
raise ProvisioningError("PR_NUMBER_INVALID")
|
|
if len(merge_parent_oids) != 2:
|
|
raise ProvisioningError("MERGE_PARENTS_INVALID")
|
|
parents = [_require_oid(value, "MERGE_PARENT_INVALID") for value in merge_parent_oids]
|
|
if parents[1] != pr_head_oid or parents[0] == parents[1]:
|
|
raise ProvisioningError("MERGE_PARENT_ORDER_INVALID")
|
|
evidence = _source_evidence(source_root, source_files)
|
|
payload: dict[str, Any] = {
|
|
"schema": SCHEMA,
|
|
"status": "OWNER_SIGNED_PROVISIONING_REQUEST",
|
|
"operation": "PROVISION_PURPOSE_BOUND_TRUSTED_RELEASE_BUILDER_SIGNER",
|
|
"repository": "aios/aios",
|
|
"source": {
|
|
"gitea_main_oid": main_oid,
|
|
"gitea_main_tree_oid": tree_oid,
|
|
"authority_main_oid_at_capture": main_oid,
|
|
"governed_main_oid": main_oid,
|
|
"authority_reconciliation_completed": True,
|
|
"pr_number": pr_number,
|
|
"pr_head_oid": pr_head_oid,
|
|
"protected_merge_commit": main_oid,
|
|
"merge_parent_oids": parents,
|
|
"source_repository_identity": "aios/aios",
|
|
"source_evidence_sha256": evidence,
|
|
},
|
|
"signing_identity": {
|
|
"key_id": "builder-attestation-ed25519-g1",
|
|
"key_generation": 1,
|
|
"algorithm": "Ed25519",
|
|
"purpose": PURPOSE,
|
|
"trust_domain": TRUST_DOMAIN,
|
|
"private_key_export": False,
|
|
"private_key_delivery": "server_side_systemd_credential_only",
|
|
"public_key_registration": "/etc/aios-gitops/trusted-builder/public-keyring.json",
|
|
"service_principal_bound": True,
|
|
"service_principal": "aios-trusted-candidate-builder.service",
|
|
},
|
|
"allowed_capabilities": sorted(ALLOWED_CAPABILITIES),
|
|
"denied_capabilities": sorted(DENIED_CAPABILITIES),
|
|
"runtime_binding": {
|
|
"builder_service": "aios-trusted-candidate-builder.service",
|
|
"builder_socket": "/run/aios-trusted-candidate-builder.sock",
|
|
"credential_directory": "/run/credentials/aios-trusted-candidate-builder.service",
|
|
"builder_policy": "config/gitops/trusted-candidate-builder-policy.json",
|
|
"builder_public_keyring": "/etc/aios-gitops/trusted-builder/public-keyring.json",
|
|
"builder_hmac_credential": "builder_hmac",
|
|
"signing_private_key_credential": "signing_private_key",
|
|
"hmac_credential_required": True,
|
|
"peer_authentication_required": True,
|
|
"execution_user": "root",
|
|
},
|
|
"provisioning_constraints": {
|
|
"one_shot": True,
|
|
"replay_protected": True,
|
|
"append_only_receipt": True,
|
|
"owner_approval_scope": "trusted-builder-signer-provisioning-only",
|
|
"production_mutation": False,
|
|
"protected_ref_mutation": False,
|
|
"tag_mutation": False,
|
|
"baseline_mutation": False,
|
|
"credential_material_in_payload": False,
|
|
},
|
|
"required_preconditions": [
|
|
"FINAL_COMMON_MAIN_PROTECTED_MERGE_VERIFIED",
|
|
"AUTHORITY_MAIN_RECONCILIATION_COMPLETED",
|
|
"FINAL_MAIN_MERGE_PARENT_ORDER_VERIFIED",
|
|
"PR_HEAD_SECOND_PARENT_VERIFIED",
|
|
"TRUSTED_BUILDER_IMMUTABLE_PACKAGE_CONTRACT_VERIFIED",
|
|
"PROVISIONER_GENERATES_PRIVATE_KEY_SERVER_SIDE",
|
|
"PUBLIC_KEYRING_PURPOSE_AND_KEY_ID_MATCH",
|
|
"BUILDER_HMAC_PROVISIONED_SERVER_SIDE",
|
|
"BUILDER_SERVICE_PEER_POLICY_VERIFIED",
|
|
"APPROVED_SIGNER_RECEIPT_APPEND_ONLY",
|
|
],
|
|
"provisioner_contract": {
|
|
"command": "/usr/local/libexec/aios-trusted-builder-signer-provisioner",
|
|
"credential_store": "/etc/credstore.encrypted/aios-trusted-candidate-builder",
|
|
"receipt_log": "/var/lib/aios-gitops/trusted-builder/provisioning-receipts.jsonl",
|
|
"signature_namespace": OWNER_NAMESPACE,
|
|
"signature_principal": OWNER_PRINCIPAL,
|
|
"private_key_export": False,
|
|
},
|
|
"issued_at": iso(utc_now()),
|
|
"expires_in_seconds": 900,
|
|
"nonce": nonce or "trusted-builder-provisioning-" + secrets.token_hex(16),
|
|
}
|
|
_validate_payload(payload)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
if output.exists():
|
|
raise ProvisioningError("PROVISIONING_OUTPUT_EXISTS")
|
|
output.write_bytes(pretty_json(payload))
|
|
os.chmod(output, 0o600)
|
|
return payload
|
|
|
|
|
|
class ProvisionerConfig:
|
|
def __init__(self) -> None:
|
|
self.credential_store = Path(os.getenv("AIOS_TRUSTED_BUILDER_CREDENTIAL_STORE", "/etc/credstore.encrypted/aios-trusted-candidate-builder"))
|
|
self.public_keyring = Path(os.getenv("AIOS_TRUSTED_BUILDER_PUBLIC_KEYRING", "/etc/aios-gitops/trusted-builder/public-keyring.json"))
|
|
self.binding_file = Path(os.getenv("AIOS_TRUSTED_BUILDER_BINDING_FILE", "/etc/aios-gitops/trusted-builder/service-principal-binding.json"))
|
|
self.receipt_log = Path(os.getenv("AIOS_TRUSTED_BUILDER_RECEIPT_LOG", "/var/lib/aios-gitops/trusted-builder/provisioning-receipts.jsonl"))
|
|
self.allowed_signers = Path(os.getenv("AIOS_TRUSTED_BUILDER_ALLOWED_SIGNERS", "/etc/aios/trust/aios-task-grant-allowed-signers"))
|
|
self.source_root = Path(os.getenv("AIOS_TRUSTED_BUILDER_SOURCE_ROOT", "/opt/aios/trusted-candidate-builder/current"))
|
|
self.authority_git_dir = Path(os.getenv("AIOS_TRUSTED_BUILDER_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))
|
|
self.service_unit = Path(os.getenv("AIOS_TRUSTED_BUILDER_SERVICE_UNIT", "/etc/systemd/system/aios-trusted-candidate-builder.service"))
|
|
self.systemd_creds = Path(os.getenv("AIOS_SYSTEMD_CREDS", "/usr/bin/systemd-creds"))
|
|
self.ssh_keygen = Path(os.getenv("AIOS_SSH_KEYGEN", "/usr/bin/ssh-keygen"))
|
|
|
|
|
|
def _safe_parent(path: Path) -> None:
|
|
if not path.is_absolute() or path.is_symlink():
|
|
raise ProvisioningError("PROVISIONING_PATH_INVALID")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if path.parent.is_symlink():
|
|
raise ProvisioningError("PROVISIONING_PATH_SYMLINK")
|
|
|
|
|
|
def _atomic_write(path: Path, data: bytes, mode: int = 0o600) -> None:
|
|
_safe_parent(path)
|
|
fd, temporary = tempfile.mkstemp(prefix=".trusted-builder-", dir=path.parent)
|
|
try:
|
|
os.fchmod(fd, mode)
|
|
with os.fdopen(fd, "wb", closefd=True) as stream:
|
|
stream.write(data)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
os.replace(temporary, path)
|
|
os.chmod(path, mode)
|
|
finally:
|
|
if os.path.exists(temporary):
|
|
os.unlink(temporary)
|
|
|
|
|
|
def _append(path: Path, row: Mapping[str, Any]) -> None:
|
|
_safe_parent(path)
|
|
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
|
|
try:
|
|
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
os.write(fd, canonical_json(dict(row)) + b"\n")
|
|
os.fsync(fd)
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
finally:
|
|
os.close(fd)
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
def _receipts(path: Path) -> list[dict[str, Any]]:
|
|
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]] = []
|
|
jsonl_valid = bool(lines)
|
|
for line in lines:
|
|
if not line:
|
|
jsonl_valid = False
|
|
break
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
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:
|
|
for path in (config.allowed_signers, signature, config.ssh_keygen):
|
|
if path.is_symlink() or not path.is_file():
|
|
raise ProvisioningError("OWNER_SIGNATURE_INPUT_INVALID")
|
|
result = subprocess.run(
|
|
[str(config.ssh_keygen), "-Y", "verify", "-f", str(config.allowed_signers), "-I", OWNER_PRINCIPAL, "-n", OWNER_NAMESPACE, "-s", str(signature)],
|
|
input=payload,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=30,
|
|
)
|
|
if result.returncode != 0:
|
|
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"],
|
|
capture_output=True, text=True, check=False, timeout=30,
|
|
)
|
|
live_tree = subprocess.run(
|
|
["git", "--git-dir", str(config.authority_git_dir), "rev-parse", f"{source['protected_merge_commit']}^{{tree}}"],
|
|
capture_output=True, text=True, check=False, timeout=30,
|
|
)
|
|
parents = subprocess.run(
|
|
["git", "--git-dir", str(config.authority_git_dir), "rev-list", "--parents", "-n", "1", source["protected_merge_commit"]],
|
|
capture_output=True, text=True, check=False, timeout=30,
|
|
)
|
|
ancestry = subprocess.run(
|
|
["git", "--git-dir", str(config.authority_git_dir), "merge-base", "--is-ancestor", source["pr_head_oid"], source["protected_merge_commit"]],
|
|
capture_output=True, text=True, check=False, timeout=30,
|
|
)
|
|
expected_parents = [source["protected_merge_commit"], *source["merge_parent_oids"]]
|
|
if (live_main.returncode != 0 or live_main.stdout.strip().lower() != source["gitea_main_oid"]
|
|
or live_tree.returncode != 0 or live_tree.stdout.strip().lower() != source["gitea_main_tree_oid"]
|
|
or parents.returncode != 0 or parents.stdout.strip().split() != expected_parents
|
|
or ancestry.returncode != 0):
|
|
raise ProvisioningError("AUTHORITY_MAIN_SOURCE_DRIFT")
|
|
if source["merge_parent_oids"][1] != source["pr_head_oid"]:
|
|
raise ProvisioningError("SOURCE_PR_HEAD_SECOND_PARENT_INVALID")
|
|
verify_package_contract(config.source_root)
|
|
for relative, expected in source["source_evidence_sha256"].items():
|
|
actual = _digest_file(config.source_root / relative)
|
|
if actual != expected:
|
|
raise ProvisioningError("SOURCE_EVIDENCE_DIGEST_MISMATCH")
|
|
_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]:
|
|
record = {
|
|
"algorithm": "Ed25519",
|
|
"purpose": PURPOSE,
|
|
"key_id": identity["key_id"],
|
|
"generation": identity["key_generation"],
|
|
"not_before": iso(now - dt.timedelta(minutes=1)),
|
|
"expires_at": iso(now + dt.timedelta(days=365)),
|
|
"status": "ACTIVE",
|
|
"public_key": base64.b64encode(public).decode("ascii"),
|
|
"public_key_sha256": sha256_bytes(public),
|
|
}
|
|
body = {"algorithm": "Ed25519", "purpose": PURPOSE, "keys": [record]}
|
|
return {**body, "keyring_sha256": sha256_bytes(canonical_json(body))}
|
|
|
|
|
|
def _encrypt(config: ProvisionerConfig, plain: Path, target: Path, name: str) -> None:
|
|
if config.systemd_creds.is_symlink() or not config.systemd_creds.is_file():
|
|
raise ProvisioningError("SYSTEMD_CREDS_UNAVAILABLE")
|
|
result = subprocess.run(
|
|
[str(config.systemd_creds), "--with-key=host", "--name=" + name, "encrypt", str(plain), str(target)],
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=60,
|
|
)
|
|
if result.returncode != 0 or target.is_symlink() or not target.is_file() or target.stat().st_size <= 0:
|
|
raise ProvisioningError("CREDENTIAL_ENCRYPTION_FAILED")
|
|
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)
|
|
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:
|
|
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
rows = _receipts(config.receipt_log)
|
|
previous = [row for row in rows if row.get("operation_key") == operation_key]
|
|
if previous:
|
|
row = previous[-1]
|
|
if any(canonical_json(item) != canonical_json(row) for item in previous):
|
|
raise ProvisioningError("RECEIPT_CONFLICT")
|
|
_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")
|
|
config.credential_store.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
os.chmod(config.credential_store, 0o700)
|
|
temp_root = Path(tempfile.mkdtemp(prefix="trusted-builder-provision-"))
|
|
try:
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
|
now = utc_now()
|
|
private = Ed25519PrivateKey.generate()
|
|
public = private.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
private_path = temp_root / "signing_private_key.pem"
|
|
private_path.write_bytes(private.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
|
|
os.chmod(private_path, 0o600)
|
|
hmac_path = temp_root / "builder_hmac"
|
|
hmac_path.write_bytes(secrets.token_bytes(32))
|
|
os.chmod(hmac_path, 0o600)
|
|
enc_private = temp_root / "signing_private_key.cred"
|
|
enc_hmac = temp_root / "builder_hmac.cred"
|
|
_encrypt(config, private_path, enc_private, "signing_private_key")
|
|
_encrypt(config, hmac_path, enc_hmac, "builder_hmac")
|
|
keyring = _keyring(payload["signing_identity"], public, now)
|
|
binding = {
|
|
"schema": "aios.trusted-builder.service-principal-binding.v1",
|
|
"purpose": 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": sha256_bytes(public),
|
|
"source_commit": payload["source"]["gitea_main_oid"],
|
|
"source_tree": payload["source"]["gitea_main_tree_oid"],
|
|
"payload_sha256": sha256_bytes(raw),
|
|
}
|
|
_atomic_write(temp_root / "public-keyring.json", pretty_json(keyring))
|
|
_atomic_write(temp_root / "service-principal-binding.json", pretty_json(binding))
|
|
config.public_keyring.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
config.binding_file.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
os.replace(temp_root / "signing_private_key.cred", config.credential_store / "signing_private_key")
|
|
os.replace(temp_root / "builder_hmac.cred", config.credential_store / "builder_hmac")
|
|
os.replace(temp_root / "public-keyring.json", config.public_keyring)
|
|
os.replace(temp_root / "service-principal-binding.json", config.binding_file)
|
|
for path in outputs:
|
|
os.chmod(path, 0o600)
|
|
receipt = {
|
|
"schema": "aios.trusted-builder-signer-provisioning-receipt.v1",
|
|
"status": "COMPLETED",
|
|
"created_at": iso(now),
|
|
"operation_key": operation_key,
|
|
"payload_sha256": sha256_bytes(raw),
|
|
"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"],
|
|
"generation": payload["signing_identity"]["key_generation"],
|
|
"algorithm": "Ed25519",
|
|
"purpose": PURPOSE,
|
|
"service_principal": payload["signing_identity"]["service_principal"],
|
|
"service_principal_bound": True,
|
|
"public_key_sha256": sha256_bytes(public),
|
|
"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,
|
|
}
|
|
_append(config.receipt_log, receipt)
|
|
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)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Provision the purpose-bound trusted-builder signer")
|
|
parser.add_argument("--payload", type=Path)
|
|
parser.add_argument("--signature", type=Path)
|
|
parser.add_argument("--result-json", type=Path)
|
|
parser.add_argument("--self-test", action="store_true")
|
|
parser.add_argument("--verify-package", action="store_true")
|
|
parser.add_argument("--source-root", type=Path)
|
|
args = parser.parse_args(argv)
|
|
if args.self_test:
|
|
print("PASS")
|
|
return 0
|
|
if args.verify_package:
|
|
if args.source_root is None:
|
|
parser.error("--verify-package requires --source-root")
|
|
try:
|
|
verify_package_contract(args.source_root)
|
|
print("PACKAGE_CONTRACT_VALID=PASS")
|
|
return 0
|
|
except ProvisioningError as exc:
|
|
print(json.dumps({"status": "BLOCKED", "error_code": exc.code}, sort_keys=True))
|
|
return 42
|
|
if not args.payload or not args.signature or not args.result_json:
|
|
parser.error("--payload, --signature and --result-json are required")
|
|
try:
|
|
result = provision(ProvisionerConfig(), args.payload, args.signature)
|
|
_atomic_write(args.result_json, pretty_json(result))
|
|
print(json.dumps(result, sort_keys=True))
|
|
return 0
|
|
except ProvisioningError as exc:
|
|
result = {"schema": "aios.trusted-builder-signer-provisioning-result.v1", "status": "BLOCKED", "error_code": exc.code}
|
|
try:
|
|
_atomic_write(args.result_json, pretty_json(result))
|
|
except Exception:
|
|
pass
|
|
print(json.dumps(result, sort_keys=True))
|
|
return 42
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|