140 lines
7.5 KiB
Python
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)
|