Files
archy/scripts/tests/os_updater_postboot_guest.py

140 lines
7.5 KiB
Python

#!/usr/bin/python3
"""Private BIOS boot: external history readmission, real owner postboot callback.
EXPECTED_EXTERNAL_SHA256 is injected by the outside disposable controller from
the exact stopped-guest ledger and previous successful effect/preservation proof.
No OS grant is reset and no apt/repair/financial restore operation is performed.
"""
import importlib.util
import json
import os
from pathlib import Path
import platform
import pwd
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'
PACKAGE=Path('/opt/ota-source')
sys.path.insert(0,str(PACKAGE/'scripts'))
import d09_store as store
import d09_wire as wire
import os_update_executor as executor
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
def run(*args):
result=subprocess.run(args,capture_output=True,text=True,timeout=120)
print('COMMAND',args,result.returncode,result.stdout,result.stderr,flush=True)
result.check_returncode()
return result.stdout
def wait(function,label):
deadline=time.monotonic()+90
while time.monotonic()<deadline:
value=function()
if value:return value
time.sleep(.2)
raise TimeoutError(label)
external_raw=(PACKAGE/'bootstrap-external.json').read_bytes()
assert wire.digest(external_raw)==EXPECTED_EXTERNAL_SHA256
external=json.loads(external_raw)
before=store.read(owner.ROOT/'ledger.json')
assert wire.digest(before)==external['ledger_sha256']
value=json.loads(before)
binding_raw=store.read(owner.BINDING)
assert wire.digest(binding_raw)==external['binding_sha256']
binding=value.get('active_binding',json.loads(binding_raw))
effect=external['effects']
assert effect['passed'] and effect['accepted_native_artifact']
assert value['receipt']['phase']=='AWAITING_OBSERVATION' and value['receipt']['config_action']=='APPLY'
boot=effect['os_status']['events'][-1]['details']['boot']
assert platform.release()==boot['release']
assert executor.boot_id()!=effect['os_status']['events'][-1]['details']['boot_id']
expected=dict(effect['preserved'],**effect['native_identity'])
expected['/var/lib/archipelago/ota-synthetic-canaries/wallet.db']=effect['forward_wallet_sha256']
assert expected=={p:executor.file_hash(p) for p in expected}
# Boot must retain the old versioned kernel and initrd as well.
plan=json.loads(store.read(executor.ROOT/'plans'/binding['os_update']['plan_sha256']/'plan.json'))
for path,sha in plan['boot']['retained_files'].items():
if path!='/boot/grub/grub.cfg':assert executor.file_hash(path)==sha
account=pwd.getpwnam('archipelago')
control=Path('/run/d09-fixture-client');control.mkdir(mode=0o700,exist_ok=True)
os.chown(control,account.pw_uid,account.pw_gid)
run('systemctl','start','archipelago-d09-lifecycle.service')
wait(lambda:owner.PRIVATE.exists(),'owner socket')
assert store.read(owner.ROOT/'ledger.json')==before
probe=owner.Owner.__new__(owner.Owner)
disposition=probe.drained_disposition(value)
index={k:binding[k] for k in ('target_binding_digest','composition_digest','transaction_id')}
fields=dict(index,predecessor_epoch_id=value['epoch'],head_sequence=value['sequence'],
preserved_head_digest=wire.digest(wire.canonical(value['receipt'])),
preserved_journal_digest=wire.digest(wire.canonical({'receipts':value['journal'],'intents':value['intents']})),
preserved_consumed_history_digest=wire.digest(wire.canonical(owner.consumption_history(value))),
preserved_target_index_digest=wire.digest(wire.canonical(index)),disposition_digest=disposition,
new_epoch_id=secrets.token_hex(16),new_grant_digest=secrets.token_hex(32),
execution_grant_digest=secrets.token_hex(32),allowed_action='REACTIVATE_RETAINED')
history=Path('/root/bootstrap-postboot-history.json');record=Path('/root/bootstrap-postboot-record.json')
sequence=len(value['readmissions'])+1
store.atomic(history,wire.canonical(dict(fields,sequence=sequence)))
store.atomic(record,wire.canonical(dict(fields,schema_version=1,external_history_sequence=sequence,
external_history_digest=wire.digest(store.read(history)),observed_boot_id=owner.boot_id())))
provision.readmit(record,history,activation=True)
def active():
current=json.loads(store.read(owner.ROOT/'ledger.json'))
return current if current['receipt']['phase']=='AWAITING_OBSERVATION' and current['receipt']['observed_invocation_id']!=value['receipt']['observed_invocation_id'] else None
latest=wait(active,'fresh candidate activation after boot')
state=owner.service()
assert executor.file_hash('/proc/'+state['MainPID']+'/exe')==binding['artifacts']['backend']
assert latest['journal'][:len(value['journal'])]==value['journal']
assert expected=={p:executor.file_hash(p) for p in expected}
source=executor.ROOT/'plans'/binding['os_update']['plan_sha256']
store.atomic(source/'postboot-preservation.json',wire.canonical(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)))
# Same fixed root-authenticated operation as --os-postboot; retain the exact
# owner reply rather than losing its reason behind the CLI's generic exception.
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as conn:
conn.settimeout(120);conn.connect(str(owner.PRIVATE))
assert struct.unpack('3i',conn.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))[1]==0
conn.sendall(wire.canonical({'method':'os_postboot'}));conn.shutdown(socket.SHUT_WR)
raw=b''
while True:
part=conn.recv(8192)
if not part:break
raw+=part
assert len(raw)<=wire.MAX
response=json.loads(raw)
print('AUTHENTICATED_OWNER_POSTBOOT_REPLY',json.dumps(response),flush=True)
if response.get('os_postboot_verified') is not True:
run('journalctl','-u','archipelago-d09-lifecycle.service','--no-pager','-n','80')
owner_pid=run('systemctl','show','archipelago-d09-lifecycle.service','-p','MainPID','--value').strip()
run('nsenter','--target',owner_pid,'--mount','--','findmnt','--target',str(executor.ROOT/'transactions'),'-o','TARGET,OPTIONS')
try:
executor.Executor(binding['os_update'],create=False).observe_postboot()
print('OUTSIDE_OWNER_READ_ONLY_POSTBOOT_CHECK_PASSED',flush=True)
except Exception as exc:
print('OUTSIDE_OWNER_READ_ONLY_POSTBOOT_CHECK',repr(exc),flush=True)
raise AssertionError(response)
status=json.loads(store.read(executor.ROOT/'transactions'/binding['os_update']['authority_digest']/'status.json'))
assert status['phase']=='POSTBOOT_CONFIRMED'
assert sum(event['phase']=='APPLY_INTENT' for event in status['events'])==1
assert expected=={p:executor.file_hash(p) for p in expected}
result=dict(passed=True,running_kernel=platform.release(),running_build=platform.version(),
os_phase=status['phase'],boot_id=executor.boot_id(),old_kernel_retained=True,
financial_data_restored=False,one_os_apply_intent=True,native_backend_sha256=binding['artifacts']['backend'],
previous_invocation=value['receipt']['observed_invocation_id'],new_invocation=state['InvocationID'],
preservation=expected,production_signature_approval=False,external_fixture_readmission=True)
Path('/var/log/os-ota-stage/bootstrap-postboot-result.json').write_bytes(wire.canonical(result))
print('BOOTSTRAP_REAL_BIOS_POSTBOOT',json.dumps(result,sort_keys=True),flush=True)