"""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()