Files
archy/tests/regression/os-ota-installed-owner-postboot.py
T

197 lines
12 KiB
Python

#!/usr/bin/env python3
"""Real installed D09 owner postboot operation after private bootstrap/OS apply.
Fresh root fixture observer supplies retained-code reactivation from an external
preboot ledger, never renewed package execution or a database restore.
"""
import importlib.util
import atexit
import json
from pathlib import Path
import platform
import secrets
import socket
import struct
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'
sys.path.insert(0,'/usr/libexec')
import os_update_executor as executor
import d09_wire as wire
# Installed executable has no .py suffix; use its explicit source loader.
from importlib.machinery import SourceFileLoader
loader=SourceFileLoader('installed_provision','/usr/libexec/archipelago-d09-provision')
spec=importlib.util.spec_from_loader(loader.name,loader)
provision=importlib.util.module_from_spec(spec)
loader.exec_module(provision)
owner=provision.owner
evidence=Path('/var/log/os-ota-stage')
def retain_diagnostics():
try:
raw=(owner.ROOT/'ledger.json').read_bytes()
(evidence/'postboot-retained-ledger.json').write_bytes(raw)
value=json.loads(raw)
units=['archipelago.service','archipelago-d09-lifecycle.service']
if value.get('execution'):units.append(value['execution']['unit'])
command=['journalctl','--no-pager','-b']
for unit in units:command+=['-u',unit]
r=subprocess.run(command,capture_output=True,text=True,timeout=30)
(evidence/'postboot-d09-journal.log').write_text(r.stdout+r.stderr)
except Exception as error:
print('POSTBOOT_DIAGNOSTIC_ERROR',repr(error),flush=True)
atexit.register(retain_diagnostics)
bootstrap=json.loads((evidence/'bootstrap-result.json').read_text())
assert bootstrap['passed']
crypto=json.loads((evidence/'crypto-result.json').read_text()) if (evidence/'crypto-result.json').exists() else None
preserved=crypto['forward_preserved'] if crypto else dict(bootstrap['preserved'])
if not crypto and bootstrap.get('forward_wallet_sha256'):
preserved['/var/lib/archipelago/ota-synthetic-canaries/wallet.db']=bootstrap['forward_wallet_sha256']
binding=json.loads(owner.BINDING.read_text())
instance=executor.Executor(binding['os_update'])
assert instance.status['phase']=='REBOOT_REQUIRED'
assert platform.release()==instance.plan['boot']['expected_release']
for path,sha in preserved.items():
assert executor.file_hash(path)==sha,path
subprocess.run(['systemctl','start','archipelago-d09-lifecycle.service'],check=True)
deadline=time.monotonic()+30
while not owner.PRIVATE.exists() and time.monotonic()<deadline:
time.sleep(.1)
raw=(owner.ROOT/'ledger.json').read_bytes()
v=json.loads(raw)
assert v['receipt'] is not None
if crypto:
external_head=Path('/opt/os-ota-external-preboot-ledger.json').read_bytes()
assert raw==external_head, 'guest retained head differs from independent preboot receipt'
assert v['receipt']['phase']=='COMPLETE' and crypto['same_boot_userspace_complete']
index={k:binding[k] for k in ('target_binding_digest','composition_digest','transaction_id')}
fields=dict(index,predecessor_epoch_id=v['epoch'],head_sequence=v['sequence'],
preserved_head_digest=wire.digest(wire.canonical(v['receipt'])),
preserved_journal_digest=wire.digest(wire.canonical(dict(receipts=v['journal'],intents=v['intents']))),
preserved_consumed_history_digest=wire.digest(wire.canonical(owner.consumption_history(v))),
preserved_target_index_digest=wire.digest(wire.canonical(index)),
allowed_action='REACTIVATE_RETAINED' if crypto else 'PRESERVE_ONLY',new_epoch_id=secrets.token_hex(16),
new_grant_digest=secrets.token_hex(32),execution_grant_digest=secrets.token_hex(32) if crypto else None)
if crypto:
probe=owner.Owner.__new__(owner.Owner)
fields['disposition_digest']=probe.drained_disposition(v)
external=Path('/root/os-ota-postboot-observer')
external.mkdir(mode=0o700)
history=dict(fields,sequence=1)
(external/'history.json').write_bytes(wire.canonical(history))
record=dict(fields,schema_version=1,observed_boot_id=owner.boot_id(),external_history_sequence=1,
external_history_digest=executor.file_hash(external/'history.json'))
(external/'readmission.json').write_bytes(wire.canonical(record))
provision.readmit(external/'readmission.json',external/'history.json',activation=bool(crypto))
if crypto:
deadline=time.monotonic()+120
while time.monotonic()<deadline:
value=json.loads((owner.ROOT/'ledger.json').read_text())
if value['receipt']['phase']=='AWAITING_OBSERVATION':break
assert value['receipt']['phase']!='CONFLICT',value['receipt']
time.sleep(.2)
else:raise TimeoutError('postboot retained candidate activation')
assert owner.service()['ActiveState']=='active'
observation=dict(version=1,authority_digest=binding['os_update']['authority_digest'],
plan_sha256=binding['os_update']['plan_sha256'],boot_id=executor.boot_id(),preservation_verified=True)
(instance.source/'postboot-preservation.json').write_bytes(wire.canonical(observation))
result=subprocess.run(['/usr/libexec/archipelago-d09-provision','--os-postboot'],capture_output=True,text=True)
print('ACTUAL_OWNER_POSTBOOT',result.returncode,result.stdout,result.stderr,flush=True)
result.check_returncode()
observed=json.loads(result.stdout)
assert observed['os_postboot_verified'] is True
status=subprocess.run(['/usr/libexec/archipelago-d09ctl','inspect'],check=True,capture_output=True,text=True)
projection=json.loads(status.stdout)
assert projection['os_update']['phase']=='POSTBOOT_CONFIRMED'
assert projection['os_update']['post_boot_verified'] is True
assert projection['os_update']['running_kernel']==platform.release()
if crypto:
# Complete the new userspace invocation using fresh real namespace evidence.
value=json.loads((owner.ROOT/'ledger.json').read_text());r=value['receipt']
pid=owner.service()['MainPID']
subprocess.run(['nsenter','-t',pid,'-m','--','runuser','-u','archipelago','--','bash','-c',
'cd /home/archipelago/Work && script -q -c pwd /dev/null'],check=True)
for directory in ('/home/archipelago/Work','/home/archipelago/.local/state/archipelago/sessions'):
subprocess.run(['nsenter','-t',pid,'-m','--','runuser','-u','archipelago','--','touch',directory+'/postboot-observed-write'],check=True)
for denied in ('/usr/local/bin/postboot-denied','/opt/archipelago/web-ui/postboot-denied'):
assert subprocess.run(['nsenter','-t',pid,'-m','--','runuser','-u','archipelago','--','touch',denied],capture_output=True).returncode!=0
before=crypto['workspace_preservation'];after=[]
for old in before:
path=Path(old[0]);st=path.stat()
after.append([str(path),st.st_ino,st.st_mode,st.st_uid,st.st_gid,executor.file_hash(path)])
assert before==after
base=dict(transaction_id=r['transaction_id'],observed_invocation_id=r['observed_invocation_id'])
namespace=dict(base,qualification='isolated native requester/real OS PTY; not native product terminal',
default_terminal_created=True,default_terminal_workspace='/home/archipelago/Work',
namespace_writable=['/home/archipelago/Work','/home/archipelago/.local/state/archipelago/sessions'])
preservation=dict(base,preserved={name:dict(before=wire.digest(wire.canonical(a)),after=wire.digest(wire.canonical(b)))
for name,a,b in zip(('work','history','custom_fragments'),before,after)})
from d09_store import atomic
def publish(value):
raw=wire.canonical(value);sha=wire.digest(raw)
atomic(owner.ROOT/'observations'/(sha+'.json'),raw)
return sha
obs={k:r[k] for k in ('transaction_id','target_binding_digest','composition_digest','observed_invocation_id',
'effective_config_observation_digest','config_current_digest')}
obs.update(namespace_observation_digest=publish(namespace),preservation_observation_digest=publish(preservation))
public=json.loads(owner.BINDING.with_name('client.json').read_text())
req=dict(schema_version=1,operation='ACK_OBSERVATION',route='OBSERVER',reason='OBSERVATION_READY',
transaction_id=r['transaction_id'],request_id=secrets.token_hex(16),target_binding_digest=r['target_binding_digest'],
composition_digest=r['composition_digest'],grant_digest=public['grants']['OBSERVER'],expected_sequence=value['sequence'],
requester_invocation_id=None,observer_receipt_digest=publish(obs),selected_release_digest=None,staging_generation=None)
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as conn:
conn.settimeout(150);conn.connect(str(owner.SOCKET))
assert struct.unpack('3i',conn.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))[1]==0
conn.sendall(wire.canonical(req));conn.shutdown(socket.SHUT_WR)
raw=b''
while part:=conn.recv(8192):raw+=part
assert wire.parse(raw)['status']=='ACCEPTED'
deadline=time.monotonic()+90
while time.monotonic()<deadline:
final=json.loads((owner.ROOT/'ledger.json').read_text())
if final['receipt']['phase']=='COMPLETE':break
assert final['receipt']['phase']!='CONFLICT',final['receipt']
time.sleep(.2)
else:raise TimeoutError('postboot userspace COMPLETE')
projection=json.loads(subprocess.check_output(['/usr/libexec/archipelago-d09ctl','inspect'],text=True))
assert projection['state']=='NARROW_ACTIVATION_OBSERVED' and projection['os_update']['post_boot_verified']
for path,sha in preserved.items():
assert executor.file_hash(path)==sha,path
receipt=dict(passed=True,actual_owner_postboot=True,actual_client_projection=projection,
fresh_boot_id=executor.boot_id(),running_kernel=platform.release(),
preservation_verified=True,readmission='root fixture retained-code reactivation from external preboot ledger' if crypto else 'PRESERVE_ONLY',
production_signature_approval=False,native_application_qualified=False)
receipt['genuine_native_crypto']=bool(crypto)
receipt['os_root']=str(executor.ROOT)
if crypto:
receipt['same_boot_userspace_complete']=True
receipt['postboot_userspace_complete']=True
receipt['package_install_events']=sum(e['phase']=='APPLY_INTENT' for e in executor.Executor(binding['os_update']).status['events'])
assert receipt['package_install_events']==1
for name,sha in crypto['installed_module_sha256'].items():
assert executor.file_hash(Path('/usr/libexec')/name)==sha,name
receipt['installed_modules_unchanged']=True
root_uuid=subprocess.check_output(['blkid','-s','UUID','-o','value','/dev/vda'],text=True).strip()
assert root_uuid==crypto['root_filesystem_uuid']
receipt['root_filesystem_uuid']=root_uuid
receipt['root_filesystem_uuid_preserved']=True
receipt['in_place_package_route']=True
assert executor.ROOT==Path('/var/lib/archipelago-os-update')
assert Path('/var/lib/archipelago').stat().st_uid==1000
assert executor.ROOT.stat().st_uid==0 and executor.ROOT.stat().st_mode & 0o022==0
assert Path('/root/os-ota-crypto-fixture/test.seed').exists()
subprocess.run(['runuser','-u','archipelago','--','touch','/var/lib/archipelago/crypto-postboot-positive-probe'],check=True)
denial=[]
for args in (['touch',str(executor.ROOT/'app-write-denied')],
['python3','-c',"import os;os.rename('/var/lib/archipelago-os-update','/var/lib/app-rename-denied')"],
['python3','-c',"open('/root/os-ota-crypto-fixture/test.seed','rb').read()"]):
r=subprocess.run(['runuser','-u','archipelago','--',*args],capture_output=True,text=True)
assert r.returncode!=0
denial.append(dict(command=args,exit=r.returncode))
receipt['app_write_positive_control']=True
receipt['custody_denials']=denial
(evidence/'owner-postboot-result.json').write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt,indent=2),flush=True)