197 lines
12 KiB
Python
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)
|