Files
archy/tests/regression/d09-c01-native.py
T

230 lines
15 KiB
Python

#!/usr/bin/python3
"""Real native FD10 component lane under PID1, never installed C01 acceptance.
Executes the compiled production Rust transport/extraction with a native root
fixture supervisor and native non-root child. Full C01 clock/storage/admission
remains separately refused. Generation inspection uses the production controller
with synthetic external appointments, not a fake healthy-process response.
"""
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import pwd
import secrets
import shutil
import socket
import subprocess
import sys
import time
assert os.geteuid()==0 and socket.gethostname()=='d09-native-qualification'
assert Path('/run/.containerenv').exists() and os.environ.get('D09_NATIVE_PID1')=='1'
PACKAGE=Path('/opt/d09-native')
for directory,_,files in os.walk(PACKAGE):
for path in [Path(directory),*(Path(directory)/name for name in files)]:path.chmod(path.stat().st_mode & 0o755)
def run(*args,check=True):
r=subprocess.run(args,capture_output=True,text=True,timeout=60)
if check and r.returncode:raise AssertionError((args,r.returncode,r.stdout,r.stderr))
return r
def write(path,value,mode=0o644):
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True);path.write_bytes(value.encode() if isinstance(value,str) else value);path.chmod(mode)
def wait(predicate,label,seconds=100):
end=time.monotonic()+seconds
while time.monotonic()<end:
value=predicate()
if value:return value
time.sleep(.1)
raise AssertionError('timed out: '+label)
def sha(path):
with Path(path).open('rb') as source:return hashlib.file_digest(source,'sha256').hexdigest()
run('systemctl','stop','archipelago.service',check=False)
run('tar','-xf',str(PACKAGE/'native-test.tar'),'-C','/')
run('tar','-xf',str(PACKAGE/'owners.tar'),'-C','/')
for path,expected in json.loads((PACKAGE/'native-artifact-sha256.json').read_text()).items():
assert sha('/'+path)==expected['sha256'],path
for path,expected in json.loads(Path('/D09-C01-SOURCE-SHA256.json').read_text()).items():
assert sha('/'+path)==expected,path
sys.path.insert(0,str(PACKAGE/'scripts'))
import d09_wire as wire
import d09_store as store
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
import c01_access_generation as generation
import c01_access_policy as policy
root=Path('/var/lib/d09-native');root.mkdir(mode=0o755)
write('/etc/nsswitch.conf','passwd: files\ngroup: files\nshadow: files\nhosts: files dns\n')
run('groupadd','--gid','61001','c01-native-app')
run('useradd','--uid','61001','--gid','61001','--no-create-home','--home-dir','/var/empty','--shell','/usr/sbin/nologin','c01-native-app')
run('usermod','-G','','archipelago')
run('usermod','-s','/usr/sbin/nologin','-L','archipelago')
(root/'manager').mkdir(mode=0o700);os.chown(root/'manager',61001,61001)
run('ip','netns','add','archipelago-managed')
run('nsenter','--net=/run/netns/archipelago-managed','--','ip','link','set','lo','up')
for p in (owner.BINDING.parent,owner.ROOT):
if p.exists():raise AssertionError('fresh authority required')
web=owner.DESTINATIONS['frontend']
if web.exists():shutil.rmtree(web)
os.chown(web.parent,0,0);web.parent.chmod(0o755)
write(web/'index.html','native handoff fixture\n')
write(web/'archipelago-runtime/image-recipe/configs/nginx-archipelago.conf',(PACKAGE/'image-recipe/configs/nginx-archipelago.conf').read_bytes())
write('/etc/systemd/system/archipelago.service','''[Unit]
Description=Native protected D09 handoff component fixture
[Service]
Type=notify
NotifyAccess=main
User=root
Group=root
NoNewPrivileges=yes
Delegate=yes
DelegateSubgroup=supervisor
NetworkNamespacePath=/run/netns/archipelago-managed
ExecStart=/usr/local/bin/archipelago --ignored --exact d09_transport::tests::pid1_supervisor --nocapture --test-threads=1
Environment=D09_NATIVE_PID1=1
TimeoutStartSec=150
Restart=no
''')
if owner.DESTINATIONS['config'].parent.exists():shutil.rmtree(owner.DESTINATIONS['config'].parent)
run('systemctl','daemon-reload')
provision.install_package()
old_native=sha(owner.DESTINATIONS['backend'])
runtime={'version':2,'manager_uid':61001,'manager_gid':61001,'runtime_uid':1000,'runtime_gid':1000,
'authority_uid':61002,'authority_gid':61002,'operator_uid':1001,'operator_gid':1001,
'runtime_user':'archipelago','home':'/home/archipelago','home_backing':str(root/'runtime-home'),
'runtime_dir':'/run/user/1000','runtime_data_dir':str(root/'runtime-data'),'graphroot':str(root/'graphroot'),
'subuids':[[int(row.split(':')[1]),int(row.split(':')[2])] for row in Path('/etc/subuid').read_text().splitlines() if row.startswith('archipelago:')],
'subgids':[[int(row.split(':')[1]),int(row.split(':')[2])] for row in Path('/etc/subgid').read_text().splitlines() if row.startswith('archipelago:')],
'user_cgroup':'/user.slice/user-1000.slice/user@1000.service',
'private_parents':[str(root/('private-'+role)) for role in ('manager','snapshot','authority')],
'workspace_helper_sha256':sha('/usr/libexec/archipelago-d09-prepare-workspace')}
for name in ('runtime-home','runtime-data','graphroot'):
(root/name).mkdir(mode=0o700);os.chown(root/name,1000,1000)
for path in runtime['private_parents']:Path(path).mkdir(mode=0o700)
write('/etc/archipelago-c01/runtime.json',wire.canonical(runtime),0o600)
launch={'version':3,'native_sha256':old_native,'application_user':'c01-native-app','authority_user':'c01-native-authority','runtime_uid':1000,'runtime_gid':1000,
**{k:str(root/k) for k in ('manager_backing','snapshot_backing','authority_backing','snapshot_key','authority_key','platform_profile','proxy_path')},
'runtime_data_dir':runtime['runtime_data_dir'],'binding':{'local_identity':[1]*32,'incarnation':[2]*32,'reader_schema':2,'writer_schema':2},
'platform_profile_sha256':'f'*64,'clock_domain':'f'*32,'host_ip':'192.0.2.1'}
write(generation.policy.LAUNCH,wire.canonical(launch),0o600)
base={'version':2,'launch_sha256':sha(generation.policy.LAUNCH),'runtime_sha256':sha('/etc/archipelago-c01/runtime.json'),
'd09_workspace':{'path':str(generation.WORKSPACE),'initial_digest':wire.digest(b'ABSENT')}}
write(policy.CONFIG,wire.canonical(base),0o600)
policy.STATE.mkdir(parents=True,exist_ok=True,mode=0o700)
generation.DIRECTORY.mkdir(mode=0o700)
Path('/run/archipelago-c01').mkdir(mode=0o755,exist_ok=True)
# This lane exercises only the production generation controller, not the full
# access-owner migration/lease grant. Its input is explicitly synthetic.
write(PACKAGE/'generation-controller.py','''import json,os,pathlib,socket,struct,sys
sys.path.insert(0,'/opt/d09-native/scripts')
import c01_access_generation as g,c01_access_policy as p,c01_access_kernel as k
v,h=p.json_read(p.CONFIG);launch,d=p.json_read(p.LAUNCH);controller=g.Generations(v,h,launch,d)
s=socket.socket(socket.AF_UNIX,socket.SOCK_SEQPACKET);s.bind('/run/archipelago-c01/runtime-access.sock');os.chmod('/run/archipelago-c01/runtime-access.sock',0o600);s.listen(4)
while True:
c,_=s.accept();c.settimeout(2)
try:
pid,uid,gid=struct.unpack('3i',c.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
assert (uid,gid)==(0,0) and k.process(pid)['group']=='/sys/fs/cgroup/system.slice/archipelago-d09-lifecycle.service'
r=json.loads(c.recv(8192));assert r['method']=='inspect'
c.sendall(g.canonical(controller.handle(r,False)))
finally:c.close()
''')
run('systemd-run','--unit=c01-generation-fixture','--property=Type=exec','/usr/bin/python3','-B',str(PACKAGE/'generation-controller.py'))
wait(lambda:Path('/run/archipelago-c01/runtime-access.sock').exists(),'generation controller')
incoming=owner.ROOT/'incoming'
for generation_name in ('new','restore'):
shutil.copytree(web,incoming/(generation_name+'-frontend'))
shutil.copy2(owner.DESTINATIONS['backend'],incoming/(generation_name+'-backend'))
if generation_name=='new':
with (incoming/'new-backend').open('ab') as stream:stream.write(b'\nB\n')
new_launch=dict(launch,native_sha256=sha(incoming/'new-backend'))
write(incoming/'new-launch',wire.canonical(new_launch),0o600)
write(incoming/'restore-launch',wire.canonical(launch),0o600)
write(incoming/'new-config',Path('/usr/libexec/archipelago-d09-terminal-workspace.conf').read_bytes())
write(incoming/'exclusion.json',wire.canonical({'scope':'synthetic native descriptor component fixture; NOT migration exclusion'}),0o600)
old_effective=wire.digest(owner.effective_configuration().encode())
transaction=secrets.token_hex(16)
binding=dict(schema_version=1,target_binding_digest=secrets.token_hex(32),composition_digest=secrets.token_hex(32),
admission_epoch_id=secrets.token_hex(16),transaction_id=transaction,
grants={r:secrets.token_hex(32) for r in ('UPDATER','AUTOMATIC','MANUAL','GUARD','OBSERVER')},
artifacts={'backend':sha(incoming/'new-backend'),'frontend':owner.object_digest(incoming/'new-frontend')},
restore_artifacts={'backend':old_native,'frontend':owner.object_digest(web)},effective_config={'APPLY':old_effective,'RESTORE':old_effective},
exclusion_evidence_digest=sha(incoming/'exclusion.json'),helper_digest=sha('/usr/libexec/archipelago-d09-prepare-workspace'),
prerequisite={'grant_digest':secrets.token_hex(32),'backend_sha256':old_native,'effective_config_digest':old_effective})
binding['release_identity']=['B',[['archipelago','B',binding['artifacts']['backend'],(incoming/'new-backend').stat().st_size]]]
binding['c01']={'version':1,'application':{'name':'c01-native-app','uid':61001,'gid':61001,'home':'/var/empty'},
'runtime':{'name':'archipelago','uid':1000,'gid':1000,'home':'/home/archipelago'},'runtime_sha256':base['runtime_sha256'],
'generations':{name:{'native_sha256':data['native_sha256'],'launch_sha256':wire.digest(wire.canonical(data))}
for name,data in [('old',launch),('new',new_launch),('restore',launch)]},'transitions':{}}
binding['objects']={name:{'old':owner.object_digest(path),'new':owner.object_digest(incoming/('new-'+name)),
'restore':owner.object_digest(incoming/('restore-'+name))} for name,path in owner.destinations(binding).items()}
for action,old,new in [('APPLY',launch,new_launch),('RESTORE',new_launch,launch)]:
record=dict(version=1,policy_sha256=sha(policy.CONFIG),runtime_sha256=base['runtime_sha256'],transaction_id=transaction,action=action,
grant_digest=secrets.token_hex(32),from_launch=old,to_launch=new,from_launch_sha256=wire.digest(wire.canonical(old)),
to_launch_sha256=wire.digest(wire.canonical(new)),from_workspace_digest=binding['objects']['config']['old' if action=='APPLY' else 'new'],
to_workspace_digest=binding['objects']['config']['new' if action=='APPLY' else 'restore'])
history=wire.canonical(record);record['external_history_digest']=wire.digest(history)
write(incoming/('c01-history-'+record['external_history_digest']+'.json'),history,0o600)
raw=wire.canonical(record);hashed=wire.digest(raw);binding['c01']['transitions'][action]=hashed
write(incoming/('c01-generation-'+hashed+'.json'),raw,0o600)
write(incoming/'admission.json',wire.canonical(binding),0o600)
provision.provision(incoming/'admission.json')
run('systemctl','start','archipelago-d09-lifecycle.service')
wait(lambda:owner.PRIVATE.exists() and Path('/run/archipelago-d09/c01.sock').exists(),'independent listeners')
started=time.monotonic()
result=provision.activate_prerequisite()
assert time.monotonic()-started<10 and not result['prerequisite_started']
wait(lambda:(root/'native-proof.json').exists(),'actual native FD10 HELLO/inspect')
proof=json.loads((root/'native-proof.json').read_text())
assert proof['native_hello'] and proof['python_exec_denied'] and proof['host_run_hidden']
assert owner.PERMIT.exists() and owner.service()['ActiveState']=='activating'
before=json.loads(store.read(owner.ROOT/'ledger.json'))
assert before['prerequisite']['state']=='START_INTENT' and before['receipt'] is None
wait(lambda:json.loads(store.read(owner.ROOT/'ledger.json'))['prerequisite']['state']=='STARTED','delayed native READY',70)
assert time.monotonic()-started>=40 and not owner.PERMIT.exists()
assert json.loads(store.read(owner.ROOT/'ledger.json'))['grants']==[binding['prerequisite']['grant_digest']]
print('PASS actual native root MainPID -> native nonroot app FD10: kernel credentials, HELLO, no Python exec, private /run, wrong selection refusal',flush=True)
print('PASS real PID1 startup >40 seconds: one grant/start intent, bounded pre-READY inspect, permit retained until native READY',flush=True)
first_invocation=owner.service()['InvocationID']
original_ledger=store.read(owner.ROOT/'ledger.json')
run('systemctl','stop','archipelago-d09-lifecycle.service')
wait(lambda:owner.service()['MainPID']=='0','owner-loss managed retirement')
print('PASS owner loss retires real native service; no disk-seeded transport authority',flush=True)
(root/'native-proof.json').rename(root/'native-proof-first.json')
run('systemctl','start','archipelago-d09-lifecycle.service')
def unknown():
response=run('/usr/libexec/archipelago-d09ctl','inspect',check=False)
return response.returncode==0 and json.loads(response.stdout)['state']=='UNKNOWN'
wait(unknown,'fresh owner refuses disk continuity')
assert store.read(owner.ROOT/'ledger.json')==original_ledger
v=json.loads(original_ledger)
index={k:binding[k] for k in ('target_binding_digest','composition_digest','transaction_id')}
probe=owner.Owner.__new__(owner.Owner)
record=dict(index,schema_version=1,allowed_action='REACTIVATE_PREREQUISITE',
predecessor_epoch_id=v['epoch'],head_sequence=v['sequence'],observed_boot_id=owner.boot_id(),
preserved_head_digest=wire.digest(original_ledger),
preserved_journal_digest=wire.digest(wire.canonical({'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)),disposition_digest=probe.drained_disposition(v),
new_epoch_id=secrets.token_hex(16),new_grant_digest=secrets.token_hex(32),execution_grant_digest=secrets.token_hex(32),
external_history_sequence=1)
history=wire.canonical(dict(record,sequence=1));record['external_history_digest']=wire.digest(history)
external=PACKAGE/'external';external.mkdir(mode=0o700)
write(external/'history.json',history,0o600);write(external/'reactivation.json',wire.canonical(record),0o600)
started=time.monotonic()
provision.readmit(external/'reactivation.json',external/'history.json',activation=True)
assert time.monotonic()-started<10
wait(lambda:(root/'native-proof.json').exists(),'new native FD10 after readmission')
assert owner.PERMIT.exists() and owner.service()['ActiveState']=='activating'
invocation=owner.service()['InvocationID'];assert invocation!=first_invocation
before_retry=store.read(owner.ROOT/'ledger.json')
provision.readmit(external/'reactivation.json',external/'history.json',activation=True)
assert store.read(owner.ROOT/'ledger.json')==before_retry and owner.service()['InvocationID']==invocation
wait(lambda:json.loads(store.read(owner.ROOT/'ledger.json'))['prerequisite']['state']=='STARTED','readmitted native READY',70)
assert time.monotonic()-started>=40 and not owner.PERMIT.exists()
assert len(json.loads(store.read(owner.ROOT/'ledger.json'))['grants'])==3
print('PASS external prerequisite readmission >40 seconds: new native FD10, UNKNOWN before authority, identical-record retry nonactivating, permit held through READY',flush=True)