Files
archy/scripts/d09_c01.py
T

98 lines
5.4 KiB
Python

"""Strict external C01 composition/principal bindings for D09 generations."""
import json
import os
from pathlib import Path
import pwd
import stat
from d09_store import read, atomic
from d09_wire import canonical, digest
LAUNCH = Path('/etc/archipelago-c01/launch.json')
GENERATION_RECORDS = Path('/var/lib/archipelago-c01/access/generations')
def hex_digest(value):
return type(value) is str and len(value)==64 and all(c in '0123456789abcdef' for c in value)
def validate(binding):
c=binding['c01']
if type(c) is not dict or set(c)!={'version','application','runtime','runtime_sha256','generations','transitions'} or type(c['version']) is not int or c['version']!=1:
raise ValueError('C01 composition fields/version')
for role in ('application','runtime'):
r=c[role]
if set(r)!={'name','uid','gid','home'} or type(r['uid']) is not int or type(r['gid']) is not int or r['uid']<=0 or r['gid']<=0:
raise ValueError('C01 named principal')
account=pwd.getpwnam(r['name'])
if (account.pw_uid,account.pw_gid,account.pw_dir)!=(r['uid'],r['gid'],r['home']) or os.getgrouplist(r['name'],r['gid'])!=[r['gid']]:
raise ValueError('C01 protected account binding')
if c['application']['uid']==c['runtime']['uid'] or c['application']['gid']==c['runtime']['gid']:
raise ValueError('C01 requester and workspace principals must differ')
runtime_raw=read(Path('/etc/archipelago-c01/runtime.json'))
import c01_runtime
runtime,runtime_digest=c01_runtime.load_config()
if not hex_digest(c['runtime_sha256']) or digest(runtime_raw)!=runtime_digest or runtime_digest!=c['runtime_sha256'] or runtime.get('version')!=2 or runtime.get('workspace_helper_sha256')!=binding['helper_digest']:
raise ValueError('C01 runtime workspace appointment')
if (runtime['manager_uid'],runtime['manager_gid'],runtime['runtime_uid'],runtime['runtime_gid'],runtime['home'])!=(
c['application']['uid'],c['application']['gid'],c['runtime']['uid'],c['runtime']['gid'],c['runtime']['home']):
raise ValueError('C01 runtime principal mapping')
if set(c['generations'])!={'old','new','restore'} or set(c['transitions'])!={'APPLY','RESTORE'}:
raise ValueError('C01 exact generation set')
expected={'old':binding['prerequisite']['backend_sha256'],'new':binding['artifacts']['backend'],'restore':binding['restore_artifacts']['backend']}
for generation, fields in c['generations'].items():
if set(fields)!={'native_sha256','launch_sha256'} or not all(hex_digest(x) for x in fields.values()) or fields['native_sha256']!=expected[generation]:
raise ValueError('C01 native/launch digest domain')
if not all(hex_digest(x) for x in c['transitions'].values()):
raise ValueError('C01 external access transition identity')
return c
def stage_records(binding, incoming):
import c01_access_generation as generation
import c01_access_policy as policy
c=validate(binding)
policy_value,policy_digest=policy.json_read(policy.CONFIG)
controller=generation.Generations(policy_value,policy_digest,*policy.json_read(policy.LAUNCH))
GENERATION_RECORDS.mkdir(mode=0o700,exist_ok=True)
for action, hashed in c['transitions'].items():
raw=read(incoming/('c01-generation-'+hashed+'.json'))
if digest(raw)!=hashed:
raise ValueError('C01 transition record changed')
record=json.loads(raw)
history=read(incoming/('c01-history-'+record['external_history_digest']+'.json'))
if digest(history)!=record['external_history_digest']:
raise ValueError('C01 external history binding')
facts=json.loads(history)
for key in ('transaction_id','action','grant_digest','from_launch_sha256','to_launch_sha256'):
if facts[key]!=record[key]: raise ValueError('C01 external latest history facts')
if record['transaction_id']!=binding['transaction_id'] or record['action']!=action:
raise ValueError('C01 transaction scope')
before,after=('old','new') if action=='APPLY' else ('new','restore')
if record['from_launch_sha256']!=c['generations'][before]['launch_sha256'] or record['to_launch_sha256']!=c['generations'][after]['launch_sha256']:
raise ValueError('C01 transition generation scope')
if record['from_workspace_digest']!=binding['objects']['config']['old' if before=='old' else 'new'] or record['to_workspace_digest']!=binding['objects']['config']['new' if after=='new' else 'restore']:
raise ValueError('C01 workspace object domain/scope')
target=GENERATION_RECORDS/(hashed+'.json')
if target.exists():
if read(target)!=raw: raise ValueError('C01 retained transition differs')
else:
atomic(target,raw)
controller.record(hashed) # full schema/invariant validation, no transition
def workspace(binding):
"""Observe the live runtime's actual workspace mapping; never run as app UID."""
from c01_runtime_view import RuntimeLease
c=validate(binding)
a,r=c['application'],c['runtime']
lease=RuntimeLease(a['uid'],a['gid'],r['uid'],r['gid'],c['runtime_sha256'])
try:
path=Path('/proc')/str(lease.identity['pid'])/'root'/r['home'].lstrip('/')/'Work'
meta=path.lstat()
if not stat.S_ISDIR(meta.st_mode) or meta.st_uid!=r['uid']:
raise ValueError('runtime Work mapping unavailable')
lease.check()
finally:
lease.close()