#!/usr/bin/python3 """Genuine new-object provisioning in the normal disposable PID1 realm. No replacement owners, clock grants or launch admission are installed. All private material is generated in this realm; evidence contains only public bindings, metadata and authenticated store anchors. """ import hashlib import importlib.util import json import os from pathlib import Path import pwd import secrets import shutil import socket import stat import subprocess import sys import time import traceback PACKAGE = Path('/opt/c01-fixture') ROOT = Path('/var/lib/c01-composed') EVIDENCE = Path('/var/lib/c01-normal') sys.path.insert(0, str(PACKAGE / 'scripts')) import c01_access_policy as policy import c01_access_generation as generation import c01_access_kernel as kernel import c01_network as network import c01_network_owner import c01_runtime import c01_nts import c01_power def run(*args, check=True, **kw): p = subprocess.run(args, capture_output=True, timeout=180, **kw) if check and p.returncode: raise RuntimeError(f'{args[0]} exit {p.returncode}: {p.stderr.decode(errors="replace")[-2000:]}') return p def write(path, data, mode=0o600): path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) with path.open('wb') as stream: stream.write(data.encode() if isinstance(data, str) else data) path.chmod(mode) def dump(path, value): write(path, json.dumps(value, indent=2, sort_keys=True)+'\n' if Path(path).is_relative_to(EVIDENCE) else generation.canonical(value)) def sha(path): with Path(path).open('rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() def verify_runtime_dependencies(): dependencies=json.loads((PACKAGE/'chrony-artifact.json').read_text()) stack=json.loads((PACKAGE/'service-stack-manifest.json').read_text())['files'] if os.environ.get('C01_RUNNING_SERVICES')=='1' else {} for path,item in dependencies.items(): canonical=path.lstrip('/') if canonical.startswith(('lib/','lib64/')):canonical='usr/'+canonical assert sha(path)==stack.get(canonical,item)['sha256'], path def account(name, uid, home='/var/empty', login=False): try: p = pwd.getpwnam(name) except KeyError: run('groupadd', '--gid', str(uid), name) run('useradd', '--uid', str(uid), '--gid', str(uid), '--no-create-home', '--home-dir', home, '--shell', '/bin/bash' if login else '/usr/sbin/nologin', name) p = pwd.getpwnam(name) assert (p.pw_uid, p.pw_gid, p.pw_dir) == (uid, uid, home) return p def unit(name, body): write('/etc/systemd/system/' + name, body, 0o644) def template(name, replacements): text = (PACKAGE / 'image-recipe/configs' / name).read_text() for key, value in replacements.items(): text = text.replace(key, str(value)) assert '@SUPERVISOR_UNIT@' not in text and '@ORIGINAL_RUNTIME' not in text return text def wait_path(path, seconds=20): end = time.monotonic() + seconds while time.monotonic() < end: if Path(path).exists(): return time.sleep(.1) raise RuntimeError('owner endpoint absent: ' + str(path)) def structures(): ROOT.mkdir(mode=0o755) work=Path('/var/lib/c01-fixture/home/Work/operator.txt') meta=work.stat() dump(EVIDENCE/'work-before.json',{'device':meta.st_dev,'inode':meta.st_ino, 'uid':meta.st_uid,'gid':meta.st_gid,'mode':stat.S_IMODE(meta.st_mode),'sha256':sha(work)}) # idmapped tmpfs is a real disposable filesystem, not persistent-disk crash # qualification. All stores survive process/owner restart within this realm. run('mount', '-t', 'tmpfs', '-o', 'mode=0755,size=384m', 'c01-composed', str(ROOT)) app = account('c01-manager', 61001) authority = account('c01-authority', 61002) account('archipelago-admin', 1001, '/home/archipelago-admin', True) # Real synthetic operator credential, never logged or copied to evidence. password = secrets.token_urlsafe(32) hashed = run('openssl', 'passwd', '-6', '-stdin', input=password.encode()).stdout.strip() run('usermod', '-p', hashed.decode(), 'archipelago-admin') write('/run/c01-fixture-admin-password',password) admin_home=Path('/home/archipelago-admin') assert not list(admin_home.iterdir()), 'fixture must not replace an existing operator home' os.chown(admin_home,1001,1001);admin_home.chmod(0o700) for role in ('manager', 'snapshot', 'authority'): parent = ROOT / ('private-' + role) parent.mkdir(mode=0o700) backing = parent / 'data' if role=='manager' and os.environ.get('C01_LAYOUT_MIGRATION')=='1': physical=Path('/var/lib/c01-fixture/data/.c01-private') physical.mkdir(mode=0o700) (physical/'data').mkdir(mode=0o700) (physical/'access').mkdir(mode=0o700) policy.STATE.mkdir(parents=True,mode=0o700) policy.STATE.chmod(0o700) run('mount','--bind',str(physical),str(parent)) run('mount','--bind',str(physical/'access'),str(policy.STATE)) else:backing.mkdir(mode=0o700) os.chown(backing, 1000, 1000) identity = (Path('/var/lib/c01-fixture/data/identity') if os.environ.get('C01_LAYOUT_MIGRATION')=='1' else ROOT / 'private-manager/data/identity') identity.mkdir(mode=0o700) if os.environ.get('C01_LAYOUT_MIGRATION')=='1':identity.chmod(0o755) os.chown(identity, 1000, 1000) secret = secrets.token_bytes(32) der = bytes.fromhex('302e0201010420') + secret + bytes.fromhex('a00706052b8104000a') public = run('openssl', 'ec', '-inform', 'DER', '-pubout', '-conv_form', 'compressed', '-outform', 'DER', input=der).stdout[-33:] assert public[0] in (2, 3) and len(public) == 33 write(identity / 'nostr_secret', secret.hex()) os.chown(identity / 'nostr_secret', 1000, 1000) keys = ROOT / 'keys' keys.mkdir(mode=0o700) for role in ('snapshot', 'authority'): write(keys / role, secrets.token_bytes(32)) proxy = '/usr/local/bin/archipelago-runtime-proxy' write(proxy, run('/usr/local/bin/archipelago', 'export-runtime-proxy').stdout, 0o755) evidence = {'kernel_release': os.uname().release, 'clocksource': Path('/sys/devices/system/clocksource/clocksource0/current_clocksource').read_text().strip(), 'suspend_evidence': None, 'elapsed_error_bound': None, 'scope': 'actual fixture identity; platform timing qualification unavailable'} dump(EVIDENCE / 'platform-input.json', evidence) profile = {k: evidence[k] for k in ('kernel_release', 'clocksource')} profile.update(evidence_sha256=sha(EVIDENCE / 'platform-input.json'), max_bracket_ns=None, max_observation_error_ns=None, max_elapsed_error_ppb=None, max_awake_ns=None) candidate_path = PACKAGE / 'candidate-appointment.json' candidate = None if candidate_path.exists(): # This input is privately pinned by the outer qualification manifest. # It is not a production-ready flag or a substitute platform identity. candidate = json.loads(candidate_path.read_text()) assert candidate['qualified'] is False and candidate['production_profile_enabled'] is False assert candidate['node'] == 'framework' and candidate['max_awake'] == {'secs':120,'nanos':0} model = candidate['characterized'] assert model['identity'] == c01_nts.platform_identity(), 'candidate hardware identity differs' assert list(bytes.fromhex(sha(PACKAGE / 'candidate-evidence.json'))) == model['evidence_sha256'] profile.update(evidence_sha256=sha(PACKAGE / 'candidate-evidence.json'), max_bracket_ns=10_000_000, max_observation_error_ns=250_000_000, max_elapsed_error_ppb=candidate['max_elapsed_error_ppb'], max_awake_ns=120_000_000_000, characterized_model=model) dump(EVIDENCE / 'candidate-binding.json', {'candidate_sha256':sha(candidate_path), 'identity':c01_nts.platform_identity(), 'evidence_sha256':profile['evidence_sha256'], 'observation_limits':'10ms sampling bracket and 250ms NTS error are refusal limits', 'production_profile_enabled':False}) dump('/etc/archipelago-c01/platform.json', profile) # Null bounds explicitly fail production PlatformQualification.validate. # The byte hash is real; no readiness/suspend evidence is fabricated. epoch_backing=ROOT/'private-authority/epoch' epoch_backing.mkdir(mode=0o700);os.chown(epoch_backing,1000,1000) write(keys/'epoch',secrets.token_bytes(32)) # This is a non-admitting protocol-test budget, NOT a qualified awake bound. # The independent platform profile remains explicitly unqualified above. epoch=dict(version=1,backing=str(epoch_backing),key=str(keys/'epoch'), delegation=dict(id=list(secrets.token_bytes(32)),allow_wake=True,max_awake={'secs':30,'nanos':0})) if candidate is not None: epoch['delegation'].update(allow_refresh=True, max_awake=candidate['max_awake'], characterized=candidate['characterized']) dump('/etc/archipelago-c01/epoch.json',epoch) dump(EVIDENCE/'epoch-test-scope.json',{'max_awake_seconds':epoch['delegation']['max_awake']['secs'], 'scope':'private conditional candidate evaluation' if candidate is not None else 'non-admitting protocol mechanism; no qualified awake profile'}) power={'version':1,'operator_uid':1001,'automatic_recovery':'fresh-nts','runtime_retirement':'whole-user-hierarchy', 'lifecycle_issuer':{'id':secrets.token_hex(32),'scope':'reactivate-current-generation', 'program_sha256':sha(PACKAGE/'scripts/c01_power_reactivate.py')}} dump(c01_power.CONFIG,power) c01_power.ROOT.mkdir(mode=0o700) c01_power.RUN.mkdir(mode=0o700) c01_power.write({'version':1,'sequence':0,'phase':'Open','configuration_sha256':sha(c01_power.CONFIG), 'boot_id':Path('/proc/sys/kernel/random/boot_id').read_text().strip(),'nonce':None, 'target':None,'coordinator':None,'supervisor':None}) write(c01_power.ROOT/'lifetime.lock',b'') launch = dict(version=4, application_user=app.pw_name, authority_user=authority.pw_name, epoch_sha256=sha('/etc/archipelago-c01/epoch.json'),power_sha256=sha(c01_power.CONFIG), runtime_uid=1000, runtime_gid=1000, runtime_data_dir='/var/lib/c01-fixture/data', **{role+'_backing':str(ROOT / ('private-'+role) / 'data') for role in ('manager','snapshot','authority')}, snapshot_key=str(keys / 'snapshot'), authority_key=str(keys / 'authority'), platform_profile='/etc/archipelago-c01/platform.json', platform_profile_sha256=sha('/etc/archipelago-c01/platform.json'), clock_domain=secrets.token_hex(16), host_ip=next(info['local'] for interface in json.loads(run('ip','-j','-4','address','show','lo').stdout) for info in interface['addr_info'] if info['scope']=='host'), proxy_path=proxy, native_sha256=sha('/usr/local/bin/archipelago'), binding=dict(local_identity=list(public[1:]), incarnation=list(secrets.token_bytes(32)), reader_schema=2, writer_schema=2)) if candidate is not None: import c01_admission # New disposable identity only. Existing-node identity/asset migration # is not performed by this provisioning path. required={ 'fips.yaml':('/etc/fips/fips.yaml',b'node:\n identity:\n persistent: true\n'), 'fips.key':('/etc/fips/fips.key',secret.hex().encode()), 'fips.pub':('/etc/fips/fips.pub',public[1:]), 'fips.nft':('/etc/fips/fips.nft',b'# Uninstalled FIPS: no kernel rules applied.\n'), '80-web-ui.nft':('/etc/fips/fips.d/80-web-ui.nft',b'# Uninstalled FIPS.\n'), '85-app-ports.nft':('/etc/fips/fips.d/85-app-ports.nft',b'# No installed apps.\n'), '90-ssh.nft':('/etc/fips/fips.d/90-ssh.nft',b'# No FIPS SSH exposure.\n'), 'wg.public':('/etc/archipelago-fips-wg/wg.public',run('wg','pubkey',input=run('wg','genkey').stdout).stdout), } for name,(path,data) in required.items(): assert not Path(path).exists(), 'never overwrite existing identity/assets' write(path,data,0o600 if name=='fips.key' else 0o644) dump('/etc/archipelago-fips-wg/plan.json',{'version':1, 'assets':{name:sha(path) for name,(path,_) in required.items()}, 'programs':{'wg':sha('/usr/bin/wg'),'systemctl':sha('/usr/bin/systemctl')}, 'fips':'uninstalled','wg':'uninstalled','peers':[]}) for path in ('/etc/tor','/var/lib/tor','/var/lib/archipelago-c01/tor',str(Path(launch['runtime_data_dir'])/'tor-hostnames')): Path(path).mkdir(parents=True,mode=0o700,exist_ok=True) torrc=Path('/etc/tor/torrc') if os.environ.get('C01_RUNNING_SERVICES')=='1' and torrc.exists(): run('systemctl','stop','tor@default.service','tor.service') assert torrc.is_file() and not torrc.is_symlink() and torrc.stat().st_uid==0 prior=torrc.stat();digest=sha(torrc) retained=EVIDENCE/'vendor-torrc';assert not retained.exists() torrc.rename(retained) dump(EVIDENCE/'vendor-torrc-preserved.json',{'inode':prior.st_ino,'sha256':digest, 'scope':'new disposable image vendor configuration, not installed-node config'}) assert not torrc.exists() tor_user='User debian-tor\n' if os.environ.get('C01_RUNNING_SERVICES')=='1' else '' write('/etc/tor/torrc','# Externally provisioned inactive Tor configuration.\n'+tor_user+'SocksPort 0\nDisableNetwork 1\nDataDirectory /var/lib/tor\n',0o644) check=run('/usr/bin/tor','--verify-config','-f','/etc/tor/torrc',check=False) dump(EVIDENCE/'tor-config-verification.json',{'exit':check.returncode,'configuration_sha256':sha('/etc/tor/torrc'), 'service_disposition':'uninstalled','daemon_started':False, 'stdout':check.stdout.decode(),'stderr':check.stderr.decode()}) assert check.returncode==0,'genuine Tor config verification refused; see tor-config-verification.json' for name in ('filebrowser-credentials.py','indeehub-maintenance-controller.py'): write(Path('/opt/archipelago/scripts')/name,(PACKAGE/'scripts'/name).read_bytes(),0o755) containment='/opt/archipelago/web-ui/archipelago-runtime/image-recipe/configs/nginx-archipelago.conf' write(containment,(PACKAGE/'image-recipe/configs/nginx-archipelago.conf').read_bytes(),0o644) contract={'version':1,'platform_sha256':launch['platform_profile_sha256'], 'required_routes':['management'], 'roles':sorted(c01_admission.ROLES),'assets':{name:sha(path) for name,path in c01_admission.ASSETS.items()}, 'services':{name:'uninstalled' for name in c01_admission.UNITS}, 'optional_unavailable':sorted(c01_admission.UNITS)} raw=(json.dumps(contract,sort_keys=True,indent=2)+'\n').encode() digest=hashlib.sha256(raw).hexdigest() write(c01_admission.POLICIES/(digest+'.json'),raw,0o600) launch.update(version=5,admission_sha256=digest) dump(EVIDENCE/'conditional-admission.json',{'policy_sha256':digest,'optional_unavailable':contract['optional_unavailable'], 'scope':'baseline startup; uninstalled FIPS/WG/Tor/NM features are not claimed accepted'}) generation.validate_launch(launch) dump(policy.LAUNCH, launch) runtime = dict(version=1, runtime_user='archipelago', runtime_uid=1000, runtime_gid=1000, manager_uid=61001, manager_gid=61001, authority_uid=61002, authority_gid=61002, operator_uid=1001, operator_gid=1001, home='/home/archipelago', home_backing='/var/lib/c01-fixture/home', runtime_dir='/run/user/1000', runtime_data_dir=launch['runtime_data_dir'], graphroot='/var/lib/c01-fixture/data/containers/storage' if os.environ.get('C01_HISTORY_MIGRATION')=='1' else '/var/lib/c01-fixture/graphroot', subuids=c01_runtime.ranges('/etc/subuid', 'archipelago'), subgids=c01_runtime.ranges('/etc/subgid', 'archipelago'), user_cgroup='/user.slice/user-1000.slice/user@1000.service', private_parents=[str(ROOT / ('private-'+role)) for role in ('manager','snapshot','authority')]) dump(policy.RUNTIME, runtime) c01_runtime.load_config() Path('/var/lib/archipelago-c01').mkdir(mode=0o700, exist_ok=True) Path('/run/archipelago-c01').mkdir(mode=0o755, exist_ok=True) modules = Path('/usr/lib/archipelago') modules.mkdir(exist_ok=True) for source in (PACKAGE / 'scripts').glob('*.py'): write(modules / source.name, source.read_bytes(), 0o644) write(modules / 'c01-provision-entry.py', 'import json,sys\nsys.path.insert(0,"/usr/lib/archipelago")\nimport c01_provision\n' 'mode=sys.argv[1]\nprint(json.dumps(c01_provision.run(inspect=mode!="provision",epoch_mode=mode[6:] if mode.startswith("epoch-") else None)),flush=True)\n', 0o644) return launch, runtime def stores(): receipts = {} original=policy.LAUNCH.read_bytes() value=json.loads(original) marker=Path(value['manager_backing'])/'unreviewed-fixture-history' assert not marker.exists() write(marker,b'synthetic retained history - must not be discarded\n') marker_meta=marker.stat();marker_digest=sha(marker) rejected=run('systemd-run','--wait','--pipe','--unit=c01-store-existing-manager', '-p','Delegate=yes','-p','DelegateSubgroup=supervisor','-p','NoNewPrivileges=yes', '-p','NetworkNamespacePath=/run/netns/archipelago-managed', '/usr/bin/python3','-B','/usr/lib/archipelago/c01-provision-entry.py','provision',check=False) assert rejected.returncode!=0 and b'existing manager state requires' in rejected.stderr assert marker.stat().st_ino==marker_meta.st_ino and sha(marker)==marker_digest assert all(not list(Path(value[role+'_backing']).iterdir()) for role in ('snapshot','authority')) dump(EVIDENCE/'existing-manager-refusal.json',{'exit':rejected.returncode,'stderr':rejected.stderr.decode(), 'marker_inode':marker_meta.st_ino,'marker_sha256':marker_digest,'stores_remained_empty':True}) marker.unlink() # teardown only this exact marker created by this fixture before=None for label,operation in [('provision','provision'),('inspect','inspect'),('reprovision','provision'), ('wrong-key','inspect'),('wrong-binding','inspect')]: if label=='wrong-key': changed=dict(value,snapshot_key=str(ROOT/'keys/wrong-snapshot')) write(changed['snapshot_key'],secrets.token_bytes(32)) dump(policy.LAUNCH,changed) elif label=='wrong-binding': changed=dict(value,binding={**value['binding'],'incarnation':list(secrets.token_bytes(32))}) dump(policy.LAUNCH,changed) else: write(policy.LAUNCH,original) index = len(receipts) p = run('systemd-run', '--wait', '--pipe', '--unit=c01-store-' + str(index), '-p', 'Delegate=yes', '-p', 'DelegateSubgroup=supervisor', '-p', 'NoNewPrivileges=yes', '-p', 'NetworkNamespacePath=/run/netns/archipelago-managed', '/usr/bin/python3', '-B', '/usr/lib/archipelago/c01-provision-entry.py', operation, check=False) receipts[str(index)] = {'case':label, 'operation':operation, 'exit':p.returncode, 'stdout':p.stdout.decode(), 'stderr':p.stderr.decode()} write(policy.LAUNCH,original) inventory={str(path.relative_to(ROOT)): {'sha256':sha(path), 'uid':path.stat().st_uid, 'gid':path.stat().st_gid,'inode':path.stat().st_ino} for role in ('snapshot','authority') for path in Path(value[role+'_backing']).iterdir() if path.is_file()} if before is None: before=inventory else: assert inventory==before, 'initial stores changed during inspection/refusal' assert all(item['uid']==item['gid']==1000 for item in inventory.values()), 'idmapped storage changed original ownership' dump(EVIDENCE/'store-preservation.json',inventory) dump(EVIDENCE / 'stores.json', receipts) assert (p.returncode == 0) == (index < 2), receipts[str(index)] assert json.loads(receipts['0']['stdout'])['anchor'] == json.loads(receipts['1']['stdout'])['anchor'] return receipts def network_owner(): if c01_network_owner.CONFIG.exists(): current=c01_network_owner.Policy() try: assert current.value['version']==2 current.check() finally:current.close() wait_path(c01_network_owner.CONTROL) return {'policy_sha256':sha(c01_network_owner.CONFIG),'retained_pre_runtime_owner':True} import runtime_uplink value = dict(version=1, namespaces={'host':list(network.process_namespace(1)), 'managed':list(network.appointed('managed')), 'runtime':list(network.appointed('runtime'))}, manifests=[], routes=[dict(name='management',source='host',target='managed',protocol='tcp',port=5678, app_id=None,projection_sha256=None,target_cgroup='/system.slice/archipelago.service/c01-application',target_uid=61001)]) value.update(version=2,uplink={'executable':'/usr/bin/pasta.avx2','sha256':sha('/usr/bin/pasta.avx2'), 'wan':list(runtime_uplink.ready()),'dns':['1.1.1.1','1.0.0.1']}) dump(c01_network_owner.CONFIG, value) unit('archipelago-c01-network.service', template('archipelago-c01-network.service', {})) write('/etc/systemd/system/user@1000.service.d/82-c01-runtime-uplink-owner.conf', template('82-c01-runtime-uplink-owner.conf',{}),0o644) write('/etc/systemd/system/user-runtime-dir@1000.service.d/83-c01-runtime-directory-lifetime.conf', template('83-c01-runtime-directory-lifetime.conf.in',{'@ORIGINAL_RUNTIME_UID@':'1000'}),0o644) run('systemctl','daemon-reload') run('systemctl','start','archipelago-c01-network.service') wait_path(c01_network_owner.CONTROL) return {'policy_sha256':sha(c01_network_owner.CONFIG), 'main_pid':run('systemctl','show', 'archipelago-c01-network.service','-p','MainPID','--value').stdout.decode().strip()} def epoch_owner(): results=[] for mode in ('provision','recover'): p=run('systemd-run','--wait','--pipe','--unit=c01-epoch-'+mode, '-p','Delegate=yes','-p','DelegateSubgroup=supervisor','-p','NoNewPrivileges=yes', '-p','NetworkNamespacePath=/run/netns/archipelago-managed', '/usr/bin/python3','-B','/usr/lib/archipelago/c01-provision-entry.py','epoch-'+mode) result=json.loads(p.stdout);results.append(result) assert result['clock_authorized'] is False and result['revoked_aliases'] is True first,second=(item['epoch_context'] for item in results) assert first['cause']=='Restart' and second['cause']=='Wake' and second['epoch']>first['epoch'] assert second['not_before']!=first['not_before'] and second['application_owner']!=first['application_owner'] assert second['nonce']!=first['nonce'] and second['domain']!=first['domain'] legacy=run('systemd-run','--wait','--pipe','--unit=c01-epoch-legacy-refusal', '-p','Delegate=yes','-p','DelegateSubgroup=supervisor','-p','NoNewPrivileges=yes', '-p','NetworkNamespacePath=/run/netns/archipelago-managed', '/usr/bin/python3','-B','/usr/lib/archipelago/c01-provision-entry.py','inspect',check=False) assert legacy.returncode!=0, 'epoch-enabled anchor silently downgraded' dump(EVIDENCE/'native-epoch.json',{'lifetimes':results,'legacy_exit':legacy.returncode,'legacy_stderr':legacy.stderr.decode()}) return {'epoch':second['epoch'],'cause':second['cause'],'clock_authorized':False} def d09_module(): spec = importlib.util.spec_from_file_location('compose_d09', PACKAGE / 'scripts/provision-d09-lifecycle.py') module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def prepare_owners(): launch, _ = policy.json_read(policy.LAUNCH) runtime, _ = policy.json_read(policy.RUNTIME) if launch['version']==5: credentials=json.loads((PACKAGE/'baseline-auth.json').read_text()) destination=Path(launch['runtime_data_dir'] if os.environ.get('C01_LAYOUT_MIGRATION')=='1' else launch['manager_backing'])/'user.json' assert not destination.exists(), 'baseline credential provisioning cannot overwrite a user' dump(destination,credentials['user']);os.chown(destination,1000,1000);destination.chmod(0o600) history=Path(runtime['home_backing'])/'.bash_history' assert not history.exists() write(history,'# retained qualification history\n',0o600);os.chown(history,1000,1000) write('/etc/systemd/system/archipelago.service.d/45-retained-operator.conf', '# Retained operator fragment: no startup override.\n',0o644) unit('archipelago-c01-power-barrier@.service',c01_power.barrier_unit()) unit('archipelago-c01-power-observer.service',c01_power.observer_unit()) unit('archipelago-c01-access-requalify.service',c01_power.access_requalify_unit()) unit('archipelago-c01-power-boot.service',c01_power.boot_unit()) write('/usr/libexec/archipelago-c01-reactivate',(PACKAGE/'scripts/c01_power_reactivate.py').read_bytes(),0o755) for target in c01_power.TARGETS: write(Path('/etc/systemd/system')/(target+'.service.d/40-c01-power.conf'),c01_power.power_dropin(target),0o644) repairs=[] for name in ('archipelago-doctor.service','archipelago-doctor.timer','archipelago-fips.service', 'archipelago-tor-helper.path','archipelago-tor-helper.service','archipelago-update.service', 'archipelago-update.timer','archipelago-wg-address.service','archipelago-wg.service','nostr-relay.service'): path=Path('/etc/systemd/system')/name if not path.exists() and not path.is_symlink(): repairs.append({'path':str(path),'status':'absent from minimal rootfs'}) continue meta=path.lstat() assert path.is_file() and not path.is_symlink() and meta.st_uid==meta.st_gid==0 and meta.st_nlink==1 assert meta.st_mode & 0o7777 in (0o644,0o664) if meta.st_mode & 0o7777 == 0o664: digest=sha(path) path.chmod(0o644) assert path.stat().st_ino==meta.st_ino and sha(path)==digest repairs.append({'path':str(path),'before_mode':'0664','after_mode':'0644','sha256':digest,'inode':meta.st_ino}) dump(EVIDENCE/'image-unit-protection.json',repairs) # Keep the production no-directory-alias inventory rule. Normalize only the # image's known usr-merge systemd links, proving the exact target inode and # bytes before/after; this does not change enable/mask/stop decisions. lib=Path('/lib') assert lib.is_symlink() and lib.lstat().st_uid==0 and os.readlink(lib) in ('usr/lib','/usr/lib') links=[] for path in Path('/etc/systemd/system').rglob('*'): if not path.is_symlink(): continue target=os.readlink(path) if not target.startswith('/lib/systemd/system/'): continue assert path.lstat().st_uid==0 canonical='/usr'+target policy.protected(Path(canonical)) before=path.stat(); digest=sha(path) assert Path(canonical).stat().st_ino==before.st_ino and sha(canonical)==digest path.unlink();path.symlink_to(canonical) assert path.stat().st_ino==before.st_ino and sha(path)==digest links.append({'path':str(path),'before':target,'after':canonical,'sha256':digest,'inode':before.st_ino}) dump(EVIDENCE/'image-unit-links.json',links) # The image's legacy root doctor/update/Tor helpers can otherwise recreate # UID1000 callers during the real transition. Retire these exact activation # routes as external fixture deployment, retaining their original inodes. legacy_units=('archipelago-doctor.timer','archipelago-doctor.service', 'archipelago-update.timer','archipelago-update.service', 'archipelago-tor-helper.path','archipelago-tor-helper.service') retired=[] archive=Path('/var/lib/archipelago-c01/legacy-units') archive.mkdir(mode=0o700,exist_ok=True) present_legacy=[name for name in legacy_units if (Path('/etc/systemd/system')/name).exists() or (Path('/etc/systemd/system')/name).is_symlink()] if present_legacy: run('systemctl','stop',*present_legacy) for name in legacy_units: path=Path('/etc/systemd/system')/name if name not in present_legacy: retired.append({'unit':name,'status':'absent; no retirement claimed'}) continue meta=path.lstat();digest=sha(path) assert path.is_file() and not path.is_symlink() and meta.st_uid==0 and meta.st_nlink==1 destination=archive/name assert not destination.exists() path.rename(destination);path.symlink_to('/dev/null') assert destination.stat().st_ino==meta.st_ino and sha(destination)==digest retired.append({'unit':name,'inode':meta.st_ino,'sha256':digest,'retained':str(destination)}) run('systemctl','daemon-reload') dump(EVIDENCE/'legacy-root-activation-retirement.json',retired) run('systemctl', 'stop', 'user@1000.service') assert all(1000 not in item['uids'] for item in kernel.all_processes()), 'fixture runtime writer not retired' # External deployment custody of immutable public payloads, not runtime # wallet/data ownership repair. Keep every byte/inode and refuse aliases. custody=[] web=Path('/opt/archipelago/web-ui') if not web.exists(): custody.append({'path':str(web),'status':'absent; asset qualification remains open'}) for path in [p for p in [web.parent, web, *sorted(web.rglob('*'))] if p.exists()]: meta=path.lstat() assert meta.st_uid in (0,1000) and meta.st_gid in (0,1000) assert stat.S_ISDIR(meta.st_mode) or stat.S_ISREG(meta.st_mode) and meta.st_nlink==1 digest=sha(path) if path.is_file() else None mode=stat.S_IMODE(meta.st_mode)&~0o022 os.chown(path,0,0);path.chmod(mode) assert path.stat().st_ino==meta.st_ino and (not path.is_file() or sha(path)==digest) if meta.st_uid or meta.st_gid or stat.S_IMODE(meta.st_mode)!=mode: custody.append({'path':str(path),'before':[meta.st_uid,meta.st_gid,stat.S_IMODE(meta.st_mode)], 'after':[0,0,mode],'inode':meta.st_ino,'sha256':digest}) dump(EVIDENCE/'image-public-payload-custody.json',custody) containment=web/'archipelago-runtime/image-recipe/configs/nginx-archipelago.conf' compiled_template=(PACKAGE/'image-recipe/configs/nginx-archipelago.conf').read_bytes() if containment.exists(): assert containment.read_bytes()==compiled_template, 'existing containment template requires explicit disposition' else: write(containment,compiled_template,0o644) dump(EVIDENCE/'image-containment-template.json',{'path':str(containment),'sha256':sha(containment), 'source':'frozen image-recipe/configs/nginx-archipelago.conf'}) home = Path(runtime['home_backing']) (home / '.config/systemd/user/default.target.wants/c01-fixture-runtime.service').unlink() for name in ('archipelago-runtime-worker.service','archipelago-runtime-podman.service'): write(home / '.config/systemd/user' / name, template(name, {}), 0o644) (home / '.config/systemd/user/default.target.wants/archipelago-runtime-worker.service').symlink_to('../archipelago-runtime-worker.service') replacements = {'@ORIGINAL_RUNTIME_UID@':'1000','@ORIGINAL_HOME@':'/home/archipelago', '@SANITIZED_RUNTIME_HOME@':runtime['home_backing'], **{'@'+role.upper()+'_BACKING_PARENT@':str(Path(launch[role+'_backing']).parent) for role in ('manager','snapshot','authority')}} write('/etc/systemd/system/user@1000.service.d/80-archipelago-runtime-user.conf', template('80-archipelago-runtime-user.conf.in', replacements), 0o644) write('/etc/systemd/system/user@1000.service.d/81-c01-runtime-access.conf', template('81-c01-runtime-access.conf.in', {'@'+role.upper()+'_PARENT@':str(Path(launch[role+'_backing']).parent) for role in ('manager','snapshot','authority')}), 0o644) unit('archipelago-c01-access.service', template('archipelago-c01-access.service.in', {'@SUPERVISOR_UNIT@':'archipelago.service','@ORIGINAL_RUNTIME_DATA@':runtime['runtime_data_dir']})) unit('archipelago-runtime-broker.service', template('archipelago-runtime-broker.service.in', {'@RUNTIME_DATA_DIR@':runtime['runtime_data_dir'],'@ORIGINAL_RUNTIME_UID@':'1000'})) unit('archipelago.service', '[Service]\nType=notify\nNotifyAccess=main\nDelegate=yes\n' 'DelegateSubgroup=supervisor\nNoNewPrivileges=yes\nUser=root\nGroup=root\n' 'NetworkNamespacePath=/run/netns/archipelago-managed\n' 'ExecStart=/usr/local/bin/archipelago c01-supervisor-v3\n' 'TimeoutStartSec=150\nTimeoutStopSec=infinity\nRestart=no\n') write('/etc/systemd/system/archipelago.service.d/76-c01-power.conf',c01_power.supervisor_dropin(),0o644) if (PACKAGE/'nts-addresses.json').exists(): verify_runtime_dependencies() account('archipelago-nts',61003,'/nonexistent') addresses=json.loads((PACKAGE/'nts-addresses.json').read_text()) with Path('/etc/hosts').open('a') as stream: for name,values in sorted(addresses.items()): assert name in ('time.cloudflare.com','nts.netnod.se') for address in values: stream.write(f'\n{address} {name}\n') write(c01_nts.CONFIG,c01_nts.configuration(c01_nts.Policy(frozenset(addresses))),0o644) unit('archipelago-nts.service',template('archipelago-nts.service',{})) for path,mode in [('/var/lib/archipelago-network-settings',0o700),('/etc/archipelago',0o755),('/etc/fips/fips.d',0o755),('/etc/tor',0o755)]: Path(path).mkdir(parents=True,mode=mode,exist_ok=True) write('/etc/systemd/system/archipelago.service.d/70-c01-supervisor.conf', template('70-c01-supervisor.conf.in',{'@PRIVATE_BACKING_PARENT@':' '.join(runtime['private_parents']), '@RUNTIME_DATA_ROOT@':runtime['runtime_data_dir']}),0o644) run('systemctl', 'daemon-reload') provision=d09_module() provision.install_package() if os.environ.get('C01_POWER_COMPONENT') == '1': # Explicit data-only native lifetime. It cannot authorize time, start # the app or emit APP_READY. Do not submit a D09 start grant for it. write('/etc/systemd/system/archipelago.service.d/75-c01-epoch-component.conf', '[Service]\nType=notify\nExecStart=\n' 'ExecStart=/usr/local/bin/archipelago c01-epoch-lifetime-v1\n',0o644) run('systemctl','daemon-reload') # Qualify the candidate's actual systemd projection before any access or # D09 lease exists. No unit is started. Restore the exact absent preimage # before inventory/transition; never modify config behind a live lease. workspace=provision.owner.DESTINATIONS['config'] assert not workspace.exists() and not workspace.is_symlink() old_effective=provision.owner.effective_configuration() workspace_bytes=Path('/usr/libexec/archipelago-d09-terminal-workspace.conf').read_bytes() write(workspace,workspace_bytes) run('systemctl','daemon-reload') candidate_effective=provision.owner.effective_configuration() assert workspace.read_bytes()==workspace_bytes workspace.unlink() run('systemctl','daemon-reload') assert provision.owner.effective_configuration()==old_effective dump(EVIDENCE/'candidate-systemd-config.json', {'old':old_effective,'new':candidate_effective, 'old_sha256':hashlib.sha256(old_effective.encode()).hexdigest(), 'new_sha256':hashlib.sha256(candidate_effective.encode()).hexdigest(), 'started':False,'preimage_restored':True}) runtime['version'] = 2 runtime['workspace_helper_sha256'] = sha('/usr/libexec/archipelago-d09-prepare-workspace') dump(policy.RUNTIME, runtime) # Production SSH/polkit services, not successful-command stubs. Path('/run/sshd').mkdir(mode=0o755, exist_ok=True) run('ssh-keygen','-q','-t','ed25519','-N','','-f','/etc/ssh/c01-fixture-host') run('ssh-keygen','-q','-t','ed25519','-N','','-f','/run/c01-fixture-admin-key') write('/etc/ssh/c01-authorized/archipelago-admin',Path('/run/c01-fixture-admin-key.pub').read_bytes(),0o644) write('/run/c01-fixture-known-hosts',b'[127.0.0.1]:22222 '+Path('/etc/ssh/c01-fixture-host.pub').read_bytes()) write('/etc/ssh/sshd_config', 'HostKey /etc/ssh/c01-fixture-host\nUsePAM no\nPort 22222\n' 'ListenAddress 127.0.0.1\nAuthorizedKeysFile /etc/ssh/c01-authorized/%u\n' 'PasswordAuthentication no\nKbdInteractiveAuthentication no\n', 0o644) run('systemctl','start','ssh.service','polkit.service') operator_route(False) if (PACKAGE/'nts-addresses.json').exists(): run('systemctl','start','archipelago-nts.service') return {'runtime_sha256':sha(policy.RUNTIME), 'd09_modules_installed':True} def conditional_contract(): import c01_admission launch,_=policy.json_read(policy.LAUNCH) require_candidate=launch['version']==5 if not require_candidate:return {'scope':'legacy non-admitting composition'} contract=c01_admission.Contract(launch) try: contract.check() return {'measured_assets':len(contract.assets),'services':contract.initial, 'policy_sha256':launch['admission_sha256']} finally:contract.close() def role_view_probe(): launch,_=policy.json_read(policy.LAUNCH) parents=' '.join(str(Path(launch[name+'_backing']).parent) for name in ('manager','snapshot','authority')) result=run('systemd-run','--wait','--pipe','--unit=c01-role-view-probe', '-p','Delegate=yes','-p','DelegateSubgroup=supervisor', '-p','User=root','-p','Group=root','-p','AmbientCapabilities=CAP_SETUID', '-p','UMask=0077', '-p','NoNewPrivileges=yes','-p','PrivateTmp=yes','-p','PrivateDevices=yes', '-p','ProtectHome=yes','-p','ProtectSystem=strict', '-p','ReadWritePaths=/sys/fs/cgroup /var/lib/archipelago-c01 '+parents+' '+launch['runtime_data_dir'], '-p','NetworkNamespacePath=/run/netns/archipelago-managed', '/usr/bin/python3','-B',str(PACKAGE/'tests/regression/c01-role-views.py'),check=False) write(EVIDENCE/'role-view-probe.log',result.stdout+result.stderr) assert result.returncode==0,'actual authority view sandbox refused; see role-view-probe.log' return {'actual_idmapped_views':True} def operator_route(after): ssh=['/usr/bin/ssh','-F','/dev/null','-p','22222','-i','/run/c01-fixture-admin-key', '-o','BatchMode=yes','-o','IdentitiesOnly=yes','-o','StrictHostKeyChecking=yes', '-o','UserKnownHostsFile=/run/c01-fixture-known-hosts','archipelago-admin@127.0.0.1'] assert run(*ssh,'id -u').stdout.strip()==b'1001', 'actual operator SSH route failed' result={'ssh_uid':1001,'host_key_sha256':sha('/etc/ssh/c01-fixture-host.pub')} if after: password=Path('/run/c01-fixture-admin-password').read_bytes()+b'\n' assert run(*ssh,"sudo -S -p '' id -u",input=password).stdout.strip()==b'0', 'actual operator sudo route failed' result['sudo_uid']=0 dump(EVIDENCE/('operator-after.json' if after else 'operator-before.json'),result) def nts_prepare(): """Start the independent observer even when another owner cannot provision.""" assert (PACKAGE/'nts-addresses.json').exists(), 'NTS dependencies absent' verify_runtime_dependencies() account('archipelago-nts',61003,'/nonexistent') addresses=json.loads((PACKAGE/'nts-addresses.json').read_text()) with Path('/etc/hosts').open('a') as stream: for name,values in sorted(addresses.items()): assert name in ('time.cloudflare.com','nts.netnod.se') for address in values: stream.write(f'\n{address} {name}\n') write(c01_nts.CONFIG,c01_nts.configuration(c01_nts.Policy(frozenset(addresses))),0o644) unit('archipelago-nts.service',template('archipelago-nts.service',{})) run('systemctl','daemon-reload') run('systemctl','start','archipelago-nts.service') return {'configuration_sha256':sha(c01_nts.CONFIG),'clock_setting':False} def nts_owner(): assert (PACKAGE/'nts-addresses.json').exists(), 'fresh same-realm NTS dependencies/egress not supplied' failures=[] started=time.monotonic() end=started+300 while time.monotonic()