| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032 |
- #!/usr/bin/env python3
- """P3-WP07 local, fail-closed enterprise delivery evidence controls."""
- from __future__ import annotations
- import argparse
- import contextlib
- import fcntl
- import hashlib
- import json
- import os
- import re
- import stat
- import tempfile
- from datetime import UTC, datetime, timedelta
- from pathlib import Path
- from typing import Any
- from cryptography.hazmat.primitives import serialization
- from cryptography.hazmat.primitives.asymmetric.ed25519 import (
- Ed25519PrivateKey,
- Ed25519PublicKey,
- )
- ROOT = Path(__file__).resolve().parents[2]
- HEAD = "20260811_495"
- ENVIRONMENTS = ("development", "test", "staging", "production")
- TRUST_STORE = ROOT / "deployment/enterprise/trust_store.json"
- MAX_APPROVAL_WINDOW = timedelta(hours=24)
- CLOCK_SKEW = timedelta(minutes=5)
- LOCAL_TEST_PRIVATE_KEY = bytes.fromhex(
- "4f3b2775a4df921336c3882f071d9edce4a6e946b928345f7bf68b55d4eaa3d8"
- )
- class DeliveryError(ValueError):
- pass
- def canonical(value: Any) -> bytes:
- return json.dumps(
- value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
- ).encode()
- def sha_bytes(value: bytes) -> str:
- return hashlib.sha256(value).hexdigest()
- def sha_file(path: Path) -> str:
- return sha_bytes(safe_bytes(path))
- def safe_bytes(path: Path) -> bytes:
- """Read one stable regular file through directory FDs without link traversal."""
- path = path.absolute()
- parts = path.parts
- if len(parts) < 2 or parts[0] != os.sep:
- raise DeliveryError(f"evidence path must be absolute: {path}")
- descriptor = os.open(
- os.sep, os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
- )
- try:
- for component in parts[1:-1]:
- next_descriptor = os.open(
- component,
- os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0),
- dir_fd=descriptor,
- )
- info = os.fstat(next_descriptor)
- if not stat.S_ISDIR(info.st_mode):
- os.close(next_descriptor)
- raise DeliveryError(f"evidence path contains a non-directory: {path}")
- os.close(descriptor)
- descriptor = next_descriptor
- name = parts[-1]
- before_name = os.stat(name, dir_fd=descriptor, follow_symlinks=False)
- if not stat.S_ISREG(before_name.st_mode) or before_name.st_nlink != 1:
- raise DeliveryError(f"evidence file must be an unlinked regular file: {path}")
- handle = os.open(
- name, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0), dir_fd=descriptor
- )
- with os.fdopen(handle, "rb") as stream:
- before = os.fstat(stream.fileno())
- if (
- before.st_ino != before_name.st_ino
- or before.st_dev != before_name.st_dev
- or before.st_nlink != 1
- ):
- raise DeliveryError(f"evidence file changed while reading: {path}")
- value = stream.read()
- after = os.fstat(stream.fileno())
- except OSError as exc:
- raise DeliveryError(f"unsafe evidence path (symbolic links are forbidden): {path}") from exc
- finally:
- os.close(descriptor)
- if (
- before.st_ino != after.st_ino
- or before.st_dev != after.st_dev
- or after.st_nlink != 1
- ):
- raise DeliveryError(f"evidence file changed while reading: {path}")
- return value
- def read_json(path: Path) -> dict[str, Any]:
- return read_json_bytes(safe_bytes(path), str(path))
- def read_json_bytes(value: bytes, label: str) -> dict[str, Any]:
- try:
- parsed = json.loads(value)
- except json.JSONDecodeError as exc:
- raise DeliveryError(f"invalid JSON evidence: {label}") from exc
- if not isinstance(parsed, dict):
- raise DeliveryError(f"JSON object required: {label}")
- return parsed
- def require_digest(value: str, field: str) -> str:
- if len(value) != 64 or set(value) - set("0123456789abcdef"):
- raise DeliveryError(f"{field} must be a lowercase sha256 digest")
- return value
- def safe_state_root(path: Path) -> Path:
- path = path.absolute()
- if path.is_symlink() or any(
- part.is_symlink() for part in [path, *path.parents] if part.exists()
- ):
- raise DeliveryError("state root and ancestors must not be symbolic links")
- path.mkdir(mode=0o700, parents=True, exist_ok=True)
- info = os.lstat(path)
- if not stat.S_ISDIR(info.st_mode) or info.st_mode & 0o077:
- raise DeliveryError("state root must be a private directory")
- return path
- def private_dir_fd(root: Path, *parts: str) -> tuple[Path, int]:
- """Open an owned state directory without following any controlled path part."""
- current = safe_state_root(root)
- descriptor = os.open(
- current, os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
- )
- try:
- for part in parts:
- if not part or "/" in part or part in {".", ".."}:
- raise DeliveryError("invalid state directory component")
- with contextlib.suppress(FileExistsError):
- os.mkdir(part, mode=0o700, dir_fd=descriptor)
- next_descriptor = os.open(
- part,
- os.O_RDONLY
- | os.O_DIRECTORY
- | getattr(os, "O_NOFOLLOW", 0),
- dir_fd=descriptor,
- )
- info = os.fstat(next_descriptor)
- if not stat.S_ISDIR(info.st_mode) or info.st_mode & 0o077:
- os.close(next_descriptor)
- raise DeliveryError("state subdirectory must be a private directory")
- os.close(descriptor)
- descriptor = next_descriptor
- current = current / part
- except OSError as exc:
- os.close(descriptor)
- raise DeliveryError("unsafe state subdirectory") from exc
- except Exception:
- os.close(descriptor)
- raise
- return current, descriptor
- def state_read_json(descriptor: int, name: str) -> dict[str, Any]:
- if "/" in name or name in {"", ".", ".."}:
- raise DeliveryError("invalid state file name")
- try:
- handle = os.open(
- name, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0), dir_fd=descriptor
- )
- with os.fdopen(handle, "rb") as stream:
- before = os.fstat(stream.fileno())
- if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1:
- raise DeliveryError("state file must be an unlinked regular file")
- value = stream.read()
- after = os.fstat(stream.fileno())
- except FileNotFoundError:
- raise
- except OSError as exc:
- raise DeliveryError("unsafe state file") from exc
- if before.st_ino != after.st_ino or after.st_nlink != 1:
- raise DeliveryError("state file changed while reading")
- try:
- parsed = json.loads(value)
- except json.JSONDecodeError as exc:
- raise DeliveryError("invalid state JSON") from exc
- if not isinstance(parsed, dict):
- raise DeliveryError("state JSON object required")
- return parsed
- def state_write_json(descriptor: int, name: str, value: dict[str, Any]) -> None:
- """Atomic, fsync-backed replacement within a previously verified directory fd."""
- if "/" in name or name in {"", ".", ".."}:
- raise DeliveryError("invalid state file name")
- temporary = f".tmp-{os.getpid()}-{os.urandom(8).hex()}"
- try:
- handle = os.open(
- temporary,
- os.O_WRONLY
- | os.O_CREAT
- | os.O_EXCL
- | getattr(os, "O_NOFOLLOW", 0),
- 0o600,
- dir_fd=descriptor,
- )
- with os.fdopen(handle, "wb") as stream:
- stream.write(
- json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True).encode()
- + b"\n"
- )
- stream.flush()
- os.fsync(stream.fileno())
- os.replace(temporary, name, src_dir_fd=descriptor, dst_dir_fd=descriptor)
- os.fsync(descriptor)
- except OSError as exc:
- raise DeliveryError("unsafe state write") from exc
- finally:
- with contextlib.suppress(FileNotFoundError):
- os.unlink(temporary, dir_fd=descriptor)
- def state_file(root: Path, name: str) -> Path:
- """Only for non-state output paths; controlled state uses descriptor helpers."""
- path = root / name
- if path.exists() and (path.is_symlink() or os.lstat(path).st_nlink != 1):
- raise DeliveryError("state file must not be linked")
- return path
- class Lock:
- def __init__(self, root: Path) -> None:
- self.root = root
- self.stream: Any | None = None
- self.descriptor: int | None = None
- def __enter__(self) -> Lock:
- _, self.descriptor = private_dir_fd(self.root)
- descriptor = os.open(
- ".lock",
- os.O_CREAT | os.O_RDWR | getattr(os, "O_NOFOLLOW", 0),
- 0o600,
- dir_fd=self.descriptor,
- )
- info = os.fstat(descriptor)
- if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
- os.close(descriptor)
- os.close(self.descriptor)
- raise DeliveryError("state lock must be an unlinked regular file")
- self.stream = os.fdopen(descriptor, "r+")
- fcntl.flock(self.stream.fileno(), fcntl.LOCK_EX)
- return self
- def __exit__(self, *_: Any) -> None:
- assert self.stream
- fcntl.flock(self.stream.fileno(), fcntl.LOCK_UN)
- self.stream.close()
- assert self.descriptor is not None
- os.close(self.descriptor)
- def write_json(path: Path, value: dict[str, Any]) -> None:
- path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
- descriptor, temporary = tempfile.mkstemp(prefix=".tmp-", dir=path.parent)
- try:
- with os.fdopen(descriptor, "wb") as stream:
- stream.write(
- json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True).encode()
- + b"\n"
- )
- stream.flush()
- os.fsync(stream.fileno())
- os.chmod(temporary, 0o600)
- os.replace(temporary, path)
- directory = os.open(path.parent, os.O_RDONLY)
- try:
- os.fsync(directory)
- finally:
- os.close(directory)
- finally:
- if os.path.exists(temporary):
- os.unlink(temporary)
- def evidence_time(value: dict[str, Any], label: str) -> None:
- try:
- issued = datetime.fromisoformat(str(value["issued_at"]).replace("Z", "+00:00"))
- expiry = datetime.fromisoformat(str(value["expires_at"]).replace("Z", "+00:00"))
- except (KeyError, ValueError, TypeError) as exc:
- raise DeliveryError(f"{label} evidence timestamps required") from exc
- now = datetime.now(UTC)
- if issued.astimezone(UTC) > now or expiry.astimezone(UTC) <= now or issued >= expiry:
- raise DeliveryError(f"{label} evidence expired or not yet valid")
- if value.get("provider") not in {"fake_local_only", "external"}:
- raise DeliveryError(f"{label} evidence provider required")
- def approval_time(value: dict[str, Any]) -> None:
- issued_text, expires_text = value.get("issued_at"), value.get("expires_at")
- if not isinstance(issued_text, str) or not isinstance(expires_text, str):
- raise DeliveryError("approval evidence timestamps required")
- rfc3339_utc = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$"
- if not re.fullmatch(rfc3339_utc, issued_text) or not re.fullmatch(
- rfc3339_utc, expires_text
- ):
- raise DeliveryError("approval evidence timestamps must be UTC RFC3339")
- try:
- issued = datetime.fromisoformat(issued_text.replace("Z", "+00:00"))
- expires = datetime.fromisoformat(expires_text.replace("Z", "+00:00"))
- except ValueError as exc:
- raise DeliveryError("approval evidence timestamps required") from exc
- now = datetime.now(UTC)
- if (
- issued > now + CLOCK_SKEW
- or expires <= now
- or issued >= expires
- or expires - issued > MAX_APPROVAL_WINDOW
- ):
- raise DeliveryError("approval evidence expired, not yet valid, or window exceeds limit")
- def validate_sbom(sbom: dict[str, Any], artifact_sha: str) -> None:
- if (
- sbom.get("bomFormat") != "CycloneDX"
- or not isinstance(sbom.get("specVersion"), str)
- or not str(sbom.get("serialNumber", "")).startswith("urn:uuid:")
- or not isinstance(sbom.get("version"), int)
- or not isinstance(sbom.get("components"), list)
- ):
- raise DeliveryError("CycloneDX SBOM schema minimum required")
- properties = sbom.get("metadata", {}).get("properties", [])
- if not isinstance(properties, list) or not any(
- isinstance(item, dict)
- and item.get("name") == "dataops:artifact-sha256"
- and item.get("value") == artifact_sha
- for item in properties
- ):
- raise DeliveryError("CycloneDX SBOM must bind actual artifact")
- def approved_signer(provider: str, key_id: str, test_only: bool) -> dict[str, Any]:
- store = read_json(TRUST_STORE)
- configured = store.get("providers", {}).get(provider)
- if not isinstance(configured, dict) or not configured.get("enabled"):
- raise DeliveryError("signing provider disabled or not pre-approved")
- if configured.get("test_only") and not test_only:
- raise DeliveryError("test-only signing provider requires explicit test-only mode")
- signer = configured.get("approved_signers", {}).get(key_id)
- if not isinstance(signer, dict):
- raise DeliveryError("signer key id is not in controlled trust store")
- if signer.get("algorithm") != "Ed25519" or not signer.get("trust_root"):
- raise DeliveryError("signer trust root is incomplete")
- try:
- public_key = bytes.fromhex(str(signer["public_key"]))
- except (KeyError, TypeError, ValueError) as exc:
- raise DeliveryError("controlled signer public key is invalid") from exc
- if len(public_key) != 32 or sha_bytes(public_key) != signer.get("public_key_sha256"):
- raise DeliveryError("controlled signer public key digest is invalid")
- return signer
- def approved_backup_encryption(provider: str, key_reference: str, test_only: bool) -> None:
- configured = read_json(TRUST_STORE).get("backup_encryption_providers", {}).get(provider)
- if not isinstance(configured, dict) or not configured.get("enabled"):
- raise DeliveryError("backup encryption provider disabled or not pre-approved")
- if configured.get("test_only") and not test_only:
- raise DeliveryError("test-only backup encryption requires explicit test-only mode")
- if key_reference not in configured.get("approved_key_references", []):
- raise DeliveryError("backup key reference is not pre-approved")
- def verify_controlled_signature(
- evidence: dict[str, Any], signed: dict[str, Any], label: str, test_only: bool
- ) -> None:
- if any(field in evidence for field in ("public_key", "trust_root", "approved_signers")):
- raise DeliveryError(f"{label} must not carry a caller supplied trust root")
- provider, key_id = evidence.get("provider"), evidence.get("key_id")
- if not isinstance(provider, str) or not isinstance(key_id, str):
- raise DeliveryError(f"{label} provider and key id required")
- if evidence.get("algorithm") != "Ed25519":
- raise DeliveryError(f"{label} algorithm must be Ed25519")
- try:
- signature = bytes.fromhex(str(evidence["signature"]))
- if len(signature) != 64:
- raise ValueError("Ed25519 signature length")
- signer = approved_signer(provider, key_id, test_only)
- Ed25519PublicKey.from_public_bytes(
- bytes.fromhex(str(signer["public_key"]))
- ).verify(signature, canonical(signed))
- except (KeyError, TypeError, ValueError) as exc:
- raise DeliveryError(f"invalid {label}") from exc
- except Exception as exc:
- raise DeliveryError(f"{label} verification failed") from exc
- def candidate_evidence(args: argparse.Namespace) -> dict[str, Any]:
- # Each caller supplied evidence file is opened, inode-checked and read once.
- raw = {name: safe_bytes(getattr(args, name)) for name in ("artifact", "sbom", "scan", "license", "signature")}
- artifact_sha = sha_bytes(raw["artifact"])
- sbom_sha = sha_bytes(raw["sbom"])
- sbom = read_json_bytes(raw["sbom"], "sbom")
- validate_sbom(sbom, artifact_sha)
- signature = read_json_bytes(raw["signature"], "signature")
- signature_sha = sha_bytes(raw["signature"])
- scan = read_json_bytes(raw["scan"], "scan")
- license_value = read_json_bytes(raw["license"], "license")
- if any(field in signature for field in ("public_key", "trust_root", "approved_signers")):
- raise DeliveryError("signature evidence must not carry a caller supplied trust root")
- evidence_time(scan, "scan")
- evidence_time(license_value, "license")
- provider = signature.get("provider")
- key_id = signature.get("key_id")
- if not isinstance(provider, str) or not isinstance(key_id, str):
- raise DeliveryError("signature provider and key id required")
- if (
- scan.get("artifact_sha256") != artifact_sha
- or scan.get("sbom_sha256") != sbom_sha
- or scan.get("signature_sha256") != signature_sha
- or scan.get("provider") != provider
- or scan.get("key_id") != key_id
- or scan.get("status") != "clear"
- ):
- raise DeliveryError("scan evidence does not clear actual artifact")
- if (
- license_value.get("artifact_sha256") != artifact_sha
- or license_value.get("sbom_sha256") != sbom_sha
- or license_value.get("signature_sha256") != signature_sha
- or license_value.get("provider") != provider
- or license_value.get("key_id") != key_id
- or license_value.get("status") != "clear"
- ):
- raise DeliveryError("license evidence does not clear actual SBOM")
- signed = {"artifact_sha256": artifact_sha, "sbom_sha256": sbom_sha}
- if (
- signature.get("algorithm") != "Ed25519"
- or signature.get("provider") != provider
- or signature.get("artifact_sha256") != artifact_sha
- or signature.get("sbom_sha256") != sbom_sha
- ):
- raise DeliveryError("signature evidence does not bind actual artifact and SBOM")
- try:
- evidence_time(signature, "signature")
- signer = approved_signer(provider, key_id, bool(args.test_only))
- public_key = bytes.fromhex(str(signer["public_key"]))
- Ed25519PublicKey.from_public_bytes(public_key).verify(
- bytes.fromhex(str(signature["signature"])), canonical(signed)
- )
- except (KeyError, ValueError, TypeError) as exc:
- raise DeliveryError("invalid signature evidence") from exc
- except Exception as exc:
- raise DeliveryError("signature evidence verification failed") from exc
- return {
- "artifact_sha256": artifact_sha,
- "sbom_sha256": sbom_sha,
- "scan_sha256": sha_bytes(raw["scan"]),
- "license_sha256": sha_bytes(raw["license"]),
- "signature_sha256": signature_sha,
- }
- def command_candidate(args: argparse.Namespace) -> dict[str, Any]:
- root = safe_state_root(args.state_dir)
- if args.environment not in ENVIRONMENTS or not args.approval_ref.strip():
- raise DeliveryError("known environment and approval reference required")
- evidence = candidate_evidence(args)
- value = {
- "environment": args.environment,
- "version": args.version,
- "approval_ref": args.approval_ref,
- "migration_head": HEAD,
- **evidence,
- }
- value["bundle_digest"] = sha_bytes(
- canonical(
- {
- "artifact_sha256": evidence["artifact_sha256"],
- "sbom_sha256": evidence["sbom_sha256"],
- "openapi_sha256": sha_file(ROOT / "docs/architecture/OPENAPI.yaml"),
- "config_schema_sha256": sha_file(
- ROOT / "deployment/helm/dataops-platform/values.schema.json"
- ),
- "migration_head": HEAD,
- }
- )
- )
- value["candidate_digest"] = sha_bytes(canonical(value))
- with Lock(root):
- _, candidates_fd = private_dir_fd(root, "candidates")
- try:
- filename = f"{value['candidate_digest']}.json"
- try:
- existing = state_read_json(candidates_fd, filename)
- except FileNotFoundError:
- existing = None
- if existing is not None and existing != value:
- raise DeliveryError("candidate collision")
- if existing is None:
- state_write_json(candidates_fd, filename, value)
- finally:
- os.close(candidates_fd)
- return {
- "status": "candidate_created",
- "candidate_digest": value["candidate_digest"],
- }
- def load_candidate(root: Path, digest: str) -> dict[str, Any]:
- require_digest(digest, "candidate_digest")
- _, descriptor = private_dir_fd(root, "candidates")
- try:
- return state_read_json(descriptor, f"{digest}.json")
- finally:
- os.close(descriptor)
- def current_record(root: Path, environment: str) -> dict[str, Any]:
- _, descriptor = private_dir_fd(root, "current")
- try:
- try:
- return state_read_json(descriptor, f"{environment}.json")
- except FileNotFoundError:
- return {"current_digest": "none", "bundle_digest": "none"}
- finally:
- os.close(descriptor)
- def write_current(root: Path, environment: str, value: dict[str, Any]) -> None:
- _, descriptor = private_dir_fd(root, "current")
- try:
- state_write_json(descriptor, f"{environment}.json", value)
- finally:
- os.close(descriptor)
- def append_history(root: Path, environment: str, value: dict[str, Any]) -> None:
- _, descriptor = private_dir_fd(root, "history", environment)
- try:
- state_write_json(descriptor, f"{value['current_digest']}.json", value)
- finally:
- os.close(descriptor)
- def command_promote(args: argparse.Namespace) -> dict[str, Any]:
- root = safe_state_root(args.state_dir)
- target_index = ENVIRONMENTS.index(args.environment)
- candidate = load_candidate(root, args.candidate_digest)
- if candidate.get("environment") != args.environment:
- raise DeliveryError("candidate environment mismatch")
- with Lock(root):
- current = current_record(root, args.environment)
- if current["current_digest"] == args.candidate_digest:
- return {
- "status": "promoted_replayed",
- "current_digest": args.candidate_digest,
- }
- if target_index:
- if current["current_digest"] != "none":
- raise DeliveryError("current digest CAS conflict")
- previous = current_record(root, ENVIRONMENTS[target_index - 1])
- if previous.get("current_digest") != args.expected_base_digest:
- raise DeliveryError(
- "promotion must use adjacent prior environment current digest"
- )
- if previous.get("bundle_digest") != candidate.get("bundle_digest"):
- raise DeliveryError(
- "promotion must retain the exact adjacent artifact bundle"
- )
- elif current["current_digest"] != args.expected_base_digest:
- raise DeliveryError("current digest CAS conflict")
- value = {
- "current_digest": args.candidate_digest,
- "previous_digest": args.expected_base_digest
- if target_index
- else current["current_digest"],
- "approval_ref": candidate["approval_ref"],
- "environment": args.environment,
- "artifact_sha256": candidate["artifact_sha256"],
- "bundle_digest": candidate["bundle_digest"],
- "previous_bundle_digest": previous.get("bundle_digest")
- if target_index
- else current.get("bundle_digest"),
- }
- write_current(root, args.environment, value)
- append_history(root, args.environment, value)
- return {"status": "promoted", "current_digest": args.candidate_digest}
- def command_rollback(args: argparse.Namespace) -> dict[str, Any]:
- root = safe_state_root(args.state_dir)
- with Lock(root):
- current = current_record(root, args.environment)
- if not args.approval_ref.strip() or args.target_digest != current.get(
- "previous_digest"
- ):
- raise DeliveryError(
- "rollback requires approval and exact recorded previous digest"
- )
- target = load_candidate(root, args.target_digest)
- if target.get("bundle_digest") != current.get("previous_bundle_digest", target.get("bundle_digest")):
- raise DeliveryError("rollback must retain recorded artifact bundle history")
- value = {
- **current,
- "current_digest": args.target_digest,
- "previous_digest": current["current_digest"],
- "previous_bundle_digest": current.get("bundle_digest"),
- "artifact_sha256": target["artifact_sha256"],
- "bundle_digest": target["bundle_digest"],
- "rollback_approval_ref": args.approval_ref,
- }
- write_current(root, args.environment, value)
- append_history(root, args.environment, value)
- return {"status": "rollback_recorded", "current_digest": args.target_digest}
- def command_backup(args: argparse.Namespace) -> dict[str, Any]:
- if args.environment == "production" and args.encryption_provider != "external":
- raise DeliveryError("production backup requires an approved external encryption provider")
- if args.encryption_provider not in {
- "fake",
- "external",
- "disabled",
- } or not args.key_reference.startswith(("kms://", "hsm://", "secretref://")):
- raise DeliveryError("safe provider and key reference required")
- approved_backup_encryption(
- args.encryption_provider, args.key_reference, bool(args.test_only)
- )
- if args.encryption_provider == "external" and (
- args.backup_signature is None or not args.signer_key_id
- ):
- raise DeliveryError("external backup requires a controlled signer key id and signature")
- if args.encryption_provider == "fake" and not args.test_only:
- raise DeliveryError("fake backup encryption requires explicit test-only mode")
- directory = safe_state_root(args.backup_dir)
- release = {
- "path": args.release_artifact.name,
- "sha256": sha_file(args.release_artifact),
- }
- entries = {
- "postgres": {**release, "mode": "logical_or_snapshot"},
- "neo4j": {**release, "mode": "snapshot"},
- "minio": {**release, "mode": "snapshot"},
- "config": {
- "path": args.config_artifact.name,
- "sha256": sha_file(args.config_artifact),
- "mode": "digest_only",
- },
- "key_references": {"reference": args.key_reference, "mode": "reference_only"},
- }
- manifest = {
- "environment": args.environment,
- "components": entries,
- "encryption_provider": args.encryption_provider,
- "key_reference": args.key_reference,
- "migration_head": HEAD,
- "signature_provider": "external"
- if args.encryption_provider == "external"
- else "fake_local_only",
- "signer_key_id": args.signer_key_id
- if args.encryption_provider == "external"
- else "wp07-local-test",
- }
- manifest["manifest_sha256"] = sha_bytes(canonical(manifest))
- signature_payload = {
- "manifest_sha256": manifest["manifest_sha256"],
- "encryption_provider": manifest["encryption_provider"],
- "key_reference": manifest["key_reference"],
- }
- if args.encryption_provider == "external":
- supplied = read_json(args.backup_signature)
- if (
- supplied.get("provider") != "external"
- or supplied.get("key_id") != args.signer_key_id
- ):
- raise DeliveryError("external backup signature signer mismatch")
- verify_controlled_signature(supplied, signature_payload, "backup signature", False)
- manifest["integrity_signature"] = {
- "algorithm": "Ed25519",
- "provider": "external",
- "key_id": args.signer_key_id,
- "signature": supplied["signature"],
- }
- else:
- signer = approved_signer("fake_local_only", "wp07-local-test", True)
- private = Ed25519PrivateKey.from_private_bytes(LOCAL_TEST_PRIVATE_KEY)
- manifest["integrity_signature"] = {
- "algorithm": "Ed25519",
- "provider": "fake_local_only",
- "key_id": "wp07-local-test",
- "signature": private.sign(canonical(signature_payload)).hex(),
- }
- if signer.get("public_key") != private.public_key().public_bytes(
- serialization.Encoding.Raw, serialization.PublicFormat.Raw
- ).hex():
- raise DeliveryError("local test signer does not match controlled trust store")
- path = state_file(directory, "backup-manifest.json")
- write_json(path, manifest)
- _, backup_fd = private_dir_fd(directory, "backup")
- try:
- state_write_json(backup_fd, "latest.json", manifest)
- finally:
- os.close(backup_fd)
- return {"status": "planned", "manifest_path": str(path)}
- def command_restore(args: argparse.Namespace) -> dict[str, Any]:
- value = read_json(args.backup_manifest)
- check = {
- key: value[key]
- for key in value
- if key not in {"manifest_sha256", "integrity_signature"}
- }
- if value.get("manifest_sha256") != sha_bytes(canonical(check)):
- raise DeliveryError("backup manifest digest mismatch")
- try:
- signature = value["integrity_signature"]
- if not isinstance(signature, dict) or signature.get("algorithm") != "Ed25519":
- raise DeliveryError("backup signature algorithm invalid")
- if (
- signature.get("provider") != value.get("signature_provider")
- or signature.get("key_id") != value.get("signer_key_id")
- ):
- raise DeliveryError("backup signature signer identity mismatch")
- if (
- value.get("encryption_provider") == "external"
- and value.get("signature_provider") != "external"
- ) or (
- value.get("encryption_provider") == "fake"
- and value.get("signature_provider") != "fake_local_only"
- ):
- raise DeliveryError("backup encryption and signer provider mismatch")
- signature_payload = {
- "manifest_sha256": value["manifest_sha256"],
- "encryption_provider": value["encryption_provider"],
- "key_reference": value["key_reference"],
- }
- verify_controlled_signature(
- signature, signature_payload, "backup signature", bool(args.test_only)
- )
- except DeliveryError:
- raise
- except (KeyError, TypeError, ValueError) as exc:
- raise DeliveryError("backup signature invalid") from exc
- except Exception as exc:
- raise DeliveryError("backup signature verification failed") from exc
- if args.target == "production" or args.target != args.confirm_target:
- raise DeliveryError("restore requires exact new non-production target")
- if args.environment == "production" and value.get("encryption_provider") != "external":
- raise DeliveryError("production restore requires an externally encrypted backup")
- approved_backup_encryption(
- str(value.get("encryption_provider")), str(value.get("key_reference")), bool(args.test_only)
- )
- components = value.get("components", {})
- if set(components) != {"postgres", "neo4j", "minio", "config", "key_references"}:
- raise DeliveryError("backup component set incomplete")
- parent = args.backup_manifest.parent
- for name, item in components.items():
- if name == "key_references":
- if not str(item.get("reference", "")).startswith(
- ("kms://", "hsm://", "secretref://")
- ):
- raise DeliveryError("backup key reference invalid")
- continue
- path = parent / str(item["path"])
- if sha_file(path) != item["sha256"]:
- raise DeliveryError("backup component checksum mismatch")
- if value.get("migration_head") != HEAD:
- raise DeliveryError("backup compatibility migration mismatch")
- restore_root = safe_state_root(args.backup_manifest.parent)
- with Lock(restore_root):
- _, restore_fd = private_dir_fd(restore_root, "restore")
- try:
- try:
- state_read_json(restore_fd, f"{args.target}.json")
- except FileNotFoundError:
- pass
- else:
- raise DeliveryError("restore target has already been used")
- state_write_json(
- restore_fd,
- f"{args.target}.json",
- {
- "target": args.target,
- "manifest_sha256": value["manifest_sha256"],
- "status": "restore_to_new_target_planned",
- },
- )
- finally:
- os.close(restore_fd)
- return {"status": "restore_to_new_target_planned", "target": args.target}
- def command_compatibility(args: argparse.Namespace) -> dict[str, Any]:
- def release(artifact_path: Path, sbom_path: Path, label: str) -> dict[str, Any]:
- artifact = safe_bytes(artifact_path)
- sbom_bytes = safe_bytes(sbom_path)
- artifact_sha = sha_bytes(artifact)
- sbom = read_json_bytes(sbom_bytes, f"{label} SBOM")
- validate_sbom(sbom, artifact_sha)
- components = sbom.get("components", [])
- identity = [
- {
- "bom-ref": component.get("bom-ref", ""),
- "name": component.get("name", ""),
- "version": component.get("version", ""),
- "purl": component.get("purl", ""),
- }
- for component in components
- if isinstance(component, dict)
- ]
- if len(identity) != len(components):
- raise DeliveryError(f"{label} SBOM component identity is malformed")
- identity.sort(key=canonical)
- return {
- "artifact_sha256": artifact_sha,
- "sbom_sha256": sha_bytes(sbom_bytes),
- "sbom_serial": sbom.get("serialNumber"),
- "component_identity_sha256": sha_bytes(canonical(identity)),
- }
- releases = {
- "base": release(args.base_artifact, args.base_sbom, "base"),
- "candidate": release(args.candidate_artifact, args.candidate_sbom, "candidate"),
- "rollback": release(args.rollback_artifact, args.rollback_sbom, "rollback"),
- }
- def contract(path: Path | None, label: str, bound_release: dict[str, Any]) -> dict[str, Any]:
- if path is None:
- raise DeliveryError("base, candidate and rollback contract manifests are required")
- supplied = read_json(path)
- source: dict[str, Path] = {}
- for field in ("openapi", "config", "chart", "values"):
- value = supplied.get(field)
- if not isinstance(value, str) or not value:
- raise DeliveryError(f"{label} compatibility contract {field} is required")
- source[field] = Path(value)
- if (
- supplied.get("artifact_sha256") != bound_release["artifact_sha256"]
- or supplied.get("sbom_sha256") != bound_release["sbom_sha256"]
- or supplied.get("component_identity_sha256")
- != bound_release["component_identity_sha256"]
- ):
- raise DeliveryError(f"{label} contract does not bind actual artifact/SBOM identity")
- chain = supplied.get("migration_chain")
- head = supplied.get("migration_head")
- if (
- not isinstance(head, str)
- or not isinstance(chain, list)
- or not chain
- or not all(isinstance(item, str) and item for item in chain)
- or len(set(chain)) != len(chain)
- or chain[-1] != head
- ):
- raise DeliveryError(f"{label} migration chain and head are required")
- raw = {field: safe_bytes(item) for field, item in source.items()}
- config = read_json_bytes(raw["config"], f"{label} config schema")
- required = config.get("required", [])
- if not isinstance(required, list) or not all(isinstance(item, str) for item in required):
- raise DeliveryError(f"{label} config required list is invalid")
- value = {
- "openapi_sha256": sha_bytes(raw["openapi"]),
- "config_schema_sha256": sha_bytes(raw["config"]),
- "chart_sha256": sha_bytes(raw["chart"]),
- "values_sha256": sha_bytes(raw["values"]),
- "migration_head": head,
- "migration_chain": chain,
- "openapi_operations": sorted(
- re.findall(r"^ (/[^:\s]+):", raw["openapi"].decode(errors="strict"), re.MULTILINE)
- ),
- "config_required": sorted(required),
- }
- value["contract_sha256"] = sha_bytes(canonical(value))
- return value
- contracts = {
- "base": contract(args.base_contract, "base", releases["base"]),
- "candidate": contract(args.candidate_contract, "candidate", releases["candidate"]),
- "rollback": contract(args.rollback_contract, "rollback", releases["rollback"]),
- }
- base, candidate_contract, rollback_contract = (
- contracts["base"], contracts["candidate"], contracts["rollback"]
- )
- if not set(base["openapi_operations"]).issubset(candidate_contract["openapi_operations"]):
- raise DeliveryError("candidate OpenAPI removes a base operation")
- if not set(candidate_contract["config_required"]).issubset(base["config_required"]):
- raise DeliveryError("candidate config introduces a newly required setting")
- if candidate_contract["migration_chain"][: len(base["migration_chain"])] != base["migration_chain"]:
- raise DeliveryError("candidate migration chain is not an ordered extension of base")
- if (
- base["chart_sha256"] != candidate_contract["chart_sha256"]
- or base["values_sha256"] != candidate_contract["values_sha256"]
- or rollback_contract != base
- ):
- raise DeliveryError("base/candidate/rollback deployment contract is incompatible")
- if releases["rollback"] != releases["base"]:
- raise DeliveryError("rollback must restore the exact tested base artifact and SBOM")
- if releases["candidate"] != releases["base"]:
- if args.compatibility_approval is None:
- raise DeliveryError("changed artifact/SBOM requires a bound compatibility approval")
- approval = read_json(args.compatibility_approval)
- approval_time(approval)
- approval_payload = {
- "base": releases["base"],
- "candidate": releases["candidate"],
- "base_contract_sha256": base["contract_sha256"],
- "candidate_contract_sha256": candidate_contract["contract_sha256"],
- "approval_ref": approval.get("approval_ref"),
- "issued_at": approval.get("issued_at"),
- "expires_at": approval.get("expires_at"),
- }
- if (
- approval.get("base") != releases["base"]
- or approval.get("candidate") != releases["candidate"]
- or approval.get("base_contract_sha256") != base["contract_sha256"]
- or approval.get("candidate_contract_sha256")
- != candidate_contract["contract_sha256"]
- or not str(approval.get("approval_ref", "")).strip()
- ):
- raise DeliveryError("compatibility approval does not bind actual release pair")
- verify_controlled_signature(
- approval, approval_payload, "compatibility approval", False
- )
- value = {
- "base_version": args.base_version,
- "candidate_version": args.candidate_version,
- "migration_head": candidate_contract["migration_head"],
- "contracts": contracts,
- "openapi_sha256": candidate_contract["openapi_sha256"],
- "config_schema_sha256": candidate_contract["config_schema_sha256"],
- "chart_sha256": candidate_contract["chart_sha256"],
- "releases": releases,
- "comparison": {
- "base_to_candidate": "verified",
- "candidate_to_rollback": "verified_exact_base",
- "openapi": "base_operations_retained",
- "config_schema": "no_new_required_setting",
- "chart_values": "base_candidate_equal",
- "migration": "ancestry_and_rollback_checked",
- },
- "rollback": releases["rollback"],
- }
- value["matrix_sha256"] = sha_bytes(canonical(value))
- write_json(args.output, value)
- return {"status": "compatible", "matrix_sha256": value["matrix_sha256"]}
- def parser() -> argparse.ArgumentParser:
- parser = argparse.ArgumentParser()
- sub = parser.add_subparsers(dest="command", required=True)
- c = sub.add_parser("candidate")
- c.add_argument("--state-dir", type=Path, required=True)
- c.add_argument("--environment", required=True)
- c.add_argument("--version", required=True)
- c.add_argument("--artifact", type=Path, required=True)
- c.add_argument("--sbom", type=Path, required=True)
- c.add_argument("--scan", type=Path, required=True)
- c.add_argument("--license", type=Path, required=True)
- c.add_argument("--signature", type=Path, required=True)
- c.add_argument("--approval-ref", required=True)
- c.add_argument("--test-only", action="store_true")
- p = sub.add_parser("promote")
- p.add_argument("--state-dir", type=Path, required=True)
- p.add_argument("--environment", required=True)
- p.add_argument("--candidate-digest", required=True)
- p.add_argument("--expected-base-digest", required=True)
- r = sub.add_parser("rollback")
- r.add_argument("--state-dir", type=Path, required=True)
- r.add_argument("--environment", required=True)
- r.add_argument("--target-digest", required=True)
- r.add_argument("--approval-ref", required=True)
- b = sub.add_parser("backup-plan")
- b.add_argument("--backup-dir", type=Path, required=True)
- b.add_argument("--release-artifact", type=Path, required=True)
- b.add_argument("--config-artifact", type=Path, required=True)
- b.add_argument("--key-reference", required=True)
- b.add_argument("--encryption-provider", required=True)
- b.add_argument("--environment", default="test")
- b.add_argument("--signer-key-id")
- b.add_argument("--backup-signature", type=Path)
- b.add_argument("--test-only", action="store_true")
- rs = sub.add_parser("restore-plan")
- rs.add_argument("--backup-manifest", type=Path, required=True)
- rs.add_argument("--target", required=True)
- rs.add_argument("--confirm-target", required=True)
- rs.add_argument("--environment", required=True)
- rs.add_argument("--test-only", action="store_true")
- cm = sub.add_parser("compatibility")
- cm.add_argument("--output", type=Path, required=True)
- cm.add_argument("--base-version", required=True)
- cm.add_argument("--candidate-version", required=True)
- cm.add_argument("--base-artifact", type=Path, required=True)
- cm.add_argument("--base-sbom", type=Path, required=True)
- cm.add_argument("--candidate-artifact", type=Path, required=True)
- cm.add_argument("--candidate-sbom", type=Path, required=True)
- cm.add_argument("--rollback-artifact", type=Path, required=True)
- cm.add_argument("--rollback-sbom", type=Path, required=True)
- cm.add_argument("--compatibility-approval", type=Path)
- cm.add_argument("--base-contract", type=Path)
- cm.add_argument("--candidate-contract", type=Path)
- cm.add_argument("--rollback-contract", type=Path)
- return parser
- def main() -> int:
- args = parser().parse_args()
- try:
- result = {
- "candidate": command_candidate,
- "promote": command_promote,
- "rollback": command_rollback,
- "backup-plan": command_backup,
- "restore-plan": command_restore,
- "compatibility": command_compatibility,
- }[args.command](args)
- print(json.dumps(result, sort_keys=True))
- return 0
- except DeliveryError as exc:
- print(json.dumps({"status": "rejected", "error": str(exc)}, sort_keys=True))
- return 2
- if __name__ == "__main__":
- raise SystemExit(main())
|