Prove isolated custody recovery and prepare governed offsite finish
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1029d-2c93-7500-a0e0-e3e089668b29
This commit is contained in:
codex 2026-10-03 20:47:48 +02:00
parent dff2d6e9f7
commit a252447058
11 changed files with 528 additions and 1 deletions

View file

@ -66,3 +66,43 @@ Security orientation reloaded: Emergent 0.2.4 root, verified-files,
orientation-only; digest
025aab4f38246219c924bc94954e70ea0e6e7f72bcaf8d4344cd599602d345c3.
No secret values, privileged role dump or token material are copied here.
## Completed isolated recovery proof
Actual disposable local Raft server: OpenBao v2.5.5. Evidence:
[isolated recovery receipt](2026-10-03-root-custody-isolated-raft-recovery.json).
The exact owner publisher and baseline guard ran against an ephemeral loopback
server and synthetic local credentials, with the real retained non-secret
empty document. Two native Raft snapshots were created. Restoring the snapshot
from before publication caused the missing source to be refused; restoring the
snapshot containing revision 1/KV1 matched the independent retained digest.
Workstation baseline/history copy and scratch restoration matched exactly,
including 0600 baseline permissions. Server, credentials and snapshots were
confined to temporary storage and cleaned up. Production was not accessed or
mutated and no production snapshot was read.
This completes the bounded algorithm/native-backend isolated rollback/restore
exercise and local receipt-holder copy/restore proof. It does not claim a
production snapshot recovery or actual workstation backup-provider readback.
The current live retained-baseline restore-check, provider retention coverage
and workload/audit receipts remain separately tracked.
## Established retention path and attended finish
No configured workstation backup job covering the new publisher directory was
found in the inspected owner backup contracts/local backup configuration. A
validated non-secret recovery copy now lives in root-custody-history/ in this
owner repository. This enters the established Forgejo Git source backup scope;
new provider archive coverage is not implied until an actual archive includes
the commit. Source retrieval/readback is tracked separately from provider proof.
The one-command attended finish packet is in openbao/root-grant/README.md.
It performs current owner baseline readback and an age-encrypted history
upload/download/decrypt equality proof through the current governed OpenBao
Nextcloud backup account/lane only. It uses existing forgejo/ storage, no new
credential, no policy widening and no local historical rclone grant. A local
historical configuration's credential-bearing coordinates may have appeared in
backup-discovery tool output. No values are retained here. The finish helper
compares that grant with the current lane in memory; if current, it reports a
safe incident status and stops before upload for coordinated existing owner
credential cutover, preserving consumers. No uninformed rotation is performed.

View file

@ -0,0 +1,18 @@
{
"digest": "33a2ae086385028db1b654aa5646b7a98bcde1c5878c804e19315dadc3f87248",
"entries_count": 0,
"kv_version": 1,
"latest_snapshot_matches_independent_baseline": true,
"ok": true,
"old_snapshot_missing_source_refused": true,
"production_mutated": false,
"production_snapshot_used": false,
"receipt_holder_backup_provider_verified": false,
"receipt_holder_scratch_restore_verified": true,
"record_id": "nk-custody:platform-root",
"revision": 1,
"scope": "disposable-loopback-openbao-raft",
"server_version": "OpenBao v2.5.5 (028992583c693c4de6350b8aa52ff85e30375a99), built 2026-06-17T11:18:48Z",
"snapshot_count": 2,
"temporary_artifacts_removed": true
}

View file

@ -0,0 +1,20 @@
# Non-authoritative publisher history recovery copy
This directory contains only the validated non-secret empty publication baseline
and immutable history receipt. It is an independent recovery copy, not a read
surface or a ceremony, and must never mint a grant. Runtime reads OpenBao only.
Retain every newer publisher revision here; do not replace/remove old receipts.
The approved owner Forgejo repository backup contract covers source/history;
this copy enters that established scope after commit/push. Exact remote Git
retrieval/readback can prove this copy survives loss of the local publisher
files. A fresh Nextcloud/Scaleway Forgejo archive containing this commit is a
separate provider-generation proof and is not implied by push or Git readback.
The attended finish helper instead directly encrypts retained publisher history
and verifies upload/download/decryption using the current governed Nextcloud
lane, reusing established owner storage and adding no new service or credential.
Controlled restoration copies baseline.json and history/ to the private operator
state directory, preserves mode0600 and independently compares the latest
record/digest/KV version against authenticated OpenBao readback. An older
snapshot alone cannot reset the independent high-water record.

