178 lines
7 KiB
Python
178 lines
7 KiB
Python
#!/usr/bin/env python3
|
|
"""Bind a no-send reasoning receipt to live GCE metadata on the replay host."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import stat
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
try:
|
|
from .private_receipt_io import print_private_receipt_summary, write_private_json
|
|
from .restore_gcp_generated_postgres_snapshot import (
|
|
DEFAULT_COMPUTE_INSTANCE,
|
|
DEFAULT_COMPUTE_ZONE,
|
|
DEFAULT_PRIVATE_NETWORK,
|
|
DEFAULT_PROJECT,
|
|
DEFAULT_RESTORE_SERVICE_ACCOUNT,
|
|
RUN_ID_RE,
|
|
TARGET_DATABASE_RE,
|
|
gce_compute_identity,
|
|
sha256_file,
|
|
)
|
|
except ImportError: # pragma: no cover - direct script execution on the GCP VM
|
|
from private_receipt_io import print_private_receipt_summary, write_private_json
|
|
from restore_gcp_generated_postgres_snapshot import (
|
|
DEFAULT_COMPUTE_INSTANCE,
|
|
DEFAULT_COMPUTE_ZONE,
|
|
DEFAULT_PRIVATE_NETWORK,
|
|
DEFAULT_PROJECT,
|
|
DEFAULT_RESTORE_SERVICE_ACCOUNT,
|
|
RUN_ID_RE,
|
|
TARGET_DATABASE_RE,
|
|
gce_compute_identity,
|
|
sha256_file,
|
|
)
|
|
|
|
REASONING_SCHEMA = "livingip.gcpGeneratedDbBlindClaimCanary.v1"
|
|
ATTESTATION_SCHEMA = "livingip.gcpReasoningComputeAttestation.v1"
|
|
|
|
|
|
def utc_now() -> str:
|
|
return datetime.now(UTC).isoformat()
|
|
|
|
|
|
def parse_timestamp(value: Any, label: str) -> datetime:
|
|
text = str(value or "")
|
|
if text.endswith("Z"):
|
|
text = text[:-1] + "+00:00"
|
|
try:
|
|
parsed = datetime.fromisoformat(text)
|
|
except ValueError as exc:
|
|
raise ValueError(f"{label} is not an ISO-8601 timestamp") from exc
|
|
if parsed.tzinfo is None:
|
|
raise ValueError(f"{label} must include a timezone")
|
|
return parsed.astimezone(UTC)
|
|
|
|
|
|
def load_reasoning_receipt(path: Path) -> dict[str, Any]:
|
|
try:
|
|
mode = path.lstat().st_mode
|
|
except OSError as exc:
|
|
raise ValueError(f"reasoning receipt is unavailable: {exc}") from exc
|
|
if not stat.S_ISREG(mode) or path.is_symlink():
|
|
raise ValueError("reasoning receipt must be a regular non-symlink file")
|
|
try:
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError("reasoning receipt is invalid JSON") from exc
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("reasoning receipt must be a JSON object")
|
|
return payload
|
|
|
|
|
|
def attest_reasoning_compute(
|
|
*,
|
|
reasoning_receipt_path: Path,
|
|
restore_run_id: str,
|
|
target_database: str,
|
|
project: str = DEFAULT_PROJECT,
|
|
expected_compute_instance: str = DEFAULT_COMPUTE_INSTANCE,
|
|
expected_compute_zone: str = DEFAULT_COMPUTE_ZONE,
|
|
expected_private_network: str = DEFAULT_PRIVATE_NETWORK,
|
|
expected_restore_service_account: str = DEFAULT_RESTORE_SERVICE_ACCOUNT,
|
|
metadata_timeout: float = 10.0,
|
|
generated_at_utc: str | None = None,
|
|
) -> dict[str, Any]:
|
|
if not RUN_ID_RE.fullmatch(restore_run_id):
|
|
raise ValueError("restore run id is invalid")
|
|
if not TARGET_DATABASE_RE.fullmatch(target_database):
|
|
raise ValueError("target database must be a disposable teleo_clone_* database")
|
|
reasoning_receipt_path = reasoning_receipt_path.resolve()
|
|
reasoning = load_reasoning_receipt(reasoning_receipt_path)
|
|
started = parse_timestamp(reasoning.get("generated_at_utc"), "reasoning start")
|
|
completed = parse_timestamp(reasoning.get("completed_at_utc"), "reasoning completion")
|
|
checks = {
|
|
"schema": reasoning.get("schema") == REASONING_SCHEMA,
|
|
"status": reasoning.get("status") == "pass" and not reasoning.get("errors"),
|
|
"target_database": reasoning.get("target_database") == target_database,
|
|
"source_compute": reasoning.get("source_compute") == expected_compute_instance,
|
|
"chronology": started <= completed,
|
|
"no_telegram_send": reasoning.get("posted_to_telegram") is False,
|
|
"no_database_write": reasoning.get("database_write_attempted") is False,
|
|
}
|
|
failed = sorted(name for name, passed in checks.items() if not passed)
|
|
if failed:
|
|
raise ValueError("reasoning receipt failed attestation preflight: " + ", ".join(failed))
|
|
|
|
compute = gce_compute_identity(
|
|
project,
|
|
expected_compute_instance,
|
|
expected_compute_zone,
|
|
expected_private_network,
|
|
expected_restore_service_account,
|
|
timeout=metadata_timeout,
|
|
)
|
|
return {
|
|
"artifact": "gcp_reasoning_compute_attestation",
|
|
"schema": ATTESTATION_SCHEMA,
|
|
"status": "pass",
|
|
"generated_at_utc": generated_at_utc or utc_now(),
|
|
"restore_run_id": restore_run_id,
|
|
"target_database": target_database,
|
|
"reasoning_receipt": {
|
|
"sha256": sha256_file(reasoning_receipt_path),
|
|
"schema": reasoning["schema"],
|
|
"generated_at_utc": reasoning["generated_at_utc"],
|
|
"completed_at_utc": reasoning["completed_at_utc"],
|
|
"claimed_source_compute": reasoning["source_compute"],
|
|
},
|
|
"compute": compute,
|
|
"checks": {**checks, "gce_metadata_identity": all(compute.get("checks", {}).values())},
|
|
"method": "gce_metadata_server_v1",
|
|
"mutation": {
|
|
"cloudsql_changed": False,
|
|
"compute_changed": False,
|
|
"database_changed": False,
|
|
"telegram_message_sent": False,
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--reasoning-receipt", required=True, type=Path)
|
|
parser.add_argument("--restore-run-id", required=True)
|
|
parser.add_argument("--target-db", required=True)
|
|
parser.add_argument("--project", default=DEFAULT_PROJECT)
|
|
parser.add_argument("--expected-compute-instance", default=DEFAULT_COMPUTE_INSTANCE)
|
|
parser.add_argument("--expected-compute-zone", default=DEFAULT_COMPUTE_ZONE)
|
|
parser.add_argument("--expected-private-network", default=DEFAULT_PRIVATE_NETWORK)
|
|
parser.add_argument("--expected-restore-service-account", default=DEFAULT_RESTORE_SERVICE_ACCOUNT)
|
|
parser.add_argument("--metadata-timeout", default=10.0, type=float)
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
receipt = attest_reasoning_compute(
|
|
reasoning_receipt_path=args.reasoning_receipt,
|
|
restore_run_id=args.restore_run_id,
|
|
target_database=args.target_db,
|
|
project=args.project,
|
|
expected_compute_instance=args.expected_compute_instance,
|
|
expected_compute_zone=args.expected_compute_zone,
|
|
expected_private_network=args.expected_private_network,
|
|
expected_restore_service_account=args.expected_restore_service_account,
|
|
metadata_timeout=args.metadata_timeout,
|
|
)
|
|
write_private_json(args.output, receipt)
|
|
except (OSError, ValueError, RuntimeError) as exc:
|
|
parser.error(str(exc))
|
|
print_private_receipt_summary(args.output, receipt)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|