2026-10-08 00:06:59 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Attended recovered-ledger rehearsal in the cached receiver image, no network."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import secrets
|
|
|
|
|
import subprocess
|
|
|
|
|
import time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from ledger_archive import (
|
|
|
|
|
Refused,
|
|
|
|
|
checkpoint,
|
|
|
|
|
private_directory,
|
|
|
|
|
private_file,
|
|
|
|
|
validate_capture,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
IMAGE = (
|
|
|
|
|
"forgejo.coulomb.social/coulomb/email-connect@sha256:"
|
2026-10-08 14:10:26 +02:00
|
|
|
"eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2"
|
2026-10-08 00:06:59 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
CODE = """import io,json,os,sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
sys.path.insert(0,'/opt/ledger-rehearsal')
|
|
|
|
|
from ledger_archive import fingerprint
|
|
|
|
|
from email_connect.transactional import SQLiteDeliveryStore,TransactionalApplication,ProviderError
|
|
|
|
|
assert os.getuid()==10001
|
|
|
|
|
path=Path('/tmp/recovered.sqlite3')
|
|
|
|
|
os.umask(0o077)
|
|
|
|
|
path.write_bytes(sys.stdin.buffer.read())
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
before=fingerprint(path)
|
|
|
|
|
store=SQLiteDeliveryStore(str(path))
|
|
|
|
|
assert fingerprint(path)==before
|
|
|
|
|
class ForbiddenProvider:
|
|
|
|
|
def send(self,*args):raise AssertionError('recovery_provider_call_forbidden')
|
|
|
|
|
app=TransactionalApplication(store,ForbiddenProvider(),'synthetic-recovery-token','https://issuer.example.test')
|
|
|
|
|
checks={'accepted_verifications':0,'unknown_verifications':0,'invalidated_handles':0,
|
|
|
|
|
'suppressions':0,'accepted_deliveries':0,'unknown_sends':0}
|
2026-10-08 12:17:14 +02:00
|
|
|
caller_column="v.caller_id" if "caller_id" in {r[1] for r in store.db.execute("PRAGMA table_info(verifications)")} else "''"
|
|
|
|
|
query=("SELECT v.registration_id,v.email,v.username,v.client_id,v.tenant,v.display_name,a.state,"+caller_column+" "
|
2026-10-08 00:06:59 +02:00
|
|
|
"FROM verifications v JOIN verification_attempts a ON a.request_id=v.request_id "
|
|
|
|
|
"WHERE a.state IN ('accepted','in_flight')")
|
|
|
|
|
for row in store.db.execute(query).fetchall():
|
2026-10-08 12:17:14 +02:00
|
|
|
app=TransactionalApplication(store,ForbiddenProvider(),'synthetic-recovery-token',
|
|
|
|
|
'https://issuer.example.test',**({'verification_namespace':row[7]} if row[7] else {}))
|
2026-10-08 00:06:59 +02:00
|
|
|
payload=dict(zip(['registration_id','normalized_email','preferred_username','client_id','tenant','display_name'],row[:6]))
|
|
|
|
|
payload['correlation_id']='synthetic-recovery-probe'
|
|
|
|
|
raw=json.dumps(payload).encode();response={}
|
|
|
|
|
body=json.loads(b''.join(app({'PATH_INFO':'/v1/registration-verifications','REQUEST_METHOD':'POST',
|
|
|
|
|
'CONTENT_LENGTH':str(len(raw)),'wsgi.input':io.BytesIO(raw),
|
|
|
|
|
'HTTP_AUTHORIZATION':'Bearer synthetic-recovery-token'},
|
|
|
|
|
lambda status,headers:response.update(status=status))))
|
|
|
|
|
if row[6]=='accepted':
|
|
|
|
|
assert response['status'].startswith('202') and body['accepted']
|
|
|
|
|
checks['accepted_verifications']+=1
|
|
|
|
|
else:
|
|
|
|
|
assert response['status'].startswith('422') and body['error']=='delivery_outcome_unknown'
|
|
|
|
|
checks['unknown_verifications']+=1
|
|
|
|
|
query=("SELECT handle_hash FROM verifications "
|
|
|
|
|
"WHERE consumed_at IS NOT NULL OR canceled_at IS NOT NULL")
|
|
|
|
|
for row in store.db.execute(query).fetchall():
|
|
|
|
|
try:store.consume_verification(row[0]);raise AssertionError('invalid_handle_reactivated')
|
|
|
|
|
except ValueError as error:assert str(error)=='verification_invalid'
|
|
|
|
|
checks['invalidated_handles']+=1
|
|
|
|
|
for row in store.db.execute('SELECT email FROM suppressions').fetchall():
|
|
|
|
|
assert store.is_suppressed(row[0]);checks['suppressions']+=1
|
|
|
|
|
query=("SELECT d.event_id,a.fingerprint FROM deliveries d "
|
|
|
|
|
"LEFT JOIN send_attempts a ON a.event_id=d.event_id")
|
|
|
|
|
for row in store.db.execute(query).fetchall():
|
|
|
|
|
assert store.delivery_status(row[0])['state']=='provider_accepted'
|
|
|
|
|
assert store.reserve_send(row[0],row[1] or 'synthetic-legacy-probe') is False
|
|
|
|
|
checks['accepted_deliveries']+=1
|
|
|
|
|
query=("SELECT event_id,fingerprint FROM send_attempts WHERE state='in_flight' "
|
|
|
|
|
"AND event_id NOT IN (SELECT event_id FROM deliveries)")
|
|
|
|
|
for row in store.db.execute(query).fetchall():
|
|
|
|
|
assert store.delivery_status(row[0])['state']=='unknown'
|
|
|
|
|
try:store.reserve_send(row[0],row[1]);raise AssertionError('unknown_send_retried')
|
|
|
|
|
except ProviderError as error:assert error.code=='delivery_outcome_unknown'
|
|
|
|
|
checks['unknown_sends']+=1
|
|
|
|
|
store.db.close()
|
|
|
|
|
assert fingerprint(path)==before
|
|
|
|
|
print(json.dumps({'status':'isolated_ledger_verified','ledger_fingerprint':before,
|
|
|
|
|
'checks':checks,'mail_sent':False,'uid':os.getuid()}))
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rehearse(recovered, receipt_path):
|
|
|
|
|
private_directory(recovered)
|
|
|
|
|
private_directory(receipt_path.parent)
|
|
|
|
|
if receipt_path.exists() or receipt_path.is_symlink():
|
|
|
|
|
raise Refused("fresh_receipt_required")
|
|
|
|
|
capture = validate_capture(recovered)
|
|
|
|
|
private_file(recovered / "unpack-receipt.json")
|
|
|
|
|
unpacked = json.loads((recovered / "unpack-receipt.json").read_text())
|
|
|
|
|
if (
|
|
|
|
|
capture["image"] != IMAGE
|
|
|
|
|
or unpacked.get("status") != "decrypted_ledger_verified"
|
|
|
|
|
or unpacked.get("ledger_fingerprint") != capture["ledger_fingerprint"]
|
|
|
|
|
):
|
|
|
|
|
raise Refused("pinned_recovered_ledger_required")
|
|
|
|
|
# Never pull a recovery image from the production forge during this rehearsal.
|
|
|
|
|
inspected = subprocess.run(
|
|
|
|
|
["docker", "image", "inspect", IMAGE], capture_output=True, timeout=30
|
|
|
|
|
)
|
|
|
|
|
if inspected.returncode:
|
|
|
|
|
raise Refused("cached_pinned_recovery_image_required")
|
|
|
|
|
started = time.monotonic()
|
|
|
|
|
module = Path(__file__).with_name("ledger_archive.py").resolve()
|
|
|
|
|
name = "email-ledger-rehearsal-" + secrets.token_hex(8)
|
|
|
|
|
try:
|
|
|
|
|
with (recovered / "database.sqlite3").open("rb") as source:
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
"docker",
|
|
|
|
|
"run",
|
|
|
|
|
"--rm",
|
|
|
|
|
"--name",
|
|
|
|
|
name,
|
|
|
|
|
"-i",
|
|
|
|
|
"--network",
|
|
|
|
|
"none",
|
|
|
|
|
"--read-only",
|
|
|
|
|
"--user",
|
|
|
|
|
"10001:10001",
|
|
|
|
|
"--cap-drop",
|
|
|
|
|
"ALL",
|
|
|
|
|
"--security-opt",
|
|
|
|
|
"no-new-privileges",
|
|
|
|
|
"--memory",
|
|
|
|
|
"512m",
|
|
|
|
|
"--cpus",
|
|
|
|
|
"0.5",
|
|
|
|
|
"--tmpfs",
|
|
|
|
|
"/tmp:rw,noexec,nosuid,size=160m,uid=10001,gid=10001,mode=0700",
|
|
|
|
|
"--mount",
|
|
|
|
|
"type=bind,source="
|
|
|
|
|
+ str(module)
|
|
|
|
|
+ ",target=/opt/ledger-rehearsal/ledger_archive.py,readonly",
|
|
|
|
|
"-e",
|
|
|
|
|
"PYTHONDONTWRITEBYTECODE=1",
|
|
|
|
|
"--entrypoint",
|
|
|
|
|
"python",
|
|
|
|
|
IMAGE,
|
|
|
|
|
"-c",
|
|
|
|
|
CODE,
|
|
|
|
|
],
|
|
|
|
|
stdin=source,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
timeout=120,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
subprocess.run(["docker", "rm", "-f", name], capture_output=True, timeout=30)
|
|
|
|
|
if result.returncode:
|
|
|
|
|
raise Refused("isolated_rehearsal_failed")
|
|
|
|
|
verification = json.loads(result.stdout)
|
|
|
|
|
if verification.get("ledger_fingerprint") != capture["ledger_fingerprint"]:
|
|
|
|
|
raise Refused("isolated_fingerprint_mismatch")
|
|
|
|
|
receipt = dict(
|
|
|
|
|
unpacked,
|
|
|
|
|
status="application_restored_verified",
|
|
|
|
|
application_restore_verified=True,
|
|
|
|
|
recovery_image=IMAGE,
|
|
|
|
|
network="disabled",
|
|
|
|
|
production_restored=False,
|
|
|
|
|
checks=verification["checks"],
|
|
|
|
|
uid=verification["uid"],
|
|
|
|
|
mail_sent=False,
|
|
|
|
|
evidence_ceiling="isolated_ledger_rehearsal",
|
|
|
|
|
elapsed_seconds=round(time.monotonic() - started, 2),
|
|
|
|
|
)
|
|
|
|
|
checkpoint(receipt_path, receipt)
|
|
|
|
|
return receipt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
parser.add_argument("--recovered", required=True, type=Path)
|
|
|
|
|
parser.add_argument("--receipt", required=True, type=Path)
|
|
|
|
|
parser.add_argument("--execute", action="store_true")
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
if not args.execute:
|
|
|
|
|
print(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{"status": "reviewed", "container_started": False, "production_restored": False}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return 0
|
|
|
|
|
print(json.dumps(rehearse(args.recovered, args.receipt)))
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
try:
|
|
|
|
|
raise SystemExit(main())
|
|
|
|
|
except Exception as error:
|
|
|
|
|
print(json.dumps({"status": "failed", "failure_type": type(error).__name__}))
|
|
|
|
|
raise SystemExit(1)
|