View file

@ -0,0 +1,21 @@
{
"digest": "33a2ae086385028db1b654aa5646b7a98bcde1c5878c804e19315dadc3f87248",
"document": {
"entries": [],
"freshness": {
"max_cache_seconds": 900,
"on_unreadable": "no-grant"
},
"kind": "netkingdom-custody-record",
"owner_system": "net-kingdom-custody",
"record_id": "nk-custody:platform-root",
"revision": 1,
"schema_version": "0.1",
"updated_at": "2026-10-03T17:47:04.035713Z"
},
"kv_version": 1,
"provenance_ref": "net-kingdom/docs/evidence/2026-10-03-root-grant-delivery-progress.md",
"record_id": "nk-custody:platform-root",
"recorded_at": "2026-10-03T18:21:14.370633+00:00",
"revision": 1
}

View file

@ -0,0 +1,21 @@
{
"digest": "33a2ae086385028db1b654aa5646b7a98bcde1c5878c804e19315dadc3f87248",
"document": {
"entries": [],
"freshness": {
"max_cache_seconds": 900,
"on_unreadable": "no-grant"
},
"kind": "netkingdom-custody-record",
"owner_system": "net-kingdom-custody",
"record_id": "nk-custody:platform-root",
"revision": 1,
"schema_version": "0.1",
"updated_at": "2026-10-03T17:47:04.035713Z"
},
"kv_version": 1,
"provenance_ref": "net-kingdom/docs/evidence/2026-10-03-root-grant-delivery-progress.md",
"record_id": "nk-custody:platform-root",
"recorded_at": "2026-10-03T18:21:14.370633+00:00",
"revision": 1
}

View file

@ -158,3 +158,37 @@ revision 1/KV1/no-grant digest and exact residual recovery/reader evidence are
recorded in [delivery evidence](../../docs/evidence/2026-10-03-platform-root-custody-delivery.md).
This completes RPF-WP-0049-T04. T05 remains evidence-blocked; generic approval
is not pending. Old operator role configuration is not an execution receipt.
## Remaining bounded attended finish
Native isolated Raft recovery and local receipt-holder scratch restoration now
pass. The operator-owned directory has no discovered workstation backup job.
The non-authoritative recovery copy in docs/evidence/root-custody-history enters
the existing owner Forgejo source backup scope; that does not imply a fresh
provider archive has included this new commit. One attended command completes
current live baseline check plus direct protected history provider readback:
```bash
BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 \
warden access openbao-platform-admin-login --exec -- \
/home/worsch/railiance-platform/scripts/root-custody-finish-attended.sh \
--state /home/worsch/.local/state/net-kingdom-root-custody/baseline.json \
--net-kingdom /home/worsch/net-kingdom \
--receipt /tmp/platform-root-finish-UNIQUE-RUN.json
```
The silent child uses the current governed OpenBao backup account/lane only,
requires the known Backup owner and create-only share shape, checks quota,
encrypts history with current escrow, creates one uniquely named artifact under
the existing forgejo/ backup folder, downloads through operator authority and
verifies ciphertext and decrypted history equality. It deletes nothing and
changes no credential or policy. Read the fresh receipt; old receipts are refused.
A local historical rclone configuration was found during backup discovery and
its credential-bearing coordinates may have appeared in tool output. It is
never reused. The helper compares that share with the current lane in memory
and records only booleans. If it is the current lane, the helper stops before
upload and requires coordinated existing credential incident/cutover recovery,
preserving consumers; it never rotates an uninformed predecessor. If different,
the historical owner's revocation status remains unverified separately from
root-grant authority. No credential-bearing coordinates are copied to evidence.

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec python3 "$(dirname "$0")/root_custody_finish.py" "$@" >/dev/null 2>&1

