coulomb-social/scripts/observe-backup-retention.py
tegwick 51a527128a
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m47s
Verify latest backup mirror and refresh native generation retention
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
2026-10-31 03:50:31 +01:00

335 lines
13 KiB
Python

#!/usr/bin/env python3
"""Read current version/custody evidence for the offline generation reviewer.
Preview is offline. --execute performs only scoped provider reads and publishes
one new private observation. It never captures, decrypts, deletes or changes IAM.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import os
from datetime import UTC, datetime
from pathlib import Path
spec = importlib.util.spec_from_file_location(
"retention_mirror", Path(__file__).with_name("mirror-paired-backup.py")
)
worker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(worker)
health = worker.health
Refused = health.Refused
PRIMARY = "railiance-platform-pg-backup"
KIT = "railiance-coulomb-social-recovery"
SCOPES = {PRIMARY: health.PREFIX, health.MIRROR_BUCKET: "archives/", KIT: "kits/"}
def status(error):
return getattr(error, "response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode")
def version_id(value):
if (
not isinstance(value, str)
or not value
or value == "null"
or any(c in value for c in "\r\n")
):
raise Refused("immutable_version_required")
return value
def observe_bucket(client, bucket):
prefix = SCOPES[bucket]
versioning = client.get_bucket_versioning(Bucket=bucket)
if versioning.get("Status") != "Enabled":
raise Refused("enabled_versioning_required")
try:
lifecycle = client.get_bucket_lifecycle_configuration(Bucket=bucket)
rules = lifecycle["Rules"]
except Exception as error:
# A bucket itself being missing or inaccessible is not "no lifecycle".
code = getattr(error, "response", {}).get("Error", {}).get("Code")
if status(error) != 404 or code != "NoSuchLifecycleConfiguration":
raise
rules = []
if not isinstance(rules, list):
raise Refused("lifecycle_rules_required")
versions, markers, seen, identities, arguments = [], [], set(), set(), {}
for _ in range(100):
page = client.list_object_versions(Bucket=bucket, Prefix=prefix, MaxKeys=1000, **arguments)
for field, destination in [("Versions", versions), ("DeleteMarkers", markers)]:
for row in page.get(field, []):
key, version = row["Key"], version_id(row["VersionId"])
if not isinstance(key, str) or not key.startswith(prefix):
raise Refused("version_outside_application_prefix")
if (key, version) in identities:
raise Refused("duplicate_or_changing_version_inventory")
identities.add((key, version))
item = {"Key": key, "VersionId": version}
if field == "Versions":
if type(row.get("Size")) is not int or row["Size"] < 0:
raise Refused("version_size_required")
item["Size"] = row["Size"]
if "IsLatest" in row:
item["IsLatest"] = row["IsLatest"]
destination.append(item)
if page.get("IsTruncated") is False:
return {
"bucket": bucket,
"prefix": prefix,
"inventory_complete": True,
"versioning": {"Status": "Enabled"},
"lifecycle": {"status": "observed", "Rules": rules},
"versions": versions,
"delete_markers": markers,
}
if page.get("IsTruncated") is not True:
raise Refused("explicit_pagination_completion_required")
cursor = page.get("NextKeyMarker"), page.get("NextVersionIdMarker")
if not isinstance(cursor[0], str) or not cursor[0].startswith(prefix) or cursor in seen:
raise Refused("invalid_or_repeated_version_cursor")
seen.add(cursor)
arguments = {"KeyMarker": cursor[0]}
if cursor[1] is not None:
arguments["VersionIdMarker"] = version_id(cursor[1])
raise Refused("version_inventory_limit_exceeded")
def observe(primary_client, reader, inventory, recovery, now=None):
now = now or datetime.now(UTC)
if not isinstance(inventory, list) or not inventory:
raise Refused("nonempty_verified_inventory_required")
expected = {}
def add(bucket, key, version, digest, size):
version_id(version)
if (
bucket not in SCOPES
or not isinstance(key, str)
or not key.startswith(SCOPES[bucket])
or not isinstance(digest, str)
or len(digest) != 64
or any(c not in "0123456789abcdef" for c in digest)
or type(size) is not int
or size <= 0
):
raise Refused("exact_version_binding_required")
identity = bucket, key, version
if identity in expected and expected[identity] != (digest, size):
raise Refused("conflicting_version_binding")
expected[identity] = digest, size
for receipt in inventory:
health.validate_mirror(receipt, receipt)
add(
PRIMARY,
receipt["object_key"],
receipt["version_id"],
receipt["ciphertext_sha256"],
receipt["ciphertext_bytes"],
)
ref = receipt["recovery_mirror"]
add(
health.MIRROR_BUCKET,
ref["object_key"],
ref["version_id"],
ref["ciphertext_sha256"],
ref["ciphertext_bytes"],
)
if "receipt_object_key" in ref:
health.validate_mirror(receipt, receipt, require_retained_receipt=True)
add(
health.MIRROR_BUCKET,
ref["receipt_object_key"],
ref["receipt_version_id"],
ref["receipt_sha256"],
ref["receipt_bytes"],
)
if (
recovery.get("status") != "application_restored_verified"
or recovery.get("application") != "coulomb-social"
or recovery.get("application_restore_verified") is not True
):
raise Refused("verified_application_recovery_required")
for name in ["protected_kit", "protected_recovery_proof"]:
ref = recovery[name]
suffix = "/recovery-kit.tar" if name == "protected_kit" else "/recovery-proof.json"
prefix = "kits/" if name == "protected_kit" else "kits/receipts/"
if (
ref.get("bucket") != KIT
or ref.get("project_id") != health.PROJECT
or ref.get("automatic_expiry") is not False
or ref.get("object_key") != prefix + ref["sha256"] + suffix
):
raise Refused("protected_custody_binding_required")
add(KIT, ref["object_key"], ref["version_id"], ref["sha256"], ref["bytes"])
clients = {PRIMARY: primary_client, health.MIRROR_BUCKET: reader, KIT: reader}
observations = [observe_bucket(clients[bucket], bucket) for bucket in SCOPES]
versions = {
(b["bucket"], v["Key"], v["VersionId"]): v for b in observations for v in b["versions"]
}
heads = []
for (bucket, key, version), (digest, size) in sorted(expected.items()):
listed = versions.get((bucket, key, version))
if listed is None and bucket == PRIMARY:
continue # Primary age expiry never removes an independent mirror generation.
if listed is None or listed["Size"] != size:
raise Refused("retained_version_missing_or_changed")
value = clients[bucket].head_object(Bucket=bucket, Key=key, VersionId=version)
if (
value.get("VersionId") != version
or value.get("ContentLength") != size
or value.get("Metadata", {}).get("sha256") != digest
or value.get("DeleteMarker")
):
raise Refused("exact_version_head_differs")
heads.append(
{
"bucket": bucket,
"object_key": key,
"version_id": version,
"sha256": digest,
"bytes": size,
}
)
sidecars = 0
for receipt in inventory:
ref = receipt["recovery_mirror"]
if "receipt_object_key" not in ref:
continue
response = reader.get_object(
Bucket=health.MIRROR_BUCKET,
Key=ref["receipt_object_key"],
VersionId=ref["receipt_version_id"],
)
with response["Body"] as stream:
payload = stream.read(ref["receipt_bytes"] + 1)
if (
response.get("VersionId") != ref["receipt_version_id"]
or response.get("ContentLength") != ref["receipt_bytes"]
or len(payload) != ref["receipt_bytes"]
or hashlib.sha256(payload).hexdigest() != ref["receipt_sha256"]
or payload != worker.canonical(worker.public_receipt(receipt, receipt))
):
raise Refused("independent_public_receipt_readback_differs")
sidecars += 1
return {
"schema": "coulomb.provider-retention-observation.v1",
"observed_at": now.isoformat(),
"head_observed_at": datetime.now(UTC).isoformat(),
"observations": observations,
"version_heads": heads,
"independent_public_receipts_fetched_verified": sidecars,
"archive_bodies_refetched": False,
"protected_kit_body_refetched": False,
"provider_mutations": False,
"credentials_printed": False,
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--execute", action="store_true", help="Read provider metadata and receipts"
)
for name in [
"inventory",
"recovery",
"credentials-directory",
"platform-scripts",
"runtime-manifest",
"output",
]:
parser.add_argument("--" + name, type=Path)
args = parser.parse_args()
if not args.execute:
print(json.dumps({"status": "preview", "provider_contacted": False}))
return
if any(
getattr(args, name) is None
for name in [
"inventory",
"recovery",
"credentials_directory",
"platform_scripts",
"runtime_manifest",
"output",
]
):
raise Refused("explicit_observation_inputs_required")
worker.load_runtime(args.platform_scripts, args.runtime_manifest)
from application_recovery_identities import private_directory, private_json
from application_recovery_resources import s3
private_directory(args.credentials_directory)
private_directory(args.output.parent)
if args.output.exists() or args.output.is_symlink():
raise Refused("fresh_observation_output_required")
inputs = {name: getattr(args, name).read_bytes() for name in ["inventory", "recovery"]}
# Use explicit static credentials: no SDK ambient identity or metadata fallback.
import boto3
from botocore.config import Config
pair = private_json(args.credentials_directory / "publisher-pair.json")
if not all(
isinstance(pair.get(k), str) and pair[k].strip()
for k in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
):
raise Refused("own_publisher_pair_required")
primary = boto3.client(
"s3",
endpoint_url="https://s3.nl-ams.scw.cloud",
region_name="nl-ams",
aws_access_key_id=pair["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=pair["AWS_SECRET_ACCESS_KEY"],
config=Config(
signature_version="s3v4",
connect_timeout=15,
read_timeout=30,
retries={"max_attempts": 0},
s3={"addressing_style": "path"},
),
)
reader = None
try:
reader = s3(private_json(args.credentials_directory / "reader-pair.json"), health.PROJECT)
result = observe(
primary, reader, json.loads(inputs["inventory"]), json.loads(inputs["recovery"])
)
finally:
primary.close()
if reader is not None:
reader.close()
result["source_sha256"] = {
name: hashlib.sha256(value).hexdigest() for name, value in inputs.items()
}
fd = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "w") as stream:
json.dump(result, stream, indent=2, sort_keys=True)
stream.write("\n")
print(
json.dumps(
{
"status": "provider_observation_completed",
"provider_mutations": False,
"exact_version_heads": len(result["version_heads"]),
"independent_receipts_verified": result[
"independent_public_receipts_fetched_verified"
],
}
)
)
if __name__ == "__main__":
try:
main()
except Exception as error:
print(json.dumps({"status": "observation_refused", "failure_type": type(error).__name__}))
raise SystemExit(1)