#!/usr/bin/python3 """QEMU-only bootstrap -> real D09 owner/worker -> actual OS transaction. Manager ELF is a synthetic Python requester, NOT accepted deployed Archipelago. OS metadata/package effects are real; release verifier is the VM-only exact-pin adapter from the installed qualification fixture, never production signing. """ import importlib.util import json import os from pathlib import Path import pwd import secrets import shutil import subprocess import sys import time assert 'QEMU' in Path('/sys/class/dmi/id/sys_vendor').read_text() assert subprocess.check_output(['findmnt', '-n', '-o', 'SOURCE', '/'], text=True).strip() == '/dev/vda' PACKAGE = Path('/opt/ota-source') NATIVE = (PACKAGE/'accepted-old-archipelago').exists() sys.path.insert(0, str(PACKAGE / 'scripts')) import os_updater_bootstrap as bootstrap import os_updater_activate as activation import os_update_executor as executor import d09_wire as wire def run(*args, check=True): r = subprocess.run(args, capture_output=True, text=True, timeout=180) print('COMMAND', args, r.returncode, r.stdout, r.stderr, flush=True) if check: r.check_returncode() return r def write(path, content, mode=0o600): path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(content.encode() if isinstance(content, str) else content) path.chmod(mode) def sha(path): return bootstrap.digest(Path(path).read_bytes()) def wait(predicate, label, timeout=900): deadline = time.monotonic() + timeout while time.monotonic() < deadline: value = predicate() if value: return value time.sleep(.2) raise TimeoutError(label) def native_health(): state = owner.service() if state['MainPID']=='0': return None r = subprocess.run(['nsenter','--target',state['MainPID'],'--net','--', 'curl','--max-time','2','--fail','--silent','http://127.0.0.1:19432/health'],capture_output=True,text=True) return r.stdout if r.returncode==0 else None # Reuse the installed fixture's actual Debian planner, NOT its direct stage # shortcut. Keep generated inputs outside protected plan storage until bootstrap. planner = (PACKAGE / 'tests/regression/os-ota-installed-stage.py').read_text() marker = " status = executor.stage(binding, 'APPLY', 'prepare')" assert planner.count(marker) == 1 prefix = planner.split(marker)[0] old = "source = executor.ROOT/'plans'/plan_sha" assert prefix.count(old) == 1 prefix = prefix.replace(old, "source = Path('/root/bootstrap-generated-plan')") context = {'__name__': 'bootstrap_planner', '__file__': str(PACKAGE / 'tests/regression/os-ota-installed-stage.py')} exec(compile(prefix + '\nexcept BaseException:\n raise\n', context['__file__'], 'exec'), context) print('BOOTSTRAP_PLANNER_COMPLETE', flush=True) spec = importlib.util.spec_from_file_location('provision', PACKAGE / 'scripts/provision-d09-lifecycle.py') provision = importlib.util.module_from_spec(spec) spec.loader.exec_module(provision) owner = provision.owner activation.untouched_authority() run('systemctl', 'stop', 'archipelago.service', check=False) try: account = pwd.getpwnam('archipelago') except KeyError: # Minimal Debian rootfs has no Archipelago principal. Establish this fixture # installation before taking preservation observations or invoking bootstrap. run('useradd','--uid','1000','--user-group','--create-home','--shell','/bin/bash','archipelago') account = pwd.getpwnam('archipelago') # A separate pre-existing fixture admin represents the independently verified # access channel. Creation is fixture setup, never done by production bootstrap. run('useradd', '--no-create-home', '--shell', '/bin/bash', 'ota-fixture-admin') admin = pwd.getpwnam('ota-fixture-admin') if executor.ROOT == Path('/var/lib/archipelago-os-update'): # Match the recorded deployed data-root ownership without weakening custody. os.chown('/var/lib/archipelago',account.pw_uid,account.pw_gid) control = Path('/run/d09-fixture-client') control.mkdir(mode=0o700) os.chown(control, account.pw_uid, account.pw_gid) cache = Path('/var/lib/archipelago/d09-requests') cache.mkdir(parents=True, mode=0o700, exist_ok=True) os.chown(cache, account.pw_uid, account.pw_gid) requester = '''import json,pathlib,subprocess,time p=pathlib.Path('/run/d09-fixture-client') while True: for request in p.glob('request-*.json'): output=request.with_name(request.name.replace('request-','result-')) if output.exists():continue r=subprocess.run(['/usr/libexec/archipelago-d09ctl',*json.loads(request.read_text())],capture_output=True,text=True,timeout=60) output.write_text(json.dumps({'exit':r.returncode,'stdout':r.stdout,'stderr':r.stderr})) time.sleep(.1) ''' write('/root/fixture-access.json', json.dumps({'fixture': True, 'independent_uid': admin.pw_uid, 'machine_id_sha256': sha('/etc/machine-id'), 'existing_access_checked': True})) write(PACKAGE / 'requester.py', requester, 0o644) python = Path(sys.executable).resolve().read_bytes() old_backend = (PACKAGE/'accepted-old-archipelago').read_bytes() if NATIVE else python new_backend = Path('/opt/os-ota-native').read_bytes() if NATIVE else python+b'\nfixture-v2\n' if NATIVE: assert wire.digest(old_backend)=='2cf004fcc7f207c65b3bbd1eac248f8c9556ec1cd0ec139011fb4b1e1ef96494' proof = json.loads(Path('/opt/os-ota-native-proof.json').read_text()) assert wire.digest(new_backend)==proof['packaged_sha256'] write(owner.DESTINATIONS['backend'], old_backend, 0o755) web = owner.DESTINATIONS['frontend'] write(web / 'index.html', 'old frontend\n', 0o644) template = Path('archipelago-runtime/image-recipe/configs/nginx-archipelago.conf') write(web / template, (PACKAGE / 'image-recipe/configs/nginx-archipelago.conf').read_bytes(), 0o644) unit = '''[Unit] Description=Disposable old/new manager requester [Service] Type=exec User=archipelago ExecStart=/usr/local/bin/archipelago /opt/ota-source/requester.py NoNewPrivileges=yes ProtectSystem=strict ProtectHome=read-only ReadWritePaths=/run/d09-fixture-client /var/lib/archipelago/d09-requests Restart=no ''' if NATIVE: manager_data = Path('/var/lib/os-bootstrap-native-manager') manager_data.mkdir(mode=0o700) os.chown(manager_data,account.pw_uid,account.pw_gid) write(PACKAGE/'requester-launch.py', '''import subprocess subprocess.Popen(['/usr/bin/python3','-B','/opt/ota-source/requester.py'],stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True) ''', 0o644) unit = unit.replace('ExecStart=/usr/local/bin/archipelago /opt/ota-source/requester.py', 'ExecStart=/usr/local/bin/archipelago\nExecStartPost=/usr/bin/python3 -B /opt/ota-source/requester-launch.py\n' 'PrivateNetwork=yes\nEnvironment=ARCHIPELAGO_DATA_DIR=/var/lib/os-bootstrap-native-manager\n' 'Environment=ARCHIPELAGO_RUNTIME_DATA_DIR=/var/lib/os-bootstrap-native-manager\n' 'Environment=ARCHIPELAGO_BIND=127.0.0.1:19432\n' 'Environment=ARCHIPELAGO_NOSTR_DISCOVERY_ENABLED=false\nEnvironment=ARCHIPELAGO_NOSTR_RELAYS=\n' 'Environment=ARCHIPELAGO_SWARM_ENABLED=false\nReadWritePaths=/var/lib/os-bootstrap-native-manager') write('/etc/systemd/system/archipelago.service', unit, 0o644) native_identity = {} if NATIVE: # Run the real accepted installation before any bootstrap/D09 owner exists. # Drain it externally only after health and nonempty identity are observed. run('systemctl','daemon-reload') run('systemctl','start','archipelago.service') health = wait(native_health,'pre-bootstrap accepted native health',timeout=90) state = owner.service() assert sha('/proc/'+state['MainPID']+'/exe')==wire.digest(old_backend) native_identity = {str(p):sha(p) for p in manager_data.joinpath('identity').iterdir() if p.is_file()} assert native_identity write('/root/accepted-old-running.json',wire.canonical(dict(backend_sha256=wire.digest(old_backend), health=health,identity=native_identity,invocation_id=state['InvocationID']))) print('PRE_BOOTSTRAP_ACCEPTED_NATIVE_RUNNING',health,flush=True) run('systemctl','stop','archipelago.service') write('/etc/systemd/system/archipelago.service.d/50-d09-lifecycle.conf', (PACKAGE / 'image-recipe/configs/50-d09-lifecycle.conf').read_bytes(), 0o644) run('systemctl', 'daemon-reload') incoming = owner.ROOT / 'incoming' incoming.mkdir(parents=True, mode=0o700) exclusion = wire.canonical({'fixture': True, 'independent_root': True}) write(incoming / 'exclusion.json', exclusion) for generation in ('new', 'restore'): shutil.copytree(web, incoming / (generation + '-frontend')) write(incoming / (generation + '-backend'), new_backend if generation=='new' else old_backend, 0o755) new_config = (PACKAGE / 'image-recipe/configs/60-terminal-workspace.conf').read_bytes() write(incoming / 'new-config', new_config) restore_digest = wire.digest(owner.effective_configuration().encode()) write(owner.DESTINATIONS['config'], new_config, 0o644) run('systemctl', 'daemon-reload') apply_digest = wire.digest(owner.effective_configuration().encode()) owner.DESTINATIONS['config'].unlink() run('systemctl', 'daemon-reload') binding = dict(schema_version=1, target_binding_digest=secrets.token_hex(32), composition_digest=secrets.token_hex(32), admission_epoch_id=secrets.token_hex(16), transaction_id=secrets.token_hex(16), grants={r: secrets.token_hex(32) for r in ('UPDATER','AUTOMATIC','MANUAL','GUARD','OBSERVER')}, objects={name: dict(old=owner.object_digest(dest), new=owner.object_digest(incoming/('new-'+name)), restore=owner.object_digest(incoming/('restore-'+name))) for name,dest in owner.DESTINATIONS.items()}, artifacts={'backend':sha(incoming/'new-backend'),'frontend':owner.object_digest(incoming/'new-frontend')}, restore_artifacts={'backend':sha(owner.DESTINATIONS['backend']),'frontend':owner.object_digest(web)}, effective_config={'APPLY':apply_digest,'RESTORE':restore_digest}, exclusion_evidence_digest=wire.digest(exclusion), helper_digest=sha(PACKAGE/'image-recipe/configs/prepare-terminal-workspace.sh'), prerequisite={'grant_digest':secrets.token_hex(32),'backend_sha256':sha(owner.DESTINATIONS['backend']), 'effective_config_digest':restore_digest}, os_update=context['binding']['os_update']) binding['release_identity'] = ['2.0.1-alpha', [['archipelago','2.0.1-alpha',binding['artifacts']['backend'],(incoming/'new-backend').stat().st_size], ['os-update-plan.json','2.0.1-alpha',binding['os_update']['plan_sha256'],(context['source']/'plan.json').stat().st_size]]] manifest = dict(version='2.0.1-alpha', components=[dict(zip(('name','new_version','sha256','size_bytes'), row)) for row in binding['release_identity'][1]], os_update=dict(version=1,plan_sha256=binding['os_update']['plan_sha256']), qualification='ROOT-APPOINTED-EXACT-FIXTURE-NOT-PRODUCTION-SIGNATURE') write(context['source']/'manifest.json', wire.canonical(manifest)) binding['os_update']['manifest_sha256'] = sha(context['source']/'manifest.json') # A tiny native exact-pin adapter in the disposable guest, with QEMU refusal. # No signing key, production verifier substitution, or production pass is implied. native_verifier = r'''#include #include #include #include #include int main(int argc,char **argv) { char vendor[256]={0}, hash[256]={0}; FILE *f=fopen("/sys/class/dmi/id/sys_vendor","r"); if(!f || !fgets(vendor,sizeof vendor,f) || !strstr(vendor,"QEMU")) return 71; fclose(f); if(argc!=4 || strcmp(argv[1],"ceremony") || strcmp(argv[2],"verify")) return 72; int p[2], status; if(pipe(p)) return 73; pid_t pid=fork(); if(pid<0) return 74; if(!pid) { close(p[0]);dup2(p[1],1);close(p[1]); execl("/usr/bin/sha256sum","sha256sum","--",argv[3],(char*)0);_exit(75); } close(p[1]); f=fdopen(p[0],"r"); if(!fgets(hash,sizeof hash,f)) return 76; fclose(f); if(waitpid(pid,&status,0)!=pid || !WIFEXITED(status) || WEXITSTATUS(status)) return 77; return strncmp(hash,"EXPECTED_SHA256",64) ? 78 : 0; } '''.replace('EXPECTED_SHA256', binding['os_update']['manifest_sha256']) write('/root/fixture-native-verifier.c', native_verifier) run('gcc','-O2','-o',str(executor.VERIFIER),'/root/fixture-native-verifier.c') executor.VERIFIER.chmod(0o755) binding['os_update']['verifier_sha256'] = sha(executor.VERIFIER) # Explicit disposable authentication adapter appointment. Production initial # custody must use the already appointed native release-root verifier; we cannot # produce a production-signed candidate here. No production code is patched. # Model independently established verifier custody and record the substitution. custody = dict(version=1,verifier_sha256=sha(executor.VERIFIER), prerequisite_backend_sha256=sha(executor.VERIFIER)) assert not (executor.ROOT/'verifier-custody.json').exists() write(executor.ROOT/'verifier-custody.json', wire.canonical(custody)) write('/root/fixture-verifier-appointment.json', wire.canonical(dict( fixture_only=True,production_signature_approval=False,custody=custody))) write(incoming / 'admission.json', wire.canonical(binding)) write(context['EVIDENCE']/'binding.json', wire.canonical(binding)) files = {name: sha(context['source']/name) for name in ('plan.json','manifest.json','preservation.json')} plan = dict(schema=1, machine_id_sha256=sha('/etc/machine-id'), current={'version':'2.0.0-alpha+uat','source_sha256':sha(PACKAGE/'scripts/d09_worker.py'), 'backend_sha256':sha(owner.DESTINATIONS['backend'])}, candidate={'version':'2.0.1-alpha','source_sha256':wire.digest(wire.canonical(files)), 'backend_sha256':binding['artifacts']['backend']}, admin={'uid':admin.pw_uid,'service_uid':account.pw_uid,'access_receipt_sha256':sha('/root/fixture-access.json')}, observations={'root/fixture-access.json':sha('/root/fixture-access.json'), 'root/fixture-verifier-appointment.json':sha('/root/fixture-verifier-appointment.json')}, files=files) if NATIVE: plan['current']['source_sha256']=sha(PACKAGE/'accepted-source-receipt.json') plan['observations'].update({'opt/ota-source/accepted-source-receipt.json':sha(PACKAGE/'accepted-source-receipt.json'), 'opt/os-ota-native-proof.json':sha('/opt/os-ota-native-proof.json'), 'root/accepted-old-running.json':sha('/root/accepted-old-running.json')}) write('/root/bootstrap-preparation.json', wire.canonical(plan)) bootstrap.prepare(Path('/'), Path('/root/bootstrap-preparation.json'), sha('/root/bootstrap-preparation.json'), context['source']) approval = dict(version=1, prepared_sha256=sha('/root/bootstrap-preparation.json'), package_root=str(PACKAGE), package_files=activation.package_inventory(PACKAGE), admission_sha256=sha(incoming/'admission.json'), os_inputs=files, helper_sha256=sha(PACKAGE/'scripts/os_updater_activate.py'), owner_dropin_sha256=bootstrap.digest(activation.owner_policy(executor.ROOT))) write('/root/bootstrap-activation.json', wire.canonical(approval)) preserved = {str(p): sha(p) for p in [Path('/etc/passwd'),Path('/etc/group'),Path('/etc/shadow'), *Path('/var/lib/archipelago/ota-synthetic-canaries').iterdir()]} try: result = activation.activate(Path('/root/bootstrap-activation.json'), sha('/root/bootstrap-activation.json')) except BaseException as exc: print('ACTIVATION_FAILURE', repr(exc), getattr(exc,'stdout',None), getattr(exc,'stderr',None), flush=True) run('journalctl','-u','archipelago-d09-lifecycle.service','-u','archipelago.service','--no-pager',check=False) raise print('BOOTSTRAP_ACTIVATED', json.dumps(result), flush=True) assert result['state'] == 'D09_PREREQUISITE_ACTIVATED' if NATIVE: print('ACCEPTED_OLD_NATIVE_HEALTH',wait(native_health,'accepted old native health',timeout=90),flush=True) assert native_identity == {path:sha(path) for path in native_identity} try: activation.activate(Path('/root/bootstrap-activation.json'), sha('/root/bootstrap-activation.json')) raise AssertionError('activation replay accepted') except ValueError as exc: assert 'existing activation intent' in str(exc) staging_generation = secrets.token_hex(16) write(control/'request-apply.json', json.dumps(['prepare', '--selected-release', json.dumps(binding['release_identity']), '--staging-generation', staging_generation]), 0o644) reply = wait(lambda: (control/'result-apply.json').read_text() if (control/'result-apply.json').exists() else None, 'client apply') print('D09_APPLY_REPLY', reply, flush=True) lost_reply = json.loads(reply)['exit'] != 0 if lost_reply: assert 'TimeoutError: timed out' in json.loads(reply)['stderr'], reply # A lost reply is not permission to replay an OS operation. Observe the # independently protected exact reservation; never send another request. def accepted_after_lost_reply(): value = json.loads((owner.ROOT/'ledger.json').read_text()) return any(item['request'].get('staging_generation')==staging_generation for item in value['requests'].values()) wait(accepted_after_lost_reply,'protected reservation after lost reply',timeout=30) print('D09_REPLY_LOST_RESERVATION_OBSERVED_NO_RETRY',flush=True) status_path = executor.ROOT/'transactions'/binding['os_update']['authority_digest']/'status.json' def completed(): if status_path.exists(): status = json.loads(status_path.read_text()) if status['phase'] == 'RECONCILIATION_REQUIRED': raise AssertionError(json.dumps(status)) if status['phase'] == 'REBOOT_REQUIRED': return status ledger = json.loads((owner.ROOT/'ledger.json').read_text()) if (ledger.get('receipt') or {}).get('phase') == 'CONFLICT': run('journalctl','--no-pager','-u','archipelago-d09-lifecycle.service', '-u',ledger['execution']['unit'],check=False) raise AssertionError(json.dumps(ledger)) status = wait(completed, 'actual OS apply') def candidate_started(): value = json.loads((owner.ROOT/'ledger.json').read_text()) receipt = value.get('receipt') or {} if receipt.get('phase') == 'CONFLICT': raise AssertionError(json.dumps(value)) return receipt if receipt.get('phase') == 'AWAITING_OBSERVATION' else None receipt = wait(candidate_started, 'candidate manager actual activation', timeout=90) current = owner.service() assert current['ActiveState']=='active' and current['InvocationID']==receipt['observed_invocation_id'] assert sha('/proc/'+current['MainPID']+'/exe')==binding['artifacts']['backend'] if NATIVE: print('CANDIDATE_NATIVE_HEALTH',wait(native_health,'candidate native health',timeout=90),flush=True) assert native_identity == {path:sha(path) for path in native_identity} assert preserved == {path: sha(path) for path in preserved} wallet = Path('/var/lib/archipelago/ota-synthetic-canaries/wallet.db') wallet.write_bytes(wallet.read_bytes() + b'forward-only-after-OS-apply\n') forward = sha(wallet) try: activation.activate(Path('/root/bootstrap-activation.json'), sha('/root/bootstrap-activation.json')) raise AssertionError('post-apply bootstrap replay accepted') except ValueError as exc: assert 'existing activation intent' in str(exc) assert sha(wallet) == forward print('BOOTSTRAP_OS_ACTUAL_EFFECTS', json.dumps({'bootstrap':result,'os_phase':status['phase'], 'os_status_sha256':wire.digest(wire.canonical(status)), 'preserved':preserved,'forward_wallet_sha256':forward,'accepted_native_artifact':NATIVE,'production_signature_approval':False}), flush=True) write('/var/log/os-ota-stage/bootstrap-result.json', wire.canonical({'passed':True,'bootstrap':result, 'os_status':status,'preserved':preserved,'forward_wallet_sha256':forward,'native_identity':native_identity, 'accepted_native_artifact':NATIVE,'production_signature_approval':False,'client_reply_lost':lost_reply, 'os_state_root':str(executor.ROOT),'application_data_uid':Path('/var/lib/archipelago').stat().st_uid})) write('/var/log/os-ota-stage/result.json', wire.canonical({'passed':True, 'scope':'bootstrap -> actual D09 owner/worker -> installed Debian OS effects', 'accepted_native_artifact':NATIVE,'production_signature_approval':False, 'postboot_confirmed':False,'phase':status['phase'],'d09_phase':receipt['phase'],'client_reply_lost':lost_reply, 'os_state_root':str(executor.ROOT),'application_data_uid':Path('/var/lib/archipelago').stat().st_uid}))