View file

@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Silent attended current-source and governed provider history recovery proof."""
import argparse
import configparser
import hmac
from urllib.parse import urlsplit,unquote
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import tempfile
import uuid
from root_grant_attended import Bao, RECORD_ID, atomic_json, check_baseline, digest, require_attended
from migrate_nextcloud_backup_account import HOST, OPERATOR_PATH, LANE, quota, request
VALIDATOR_DIGEST='9156e399c8d92a45b817bfc3738c89843487ce57e1718ed747f804f95cc719bb'
SCHEMA_DIGEST='bb6cd6da5dc48eddcf118ae78c2cacfafd5f860098fd8a8368fe6bc845776d07'
def checked_bundle(state_path, validator):
state=json.loads(state_path.read_text())
if state.get('pending') or validator.validate(state['document']) or state['record_id']!=RECORD_ID or digest(state['document'])!=state['digest']:
raise ValueError('retained_state_invalid')
history=[]
for path in sorted((state_path.parent/'history').glob('*.json')):
if path.is_symlink() or path.stat().st_size>262144:
raise ValueError('history_invalid')
receipt=json.loads(path.read_text())
if validator.validate(receipt['document']) or receipt['record_id']!=RECORD_ID or receipt['digest']!=digest(receipt['document']):
raise ValueError('history_invalid')
history.append(receipt)
if state not in history:
raise ValueError('latest_history_missing')
return state,json.dumps({'baseline':state,'history':history},sort_keys=True,separators=(',',':')).encode()
def historical_config_comparison(config_path, values):
if not config_path.exists():
return {"historical_config_present":False,"historical_share_matches_current_lane":None}
try:
config=configparser.ConfigParser(interpolation=None)
config.read(config_path)
remote=config['nextcloud-backup']
candidates=[remote.get('user','')]+unquote(urlsplit(remote.get('url','')).path).split('/')
matched=any(hmac.compare_digest(value,values['NC_WEBDAV_TOKEN']) for value in candidates if value)
return {"historical_config_present":True,"historical_share_matches_current_lane":matched,
"historical_credential_status":"current-lane-incident" if matched else "historical-owner-and-revocation-unverified"}
except Exception:
return {"historical_config_present":True,"historical_share_matches_current_lane":None,
"historical_credential_status":"comparison-unavailable"}
def roundtrip(bundle, lane, operator, network=request, get_quota=quota):
values=lane['data']
if operator['BACKUP_USERNAME']!='Backup':
raise ValueError('governed_owner_mismatch')
auth=(operator['BACKUP_USERNAME'],operator['BACKUP_PASSWORD'])
upload=values['NC_WEBDAV_URL']
if not upload.startswith(HOST+'/public.php/dav/filesdrop/') or values['NC_WEBDAV_TOKEN']!=upload.rsplit('/',1)[-1]:
raise ValueError('current_lane_contract_mismatch')
key=(values['AGE_PRIVATE_KEY'].strip()+'\n').encode()
def process(command,payload):
result=subprocess.run(command,input=payload,capture_output=True,timeout=30)
if result.returncode:
raise ValueError('protected_process_failed')
return result.stdout
recipient=process(['age-keygen','-y','/dev/stdin'],key).decode().strip()
encrypted=process(['age','-r',recipient],bundle)
if len(encrypted)>1048576 or get_quota(auth)['available_bytes']<len(encrypted):
raise ValueError('backup_size_or_quota_invalid')
name='root-custody-history-'+datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')+'-'+uuid.uuid4().hex+'.json.age'
status,_=network(upload+'/forgejo/'+name,'PUT',encrypted,(values['NC_WEBDAV_TOKEN'],''),{'Content-Type':'application/octet-stream','If-None-Match':'*'})
if status!=201:
raise ValueError('new_artifact_upload_failed')
status,fetched=network(HOST+'/remote.php/dav/files/Backup/railiance-backups/forgejo/'+name,'GET',auth=auth)
if status!=200 or hashlib.sha256(fetched).digest()!=hashlib.sha256(encrypted).digest():
raise ValueError('provider_readback_mismatch')
with tempfile.TemporaryDirectory(prefix='root-custody-recovery-') as tmp:
cipher=Path(tmp)/'history.json.age'
cipher.write_bytes(fetched)
cipher.chmod(0o600)
restored=process(['age','-d','-i','/dev/stdin',str(cipher)],key)
if restored!=bundle:
raise ValueError('decrypted_history_mismatch')
return {'provider':'existing-governed-nextcloud-backup','artifact':name,'upload_http_status':201,'readback_http_status':200,'ciphertext_sha256':hashlib.sha256(encrypted).hexdigest(),'decrypted_history_matches':True,'lane_kv_version':lane['metadata']['version'],'local_historical_rclone_used':False}
def main():
p=argparse.ArgumentParser()
p.add_argument('--state',type=Path,required=True)
p.add_argument('--net-kingdom',type=Path,required=True)
p.add_argument('--receipt',type=Path,required=True)
args=p.parse_args()
try:
if args.receipt.exists() or args.receipt.is_symlink():
return 1
require_attended()
if os.getenv('BAO_ADDR')!='http://127.0.0.1:18200':
return 1
source=args.net_kingdom/'tools/custody-record/validate.py'
schema=args.net_kingdom/'canon/schemas/custody-record_v0.1.schema.json'
if hashlib.sha256(source.read_bytes()).hexdigest()!=VALIDATOR_DIGEST or hashlib.sha256(schema.read_bytes()).hexdigest()!=SCHEMA_DIGEST:
return 1
spec=importlib.util.spec_from_file_location('custody_validator',source)
validator=importlib.util.module_from_spec(spec);spec.loader.exec_module(validator)
import fcntl
with (args.state.parent/'publisher.lock').open('a') as lock:
fcntl.flock(lock,fcntl.LOCK_EX)
state,bundle=checked_bundle(args.state,validator)
bao=Bao()
check_baseline(bao.read('platform/data/custody/net-kingdom/platform-root'),state,validator)
# Only current owner-held lane; no cached rclone config or caller token.
lane=bao.read(LANE)
operator=bao.read(OPERATOR_PATH)['data']
comparison=historical_config_comparison(Path('/home/worsch/.config/rclone/rclone.conf'),lane['data'])
if comparison['historical_share_matches_current_lane'] is True:
atomic_json(args.receipt,{'ok':False,'current_live_baseline_matches':True,**comparison,
'error':'current_backup_lane_requires_coordinated_existing_incident_recovery','recorded_at':datetime.now(timezone.utc).isoformat()})
return 1
receipt={**roundtrip(bundle,lane,operator),**comparison}
atomic_json(args.receipt,{'ok':True,'scope':'current-owner-read-and-governed-history-provider-roundtrip','record_id':RECORD_ID,'revision':state['revision'],'kv_version':state['kv_version'],'digest':state['digest'],'current_live_baseline_matches':True,'history_entries':len(json.loads(bundle)['history']),'recorded_at':datetime.now(timezone.utc).isoformat(),**receipt})
return 0
except Exception:
# Server/credential/HTTP/process diagnostics are never disclosed.
return 1
if __name__=='__main__':
raise SystemExit(main())

View file

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Disposable loopback Raft recovery and workstation receipt-copy proof.
No live admin login; no production backup is read; no key/token is emitted.
Temporary local-server credentials are generated and used only inside this run.
"""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import socket
import subprocess
import sys
import tempfile
import time
sys.path.insert(0, str(Path(__file__).resolve().parent))
import root_grant_attended as owner
def run(args, environ, optional=False):
result = subprocess.run(["bao", *args], env=environ, capture_output=True, timeout=20)
if result.returncode and not optional:
raise RuntimeError("isolated_command_failed")
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--state', type=Path, required=True)
parser.add_argument('--net-kingdom', type=Path, required=True)
parser.add_argument('--receipt', type=Path, required=True)
args = parser.parse_args()
if args.receipt.exists():
raise RuntimeError('unique_receipt_required')
source = args.net_kingdom / 'tools/custody-record/validate.py'
if hashlib.sha256(source.read_bytes()).hexdigest() != '9156e399c8d92a45b817bfc3738c89843487ce57e1718ed747f804f95cc719bb':
raise RuntimeError('reviewed_validator_required')
spec = importlib.util.spec_from_file_location('custody_validator', source)
validator = importlib.util.module_from_spec(spec)
spec.loader.exec_module(validator)
retained = json.loads(args.state.read_text())
if retained.get('pending') or validator.validate(retained['document']) or retained['document']['entries']:
raise RuntimeError('reviewed_empty_baseline_required')
state_digest = owner.digest(retained['document'])
if state_digest != retained['digest'] or retained['kv_version'] != 1:
raise RuntimeError('baseline_digest_or_version_mismatch')
with tempfile.TemporaryDirectory(prefix='nk-root-raft-proof-') as temporary:
scratch = Path(temporary)
copied = scratch / 'receipt-holder-copy'
copied.mkdir(mode=0o700)
shutil.copy2(args.state, copied/'baseline.json')
shutil.copytree(args.state.parent/'history', copied/'history')
restored = scratch/'receipt-holder-restored'
shutil.copytree(copied, restored)
actual_state = json.loads((restored/'baseline.json').read_text())
history_file = restored/'history'/('1-'+state_digest+'.json')
if actual_state != retained or json.loads(history_file.read_text()) != retained:
raise RuntimeError('receipt_holder_restore_differs')
if ((restored/'baseline.json').stat().st_mode & 0o777) != 0o600:
raise RuntimeError('receipt_holder_permissions_differ')
raft = scratch/'raft'
raft.mkdir(mode=0o700)
sock = socket.socket()
sock.bind(('127.0.0.1',0))
port = sock.getsockname()[1]
sock.close()
if port in (8200,18200):
raise RuntimeError('reserved_live_port_refused')
config = scratch/'server.hcl'
config.write_text('disable_mlock=true\nstorage "raft" {\n path="'+str(raft)+'"\n node_id="isolated-root-recovery"\n}\nlistener "tcp" {\n address="127.0.0.1:'+str(port)+'"\n tls_disable=true\n}\napi_addr="http://127.0.0.1:'+str(port)+'"\ncluster_addr="http://127.0.0.1:'+str(port+1)+'"\n')
env = {key:value for key,value in os.environ.items() if key not in ('BAO_TOKEN','VAULT_TOKEN','BAO_ADDR','VAULT_ADDR','VAULT_NAMESPACE','BAO_NAMESPACE')}
env.update(BAO_ADDR='http://127.0.0.1:'+str(port), VAULT_ADDR='http://127.0.0.1:'+str(port), GOMAXPROCS='2')
server = subprocess.Popen(['bao','server','-config='+str(config)],env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
try:
for attempt in range(60):
status = run(['status','-format=json'],env,optional=True)
if status.stdout:
break
if server.poll() is not None:
raise RuntimeError('isolated_server_exited')
time.sleep(.1)
initialized = json.loads(run(['operator','init','-format=json','-key-shares=1','-key-threshold=1'],env).stdout)
seal = initialized['unseal_keys_b64'][0]
env['BAO_TOKEN'] = initialized['root_token']
# Key is passed only through captured child argv to an isolated process;
# never logged, persisted in source or emitted in evidence.
run(['operator','unseal',seal],env)
run(['secrets','enable','-path=platform','kv-v2'],env)
older = scratch/'before-custody.snap'
latest = scratch/'with-custody.snap'
run(['operator','raft','snapshot','save',str(older)],env)
# Exact owner library, on isolated transport only; env is confined
# to a short-lived child script invocation below for subprocess Bao.
class IsolatedBao(owner.Bao):
def call(self, command, payload=None, optional=False):
result = subprocess.run(['bao',*command],input=None if payload is None else json.dumps(payload).encode(),env=env,capture_output=True,timeout=15)
if result.returncode:
if optional and result.returncode==2 and result.stderr.strip()==('No value found at '+command[-1]).encode():
return None
raise owner.Refused('isolated_request_failed')
return json.loads(result.stdout) if result.stdout.strip() else {}
bao = IsolatedBao()
synthetic_state = scratch/'publisher.json'
owner.publish(bao,retained['document'],synthetic_state,validator,'isolated-copy-of-approved-empty-baseline')
owner.check_baseline(bao.read(owner.PATH),actual_state,validator)
run(['operator','raft','snapshot','save',str(latest)],env)
run(['operator','raft','snapshot','restore','-force',str(older)],env)
for attempt in range(60):
try:
old_read = bao.read(owner.PATH,optional=True)
break
except owner.Refused:
run(['operator','unseal',seal],env,optional=True)
time.sleep(.1)
else:
raise RuntimeError('isolated_old_snapshot_unreadable')
refused = False
try:
owner.check_baseline(old_read,actual_state,validator)
except owner.Refused:
refused = True
if not refused:
raise RuntimeError('old_snapshot_not_refused')
run(['operator','raft','snapshot','restore','-force',str(latest)],env)
for attempt in range(60):
try:
latest_read = bao.read(owner.PATH)
owner.check_baseline(latest_read,actual_state,validator)
break
except (owner.Refused,KeyError,TypeError):
run(['operator','unseal',seal],env,optional=True)
time.sleep(.1)
else:
raise RuntimeError('latest_snapshot_did_not_match')
evidence={'ok':True,'scope':'disposable-loopback-openbao-raft','production_mutated':False,'production_snapshot_used':False,'receipt_holder_scratch_restore_verified':True,'receipt_holder_backup_provider_verified':False,'old_snapshot_missing_source_refused':True,'latest_snapshot_matches_independent_baseline':True,'record_id':owner.RECORD_ID,'revision':1,'kv_version':1,'digest':state_digest,'entries_count':0,'server_version':run(['version'],env).stdout.decode().strip(),'snapshot_count':2,'temporary_artifacts_removed':True}
finally:
server.terminate()
try:
server.wait(timeout=10)
except subprocess.TimeoutExpired:
server.kill()
server.wait()
owner.atomic_json(args.receipt,evidence)
print(json.dumps(evidence,sort_keys=True))
if __name__=='__main__':
try:
main()
except Exception:
print('{"ok":false,"error":"isolated_recovery_proof_failed"}')
sys.exit(1)

View file

@ -0,0 +1,69 @@
import configparser
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import subprocess
import unittest
from unittest.mock import patch
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'scripts'))
import root_custody_finish as finish
import root_grant_attended as owner
spec=importlib.util.spec_from_file_location('validator',ROOT.parent/'net-kingdom/tools/custody-record/validate.py')
validator=importlib.util.module_from_spec(spec);spec.loader.exec_module(validator)
class FinishTests(unittest.TestCase):
def test_retained_source_copy_contains_latest_valid_history(self):
path=ROOT/'docs/evidence/root-custody-history/baseline.json'
state,bundle=finish.checked_bundle(path,validator)
self.assertEqual(state['document']['entries'],[])
self.assertIn(state,json.loads(bundle)['history'])
def test_current_historical_share_is_classified_without_values(self):
with tempfile.TemporaryDirectory() as tmp:
path=Path(tmp)/'rclone.conf'
path.write_text('[nextcloud-backup]\nurl=https://example.invalid/public.php/dav/filesdrop/TEST-SHARE-VALUE\nuser=TEST-SHARE-VALUE\n')
result=finish.historical_config_comparison(path,{'NC_WEBDAV_TOKEN':'TEST-SHARE-VALUE'})
self.assertTrue(result['historical_share_matches_current_lane'])
self.assertNotIn('TEST-SHARE-VALUE',json.dumps(result))
result=finish.historical_config_comparison(path,{'NC_WEBDAV_TOKEN':'CURRENT-DIFFERENT'})
self.assertFalse(result['historical_share_matches_current_lane'])
self.assertEqual(result['historical_credential_status'],'historical-owner-and-revocation-unverified')
def test_unverified_upload_destination_refused_before_network(self):
lane={'data':{'NC_WEBDAV_URL':'https://unapproved.invalid/private','NC_WEBDAV_TOKEN':'TEST'}}
with patch.object(finish,'request') as network:
with self.assertRaises(ValueError):
finish.roundtrip(b'{}',lane,{'BACKUP_USERNAME':'Backup','BACKUP_PASSWORD':'TEST'},network=network)
network.assert_not_called()
def test_actual_age_roundtrip_with_disposable_key_and_memory_provider(self):
key=subprocess.run(['age-keygen'],capture_output=True,check=True).stdout.decode()
lane={'data':{'NC_WEBDAV_URL':finish.HOST+'/public.php/dav/filesdrop/DISPOSABLE-SHARE',
'NC_WEBDAV_TOKEN':'DISPOSABLE-SHARE','AGE_PRIVATE_KEY':key},'metadata':{'version':9}}
stored={}
def memory_provider(url,method='GET',body=None,auth=None,headers=None):
if method=='PUT':
self.assertEqual(headers['If-None-Match'],'*')
stored['encrypted']=body
return 201,b''
return 200,stored['encrypted']
receipt=finish.roundtrip(b'{"nonsecret":"receipt-fixture"}',lane,
{'BACKUP_USERNAME':'Backup','BACKUP_PASSWORD':'DISPOSABLE-PASSWORD'},
network=memory_provider,get_quota=lambda auth:{'available_bytes':1048576})
self.assertTrue(receipt['decrypted_history_matches'])
self.assertNotIn(key,json.dumps(receipt))
self.assertNotIn('DISPOSABLE-SHARE',json.dumps(receipt))
def test_missing_latest_history_cannot_make_backup(self):
with tempfile.TemporaryDirectory() as tmp:
path=Path(tmp)/'baseline.json'
path.write_text((ROOT/'docs/evidence/root-custody-history/baseline.json').read_text())
(path.parent/'history').mkdir()
with self.assertRaises(ValueError):
finish.checked_bundle(path,validator)
if __name__=='__main__':unittest.main()

