2217 lines
91 KiB
Python
2217 lines
91 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify first-release snapshot evidence and emit a non-writing restore plan.
|
|
|
|
This command intentionally has no apply mode. A whole-host restore must run
|
|
from an independently authorized external control plane; this planner only
|
|
verifies immutable evidence, detached signatures, and the exact recovery scope.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import sys
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any, Callable, Mapping
|
|
|
|
from cryptography.exceptions import InvalidSignature
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
|
|
|
|
EVIDENCE_SCHEMA = "yuqei.first-release-snapshot-evidence.v2"
|
|
KEYRING_SCHEMA = "yuqei.first-release-recovery-keyring.v2"
|
|
APPROVAL_SCHEMA = "yuqei.first-release-recovery-approval.v2"
|
|
NO_PRIOR_RELEASE_SCHEMA = "yuqei.no-prior-successful-release.v1"
|
|
SNAPSHOT_RECEIPT_SCHEMA = "yuqei.host-snapshot-receipt.v1"
|
|
BACKUP_MANIFEST_SCHEMA_V2 = "yuqei-production-backup.v2"
|
|
BACKUP_MANIFEST_SCHEMA = "yuqei-production-backup.v3"
|
|
READ_ONLY_BACKUP_MANIFEST_SCHEMAS = frozenset(
|
|
{BACKUP_MANIFEST_SCHEMA_V2, BACKUP_MANIFEST_SCHEMA}
|
|
)
|
|
SOURCE_BACKUP_MANIFEST_SCHEMA = "yuqei-production-backup-artifacts.v2"
|
|
RESTORE_TEST_SCHEMA = "yuqei.first-release-restore-test.v1"
|
|
PLAN_SCHEMA = "yuqei.first-release-snapshot-restore-plan.v2"
|
|
READINESS_RECEIPT_SCHEMA = "yuqei.first-release-recovery-readiness-receipt.v2"
|
|
IMAGE_IDENTITY_SCHEMA = "yuqei.release-image-identity.v2"
|
|
PACKAGE_SCHEMA = "yuqei.release-package.v2"
|
|
READ_ONLY_PACKAGE_SCHEMAS = frozenset(
|
|
{"yuqei.release-package.v1", PACKAGE_SCHEMA}
|
|
)
|
|
SOURCE_MANIFEST_SCHEMA = "yuqei.release-source-manifest.v1"
|
|
CHECKSUM_MANIFEST_SCHEMA = "yuqei.release-package-checksums.v1"
|
|
INSTALLABLE_MANIFEST_SCHEMA = "yuqei.installable-release-manifest.v1"
|
|
RECOVERY_STRATEGY = "first_release_snapshot_restore"
|
|
RECOVERY_EVIDENCE_STATUS = "runtime_signed_evidence_required"
|
|
|
|
REQUIRED_ROLES = frozenset({"release_owner"})
|
|
REQUIRED_NAMED_REVIEWERS = 1
|
|
SOLE_REVIEWER_ID = "chen-yuqi"
|
|
SOLE_REVIEWER_NAME = "陈钰麒"
|
|
SOLE_REVIEWER_TITLE = "总经理"
|
|
REQUIRED_COMPONENTS = (
|
|
"host_runtime_state",
|
|
"postgres_consistent_backup",
|
|
"object_storage_state",
|
|
"release_environment_and_tls_bindings",
|
|
)
|
|
REQUIRED_DATABASES = ("contract", "ai", "operations")
|
|
REQUIRED_STORAGE_TARGETS = ("contract_files_volume", "minio_object_store")
|
|
APPLICATION_IMAGE_KEYS = (
|
|
"CONTRACT_API_IMAGE",
|
|
"CONTRACT_WEB_IMAGE",
|
|
"AI_PLATFORM_API_IMAGE",
|
|
"AI_WORKER_IMAGE",
|
|
)
|
|
INFRASTRUCTURE_IMAGE_KEYS = (
|
|
"POSTGRES_IMAGE",
|
|
"POSTGRES_CLIENT_IMAGE",
|
|
"REDIS_IMAGE",
|
|
"MINIO_IMAGE",
|
|
"MINIO_MC_IMAGE",
|
|
"NGINX_IMAGE",
|
|
"BACKUP_UTILITY_IMAGE",
|
|
)
|
|
PACKAGE_FILE = "release-package.json"
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
PACKAGE_VALIDATOR_SOURCE_CLOSURE = (
|
|
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
|
"yuqei-infra/deploy/release-package.py",
|
|
"yuqei-infra/backup/run-production-backup.py",
|
|
"yuqei-infra/backup/verify-production-backup.py",
|
|
"yuqei-infra/deploy/preflight-production.py",
|
|
"yuqei-infra/deploy/verify-production-compose.py",
|
|
"yuqei-infra/scripts/build-installable-release.py",
|
|
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
|
|
"yuqei-infra/scripts/build-third-party-materials.py",
|
|
"yuqei-infra/release-trust/third-party-materials-baseline.json",
|
|
"yuqei-infra/scripts/linux_release_common.py",
|
|
)
|
|
BACKUP_APPLICATION_IMAGE_KEYS = (
|
|
"CONTRACT_API_IMAGE",
|
|
"AI_PLATFORM_API_IMAGE",
|
|
"AI_WORKER_IMAGE",
|
|
)
|
|
BACKUP_OBJECT_STORAGE_IMAGE_KEYS = ("MINIO_IMAGE", "MINIO_MC_IMAGE")
|
|
MAX_EVIDENCE_AGE = timedelta(hours=24)
|
|
MAX_CLOCK_SKEW = timedelta(minutes=5)
|
|
MAX_JSON_BYTES = 8 * 1024 * 1024
|
|
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
IMAGE_REFERENCE_RE = re.compile(
|
|
r"^[a-z0-9](?:[a-z0-9._:/-]{0,510}[a-z0-9])?@sha256:[0-9a-f]{64}$"
|
|
)
|
|
IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,255}$")
|
|
TRUSTED_ROOT_KEY_ID = "yuqei-first-release-recovery-root-v1"
|
|
# Provision these pins together in a separately reviewed frozen release. Empty
|
|
# values intentionally keep the planner fail closed. A caller must never be
|
|
# able to nominate its own root or keyring revocation state on the command line.
|
|
TRUSTED_ROOT_PUBLIC_KEY_SHA256 = ""
|
|
TRUSTED_CURRENT_KEYRING_PAYLOAD_SHA256 = ""
|
|
MIN_KEYRING_EPOCH: int | None = None
|
|
|
|
|
|
class SnapshotRestorePlanError(RuntimeError):
|
|
"""Raised when first-release recovery evidence is unsafe or incomplete."""
|
|
|
|
|
|
def canonical_json_bytes(value: object) -> bytes:
|
|
try:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
except (TypeError, ValueError) as exc:
|
|
raise SnapshotRestorePlanError("value cannot be encoded as canonical JSON") from exc
|
|
|
|
|
|
def canonical_sha256(value: object) -> str:
|
|
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
|
|
|
|
|
def runtime_host_identity_sha256(
|
|
host_id: object,
|
|
docker_daemon_identity: Mapping[str, object],
|
|
) -> str:
|
|
"""Derive the recovery host identity from stable local Docker/host fields."""
|
|
|
|
normalized_host_id = _identifier(host_id, label="host_id")
|
|
required = ("docker_host", "daemon_id", "daemon_name", "docker_root_dir")
|
|
values: dict[str, str] = {}
|
|
for field in required:
|
|
value = docker_daemon_identity.get(field)
|
|
if not isinstance(value, str) or not value.strip():
|
|
raise SnapshotRestorePlanError(
|
|
f"Docker daemon identity field is missing for host binding: {field}"
|
|
)
|
|
values[field] = value
|
|
return canonical_sha256({"host_id": normalized_host_id, **values})
|
|
|
|
|
|
def file_sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _reject_constant(value: str) -> None:
|
|
raise SnapshotRestorePlanError(f"non-finite JSON value is forbidden: {value}")
|
|
|
|
|
|
def _reject_duplicate_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
result: dict[str, Any] = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise SnapshotRestorePlanError(f"duplicate JSON key is forbidden: {key}")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _absolute(path: Path) -> Path:
|
|
return Path(os.path.abspath(os.fspath(path)))
|
|
|
|
|
|
def _metadata_is_reparse(metadata: os.stat_result) -> bool:
|
|
reparse_attribute = int(getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400))
|
|
return bool(
|
|
stat.S_ISLNK(metadata.st_mode)
|
|
or int(getattr(metadata, "st_file_attributes", 0)) & reparse_attribute
|
|
or int(getattr(metadata, "st_reparse_tag", 0)) != 0
|
|
)
|
|
|
|
|
|
def _reject_symlink_components(path: Path, *, label: str) -> None:
|
|
absolute = _absolute(path)
|
|
current = Path(absolute.anchor)
|
|
for part in absolute.parts[1:]:
|
|
current /= part
|
|
try:
|
|
metadata = os.lstat(current)
|
|
if _metadata_is_reparse(metadata):
|
|
raise SnapshotRestorePlanError(f"{label} contains a symbolic link")
|
|
except FileNotFoundError:
|
|
continue
|
|
except OSError as exc:
|
|
raise SnapshotRestorePlanError(f"cannot inspect {label} path") from exc
|
|
|
|
|
|
def _regular_file(path: Path, *, label: str, max_bytes: int = MAX_JSON_BYTES) -> Path:
|
|
lexical = _absolute(path)
|
|
_reject_symlink_components(lexical, label=label)
|
|
try:
|
|
resolved = lexical.resolve(strict=True)
|
|
metadata = resolved.stat()
|
|
except OSError as exc:
|
|
raise SnapshotRestorePlanError(f"{label} is missing") from exc
|
|
if lexical.is_symlink() or not stat.S_ISREG(metadata.st_mode):
|
|
raise SnapshotRestorePlanError(f"{label} must be a regular file")
|
|
if metadata.st_nlink != 1:
|
|
raise SnapshotRestorePlanError(f"{label} must not be hard linked")
|
|
if metadata.st_size <= 0 or metadata.st_size > max_bytes:
|
|
raise SnapshotRestorePlanError(f"{label} has an invalid size")
|
|
return resolved
|
|
|
|
|
|
def _regular_directory(path: Path, *, label: str) -> Path:
|
|
lexical = _absolute(path)
|
|
_reject_symlink_components(lexical, label=label)
|
|
try:
|
|
resolved = lexical.resolve(strict=True)
|
|
except OSError as exc:
|
|
raise SnapshotRestorePlanError(f"{label} is missing") from exc
|
|
if lexical.is_symlink() or not resolved.is_dir():
|
|
raise SnapshotRestorePlanError(f"{label} must be a real directory")
|
|
return resolved
|
|
|
|
|
|
def load_json(path: Path, *, label: str) -> dict[str, Any]:
|
|
target = _regular_file(path, label=label)
|
|
try:
|
|
payload = json.loads(
|
|
target.read_text(encoding="utf-8-sig"),
|
|
object_pairs_hook=_reject_duplicate_pairs,
|
|
parse_constant=_reject_constant,
|
|
)
|
|
except SnapshotRestorePlanError:
|
|
raise
|
|
except (OSError, UnicodeError, ValueError) as exc:
|
|
raise SnapshotRestorePlanError(f"{label} is not strict UTF-8 JSON") from exc
|
|
if not isinstance(payload, dict):
|
|
raise SnapshotRestorePlanError(f"{label} must be a JSON object")
|
|
return payload
|
|
|
|
|
|
def _metadata_state(metadata: os.stat_result) -> tuple[int, ...]:
|
|
"""Return metadata that must stay stable for one already-open file view."""
|
|
|
|
return (
|
|
metadata.st_dev,
|
|
metadata.st_ino,
|
|
metadata.st_mode,
|
|
metadata.st_nlink,
|
|
metadata.st_size,
|
|
metadata.st_mtime_ns,
|
|
metadata.st_ctime_ns,
|
|
int(getattr(metadata, "st_file_attributes", 0)),
|
|
int(getattr(metadata, "st_reparse_tag", 0)),
|
|
)
|
|
|
|
|
|
def _assert_external_file_metadata(
|
|
metadata: os.stat_result,
|
|
*,
|
|
label: str,
|
|
max_bytes: int,
|
|
) -> None:
|
|
if (
|
|
not stat.S_ISREG(metadata.st_mode)
|
|
or metadata.st_nlink != 1
|
|
or metadata.st_ino == 0
|
|
or _metadata_is_reparse(metadata)
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
f"{label} must be a single-link non-reparse regular file"
|
|
)
|
|
if metadata.st_size <= 0 or metadata.st_size > max_bytes:
|
|
raise SnapshotRestorePlanError(f"{label} has an invalid size")
|
|
|
|
|
|
def _strict_json_object_from_bytes(raw: bytes, *, label: str) -> dict[str, Any]:
|
|
try:
|
|
payload = json.loads(
|
|
raw.decode("utf-8-sig"),
|
|
object_pairs_hook=_reject_duplicate_pairs,
|
|
parse_constant=_reject_constant,
|
|
)
|
|
except SnapshotRestorePlanError:
|
|
raise
|
|
except (UnicodeError, ValueError) as exc:
|
|
raise SnapshotRestorePlanError(f"{label} is not strict UTF-8 JSON") from exc
|
|
if not isinstance(payload, dict):
|
|
raise SnapshotRestorePlanError(f"{label} must be a JSON object")
|
|
return payload
|
|
|
|
|
|
def _read_stable_external_json(
|
|
path: Path,
|
|
*,
|
|
label: str,
|
|
expected_sha256: str,
|
|
max_bytes: int = MAX_JSON_BYTES,
|
|
) -> tuple[dict[str, Any], Path]:
|
|
"""Read, hash, parse, and revalidate an external record via one descriptor."""
|
|
|
|
lexical = _absolute(path)
|
|
_reject_symlink_components(lexical, label=label)
|
|
try:
|
|
path_before = os.lstat(lexical)
|
|
except OSError as exc:
|
|
raise SnapshotRestorePlanError(f"{label} is missing") from exc
|
|
_assert_external_file_metadata(path_before, label=label, max_bytes=max_bytes)
|
|
|
|
flags = (
|
|
os.O_RDONLY
|
|
| getattr(os, "O_BINARY", 0)
|
|
| getattr(os, "O_NOFOLLOW", 0)
|
|
)
|
|
try:
|
|
descriptor = os.open(lexical, flags)
|
|
except OSError as exc:
|
|
raise SnapshotRestorePlanError(f"{label} cannot be opened safely") from exc
|
|
try:
|
|
opened_before = os.fstat(descriptor)
|
|
_assert_external_file_metadata(opened_before, label=label, max_bytes=max_bytes)
|
|
if (
|
|
not os.path.samestat(path_before, opened_before)
|
|
or path_before.st_size != opened_before.st_size
|
|
or path_before.st_mtime_ns != opened_before.st_mtime_ns
|
|
):
|
|
raise SnapshotRestorePlanError(f"{label} path identity changed before reading")
|
|
|
|
chunks: list[bytes] = []
|
|
total = 0
|
|
while True:
|
|
try:
|
|
chunk = os.read(descriptor, min(1024 * 1024, max_bytes + 1 - total))
|
|
except OSError as exc:
|
|
raise SnapshotRestorePlanError(f"{label} cannot be read safely") from exc
|
|
if not chunk:
|
|
break
|
|
chunks.append(chunk)
|
|
total += len(chunk)
|
|
if total > max_bytes:
|
|
raise SnapshotRestorePlanError(f"{label} has an invalid size")
|
|
raw = b"".join(chunks)
|
|
if len(raw) != opened_before.st_size:
|
|
raise SnapshotRestorePlanError(f"{label} size changed while reading")
|
|
if hashlib.sha256(raw).hexdigest() != expected_sha256:
|
|
raise SnapshotRestorePlanError(f"{label} SHA-256 mismatch")
|
|
payload = _strict_json_object_from_bytes(raw, label=label)
|
|
|
|
opened_after = os.fstat(descriptor)
|
|
_assert_external_file_metadata(opened_after, label=label, max_bytes=max_bytes)
|
|
if _metadata_state(opened_after) != _metadata_state(opened_before):
|
|
raise SnapshotRestorePlanError(f"{label} metadata changed while reading")
|
|
try:
|
|
path_after = os.lstat(lexical)
|
|
except OSError as exc:
|
|
raise SnapshotRestorePlanError(f"{label} path changed while reading") from exc
|
|
_assert_external_file_metadata(path_after, label=label, max_bytes=max_bytes)
|
|
if (
|
|
_metadata_state(path_after) != _metadata_state(path_before)
|
|
or not os.path.samestat(path_after, opened_after)
|
|
or path_after.st_size != opened_after.st_size
|
|
or path_after.st_mtime_ns != opened_after.st_mtime_ns
|
|
):
|
|
raise SnapshotRestorePlanError(f"{label} path identity changed while reading")
|
|
_reject_symlink_components(lexical, label=label)
|
|
return payload, lexical
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _exact(value: object, fields: set[str], *, label: str) -> dict[str, Any]:
|
|
if not isinstance(value, dict) or set(value) != fields:
|
|
raise SnapshotRestorePlanError(f"{label} fields are missing or unknown")
|
|
return dict(value)
|
|
|
|
|
|
def _sha(value: object, *, label: str) -> str:
|
|
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
|
|
raise SnapshotRestorePlanError(f"{label} must be a lowercase SHA-256")
|
|
return value
|
|
|
|
|
|
def _identifier(value: object, *, label: str) -> str:
|
|
if not isinstance(value, str) or not IDENTIFIER_RE.fullmatch(value):
|
|
raise SnapshotRestorePlanError(f"{label} is missing or unsafe")
|
|
return value
|
|
|
|
|
|
def _timestamp(value: object, *, label: str) -> datetime:
|
|
if not isinstance(value, str):
|
|
raise SnapshotRestorePlanError(f"{label} must be RFC3339 text")
|
|
try:
|
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise SnapshotRestorePlanError(f"{label} is not RFC3339") from exc
|
|
if parsed.tzinfo is None:
|
|
raise SnapshotRestorePlanError(f"{label} must include a timezone")
|
|
return parsed.astimezone(UTC)
|
|
|
|
|
|
def _embedded_hash(payload: Mapping[str, Any], field: str, *, label: str) -> None:
|
|
expected = _sha(payload.get(field), label=f"{label} {field}")
|
|
unsigned = {key: value for key, value in payload.items() if key != field}
|
|
if canonical_sha256(unsigned) != expected:
|
|
raise SnapshotRestorePlanError(f"{label} payload hash mismatch")
|
|
|
|
|
|
def _safe_relative(value: object, *, label: str) -> str:
|
|
if not isinstance(value, str) or not value or "\\" in value or "\x00" in value:
|
|
raise SnapshotRestorePlanError(f"{label} must be a canonical relative path")
|
|
pure = PurePosixPath(value)
|
|
if pure.is_absolute() or pure.as_posix() != value or any(
|
|
part in {"", ".", ".."} for part in pure.parts
|
|
):
|
|
raise SnapshotRestorePlanError(f"{label} is unsafe")
|
|
return value
|
|
|
|
|
|
def _package_artifact(
|
|
package_file: Path,
|
|
package: Mapping[str, Any],
|
|
name: str,
|
|
) -> tuple[Path, str]:
|
|
artifacts = package.get("artifacts")
|
|
if not isinstance(artifacts, dict):
|
|
raise SnapshotRestorePlanError("release package has no artifact inventory")
|
|
record = _exact(artifacts.get(name), {"path", "sha256"}, label=f"artifact {name}")
|
|
relative = _safe_relative(record.get("path"), label=f"artifact {name} path")
|
|
expected = _sha(record.get("sha256"), label=f"artifact {name} SHA-256")
|
|
root = package_file.parent.resolve()
|
|
target = _regular_file(root.joinpath(*PurePosixPath(relative).parts), label=f"artifact {name}")
|
|
try:
|
|
target.relative_to(root)
|
|
except ValueError as exc:
|
|
raise SnapshotRestorePlanError(f"artifact {name} escapes the package") from exc
|
|
if file_sha256(target) != expected:
|
|
raise SnapshotRestorePlanError(f"artifact {name} SHA-256 mismatch")
|
|
return target, expected
|
|
|
|
|
|
def _external_record(value: object, *, label: str) -> tuple[dict[str, Any], str, Path]:
|
|
record = _exact(value, {"path", "sha256"}, label=f"{label} record")
|
|
raw_path = record.get("path")
|
|
if not isinstance(raw_path, str) or not Path(raw_path).is_absolute():
|
|
raise SnapshotRestorePlanError(f"{label} path must be absolute")
|
|
expected = _sha(record.get("sha256"), label=f"{label} external SHA-256")
|
|
payload, path = _read_stable_external_json(
|
|
Path(raw_path),
|
|
label=label,
|
|
expected_sha256=expected,
|
|
)
|
|
return payload, expected, path
|
|
|
|
|
|
def _validate_source_closure(
|
|
release_manifest: Mapping[str, Any],
|
|
*,
|
|
repo_root: Path,
|
|
) -> None:
|
|
if release_manifest.get("schema_version") != SOURCE_MANIFEST_SCHEMA:
|
|
raise SnapshotRestorePlanError("unsupported release source manifest schema")
|
|
_embedded_hash(release_manifest, "manifest_payload_sha256", label="release source manifest")
|
|
files = release_manifest.get("files")
|
|
items = files.get("items") if isinstance(files, dict) else None
|
|
if not isinstance(items, list) or files.get("count") != len(items):
|
|
raise SnapshotRestorePlanError("release source manifest file inventory is invalid")
|
|
by_path: dict[str, Mapping[str, Any]] = {}
|
|
for raw in items:
|
|
if not isinstance(raw, dict):
|
|
raise SnapshotRestorePlanError("release source manifest has an invalid file record")
|
|
relative = _safe_relative(raw.get("path"), label="release source path")
|
|
if relative in by_path:
|
|
raise SnapshotRestorePlanError("release source manifest repeats a path")
|
|
by_path[relative] = raw
|
|
root = _regular_directory(repo_root, label="repository root")
|
|
for relative in PACKAGE_VALIDATOR_SOURCE_CLOSURE:
|
|
record = by_path.get(relative)
|
|
if record is None:
|
|
raise SnapshotRestorePlanError(
|
|
f"release source manifest does not bind validator source: {relative}"
|
|
)
|
|
expected_sha = _sha(record.get("sha256"), label=f"{relative} source SHA-256")
|
|
expected_size = record.get("size")
|
|
if not isinstance(expected_size, int) or isinstance(expected_size, bool) or expected_size <= 0:
|
|
raise SnapshotRestorePlanError(f"{relative} source size is invalid")
|
|
target = _regular_file(
|
|
root.joinpath(*PurePosixPath(relative).parts),
|
|
label=f"validator source {relative}",
|
|
)
|
|
try:
|
|
target.relative_to(root)
|
|
except ValueError as exc:
|
|
raise SnapshotRestorePlanError("validator source escapes the repository") from exc
|
|
if target.stat().st_size != expected_size or file_sha256(target) != expected_sha:
|
|
raise SnapshotRestorePlanError(
|
|
f"repository validator source differs from the release manifest: {relative}"
|
|
)
|
|
|
|
|
|
def _bootstrap_release_package(
|
|
package_dir: Path,
|
|
expected_package_sha256: str,
|
|
*,
|
|
repo_root: Path,
|
|
) -> tuple[Path, dict[str, Any], str, dict[str, Any], str]:
|
|
root = _regular_directory(package_dir, label="release package directory")
|
|
package_path = _regular_file(root / PACKAGE_FILE, label="release package")
|
|
expected = _sha(expected_package_sha256, label="expected package SHA-256")
|
|
actual = file_sha256(package_path)
|
|
if actual != expected:
|
|
raise SnapshotRestorePlanError("release package does not match the approved SHA-256")
|
|
package = load_json(package_path, label="release package")
|
|
_embedded_hash(package, "package_payload_sha256", label="release package")
|
|
release_path, release_sha = _package_artifact(
|
|
package_path, package, "release_manifest"
|
|
)
|
|
release_manifest = load_json(release_path, label="release source manifest")
|
|
_validate_source_closure(release_manifest, repo_root=repo_root)
|
|
return root, package, actual, release_manifest, release_sha
|
|
|
|
|
|
def _load_release_package_validator(repo_root: Path) -> Callable[..., Any]:
|
|
root = _regular_directory(repo_root, label="repository root")
|
|
relative = "yuqei-infra/deploy/release-package.py"
|
|
path = _regular_file(
|
|
root.joinpath(*PurePosixPath(relative).parts),
|
|
label="official release-package validator",
|
|
)
|
|
expected_common = root / "yuqei-infra/scripts/linux_release_common.py"
|
|
ambient_common = sys.modules.get("linux_release_common")
|
|
if ambient_common is not None:
|
|
ambient_file = getattr(ambient_common, "__file__", None)
|
|
if ambient_file is None or Path(ambient_file).resolve() != expected_common.resolve():
|
|
raise SnapshotRestorePlanError("ambient linux_release_common module is forbidden")
|
|
module_name = "yuqei_first_release_official_release_package"
|
|
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
if spec is None or spec.loader is None:
|
|
raise SnapshotRestorePlanError("official release-package validator cannot be loaded")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[module_name] = module
|
|
previous = sys.dont_write_bytecode
|
|
sys.dont_write_bytecode = True
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
except Exception as exc:
|
|
sys.modules.pop(module_name, None)
|
|
raise SnapshotRestorePlanError("official release-package validator failed to load") from exc
|
|
finally:
|
|
sys.dont_write_bytecode = previous
|
|
validator = getattr(module, "validate_release_package", None)
|
|
if not callable(validator):
|
|
raise SnapshotRestorePlanError("official release-package validator entrypoint is missing")
|
|
return validator
|
|
|
|
|
|
def _validate_image_map(
|
|
value: object,
|
|
expected_keys: tuple[str, ...],
|
|
*,
|
|
label: str,
|
|
) -> dict[str, dict[str, str]]:
|
|
if not isinstance(value, dict) or set(value) != set(expected_keys):
|
|
raise SnapshotRestorePlanError(f"{label} does not cover the fixed image inventory")
|
|
result: dict[str, dict[str, str]] = {}
|
|
for key in expected_keys:
|
|
record = value.get(key)
|
|
if not isinstance(record, dict) or set(record) != {"reference", "digest"}:
|
|
raise SnapshotRestorePlanError(f"{label}.{key} identity is incomplete")
|
|
reference = record.get("reference")
|
|
digest = record.get("digest")
|
|
if (
|
|
not isinstance(reference, str)
|
|
or not IMAGE_REFERENCE_RE.fullmatch(reference)
|
|
or not isinstance(digest, str)
|
|
or not DIGEST_RE.fullmatch(digest)
|
|
or reference.rsplit("@", 1)[1] != digest
|
|
):
|
|
raise SnapshotRestorePlanError(f"{label}.{key} identity is invalid")
|
|
result[key] = {"reference": reference, "digest": digest}
|
|
return result
|
|
|
|
|
|
def _validate_offline_image_ids(
|
|
value: object,
|
|
*,
|
|
application_images: Mapping[str, Mapping[str, str]],
|
|
infrastructure_images: Mapping[str, Mapping[str, str]],
|
|
) -> dict[str, str]:
|
|
expected_images = {**application_images, **infrastructure_images}
|
|
if not isinstance(value, (list, tuple)) or len(value) != len(expected_images):
|
|
raise SnapshotRestorePlanError(
|
|
"official validator Docker import plan does not cover eleven images"
|
|
)
|
|
offline_image_ids: dict[str, str] = {}
|
|
for raw in value:
|
|
if not isinstance(raw, Mapping):
|
|
raise SnapshotRestorePlanError(
|
|
"official validator Docker import plan entry is invalid"
|
|
)
|
|
environment_key = raw.get("environment_key")
|
|
if not isinstance(environment_key, str) or environment_key not in expected_images:
|
|
raise SnapshotRestorePlanError(
|
|
"official validator Docker import plan image key is invalid"
|
|
)
|
|
if environment_key in offline_image_ids:
|
|
raise SnapshotRestorePlanError(
|
|
"official validator Docker import plan repeats an image key"
|
|
)
|
|
expected = expected_images[environment_key]
|
|
loaded_image_id = raw.get("expected_loaded_image_id")
|
|
if (
|
|
not isinstance(loaded_image_id, str)
|
|
or not DIGEST_RE.fullmatch(loaded_image_id)
|
|
or raw.get("offline_compose_override") != loaded_image_id
|
|
or raw.get("registry_reference_for_online_release")
|
|
!= expected["reference"]
|
|
or raw.get("expected_oci_manifest_digest") != expected["digest"]
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"official validator Docker import plan identity drifted"
|
|
)
|
|
offline_image_ids[environment_key] = loaded_image_id
|
|
if set(offline_image_ids) != set(expected_images):
|
|
raise SnapshotRestorePlanError(
|
|
"official validator Docker import plan image inventory is incomplete"
|
|
)
|
|
return offline_image_ids
|
|
|
|
|
|
def _validate_package(
|
|
package_dir: Path,
|
|
expected_package_sha256: str,
|
|
*,
|
|
repo_root: Path,
|
|
package_validator: Callable[..., Any] | None = None,
|
|
require_installable_package_v2: bool = False,
|
|
) -> dict[str, Any]:
|
|
root, package, actual, bootstrapped_release, release_sha = _bootstrap_release_package(
|
|
package_dir,
|
|
expected_package_sha256,
|
|
repo_root=repo_root,
|
|
)
|
|
validator = package_validator or _load_release_package_validator(repo_root)
|
|
try:
|
|
validated = validator(
|
|
root,
|
|
expected_package_sha256=expected_package_sha256,
|
|
repo_root=_regular_directory(repo_root, label="repository root"),
|
|
)
|
|
except Exception as exc:
|
|
raise SnapshotRestorePlanError(
|
|
"official release-package validation rejected the candidate"
|
|
) from exc
|
|
if getattr(validated, "root", None) != root:
|
|
raise SnapshotRestorePlanError("official validator returned another package root")
|
|
if getattr(validated, "package_sha256", None) != actual:
|
|
raise SnapshotRestorePlanError("official validator package hash drifted")
|
|
validated_package = getattr(validated, "package", None)
|
|
package_schema = package.get("schema_version")
|
|
if (
|
|
validated_package != package
|
|
or package_schema not in READ_ONLY_PACKAGE_SCHEMAS
|
|
or (require_installable_package_v2 and package_schema != PACKAGE_SCHEMA)
|
|
):
|
|
raise SnapshotRestorePlanError("official validator package payload/schema drifted")
|
|
release_manifest = getattr(validated, "release_manifest", None)
|
|
if release_manifest != bootstrapped_release:
|
|
raise SnapshotRestorePlanError("official validator release source manifest drifted")
|
|
if getattr(validated, "release_manifest_sha256", None) != release_sha:
|
|
raise SnapshotRestorePlanError("official validator release source hash drifted")
|
|
|
|
bound_artifacts: dict[str, tuple[dict[str, Any], str]] = {}
|
|
for artifact_name, attribute, schema in (
|
|
("checksum_manifest", "checksum_manifest", CHECKSUM_MANIFEST_SCHEMA),
|
|
("image_identity", "image_identity", IMAGE_IDENTITY_SCHEMA),
|
|
("installable_manifest", "installable_manifest", INSTALLABLE_MANIFEST_SCHEMA),
|
|
):
|
|
path, digest = _package_artifact(root / PACKAGE_FILE, package, artifact_name)
|
|
raw = load_json(path, label=artifact_name.replace("_", " "))
|
|
if raw.get("schema_version") != schema or getattr(validated, attribute, None) != raw:
|
|
raise SnapshotRestorePlanError(f"official validator {artifact_name} drifted")
|
|
if getattr(validated, f"{attribute}_sha256", None) != digest:
|
|
raise SnapshotRestorePlanError(f"official validator {artifact_name} hash drifted")
|
|
bound_artifacts[artifact_name] = (raw, digest)
|
|
|
|
identity, image_sha = bound_artifacts["image_identity"]
|
|
_embedded_hash(identity, "identity_payload_sha256", label="image identity")
|
|
release_id = _identifier(package.get("release_id"), label="release_id")
|
|
checksum_manifest = bound_artifacts["checksum_manifest"][0]
|
|
if checksum_manifest.get("release_id") != release_id:
|
|
raise SnapshotRestorePlanError("checksum manifest release binding is invalid")
|
|
checksum_files = checksum_manifest.get("files")
|
|
if not isinstance(checksum_files, list):
|
|
raise SnapshotRestorePlanError("checksum manifest file inventory is missing")
|
|
checksums_by_path = {
|
|
item.get("path"): item
|
|
for item in checksum_files
|
|
if isinstance(item, dict) and isinstance(item.get("path"), str)
|
|
}
|
|
for artifact_name in ("release_manifest", "image_identity", "installable_manifest"):
|
|
artifact_record = package["artifacts"].get(artifact_name)
|
|
checksum_record = (
|
|
checksums_by_path.get(artifact_record.get("path"))
|
|
if isinstance(artifact_record, dict)
|
|
else None
|
|
)
|
|
if (
|
|
not isinstance(checksum_record, dict)
|
|
or checksum_record.get("sha256") != artifact_record.get("sha256")
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
f"checksum manifest does not bind {artifact_name}"
|
|
)
|
|
if identity.get("release_id") != release_id:
|
|
raise SnapshotRestorePlanError("image identity release differs from the package")
|
|
source_commit = identity.get("source_commit")
|
|
source_candidate = release_manifest.get("candidate")
|
|
if (
|
|
not isinstance(source_commit, str)
|
|
or not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", source_commit)
|
|
or not isinstance(source_candidate, dict)
|
|
or source_candidate.get("commit") != source_commit
|
|
or identity.get("source_manifest_sha256") != release_sha
|
|
):
|
|
raise SnapshotRestorePlanError("image identity source binding is invalid")
|
|
application_images = _validate_image_map(
|
|
identity.get("application_images"),
|
|
APPLICATION_IMAGE_KEYS,
|
|
label="application_images",
|
|
)
|
|
infrastructure_images = _validate_image_map(
|
|
identity.get("infrastructure_images"),
|
|
INFRASTRUCTURE_IMAGE_KEYS,
|
|
label="infrastructure_images",
|
|
)
|
|
if (
|
|
getattr(validated, "application_images", None) != application_images
|
|
or getattr(validated, "infrastructure_images", None) != infrastructure_images
|
|
):
|
|
raise SnapshotRestorePlanError("official validator eleven-image identity drifted")
|
|
offline_image_ids = _validate_offline_image_ids(
|
|
getattr(validated, "docker_import_plan", None),
|
|
application_images=application_images,
|
|
infrastructure_images=infrastructure_images,
|
|
)
|
|
|
|
installable = bound_artifacts["installable_manifest"][0]
|
|
installable_candidate = installable.get("candidate")
|
|
if not isinstance(installable_candidate, dict) or installable_candidate.get("commit") != source_commit:
|
|
raise SnapshotRestorePlanError("installable manifest candidate binding is invalid")
|
|
installable_images = installable.get("images")
|
|
if not isinstance(installable_images, list) or len(installable_images) != len(
|
|
APPLICATION_IMAGE_KEYS
|
|
):
|
|
raise SnapshotRestorePlanError("installable manifest four-image inventory is incomplete")
|
|
installable_by_key: dict[str, Mapping[str, Any]] = {}
|
|
for raw in installable_images:
|
|
if not isinstance(raw, dict) or not isinstance(raw.get("environment_key"), str):
|
|
raise SnapshotRestorePlanError("installable manifest image record is invalid")
|
|
key = raw["environment_key"]
|
|
if key in installable_by_key:
|
|
raise SnapshotRestorePlanError("installable manifest repeats an image key")
|
|
installable_by_key[key] = raw
|
|
if set(installable_by_key) != set(APPLICATION_IMAGE_KEYS):
|
|
raise SnapshotRestorePlanError("installable manifest four-image keys are incomplete")
|
|
for key, identity_record in application_images.items():
|
|
installable_record = installable_by_key[key]
|
|
if (
|
|
installable_record.get("registry_reference") != identity_record["reference"]
|
|
or installable_record.get("manifest_digest") != identity_record["digest"]
|
|
):
|
|
raise SnapshotRestorePlanError("installable/application image identity drifted")
|
|
|
|
recovery = identity.get("recovery")
|
|
recovery_fields = {
|
|
"strategy",
|
|
"evidence_status",
|
|
"first_release_only",
|
|
"predecessor_package_required",
|
|
"database_downgrade_allowed",
|
|
"required_restore_scopes",
|
|
"required_postgres_databases",
|
|
"required_storage_targets",
|
|
}
|
|
if not isinstance(recovery, dict) or set(recovery) != recovery_fields:
|
|
raise SnapshotRestorePlanError("package first-release recovery fields are incomplete")
|
|
if (
|
|
recovery.get("strategy") != RECOVERY_STRATEGY
|
|
or recovery.get("evidence_status") != RECOVERY_EVIDENCE_STATUS
|
|
or recovery.get("first_release_only") is not True
|
|
or recovery.get("predecessor_package_required") is not False
|
|
or recovery.get("database_downgrade_allowed") is not False
|
|
or recovery.get("required_restore_scopes") != list(REQUIRED_COMPONENTS)
|
|
or recovery.get("required_postgres_databases") != list(REQUIRED_DATABASES)
|
|
or recovery.get("required_storage_targets") != list(REQUIRED_STORAGE_TARGETS)
|
|
):
|
|
raise SnapshotRestorePlanError("package first-release recovery policy is unsafe")
|
|
trusted_receipt = _sha(
|
|
package.get("trusted_build_receipt_sha256"),
|
|
label="trusted build receipt SHA-256",
|
|
)
|
|
installable_receipt = installable.get("build_receipt")
|
|
if (
|
|
not isinstance(installable_receipt, dict)
|
|
or installable_receipt.get("sha256") != trusted_receipt
|
|
):
|
|
raise SnapshotRestorePlanError("installable trusted build receipt binding is invalid")
|
|
if getattr(validated, "trusted_build_receipt_sha256", None) != trusted_receipt:
|
|
raise SnapshotRestorePlanError("official validator trusted build receipt drifted")
|
|
return {
|
|
"release_id": release_id,
|
|
"package_schema_version": package_schema,
|
|
"package_sha256": actual,
|
|
"source_commit": source_commit,
|
|
"release_manifest_sha256": release_sha,
|
|
"checksum_manifest_sha256": bound_artifacts["checksum_manifest"][1],
|
|
"image_identity_sha256": image_sha,
|
|
"installable_manifest_sha256": bound_artifacts["installable_manifest"][1],
|
|
"trusted_build_receipt_sha256": trusted_receipt,
|
|
"compose_project_name": package.get("compose_project_name"),
|
|
"application_image_count": len(application_images),
|
|
"infrastructure_image_count": len(infrastructure_images),
|
|
"application_images": application_images,
|
|
"infrastructure_images": infrastructure_images,
|
|
"offline_image_ids": offline_image_ids,
|
|
}
|
|
|
|
|
|
def _validate_keyring(
|
|
keyring: Mapping[str, Any],
|
|
*,
|
|
root_public_key: Ed25519PublicKey,
|
|
now: datetime,
|
|
) -> dict[str, dict[str, Any]]:
|
|
_exact(
|
|
keyring,
|
|
{
|
|
"schema_version",
|
|
"root_key_id",
|
|
"epoch",
|
|
"issued_at",
|
|
"expires_at",
|
|
"keys",
|
|
"keyring_payload_sha256",
|
|
"root_signature_base64",
|
|
},
|
|
label="recovery keyring",
|
|
)
|
|
if keyring.get("schema_version") != KEYRING_SCHEMA:
|
|
raise SnapshotRestorePlanError("unsupported recovery keyring schema")
|
|
unsigned = {
|
|
key: keyring[key]
|
|
for key in (
|
|
"schema_version",
|
|
"root_key_id",
|
|
"epoch",
|
|
"issued_at",
|
|
"expires_at",
|
|
"keys",
|
|
)
|
|
}
|
|
embedded = _sha(
|
|
keyring.get("keyring_payload_sha256"), label="keyring payload hash"
|
|
)
|
|
if canonical_sha256(unsigned) != embedded:
|
|
raise SnapshotRestorePlanError("recovery keyring payload hash mismatch")
|
|
if not SHA256_RE.fullmatch(TRUSTED_CURRENT_KEYRING_PAYLOAD_SHA256):
|
|
raise SnapshotRestorePlanError(
|
|
"current first-release recovery keyring pin is not provisioned"
|
|
)
|
|
if (
|
|
not isinstance(MIN_KEYRING_EPOCH, int)
|
|
or isinstance(MIN_KEYRING_EPOCH, bool)
|
|
or MIN_KEYRING_EPOCH < 1
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"minimum first-release recovery keyring epoch is not provisioned"
|
|
)
|
|
if keyring.get("root_key_id") != TRUSTED_ROOT_KEY_ID:
|
|
raise SnapshotRestorePlanError("recovery keyring uses the wrong root key")
|
|
try:
|
|
root_public_key.verify(
|
|
base64.b64decode(keyring.get("root_signature_base64"), validate=True),
|
|
canonical_json_bytes(unsigned),
|
|
)
|
|
except (TypeError, ValueError, InvalidSignature) as exc:
|
|
raise SnapshotRestorePlanError("recovery keyring root signature is invalid") from exc
|
|
epoch = keyring.get("epoch")
|
|
if not isinstance(epoch, int) or isinstance(epoch, bool) or epoch < 1:
|
|
raise SnapshotRestorePlanError("recovery keyring epoch is invalid")
|
|
if epoch < MIN_KEYRING_EPOCH:
|
|
raise SnapshotRestorePlanError("recovery keyring epoch is older than the trusted minimum")
|
|
if embedded != TRUSTED_CURRENT_KEYRING_PAYLOAD_SHA256:
|
|
raise SnapshotRestorePlanError(
|
|
"recovery keyring is not the currently trusted revocation state"
|
|
)
|
|
issued = _timestamp(keyring.get("issued_at"), label="keyring issued_at")
|
|
expires = _timestamp(keyring.get("expires_at"), label="keyring expires_at")
|
|
if issued > now + MAX_CLOCK_SKEW or now >= expires or expires <= issued:
|
|
raise SnapshotRestorePlanError("recovery keyring is expired or not yet valid")
|
|
raw_keys = keyring.get("keys")
|
|
if not isinstance(raw_keys, list) or not raw_keys:
|
|
raise SnapshotRestorePlanError("recovery keyring has insufficient keys")
|
|
result: dict[str, dict[str, Any]] = {}
|
|
public_keys: set[bytes] = set()
|
|
root_public_raw = root_public_key.public_bytes(
|
|
serialization.Encoding.Raw,
|
|
serialization.PublicFormat.Raw,
|
|
)
|
|
for raw in raw_keys:
|
|
item = _exact(
|
|
raw,
|
|
{
|
|
"key_id",
|
|
"approver_id",
|
|
"role",
|
|
"public_key_base64",
|
|
"not_before",
|
|
"not_after",
|
|
"revoked",
|
|
},
|
|
label="recovery key",
|
|
)
|
|
key_id = _identifier(item.get("key_id"), label="key_id")
|
|
if key_id in result:
|
|
raise SnapshotRestorePlanError("recovery keyring repeats a key_id")
|
|
if _identifier(item.get("approver_id"), label="approver_id") != SOLE_REVIEWER_ID:
|
|
raise SnapshotRestorePlanError("recovery keyring contains an unauthorized reviewer")
|
|
role = item.get("role")
|
|
if role not in REQUIRED_ROLES:
|
|
raise SnapshotRestorePlanError("recovery key has an unsupported role")
|
|
if item.get("revoked") is not False:
|
|
raise SnapshotRestorePlanError("revoked recovery keys are forbidden")
|
|
try:
|
|
public_raw = base64.b64decode(item.get("public_key_base64"), validate=True)
|
|
public_key = Ed25519PublicKey.from_public_bytes(public_raw)
|
|
except (TypeError, ValueError) as exc:
|
|
raise SnapshotRestorePlanError("recovery public key is invalid") from exc
|
|
if public_raw in public_keys:
|
|
raise SnapshotRestorePlanError(
|
|
"recovery keyring reuses an Ed25519 public key across recovery roles"
|
|
)
|
|
if public_raw == root_public_raw:
|
|
raise SnapshotRestorePlanError(
|
|
"recovery approval key must differ from the offline root key"
|
|
)
|
|
public_keys.add(public_raw)
|
|
not_before = _timestamp(item.get("not_before"), label="key not_before")
|
|
not_after = _timestamp(item.get("not_after"), label="key not_after")
|
|
if not_after <= not_before:
|
|
raise SnapshotRestorePlanError("recovery key validity window is invalid")
|
|
result[key_id] = {
|
|
**item,
|
|
"public_key": public_key,
|
|
"not_before_parsed": not_before,
|
|
"not_after_parsed": not_after,
|
|
}
|
|
return result
|
|
|
|
|
|
def _validate_no_prior_release(
|
|
value: object,
|
|
*,
|
|
environment_id: str,
|
|
release_id: str,
|
|
) -> tuple[dict[str, Any], str, datetime]:
|
|
payload, digest, _path = _external_record(value, label="no-prior-release receipt")
|
|
_exact(
|
|
payload,
|
|
{
|
|
"schema_version",
|
|
"environment_id",
|
|
"candidate_release_id",
|
|
"successful_release_count",
|
|
"status",
|
|
"observed_at",
|
|
"receipt_payload_sha256",
|
|
},
|
|
label="no-prior-release receipt",
|
|
)
|
|
if payload.get("schema_version") != NO_PRIOR_RELEASE_SCHEMA:
|
|
raise SnapshotRestorePlanError("unsupported no-prior-release receipt schema")
|
|
_embedded_hash(payload, "receipt_payload_sha256", label="no-prior-release receipt")
|
|
if (
|
|
payload.get("environment_id") != environment_id
|
|
or payload.get("candidate_release_id") != release_id
|
|
or payload.get("successful_release_count") != 0
|
|
or payload.get("status") != "confirmed_no_prior_successful_release"
|
|
):
|
|
raise SnapshotRestorePlanError("no-prior-release receipt does not prove a first release")
|
|
return payload, digest, _timestamp(payload.get("observed_at"), label="no-prior observed_at")
|
|
|
|
|
|
def _validate_snapshot_component(
|
|
value: object,
|
|
*,
|
|
host_id: str,
|
|
consistency_set_id: str,
|
|
) -> tuple[dict[str, Any], str, datetime]:
|
|
component = _exact(
|
|
value,
|
|
{"provider_receipt", "off_host", "immutable", "status"},
|
|
label="host_runtime_state",
|
|
)
|
|
if component.get("off_host") is not True or component.get("immutable") is not True or component.get("status") != "available":
|
|
raise SnapshotRestorePlanError("host snapshot must be off-host, immutable, and available")
|
|
receipt, digest, _path = _external_record(
|
|
component.get("provider_receipt"), label="host snapshot provider receipt"
|
|
)
|
|
_exact(
|
|
receipt,
|
|
{
|
|
"schema_version",
|
|
"provider",
|
|
"control_plane_id",
|
|
"snapshot_id",
|
|
"source_host_id",
|
|
"consistency_set_id",
|
|
"off_host",
|
|
"immutable",
|
|
"status",
|
|
"created_at",
|
|
"receipt_payload_sha256",
|
|
},
|
|
label="host snapshot provider receipt",
|
|
)
|
|
if receipt.get("schema_version") != SNAPSHOT_RECEIPT_SCHEMA:
|
|
raise SnapshotRestorePlanError("unsupported host snapshot receipt schema")
|
|
_embedded_hash(receipt, "receipt_payload_sha256", label="host snapshot receipt")
|
|
if (
|
|
receipt.get("source_host_id") != host_id
|
|
or receipt.get("consistency_set_id") != consistency_set_id
|
|
or receipt.get("off_host") is not True
|
|
or receipt.get("immutable") is not True
|
|
or receipt.get("status") != "available"
|
|
):
|
|
raise SnapshotRestorePlanError("host snapshot receipt does not match the recovery target")
|
|
_identifier(receipt.get("provider"), label="snapshot provider")
|
|
_identifier(receipt.get("control_plane_id"), label="snapshot control_plane_id")
|
|
_identifier(receipt.get("snapshot_id"), label="snapshot_id")
|
|
return receipt, digest, _timestamp(receipt.get("created_at"), label="snapshot created_at")
|
|
|
|
|
|
def _validate_source_backup_manifest_binding(
|
|
manifest: Mapping[str, Any],
|
|
*,
|
|
package_binding: Mapping[str, Any],
|
|
) -> tuple[dict[str, Any], str]:
|
|
source, source_sha, _source_path = _external_record(
|
|
manifest.get("source_backup_manifest"),
|
|
label="source artifacts.v2 backup manifest",
|
|
)
|
|
if (
|
|
source_sha != manifest.get("source_backup_manifest_sha256")
|
|
or source.get("schema_version") != SOURCE_BACKUP_MANIFEST_SCHEMA
|
|
or manifest.get("source_backup_schema") != SOURCE_BACKUP_MANIFEST_SCHEMA
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"formal backup manifest source artifacts.v2 binding is invalid"
|
|
)
|
|
_exact(
|
|
source,
|
|
{
|
|
"schema_version",
|
|
"backup_id",
|
|
"created_at",
|
|
"release_id",
|
|
"compose_project_name",
|
|
"consistency_mode",
|
|
"database_names",
|
|
"images",
|
|
"object_storage_mirror",
|
|
"artifacts",
|
|
},
|
|
label="source artifacts.v2 backup manifest",
|
|
)
|
|
if (
|
|
source.get("backup_id") != manifest.get("backup_id")
|
|
or source.get("created_at") != manifest.get("completed_at")
|
|
or source.get("release_id") != manifest.get("release_id")
|
|
or source.get("consistency_mode") != "quiesced"
|
|
or source.get("compose_project_name")
|
|
!= package_binding.get("compose_project_name")
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"source artifacts.v2 backup manifest differs from the recovery set"
|
|
)
|
|
_timestamp(source.get("created_at"), label="source backup created_at")
|
|
database_names = _exact(
|
|
source.get("database_names"),
|
|
{"contract", "ai_platform", "operations"},
|
|
label="source backup database names",
|
|
)
|
|
for value in database_names.values():
|
|
_identifier(value, label="source backup database name")
|
|
|
|
source_images = _exact(
|
|
source.get("images"),
|
|
{"contract_api", "ai_platform_api", "ai_worker"},
|
|
label="source backup application images",
|
|
)
|
|
expected_application = {
|
|
"CONTRACT_API_IMAGE": source_images["contract_api"],
|
|
"AI_PLATFORM_API_IMAGE": source_images["ai_platform_api"],
|
|
"AI_WORKER_IMAGE": source_images["ai_worker"],
|
|
}
|
|
if manifest.get("application_runtime_images") != expected_application:
|
|
raise SnapshotRestorePlanError(
|
|
"formal backup application images differ from source artifacts.v2"
|
|
)
|
|
|
|
object_storage = _exact(
|
|
source.get("object_storage_mirror"),
|
|
{
|
|
"engine",
|
|
"engine_version",
|
|
"engine_image",
|
|
"client",
|
|
"client_version",
|
|
"client_image",
|
|
"engine_runtime_image",
|
|
"engine_resolved_image_id",
|
|
"client_resolved_image_id",
|
|
"bucket",
|
|
"path",
|
|
"object_count",
|
|
"total_size_bytes",
|
|
"content_index_sha256",
|
|
},
|
|
label="source backup object-storage mirror",
|
|
)
|
|
if (
|
|
object_storage.get("engine") != "rustfs"
|
|
or object_storage.get("engine_version") != "1.0.0"
|
|
or object_storage.get("client") != "rustfs-rc"
|
|
or object_storage.get("client_version") != "0.1.36"
|
|
or object_storage.get("path") != "minio"
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"source backup object-storage implementation is unsupported"
|
|
)
|
|
offline_runtime = isinstance(
|
|
object_storage.get("engine_runtime_image"), str
|
|
) and DIGEST_RE.fullmatch(str(object_storage["engine_runtime_image"]))
|
|
expected_object_storage = {
|
|
"MINIO_IMAGE": {
|
|
"candidate_reference": object_storage.get("engine_image"),
|
|
"runtime_reference": object_storage.get("engine_runtime_image"),
|
|
"resolved_image_id": object_storage.get("engine_resolved_image_id"),
|
|
},
|
|
"MINIO_MC_IMAGE": {
|
|
"candidate_reference": object_storage.get("client_image"),
|
|
"runtime_reference": (
|
|
object_storage.get("client_resolved_image_id")
|
|
if offline_runtime
|
|
else object_storage.get("client_image")
|
|
),
|
|
"resolved_image_id": object_storage.get("client_resolved_image_id"),
|
|
},
|
|
}
|
|
if manifest.get("object_storage_runtime_images") != expected_object_storage:
|
|
raise SnapshotRestorePlanError(
|
|
"formal backup object-storage images differ from source artifacts.v2"
|
|
)
|
|
|
|
raw_artifacts = source.get("artifacts")
|
|
if not isinstance(raw_artifacts, list) or not raw_artifacts:
|
|
raise SnapshotRestorePlanError("source backup artifact inventory is missing")
|
|
artifacts: dict[str, dict[str, Any]] = {}
|
|
folded_artifact_paths: set[str] = set()
|
|
for raw in raw_artifacts:
|
|
record = _exact(
|
|
raw,
|
|
{"path", "size_bytes", "sha256"},
|
|
label="source backup artifact",
|
|
)
|
|
relative = _safe_relative(record.get("path"), label="source backup artifact path")
|
|
size = record.get("size_bytes")
|
|
digest = record.get("sha256")
|
|
folded_relative = relative.casefold()
|
|
if (
|
|
relative in artifacts
|
|
or folded_relative in folded_artifact_paths
|
|
or not isinstance(size, int)
|
|
or isinstance(size, bool)
|
|
or size < 0
|
|
):
|
|
raise SnapshotRestorePlanError("source backup artifact inventory is invalid")
|
|
_sha(digest, label="source backup artifact SHA-256")
|
|
artifacts[relative] = dict(record)
|
|
folded_artifact_paths.add(folded_relative)
|
|
|
|
database_paths = {
|
|
"contract": "postgres-contract.dump",
|
|
"ai": "postgres-ai_platform.dump",
|
|
"operations": "postgres-operations.dump",
|
|
}
|
|
databases = manifest.get("databases")
|
|
if not isinstance(databases, Mapping):
|
|
raise SnapshotRestorePlanError("formal backup database inventory is missing")
|
|
for name, relative in database_paths.items():
|
|
artifact = artifacts.get(relative)
|
|
record = databases.get(name)
|
|
if (
|
|
not isinstance(artifact, Mapping)
|
|
or not isinstance(record, Mapping)
|
|
or record.get("artifact_sha256") != artifact.get("sha256")
|
|
or record.get("size_bytes") != artifact.get("size_bytes")
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
f"formal backup database {name} differs from source artifacts.v2"
|
|
)
|
|
contract_artifact = artifacts.get("contract-files.tar.gz")
|
|
contract_files = manifest.get("contract_files")
|
|
if (
|
|
not isinstance(contract_artifact, Mapping)
|
|
or not isinstance(contract_files, Mapping)
|
|
or contract_files.get("archive_sha256") != contract_artifact.get("sha256")
|
|
or contract_files.get("size_bytes") != contract_artifact.get("size_bytes")
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"formal backup contract files differ from source artifacts.v2"
|
|
)
|
|
minio_records = [
|
|
record
|
|
for path, record in artifacts.items()
|
|
if path.startswith("minio/")
|
|
]
|
|
minio_records.sort(key=lambda item: str(item["path"]).encode("utf-8"))
|
|
minio = manifest.get("minio")
|
|
if (
|
|
not isinstance(minio, Mapping)
|
|
or minio.get("object_count") != len(minio_records)
|
|
or minio.get("total_size_bytes")
|
|
!= sum(int(item["size_bytes"]) for item in minio_records)
|
|
or minio.get("content_index_sha256") != canonical_sha256(minio_records)
|
|
or minio.get("object_count") != object_storage.get("object_count")
|
|
or minio.get("total_size_bytes") != object_storage.get("total_size_bytes")
|
|
or minio.get("content_index_sha256")
|
|
!= object_storage.get("content_index_sha256")
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"formal backup object inventory differs from source artifacts.v2"
|
|
)
|
|
return source, source_sha
|
|
|
|
|
|
def _validate_formal_backup_image_binding(
|
|
manifest: Mapping[str, Any],
|
|
*,
|
|
package_binding: Mapping[str, Any],
|
|
) -> dict[str, Any]:
|
|
_source, source_backup_sha = _validate_source_backup_manifest_binding(
|
|
manifest,
|
|
package_binding=package_binding,
|
|
)
|
|
application_images = package_binding.get("application_images")
|
|
infrastructure_images = package_binding.get("infrastructure_images")
|
|
offline_image_ids = package_binding.get("offline_image_ids")
|
|
if (
|
|
not isinstance(application_images, Mapping)
|
|
or not isinstance(infrastructure_images, Mapping)
|
|
or not isinstance(offline_image_ids, Mapping)
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"release package does not expose candidate runtime image identities"
|
|
)
|
|
|
|
application_runtime = _exact(
|
|
manifest.get("application_runtime_images"),
|
|
set(BACKUP_APPLICATION_IMAGE_KEYS),
|
|
label="backup application runtime images",
|
|
)
|
|
runtime_modes: set[str] = set()
|
|
normalized_application: dict[str, str] = {}
|
|
for key in BACKUP_APPLICATION_IMAGE_KEYS:
|
|
candidate = application_images.get(key)
|
|
offline_image_id = offline_image_ids.get(key)
|
|
runtime_identity = application_runtime.get(key)
|
|
if (
|
|
not isinstance(candidate, Mapping)
|
|
or not isinstance(candidate.get("reference"), str)
|
|
or not isinstance(offline_image_id, str)
|
|
or not DIGEST_RE.fullmatch(offline_image_id)
|
|
or not isinstance(runtime_identity, str)
|
|
or runtime_identity not in {candidate["reference"], offline_image_id}
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
f"backup application runtime image {key} differs from the release package"
|
|
)
|
|
runtime_modes.add(
|
|
"online" if runtime_identity == candidate["reference"] else "offline"
|
|
)
|
|
normalized_application[key] = runtime_identity
|
|
|
|
object_storage_runtime = _exact(
|
|
manifest.get("object_storage_runtime_images"),
|
|
set(BACKUP_OBJECT_STORAGE_IMAGE_KEYS),
|
|
label="backup object-storage runtime images",
|
|
)
|
|
normalized_object_storage: dict[str, dict[str, str]] = {}
|
|
for key in BACKUP_OBJECT_STORAGE_IMAGE_KEYS:
|
|
record = _exact(
|
|
object_storage_runtime.get(key),
|
|
{"candidate_reference", "runtime_reference", "resolved_image_id"},
|
|
label=f"backup object-storage runtime image {key}",
|
|
)
|
|
candidate = infrastructure_images.get(key)
|
|
offline_image_id = offline_image_ids.get(key)
|
|
if (
|
|
not isinstance(candidate, Mapping)
|
|
or not isinstance(candidate.get("reference"), str)
|
|
or record.get("candidate_reference") != candidate["reference"]
|
|
or not isinstance(offline_image_id, str)
|
|
or not DIGEST_RE.fullmatch(offline_image_id)
|
|
or record.get("resolved_image_id") != offline_image_id
|
|
or record.get("runtime_reference")
|
|
not in {candidate["reference"], offline_image_id}
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
f"backup object-storage runtime image {key} differs from the release package"
|
|
)
|
|
runtime_modes.add(
|
|
"online"
|
|
if record["runtime_reference"] == candidate["reference"]
|
|
else "offline"
|
|
)
|
|
normalized_object_storage[key] = dict(record)
|
|
if len(runtime_modes) != 1:
|
|
raise SnapshotRestorePlanError(
|
|
"backup runtime image identities mix online and offline release modes"
|
|
)
|
|
return {
|
|
"source_backup_manifest_sha256": source_backup_sha,
|
|
"runtime_mode": next(iter(runtime_modes)),
|
|
"application_runtime_images": normalized_application,
|
|
"object_storage_runtime_images": normalized_object_storage,
|
|
}
|
|
|
|
|
|
def _validate_backup_components(
|
|
postgres_value: object,
|
|
object_value: object,
|
|
*,
|
|
package_binding: Mapping[str, Any],
|
|
environment_id: str,
|
|
consistency_set_id: str,
|
|
require_formal_backup_manifest: bool = False,
|
|
) -> tuple[dict[str, Any], str, datetime]:
|
|
postgres = _exact(
|
|
postgres_value,
|
|
{"backup_manifest", "consistency_mode", "databases"},
|
|
label="postgres_consistent_backup",
|
|
)
|
|
if postgres.get("consistency_mode") != "quiesced" or postgres.get("databases") != list(REQUIRED_DATABASES):
|
|
raise SnapshotRestorePlanError("postgres backup must be quiesced and cover contract/ai/operations")
|
|
objects = _exact(
|
|
object_value,
|
|
{"backup_manifest", "stores"},
|
|
label="object_storage_state",
|
|
)
|
|
if objects.get("stores") != list(REQUIRED_STORAGE_TARGETS):
|
|
raise SnapshotRestorePlanError(
|
|
"object storage state must cover contract_files_volume and minio_object_store"
|
|
)
|
|
if objects.get("backup_manifest") != postgres.get("backup_manifest"):
|
|
raise SnapshotRestorePlanError("database and object storage must share one backup manifest")
|
|
manifest, digest, _path = _external_record(
|
|
postgres.get("backup_manifest"), label="consistent backup manifest"
|
|
)
|
|
schema_version = manifest.get("schema_version")
|
|
if schema_version not in READ_ONLY_BACKUP_MANIFEST_SCHEMAS:
|
|
raise SnapshotRestorePlanError("unsupported consistent backup manifest schema")
|
|
if require_formal_backup_manifest and schema_version != BACKUP_MANIFEST_SCHEMA:
|
|
raise SnapshotRestorePlanError(
|
|
"legacy v2 backup manifest is read-only and cannot mint new release evidence"
|
|
)
|
|
expected_fields = {
|
|
"schema_version",
|
|
"backup_id",
|
|
"release_id",
|
|
"environment_id",
|
|
"consistency_set_id",
|
|
"consistency_mode",
|
|
"databases",
|
|
"contract_files",
|
|
"minio",
|
|
"completed_at",
|
|
"status",
|
|
"manifest_payload_sha256",
|
|
}
|
|
if schema_version == BACKUP_MANIFEST_SCHEMA:
|
|
expected_fields.update(
|
|
{
|
|
"source_backup_schema",
|
|
"source_backup_manifest_sha256",
|
|
"source_backup_manifest",
|
|
"application_runtime_images",
|
|
"object_storage_runtime_images",
|
|
}
|
|
)
|
|
_exact(
|
|
manifest,
|
|
expected_fields,
|
|
label="consistent backup manifest",
|
|
)
|
|
_embedded_hash(manifest, "manifest_payload_sha256", label="consistent backup manifest")
|
|
release_id = _identifier(package_binding.get("release_id"), label="release_id")
|
|
if (
|
|
manifest.get("release_id") != release_id
|
|
or manifest.get("environment_id") != environment_id
|
|
or manifest.get("consistency_set_id") != consistency_set_id
|
|
or manifest.get("consistency_mode") != "quiesced"
|
|
or manifest.get("status") != "complete"
|
|
):
|
|
raise SnapshotRestorePlanError("consistent backup manifest is bound to another recovery set")
|
|
databases = manifest.get("databases")
|
|
if (
|
|
not isinstance(databases, dict)
|
|
or len(databases) != len(REQUIRED_DATABASES)
|
|
or set(databases) != set(REQUIRED_DATABASES)
|
|
):
|
|
raise SnapshotRestorePlanError("consistent backup manifest database inventory is incomplete")
|
|
for name in REQUIRED_DATABASES:
|
|
record = _exact(databases[name], {"artifact_sha256", "size_bytes", "schema_revision"}, label=f"{name} backup")
|
|
_sha(record.get("artifact_sha256"), label=f"{name} backup SHA-256")
|
|
if not isinstance(record.get("size_bytes"), int) or isinstance(record.get("size_bytes"), bool) or record["size_bytes"] <= 0:
|
|
raise SnapshotRestorePlanError(f"{name} backup size is invalid")
|
|
_identifier(record.get("schema_revision"), label=f"{name} schema revision")
|
|
contract_files = _exact(
|
|
manifest.get("contract_files"),
|
|
{"archive_sha256", "size_bytes", "file_count"},
|
|
label="contract_files backup",
|
|
)
|
|
_sha(contract_files.get("archive_sha256"), label="contract_files archive SHA-256")
|
|
minio = _exact(
|
|
manifest.get("minio"),
|
|
{"content_index_sha256", "object_count", "total_size_bytes"},
|
|
label="MinIO backup",
|
|
)
|
|
_sha(minio.get("content_index_sha256"), label="MinIO content index SHA-256")
|
|
for record, fields, label in (
|
|
(contract_files, ("size_bytes", "file_count"), "contract_files"),
|
|
(minio, ("object_count", "total_size_bytes"), "MinIO"),
|
|
):
|
|
if any(not isinstance(record.get(field), int) or isinstance(record.get(field), bool) or record[field] < 0 for field in fields):
|
|
raise SnapshotRestorePlanError(f"{label} backup counters are invalid")
|
|
if schema_version == BACKUP_MANIFEST_SCHEMA:
|
|
_validate_formal_backup_image_binding(
|
|
manifest,
|
|
package_binding=package_binding,
|
|
)
|
|
return manifest, digest, _timestamp(manifest.get("completed_at"), label="backup completed_at")
|
|
|
|
|
|
def _validate_bindings(value: object) -> dict[str, str]:
|
|
bindings = _exact(
|
|
value,
|
|
{
|
|
"production_env_sha256",
|
|
"tls_certificate_sha256",
|
|
"tls_private_key_sha256",
|
|
"tls_trusted_ca_sha256",
|
|
},
|
|
label="release_environment_and_tls_bindings",
|
|
)
|
|
return {key: _sha(bindings[key], label=key) for key in sorted(bindings)}
|
|
|
|
|
|
def _validate_restore_test(
|
|
value: object,
|
|
*,
|
|
release_id: str,
|
|
environment_id: str,
|
|
consistency_set_id: str,
|
|
snapshot_receipt_sha256: str,
|
|
backup_manifest_sha256: str,
|
|
bindings_sha256: str,
|
|
) -> tuple[dict[str, Any], str, datetime]:
|
|
payload, digest, _path = _external_record(value, label="independent restore test")
|
|
_exact(
|
|
payload,
|
|
{
|
|
"schema_version",
|
|
"release_id",
|
|
"source_environment_id",
|
|
"restore_environment_id",
|
|
"restore_environment_class",
|
|
"consistency_set_id",
|
|
"snapshot_receipt_sha256",
|
|
"backup_manifest_sha256",
|
|
"release_environment_and_tls_bindings_sha256",
|
|
"host_snapshot_restored",
|
|
"databases_restored",
|
|
"contract_files_restored",
|
|
"minio_restored",
|
|
"environment_tls_bindings_verified",
|
|
"database_downgrade_performed",
|
|
"status",
|
|
"tested_at",
|
|
"evidence_payload_sha256",
|
|
},
|
|
label="independent restore test",
|
|
)
|
|
if payload.get("schema_version") != RESTORE_TEST_SCHEMA:
|
|
raise SnapshotRestorePlanError("unsupported independent restore-test schema")
|
|
_embedded_hash(payload, "evidence_payload_sha256", label="independent restore test")
|
|
if (
|
|
payload.get("release_id") != release_id
|
|
or payload.get("source_environment_id") != environment_id
|
|
or payload.get("restore_environment_id") == environment_id
|
|
or payload.get("restore_environment_class") != "isolated_uat"
|
|
or payload.get("consistency_set_id") != consistency_set_id
|
|
or payload.get("snapshot_receipt_sha256") != snapshot_receipt_sha256
|
|
or payload.get("backup_manifest_sha256") != backup_manifest_sha256
|
|
or payload.get("release_environment_and_tls_bindings_sha256")
|
|
!= bindings_sha256
|
|
or payload.get("host_snapshot_restored") is not True
|
|
or payload.get("databases_restored") != list(REQUIRED_DATABASES)
|
|
or payload.get("contract_files_restored") is not True
|
|
or payload.get("minio_restored") is not True
|
|
or payload.get("environment_tls_bindings_verified") is not True
|
|
or payload.get("database_downgrade_performed") is not False
|
|
or payload.get("status") != "passed"
|
|
):
|
|
raise SnapshotRestorePlanError("independent restore test is incomplete or mismatched")
|
|
_identifier(payload.get("restore_environment_id"), label="restore environment_id")
|
|
return payload, digest, _timestamp(payload.get("tested_at"), label="restore tested_at")
|
|
|
|
|
|
def _validate_approvals(
|
|
value: object,
|
|
*,
|
|
evidence_payload_sha256: str,
|
|
trusted_keys: Mapping[str, Mapping[str, Any]],
|
|
evidence_not_before: datetime,
|
|
now: datetime,
|
|
) -> list[dict[str, Any]]:
|
|
if not isinstance(value, list) or len(value) != 1:
|
|
raise SnapshotRestorePlanError("exactly one recovery approval is required")
|
|
roles: set[str] = set()
|
|
people: set[str] = set()
|
|
key_ids: set[str] = set()
|
|
approval_ids: set[str] = set()
|
|
approvals: list[dict[str, Any]] = []
|
|
fields = {
|
|
"schema_version",
|
|
"approval_id",
|
|
"approver_id",
|
|
"role",
|
|
"key_id",
|
|
"signed_at",
|
|
"expires_at",
|
|
"evidence_payload_sha256",
|
|
"signature_base64",
|
|
}
|
|
for raw in value:
|
|
item = _exact(raw, fields, label="recovery approval")
|
|
if item.get("schema_version") != APPROVAL_SCHEMA:
|
|
raise SnapshotRestorePlanError("unsupported recovery approval schema")
|
|
approval_id = _identifier(item.get("approval_id"), label="approval_id")
|
|
if approval_id in approval_ids:
|
|
raise SnapshotRestorePlanError("recovery approvals repeat an approval_id")
|
|
approval_ids.add(approval_id)
|
|
person = _identifier(item.get("approver_id"), label="approver_id")
|
|
if person != SOLE_REVIEWER_ID:
|
|
raise SnapshotRestorePlanError("recovery approval uses the wrong named reviewer")
|
|
role = item.get("role")
|
|
if role not in REQUIRED_ROLES:
|
|
raise SnapshotRestorePlanError("recovery approval role is invalid")
|
|
key_id = _identifier(item.get("key_id"), label="approval key_id")
|
|
key = trusted_keys.get(key_id)
|
|
if key is None or key["approver_id"] != person or key["role"] != role:
|
|
raise SnapshotRestorePlanError("recovery approval does not match a trusted key")
|
|
if item.get("evidence_payload_sha256") != evidence_payload_sha256:
|
|
raise SnapshotRestorePlanError("recovery approval is bound to different evidence")
|
|
signed_at = _timestamp(item.get("signed_at"), label="approval signed_at")
|
|
expires_at = _timestamp(item.get("expires_at"), label="approval expires_at")
|
|
if (
|
|
signed_at < evidence_not_before
|
|
or signed_at > now + MAX_CLOCK_SKEW
|
|
or expires_at <= signed_at
|
|
or now >= expires_at
|
|
or signed_at < key["not_before_parsed"]
|
|
or expires_at > key["not_after_parsed"]
|
|
):
|
|
raise SnapshotRestorePlanError("recovery approval validity window is invalid")
|
|
signed = {field: item[field] for field in item if field != "signature_base64"}
|
|
try:
|
|
signature = base64.b64decode(item.get("signature_base64"), validate=True)
|
|
key["public_key"].verify(signature, canonical_json_bytes(signed))
|
|
except (TypeError, ValueError, InvalidSignature) as exc:
|
|
raise SnapshotRestorePlanError("recovery approval signature is invalid") from exc
|
|
roles.add(str(role))
|
|
people.add(person)
|
|
key_ids.add(key_id)
|
|
approvals.append(item)
|
|
if (
|
|
roles != REQUIRED_ROLES
|
|
or len(people) != REQUIRED_NAMED_REVIEWERS
|
|
or len(key_ids) != len(REQUIRED_ROLES)
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"recovery approval requires the sole release owner and one operational key"
|
|
)
|
|
return approvals
|
|
|
|
|
|
def validate_evidence(
|
|
evidence: Mapping[str, Any],
|
|
*,
|
|
package_binding: Mapping[str, Any],
|
|
keyring: Mapping[str, Any],
|
|
trusted_root_public_key: Ed25519PublicKey,
|
|
now: datetime,
|
|
require_formal_backup_manifest: bool = False,
|
|
) -> dict[str, Any]:
|
|
if now.tzinfo is None:
|
|
raise SnapshotRestorePlanError("validation time must include a timezone")
|
|
current = now.astimezone(UTC)
|
|
_exact(
|
|
evidence,
|
|
{
|
|
"schema_version",
|
|
"evidence_id",
|
|
"mode",
|
|
"release_binding",
|
|
"target",
|
|
"consistency_set_id",
|
|
"captured_at",
|
|
"expires_at",
|
|
"no_prior_successful_release_receipt",
|
|
"components",
|
|
"independent_restore_test",
|
|
"evidence_payload_sha256",
|
|
"approvals",
|
|
},
|
|
label="first-release recovery evidence",
|
|
)
|
|
if evidence.get("schema_version") != EVIDENCE_SCHEMA or evidence.get("mode") != "FIRST_RELEASE_PREINSTALL_SNAPSHOT_RESTORE":
|
|
raise SnapshotRestorePlanError("unsupported first-release recovery evidence")
|
|
evidence_id = _identifier(evidence.get("evidence_id"), label="evidence_id")
|
|
release_binding = _exact(
|
|
evidence.get("release_binding"),
|
|
{"release_id", "package_sha256", "source_commit", "image_identity_sha256"},
|
|
label="release binding",
|
|
)
|
|
if release_binding != {
|
|
key: package_binding[key]
|
|
for key in ("release_id", "package_sha256", "source_commit", "image_identity_sha256")
|
|
}:
|
|
raise SnapshotRestorePlanError("recovery evidence is bound to a different package")
|
|
target = _exact(
|
|
evidence.get("target"),
|
|
{
|
|
"environment_id",
|
|
"environment_class",
|
|
"host_id",
|
|
"host_identity_sha256",
|
|
"docker_daemon_identity_sha256",
|
|
"compose_project_name",
|
|
},
|
|
label="recovery target",
|
|
)
|
|
environment_id = _identifier(target.get("environment_id"), label="environment_id")
|
|
host_id = _identifier(target.get("host_id"), label="host_id")
|
|
if target.get("environment_class") not in {"isolated_uat", "production"}:
|
|
raise SnapshotRestorePlanError("recovery target environment class is invalid")
|
|
if target.get("compose_project_name") != package_binding.get("compose_project_name"):
|
|
raise SnapshotRestorePlanError("recovery target Compose project differs from the package")
|
|
_sha(target.get("host_identity_sha256"), label="host identity SHA-256")
|
|
_sha(
|
|
target.get("docker_daemon_identity_sha256"),
|
|
label="Docker daemon identity SHA-256",
|
|
)
|
|
consistency_set_id = _identifier(
|
|
evidence.get("consistency_set_id"), label="consistency_set_id"
|
|
)
|
|
captured_at = _timestamp(evidence.get("captured_at"), label="captured_at")
|
|
expires_at = _timestamp(evidence.get("expires_at"), label="expires_at")
|
|
if (
|
|
captured_at > current + MAX_CLOCK_SKEW
|
|
or current - captured_at > MAX_EVIDENCE_AGE
|
|
or expires_at <= captured_at
|
|
or current >= expires_at
|
|
or expires_at - captured_at > MAX_EVIDENCE_AGE
|
|
):
|
|
raise SnapshotRestorePlanError("first-release recovery evidence is stale or expired")
|
|
|
|
no_prior, no_prior_sha, no_prior_at = _validate_no_prior_release(
|
|
evidence.get("no_prior_successful_release_receipt"),
|
|
environment_id=environment_id,
|
|
release_id=package_binding["release_id"],
|
|
)
|
|
components = _exact(
|
|
evidence.get("components"), set(REQUIRED_COMPONENTS), label="recovery components"
|
|
)
|
|
snapshot, snapshot_sha, snapshot_at = _validate_snapshot_component(
|
|
components["host_runtime_state"],
|
|
host_id=host_id,
|
|
consistency_set_id=consistency_set_id,
|
|
)
|
|
backup, backup_sha, backup_at = _validate_backup_components(
|
|
components["postgres_consistent_backup"],
|
|
components["object_storage_state"],
|
|
package_binding=package_binding,
|
|
environment_id=environment_id,
|
|
consistency_set_id=consistency_set_id,
|
|
require_formal_backup_manifest=require_formal_backup_manifest,
|
|
)
|
|
bindings = _validate_bindings(components["release_environment_and_tls_bindings"])
|
|
bindings_sha = canonical_sha256(bindings)
|
|
restore, restore_sha, restored_at = _validate_restore_test(
|
|
evidence.get("independent_restore_test"),
|
|
release_id=package_binding["release_id"],
|
|
environment_id=environment_id,
|
|
consistency_set_id=consistency_set_id,
|
|
snapshot_receipt_sha256=snapshot_sha,
|
|
backup_manifest_sha256=backup_sha,
|
|
bindings_sha256=bindings_sha,
|
|
)
|
|
evidence_times = (no_prior_at, snapshot_at, backup_at, restored_at)
|
|
if any(item > current + MAX_CLOCK_SKEW for item in evidence_times):
|
|
raise SnapshotRestorePlanError("recovery component timestamp is in the future")
|
|
if any(current - item > MAX_EVIDENCE_AGE for item in evidence_times):
|
|
raise SnapshotRestorePlanError("recovery component evidence is stale")
|
|
if restored_at < max(snapshot_at, backup_at) or captured_at < max(evidence_times):
|
|
raise SnapshotRestorePlanError("restore test or capture predates the bound snapshot/backup")
|
|
|
|
unsigned = {
|
|
key: evidence[key]
|
|
for key in evidence
|
|
if key not in {"evidence_payload_sha256", "approvals"}
|
|
}
|
|
evidence_hash = _sha(
|
|
evidence.get("evidence_payload_sha256"), label="evidence payload hash"
|
|
)
|
|
if canonical_sha256(unsigned) != evidence_hash:
|
|
raise SnapshotRestorePlanError("first-release evidence payload hash mismatch")
|
|
trusted_keys = _validate_keyring(
|
|
keyring, root_public_key=trusted_root_public_key, now=current
|
|
)
|
|
approvals = _validate_approvals(
|
|
evidence.get("approvals"),
|
|
evidence_payload_sha256=evidence_hash,
|
|
trusted_keys=trusted_keys,
|
|
evidence_not_before=max(captured_at, no_prior_at, snapshot_at, backup_at, restored_at),
|
|
now=current,
|
|
)
|
|
return {
|
|
"evidence_id": evidence_id,
|
|
"evidence_payload_sha256": evidence_hash,
|
|
"release_binding": release_binding,
|
|
"target": target,
|
|
"consistency_set_id": consistency_set_id,
|
|
"captured_at": evidence["captured_at"],
|
|
"expires_at": evidence["expires_at"],
|
|
"freshness": {
|
|
"maximum_age_seconds": int(MAX_EVIDENCE_AGE.total_seconds()),
|
|
"no_prior_observed_at": no_prior["observed_at"],
|
|
"snapshot_created_at": snapshot["created_at"],
|
|
"backup_completed_at": backup["completed_at"],
|
|
"restore_tested_at": restore["tested_at"],
|
|
},
|
|
"no_prior_release_receipt_sha256": no_prior_sha,
|
|
"snapshot": {
|
|
"provider": snapshot["provider"],
|
|
"control_plane_id": snapshot["control_plane_id"],
|
|
"snapshot_id": snapshot["snapshot_id"],
|
|
"provider_receipt_sha256": snapshot_sha,
|
|
},
|
|
"backup": {
|
|
"backup_id": backup["backup_id"],
|
|
"schema_version": backup["schema_version"],
|
|
"manifest_sha256": backup_sha,
|
|
"source_backup_manifest_sha256": backup.get(
|
|
"source_backup_manifest_sha256"
|
|
),
|
|
"databases": list(REQUIRED_DATABASES),
|
|
"storage_targets": list(REQUIRED_STORAGE_TARGETS),
|
|
},
|
|
"bindings": {**bindings, "bindings_payload_sha256": bindings_sha},
|
|
"restore_test": {
|
|
"restore_environment_id": restore["restore_environment_id"],
|
|
"evidence_sha256": restore_sha,
|
|
"tested_at": restore["tested_at"],
|
|
},
|
|
"reviewer": {
|
|
"approver_id": SOLE_REVIEWER_ID,
|
|
"name": SOLE_REVIEWER_NAME,
|
|
"title": SOLE_REVIEWER_TITLE,
|
|
},
|
|
"approvals": approvals,
|
|
"keyring": {
|
|
"epoch": keyring["epoch"],
|
|
"payload_sha256": keyring["keyring_payload_sha256"],
|
|
"expires_at": keyring["expires_at"],
|
|
},
|
|
}
|
|
|
|
|
|
def build_plan(
|
|
*,
|
|
package_binding: Mapping[str, Any],
|
|
validated_evidence: Mapping[str, Any],
|
|
trusted_root_public_key_sha256: str,
|
|
generated_at: datetime,
|
|
) -> dict[str, Any]:
|
|
if generated_at.tzinfo is None:
|
|
raise SnapshotRestorePlanError("plan time must include a timezone")
|
|
unsigned: dict[str, Any] = {
|
|
"schema_version": PLAN_SCHEMA,
|
|
"status": "signature_verified_against_frozen_pins_plan_only",
|
|
"generated_at": generated_at.astimezone(UTC).isoformat().replace("+00:00", "Z"),
|
|
"mode": "FIRST_RELEASE_PREINSTALL_SNAPSHOT_RESTORE",
|
|
"release_binding": dict(validated_evidence["release_binding"]),
|
|
"target": dict(validated_evidence["target"]),
|
|
"consistency_set_id": validated_evidence["consistency_set_id"],
|
|
"evidence_id": validated_evidence["evidence_id"],
|
|
"evidence_payload_sha256": validated_evidence["evidence_payload_sha256"],
|
|
"trusted_root_public_key_sha256": _sha(
|
|
trusted_root_public_key_sha256, label="trusted root public key SHA-256"
|
|
),
|
|
"keyring": dict(validated_evidence["keyring"]),
|
|
"reviewer": dict(validated_evidence["reviewer"]),
|
|
"approval_ids": sorted(
|
|
item["approval_id"] for item in validated_evidence["approvals"]
|
|
),
|
|
"snapshot": dict(validated_evidence["snapshot"]),
|
|
"backup": dict(validated_evidence["backup"]),
|
|
"bindings": dict(validated_evidence["bindings"]),
|
|
"independent_restore_test": dict(validated_evidence["restore_test"]),
|
|
"restore_sequence": [
|
|
"obtain_separate_destructive_restore_authorization",
|
|
"confirm_no_public_traffic_or_confirm_old_service_switchback",
|
|
"isolate_and_power_off_exact_target_host",
|
|
"restore_immutable_host_runtime_snapshot_from_external_control_plane",
|
|
"verify_contract_ai_operations_database_state",
|
|
"verify_contract_files_and_minio_state",
|
|
"verify_release_environment_and_tls_hash_bindings",
|
|
"run_health_and_main_journey_acceptance",
|
|
],
|
|
"required_components": list(REQUIRED_COMPONENTS),
|
|
"database_downgrade_allowed": False,
|
|
"fake_predecessor_package_allowed": False,
|
|
"execution_entrypoint_available": False,
|
|
"apply_allowed": False,
|
|
"writes_performed": False,
|
|
"docker_invoked": False,
|
|
"remote_connection_attempted": False,
|
|
"traffic_or_dns_change_performed": False,
|
|
"claims_not_made": [
|
|
"No snapshot was created or restored by this plan-only command.",
|
|
"Temporary test keys do not establish a production trust anchor.",
|
|
"This plan is not UAT, production, pilot, public-release, or legal approval.",
|
|
],
|
|
}
|
|
unsigned["plan_payload_sha256"] = canonical_sha256(unsigned)
|
|
return unsigned
|
|
|
|
|
|
def build_install_readiness_receipt(
|
|
*,
|
|
package_binding: Mapping[str, Any],
|
|
validated_evidence: Mapping[str, Any],
|
|
trusted_root_public_key_sha256: str,
|
|
generated_at: datetime,
|
|
) -> dict[str, Any]:
|
|
if generated_at.tzinfo is None:
|
|
raise SnapshotRestorePlanError("receipt time must include a timezone")
|
|
backup = validated_evidence.get("backup")
|
|
if (
|
|
not isinstance(backup, Mapping)
|
|
or backup.get("schema_version") != BACKUP_MANIFEST_SCHEMA
|
|
or not isinstance(backup.get("source_backup_manifest_sha256"), str)
|
|
):
|
|
raise SnapshotRestorePlanError(
|
|
"formal install readiness requires a verified v3 backup manifest"
|
|
)
|
|
approval_expiries = [
|
|
_timestamp(item["expires_at"], label="approval expires_at")
|
|
for item in validated_evidence["approvals"]
|
|
]
|
|
freshness_expiries = [
|
|
_timestamp(value, label=f"freshness {key}") + MAX_EVIDENCE_AGE
|
|
for key, value in validated_evidence["freshness"].items()
|
|
if key.endswith("_at")
|
|
]
|
|
expires_at = min(
|
|
_timestamp(validated_evidence["expires_at"], label="evidence expires_at"),
|
|
_timestamp(
|
|
validated_evidence["keyring"]["expires_at"],
|
|
label="keyring expires_at",
|
|
),
|
|
*approval_expiries,
|
|
*freshness_expiries,
|
|
)
|
|
moment = generated_at.astimezone(UTC)
|
|
if moment >= expires_at:
|
|
raise SnapshotRestorePlanError("recovery readiness receipt would already be expired")
|
|
release_binding = {
|
|
key: package_binding[key]
|
|
for key in (
|
|
"release_id",
|
|
"package_sha256",
|
|
"source_commit",
|
|
"release_manifest_sha256",
|
|
"checksum_manifest_sha256",
|
|
"image_identity_sha256",
|
|
"installable_manifest_sha256",
|
|
"trusted_build_receipt_sha256",
|
|
)
|
|
}
|
|
unsigned: dict[str, Any] = {
|
|
"schema_version": READINESS_RECEIPT_SCHEMA,
|
|
"result": "PASS_RECOVERY_READINESS",
|
|
"purpose": "release_package_install_apply_gate",
|
|
"install_apply_gate": True,
|
|
"snapshot_restore_apply_allowed": False,
|
|
"generated_at": moment.isoformat().replace("+00:00", "Z"),
|
|
"expires_at": expires_at.isoformat().replace("+00:00", "Z"),
|
|
"release_binding": release_binding,
|
|
"target": dict(validated_evidence["target"]),
|
|
"image_inventory": {
|
|
"application_image_count": package_binding["application_image_count"],
|
|
"infrastructure_image_count": package_binding["infrastructure_image_count"],
|
|
"total_image_count": package_binding["application_image_count"]
|
|
+ package_binding["infrastructure_image_count"],
|
|
},
|
|
"evidence": {
|
|
"evidence_id": validated_evidence["evidence_id"],
|
|
"evidence_payload_sha256": validated_evidence["evidence_payload_sha256"],
|
|
"evidence_artifact_sha256": validated_evidence["raw_artifact_sha256"][
|
|
"evidence"
|
|
],
|
|
"captured_at": validated_evidence["captured_at"],
|
|
"expires_at": validated_evidence["expires_at"],
|
|
"freshness": dict(validated_evidence["freshness"]),
|
|
"no_prior_successful_release_receipt_sha256": validated_evidence[
|
|
"no_prior_release_receipt_sha256"
|
|
],
|
|
},
|
|
"trust": {
|
|
"root_key_id": TRUSTED_ROOT_KEY_ID,
|
|
"trusted_root_public_key_sha256": _sha(
|
|
trusted_root_public_key_sha256,
|
|
label="trusted root public key SHA-256",
|
|
),
|
|
"keyring_epoch": validated_evidence["keyring"]["epoch"],
|
|
"keyring_payload_sha256": validated_evidence["keyring"]["payload_sha256"],
|
|
"keyring_artifact_sha256": validated_evidence["raw_artifact_sha256"][
|
|
"keyring"
|
|
],
|
|
},
|
|
"reviewer": dict(validated_evidence["reviewer"]),
|
|
"approvals": [dict(item) for item in validated_evidence["approvals"]],
|
|
"snapshot": dict(validated_evidence["snapshot"]),
|
|
"backup": dict(validated_evidence["backup"]),
|
|
"independent_restore_test": dict(validated_evidence["restore_test"]),
|
|
"required_restore_scopes": list(REQUIRED_COMPONENTS),
|
|
"required_postgres_databases": list(REQUIRED_DATABASES),
|
|
"required_storage_targets": list(REQUIRED_STORAGE_TARGETS),
|
|
"release_environment_and_tls_bindings": dict(
|
|
validated_evidence["bindings"]
|
|
),
|
|
"claims_not_made": [
|
|
"This receipt does not authorize or execute snapshot restoration.",
|
|
"This receipt does not authorize traffic, DNS, UAT, pilot, production, or public release.",
|
|
"The release-package install gate must revalidate the raw package and evidence inputs before use.",
|
|
],
|
|
}
|
|
unsigned["receipt_payload_sha256"] = canonical_sha256(unsigned)
|
|
return unsigned
|
|
|
|
|
|
def _read_root_public_key(path: Path) -> tuple[Ed25519PublicKey, str]:
|
|
if not SHA256_RE.fullmatch(TRUSTED_ROOT_PUBLIC_KEY_SHA256):
|
|
raise SnapshotRestorePlanError(
|
|
"organization first-release recovery root key is not provisioned"
|
|
)
|
|
target = _regular_file(path, label="trusted root public key", max_bytes=32)
|
|
raw = target.read_bytes()
|
|
if len(raw) != 32:
|
|
raise SnapshotRestorePlanError("trusted root public key must contain 32 raw bytes")
|
|
actual = hashlib.sha256(raw).hexdigest()
|
|
if actual != TRUSTED_ROOT_PUBLIC_KEY_SHA256:
|
|
raise SnapshotRestorePlanError(
|
|
"trusted root public key does not match the frozen trust anchor"
|
|
)
|
|
try:
|
|
return Ed25519PublicKey.from_public_bytes(raw), actual
|
|
except ValueError as exc:
|
|
raise SnapshotRestorePlanError("trusted root public key is invalid") from exc
|
|
|
|
|
|
def _verify_raw_inputs(
|
|
*,
|
|
package_dir: Path,
|
|
expected_package_sha256: str,
|
|
repo_root: Path,
|
|
evidence_file: Path,
|
|
keyring_file: Path,
|
|
trusted_root_public_key_file: Path,
|
|
now: datetime,
|
|
package_validator_for_tests: Callable[..., Any] | None = None,
|
|
require_installable_package_v2: bool = False,
|
|
require_formal_backup_manifest: bool = False,
|
|
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
|
package_binding = _validate_package(
|
|
package_dir,
|
|
expected_package_sha256,
|
|
repo_root=repo_root,
|
|
package_validator=package_validator_for_tests,
|
|
require_installable_package_v2=require_installable_package_v2,
|
|
)
|
|
evidence = load_json(evidence_file, label="first-release recovery evidence")
|
|
keyring = load_json(keyring_file, label="recovery keyring")
|
|
root_key, root_sha = _read_root_public_key(trusted_root_public_key_file)
|
|
validated = validate_evidence(
|
|
evidence,
|
|
package_binding=package_binding,
|
|
keyring=keyring,
|
|
trusted_root_public_key=root_key,
|
|
now=now,
|
|
require_formal_backup_manifest=require_formal_backup_manifest,
|
|
)
|
|
validated["raw_artifact_sha256"] = {
|
|
"evidence": file_sha256(
|
|
_regular_file(evidence_file, label="first-release recovery evidence")
|
|
),
|
|
"keyring": file_sha256(_regular_file(keyring_file, label="recovery keyring")),
|
|
"trusted_root_public_key": root_sha,
|
|
}
|
|
return package_binding, validated, root_sha
|
|
|
|
|
|
def verify_and_build_output(
|
|
*,
|
|
package_dir: Path,
|
|
expected_package_sha256: str,
|
|
repo_root: Path,
|
|
evidence_file: Path,
|
|
keyring_file: Path,
|
|
trusted_root_public_key_file: Path,
|
|
output_kind: str,
|
|
now: datetime,
|
|
package_validator_for_tests: Callable[..., Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Revalidate every raw input and return one non-writing output document.
|
|
|
|
``package_validator_for_tests`` exists only for isolated unit fixtures.
|
|
The CLI never exposes or supplies it and therefore always loads the
|
|
source-bound official validator and its complete trust-input closure.
|
|
"""
|
|
|
|
if output_kind not in {"plan", "install-readiness-receipt"}:
|
|
raise SnapshotRestorePlanError("unsupported output kind")
|
|
package_binding, validated, root_sha = _verify_raw_inputs(
|
|
package_dir=package_dir,
|
|
expected_package_sha256=expected_package_sha256,
|
|
repo_root=repo_root,
|
|
evidence_file=evidence_file,
|
|
keyring_file=keyring_file,
|
|
trusted_root_public_key_file=trusted_root_public_key_file,
|
|
now=now,
|
|
package_validator_for_tests=package_validator_for_tests,
|
|
require_installable_package_v2=(output_kind == "install-readiness-receipt"),
|
|
require_formal_backup_manifest=(output_kind == "install-readiness-receipt"),
|
|
)
|
|
if output_kind == "plan":
|
|
return build_plan(
|
|
package_binding=package_binding,
|
|
validated_evidence=validated,
|
|
trusted_root_public_key_sha256=root_sha,
|
|
generated_at=now,
|
|
)
|
|
return build_install_readiness_receipt(
|
|
package_binding=package_binding,
|
|
validated_evidence=validated,
|
|
trusted_root_public_key_sha256=root_sha,
|
|
generated_at=now,
|
|
)
|
|
|
|
|
|
def validate_install_readiness(
|
|
*,
|
|
package_file: Path,
|
|
expected_package_sha256: str,
|
|
evidence_file: Path,
|
|
keyring_file: Path,
|
|
trusted_root_public_key_file: Path,
|
|
receipt_file: Path,
|
|
expected_receipt_sha256: str,
|
|
now: datetime,
|
|
) -> dict[str, Any]:
|
|
"""Rebuild and verify an install-gate receipt from every raw input.
|
|
|
|
This adapter is intentionally read-only and is the only receipt interface
|
|
intended for the release-package install gate. It never trusts a receipt
|
|
without re-running the official package validator and all recovery checks.
|
|
"""
|
|
|
|
package_path = _regular_file(package_file, label="release package")
|
|
if package_path.name != PACKAGE_FILE:
|
|
raise SnapshotRestorePlanError(f"package file must be named {PACKAGE_FILE}")
|
|
receipt_path = _regular_file(receipt_file, label="recovery readiness receipt")
|
|
expected = _sha(expected_receipt_sha256, label="expected readiness receipt SHA-256")
|
|
if file_sha256(receipt_path) != expected:
|
|
raise SnapshotRestorePlanError("recovery readiness receipt SHA-256 mismatch")
|
|
receipt = load_json(receipt_path, label="recovery readiness receipt")
|
|
_embedded_hash(receipt, "receipt_payload_sha256", label="recovery readiness receipt")
|
|
canonical = canonical_json_bytes(receipt)
|
|
if receipt_path.read_bytes() not in {canonical, canonical + b"\n"}:
|
|
raise SnapshotRestorePlanError("recovery readiness receipt is not canonical JSON")
|
|
if (
|
|
receipt.get("schema_version") != READINESS_RECEIPT_SCHEMA
|
|
or receipt.get("result") != "PASS_RECOVERY_READINESS"
|
|
or receipt.get("purpose") != "release_package_install_apply_gate"
|
|
or receipt.get("install_apply_gate") is not True
|
|
or receipt.get("snapshot_restore_apply_allowed") is not False
|
|
):
|
|
raise SnapshotRestorePlanError("document is not an install readiness receipt")
|
|
if now.tzinfo is None:
|
|
raise SnapshotRestorePlanError("validation time must include a timezone")
|
|
if now.astimezone(UTC) >= _timestamp(receipt.get("expires_at"), label="receipt expires_at"):
|
|
raise SnapshotRestorePlanError("recovery readiness receipt is expired")
|
|
generated_at = _timestamp(receipt.get("generated_at"), label="receipt generated_at")
|
|
if generated_at > now.astimezone(UTC) + MAX_CLOCK_SKEW:
|
|
raise SnapshotRestorePlanError("recovery readiness receipt is from the future")
|
|
package_binding, validated, root_sha = _verify_raw_inputs(
|
|
package_dir=package_path.parent,
|
|
expected_package_sha256=expected_package_sha256,
|
|
repo_root=REPO_ROOT,
|
|
evidence_file=evidence_file,
|
|
keyring_file=keyring_file,
|
|
trusted_root_public_key_file=trusted_root_public_key_file,
|
|
now=now,
|
|
require_installable_package_v2=True,
|
|
require_formal_backup_manifest=True,
|
|
)
|
|
rebuilt = build_install_readiness_receipt(
|
|
package_binding=package_binding,
|
|
validated_evidence=validated,
|
|
trusted_root_public_key_sha256=root_sha,
|
|
generated_at=generated_at,
|
|
)
|
|
if rebuilt != receipt:
|
|
raise SnapshotRestorePlanError(
|
|
"recovery readiness receipt differs from revalidated raw inputs"
|
|
)
|
|
return rebuilt
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--package-dir", type=Path, required=True)
|
|
parser.add_argument("--expected-package-sha256", required=True)
|
|
parser.add_argument("--repo-root", type=Path, required=True)
|
|
parser.add_argument("--evidence", type=Path, required=True)
|
|
parser.add_argument("--keyring", type=Path, required=True)
|
|
parser.add_argument("--trusted-root-public-key", type=Path, required=True)
|
|
parser.add_argument(
|
|
"--output-kind",
|
|
choices=("plan", "install-readiness-receipt"),
|
|
default="plan",
|
|
)
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
try:
|
|
now = datetime.now(UTC)
|
|
output = verify_and_build_output(
|
|
package_dir=args.package_dir,
|
|
expected_package_sha256=args.expected_package_sha256,
|
|
repo_root=args.repo_root,
|
|
evidence_file=args.evidence,
|
|
keyring_file=args.keyring,
|
|
trusted_root_public_key_file=args.trusted_root_public_key,
|
|
output_kind=args.output_kind,
|
|
now=now,
|
|
)
|
|
except (OSError, SnapshotRestorePlanError) as exc:
|
|
schema = (
|
|
READINESS_RECEIPT_SCHEMA
|
|
if args.output_kind == "install-readiness-receipt"
|
|
else PLAN_SCHEMA
|
|
)
|
|
print(
|
|
canonical_json_bytes(
|
|
{"schema_version": schema, "status": "rejected", "error": str(exc)}
|
|
).decode("utf-8"),
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
print(canonical_json_bytes(output).decode("utf-8"))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|