#!/usr/bin/python3 -I -S """Root-owned re-admission of TWO finite kiosk/relay runtime producers only. No RPC or caller-selected PID/path/command. The protected appointment delegates re-registration after producer death to this service, against the same binary, unit cgroup, namespace, UID and network policy. The route owner remains the socket-capability/lifetime authority; live generations are never replaced. """ import hashlib import json import os from pathlib import Path import select import socket import stat import struct import sys import time import types if __name__ == '__main__': for name in ('c01_network','c01_endpoints','runtime_uplink','c01_uplink','c01_network_mdns','c01_retained_producer','c01_network_plan','c01_network_owner'): source=Path(__file__).resolve().with_name(name+'.py') for item in (source,*source.parents): meta=item.lstat() if meta.st_uid or meta.st_mode&0o022 or stat.S_ISLNK(meta.st_mode):raise ValueError('unprotected endpoint owner code') module=types.ModuleType(name);module.__file__=str(source);sys.modules[name]=module exec(compile(source.read_bytes(),str(source),'exec'),module.__dict__) import c01_network_owner as network CONFIG=Path('/etc/archipelago-c01/kiosk-endpoints.json') PREFIX='/user.slice/user-1000.slice/user@1000.service/app.slice/' PRODUCERS={ 'app/archipelago-kiosk/tcp/9222/managed':('archipelago-kiosk-browser.service','/usr/lib/chromium/chromium'), 'app/nostr-relay/tcp/7777/host':('archipelago-runtime-nostr-relay.service','/usr/local/bin/nostr-rs-relay'), 'app/nostr-relay/tcp/7777/managed':('archipelago-runtime-nostr-relay.service','/usr/local/bin/nostr-rs-relay'), } def require(value,reason): if not value:raise ValueError(reason) def executable(path): path=Path(path) for item in (path,*path.parents): m=item.lstat() require(m.st_uid==0 and not m.st_mode&0o022 and not stat.S_ISLNK(m.st_mode),'mutable endpoint executable') m=path.stat() require(stat.S_ISREG(m.st_mode) and m.st_nlink==1,'endpoint executable type') return (m.st_dev,m.st_ino,m.st_size,m.st_mtime_ns,m.st_ctime_ns) def load(policy): raw=network.protected_read(CONFIG);value=json.loads(raw,object_pairs_hook=network.exact) require(set(value)=={'version','policy_sha256','executables','routes'} and type(value['version']) is int and value['version']==1 and value['policy_sha256']==policy.sha,'kiosk endpoint appointment') require(type(value['routes']) is list and 1<=len(value['routes'])<=3 and len(set(value['routes']))==len(value['routes']) and set(value['routes'])<=set(PRODUCERS),'finite kiosk routes') paths={PRODUCERS[name][1] for name in value['routes']} require(set(value['executables'])==paths,'exact kiosk executable closure') pins={} for path in paths: pins[path]=executable(path) with open(path,'rb') as stream: require(hashlib.file_digest(stream,'sha256').hexdigest()==value['executables'][path],'kiosk executable digest') require(executable(path)==pins[path],'kiosk executable changed while hashing') for name in value['routes']: route=policy.routes.get(name) require(route is not None and route['target']=='runtime' and route['target_uid']==1000 and route['target_cgroup']==PREFIX+PRODUCERS[name][0],'kiosk exact runtime producer') return value,network.digest(raw),pins def request(policy,body): with socket.socket(socket.AF_UNIX,socket.SOCK_SEQPACKET|socket.SOCK_CLOEXEC) as channel: channel.settimeout(2);channel.connect(str(network.CONTROL)) pid,uid,gid=struct.unpack('3i',channel.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12)) require((uid,gid)==(0,0),'network control credentials') network.require_main(pid,'archipelago-c01-network.service','/system.slice/archipelago-c01-network.service') channel.sendall(network.canonical({'policy_sha256':policy.sha,**body})) raw,anc,flags,_=channel.recvmsg(16385,0) require(not anc and not flags and 0