View file

@ -103,7 +103,7 @@ id: RPF-WP-0049-T05
status: wait
priority: high
depends_on: [RPF-WP-0049-T04]
blocking_reason: "Admission and empty publication receipts verified; workload success/denial/outage/audit correlation and independent workstation receipt-holder backup/readback plus isolated OpenBao rollback/restore evidence remain. No new generic approval gate."
blocking_reason: "Admission and empty publication receipts verified; workload success/denial/outage/audit correlation, current live retained-baseline restore-check and established receipt-holder backup coverage/provider readback remain. Actual disposable native-Raft rollback/restore and local holder copy/restore proof passed. No new generic approval gate."
state_hub_task_id: "871e3236-61c0-58a1-a307-399cd662be6b"
```
@ -159,3 +159,15 @@ T05 remains wait for workload and independent recovery receipts; the source
workplan stays blocked on evidence, not another generic approval. The publisher
state holder is already independent/off-host relative to railiance01. No new
service dependency is introduced solely to duplicate that holder.
## Native isolated recovery completed — 2026-10-03
Ran scripts/root_custody_restore_probe.py with an actual disposable loopback
OpenBao v2.5.5 Raft server, exact owner publisher/baseline guard and retained
non-secret empty document. Old snapshot missing source refused; latest snapshot
matched revision 1/KV1 digest. Scratch workstation baseline/history restore and
mode 0600 matched. Temporary daemon/snapshots/credentials removed. No production
snapshot or live admin login used. Evidence:
docs/evidence/2026-10-03-root-custody-isolated-raft-recovery.json.
Current live restore-check and established backup coverage/provider readback
remain separate evidence requirements, not new generic approvals.