#!/usr/bin/env python3 """Native crypto + real bootstrap/D09/OS test. Not candidate qualification. Reuses the peer lane's guest fixture scaffolding with explicit, checked fixture- only substitutions. Production helpers/crypto/executor are never monkeypatched. """ import hashlib import json import os from pathlib import Path import subprocess import socket import struct 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') sys.path.insert(0,str(PACKAGE/'scripts')) import os_update_executor as executor import os_updater_activate as activation import os_updater_bootstrap as bootstrap assert executor.ROOT==activation.OS_ROOT==Path('/var/lib/archipelago-os-update'), 'correct root custody prerequisite' assert 'var/lib/archipelago-os-update/transactions' in bootstrap.OS_TRANSACTION_ROOTS assert not (PACKAGE/'accepted-old-archipelago').exists(), 'isolated fixture must not claim accepted native candidate' fixture=Path('/opt/os-ota-crypto-native') proof=json.loads(Path('/opt/os-ota-crypto-fixture.json').read_text()) assert proof['scope']=='ISOLATED_COMPILETIME_TEST_ROOT_NOT_CANDIDATE' assert executor.file_hash(fixture)==proof['binary_sha256'] fixture.chmod(0o755) keys=Path('/root/os-ota-crypto-fixture') keys.mkdir(mode=0o700) Path('/root/os-ota-crypto.seed').rename(keys/'test.seed') (keys/'test.seed').chmod(0o600) (keys/'wrong.seed').write_bytes(os.urandom(32)) (keys/'wrong.seed').chmod(0o600) def preservation_probes(context): account=context['account'] home=Path('/home/archipelago') probes=[home/'Work/operator-fixture.txt',home/'.local/state/archipelago/sessions/history-fixture.txt', Path('/etc/systemd/system/archipelago.service.d/90-operator-fixture.conf')] for path in probes[:2]: path.parent.mkdir(parents=True,exist_ok=True) path.write_text('preserve exact fixture bytes, identity and intent\n') path.chmod(0o600) for parent in (path,*path.parents): if parent==home.parent:break os.chown(parent,account.pw_uid,account.pw_gid) probes[2].parent.mkdir(parents=True,exist_ok=True) probes[2].write_text('[Service]\nEnvironment=OS_OTA_OPERATOR_CANARY=retained\n') context['crypto_preservation_paths']=probes context['crypto_preservation_before']=[path_identity(p) for p in probes] def path_identity(path): st=path.stat() return [str(path),st.st_ino,st.st_mode,st.st_uid,st.st_gid,executor.file_hash(path)] def capture_owner(context): pid=int(subprocess.check_output(['systemctl','show','archipelago-d09-lifecycle.service','-p','MainPID','--value'],text=True)) context['crypto_owner_before']=context['owner'].process(pid) context['crypto_epoch_before']=json.loads((context['owner'].ROOT/'ledger.json').read_text())['epoch'] context['crypto_root_uuid']=subprocess.check_output(['blkid','-s','UUID','-o','value','/dev/vda'],text=True).strip() def crypto_appointment(context): binding=context['binding'] source=context['context']['source'] manifest=source/'manifest.json' unsigned=source/'unsigned-fixture-manifest.json' manifest.rename(unsigned) subprocess.run([str(fixture),'fixture-sign',str(keys/'test.seed'),str(unsigned),str(manifest)],check=True) checks=[] def verify(name,path,accept,env=None): r=subprocess.run([str(fixture),'ceremony','verify',str(path)],capture_output=True,text=True,env=env) assert (r.returncode==0)==accept,(name,r.stdout,r.stderr) checks.append(dict(name=name,exit=r.returncode,stdout=r.stdout,stderr=r.stderr)) verify('genuine-fixture-signature',manifest,True) verify('unsigned-refused',unsigned,False) tamper=json.loads(manifest.read_text());tamper['qualification']='tampered after signature' (source/'tampered.json').write_text(json.dumps(tamper)) verify('tampered-signature-refused',source/'tampered.json',False) subprocess.run([str(fixture),'fixture-sign',str(keys/'wrong.seed'),str(unsigned),str(source/'wrong-key.json')],check=True) verify('valid-signature-wrong-root-refused',source/'wrong-key.json',False) env=dict(os.environ,ARCHY_RELEASE_ROOT_PUBKEY='00'*32) verify('runtime-env-cannot-replace-compiled-test-pin',manifest,True,env) production=Path('/opt/os-ota-native') if production.exists(): production.chmod(0o755) r=subprocess.run([str(production),'ceremony','verify',str(manifest)],capture_output=True,text=True) assert r.returncode!=0 and 'pinned release-root anchor' in r.stderr,r.stderr checks.append(dict(name='production-root-refuses-test-signature',exit=r.returncode,stderr=r.stderr)) binding['os_update']['manifest_sha256']=executor.file_hash(manifest) # Planner adapter is retained as evidence, replaced only on this fresh VM by # genuine native verification using the isolated test anchor. if executor.VERIFIER.exists(): assert 'ROOT_APPOINTED_EXACT_FIXTURE_ONLY_NOT_PRODUCTION_SIGNATURE' in executor.VERIFIER.read_text() executor.VERIFIER.rename(source/'retained-planner-exact-pin-adapter.py') import shutil shutil.copyfile(fixture,executor.VERIFIER) executor.VERIFIER.chmod(0o755) binding['os_update']['verifier_sha256']=executor.file_hash(executor.VERIFIER) assert binding['os_update']['verifier_sha256']==binding['prerequisite']['backend_sha256'] assert not (executor.ROOT/'verifier-custody.json').exists(), 'custody must be established by actual provisioner' (Path('/root/fixture-verifier-appointment.json')).write_text(json.dumps(dict( fixture_only=True,cryptographic_verification=True,production_signature_approval=False,proof=proof))) (Path('/var/log/os-ota-stage')/'native-crypto-checks.json').write_text(json.dumps(checks,indent=2)+'\n') print('GENUINE_NATIVE_CRYPTO_CHECKS',json.dumps(checks),flush=True) source=PACKAGE/'scripts/tests/os_updater_activation_guest.py' raw=source.read_text() original_hash=hashlib.sha256(raw.encode()).hexdigest() old='python = Path(sys.executable).resolve().read_bytes()' assert raw.count(old)==1 raw=raw.replace(old,"python = Path('/opt/os-ota-crypto-native').read_bytes()") old='ExecStart=/usr/local/bin/archipelago /opt/ota-source/requester.py' assert old in raw # NATIVE=False; replacing the base unit command does not activate the separate # accepted-candidate branch of the peer fixture. raw=raw.replace(old,'ExecStart=/usr/local/bin/archipelago') old='NoNewPrivileges=yes\nProtectSystem=strict' assert raw.count(old)==1 raw=raw.replace(old,'RuntimeDirectory=d09-fixture-client\nRuntimeDirectoryMode=0700\nRuntimeDirectoryPreserve=yes\n'+old) start=raw.index('# A tiny native exact-pin adapter in the disposable guest') end=raw.index("write(incoming / 'admission.json', wire.canonical(binding))",start) raw=raw[:start]+'crypto_appointment(globals())\n'+raw[end:] raw=raw.replace('ROOT-APPOINTED-EXACT-FIXTURE-NOT-PRODUCTION-SIGNATURE','NATIVE_CRYPTO_COMPILETIME_TEST_ROOT_NOT_CANDIDATE') marker='# A separate pre-existing fixture admin represents the independently verified' assert raw.count(marker)==1 raw=raw.replace(marker,'preservation_probes(globals())\n'+marker) marker="assert result['state'] == 'D09_PREREQUISITE_ACTIVATED'" assert raw.count(marker)==1 raw=raw.replace(marker,marker+'\ncapture_owner(globals())') context=dict(__name__='isolated_crypto_bootstrap',__file__=str(source),crypto_appointment=crypto_appointment, preservation_probes=preservation_probes,capture_owner=capture_owner) transformed=Path('/root/os-ota-crypto-transformed-guest.py') transformed.write_text(raw) Path('/var/log/journal').mkdir(exist_ok=True) subprocess.run(['journalctl','--flush'],check=True) try: exec(compile(raw,str(transformed),'exec'),context) finally: evidence=Path('/var/log/os-ota-stage') evidence.mkdir(exist_ok=True) units=['archipelago-d09-lifecycle.service','archipelago.service'] ledger=Path('/var/lib/archipelago-d09/ledger.json') if ledger.exists(): value=json.loads(ledger.read_text()) (evidence/'retained-d09-ledger.json').write_bytes(ledger.read_bytes()) if value.get('execution'): units.append(value['execution']['unit']) command=['journalctl','--no-pager','-o','short-monotonic'] for unit in units:command+=['-u',unit] r=subprocess.run(command,capture_output=True,text=True) (evidence/'crypto-d09-journal.log').write_text(r.stdout+r.stderr) r=subprocess.run(['systemctl','show',*units],capture_output=True,text=True) (evidence/'crypto-d09-units.log').write_text(r.stdout+r.stderr) owner=context['owner'] deadline=time.monotonic()+120 while time.monotonic()