"""Data-only, explicit legacy import appointment and live cold-writer exclusion. This does not move files, stop services, create keys, initialize an existing protected store, or authorize a clock. Layout retirement is a separately reviewed access transition. Existing encrypted stores use inspect/recover with their retained keys and binding, never the legacy importer. """ import hashlib import os from pathlib import Path import stat import c01_access as access import c01_access_kernel as kernel import c01_access_policy as policy_module import c01_network import c01_views as views APPROVALS = Path('/etc/archipelago-c01/migrations') require = policy_module.require def refuse_existing_legacy_anchor(launch): # A retained plaintext file may coexist with a newer protected store. It is # never permission to initialize a new incarnation from that stale file. for root in (launch['manager_backing'], launch['runtime_data_dir']): path=Path(root)/'federation/protected-pending' require(not os.path.lexists(path), 'existing protected history requires retained-key recovery, not legacy import') def validate(value, launch, launch_digest, access_digest): require(type(value) is dict and set(value) == { 'version', 'operation', 'authorization', 'launch_sha256', 'access_sha256', 'binding', 'source', 'source_public_sha256', 'approved_watermark'}, 'migration approval fields') require(type(value['version']) is int and value['version'] == 1 and value['operation'] == 'import-legacy-requests-v1' and value['authorization'] in ('one-qualified-guarded-framework-migration', 'one-qualified-guarded-yaya-migration'), 'migration authority/scope') require(value['launch_sha256'] == launch_digest and value['access_sha256'] == access_digest and value['binding'] == launch['binding'], 'migration configuration/binding differs') require(type(value['approved_watermark']) is int and 0 < value['approved_watermark'] < 2**64, 'explicit migration floor required; no zero/default/history inference') require(policy_module.hex_digest(value['source_public_sha256']), 'migration public identity digest') source = value['source'] require(type(source) is dict and set(source) == {'path','device','inode','uid','gid','bytes','sha256'}, 'migration source fields') require(source['path'] == str(Path(launch['manager_backing'])/'federation/pending_requests.json'), 'migration source must be retained private manager history') require(all(type(source[k]) is int and source[k] >= 0 for k in ('device','inode','uid','gid','bytes')) and source['inode'] > 0 and 0 < source['bytes'] <= 4*1024*1024 and (source['uid'],source['gid']) == (launch['runtime_uid'],launch['runtime_gid']) and policy_module.hex_digest(source['sha256']), 'migration source custody/size/digest') class LegacyImport: def __init__(self, digest, launch): require(os.getresuid() == (0,0,0) and os.getresgid() == (0,0,0), 'root migration coordinator required') require(policy_module.hex_digest(digest), 'content-addressed migration approval required') self.source_fd = None self.journal = None self.policy = policy_module.Policy() require(Path(__file__).resolve().parent == Path('/usr/lib/archipelago') and '/usr/lib/archipelago' in self.policy.value['trees'], 'migration coordinator closure must be in reviewed access inventory') current, launch_digest = policy_module.json_read(policy_module.LAUNCH) require(current == launch, 'migration launch changed') self.path = APPROVALS/(digest+'.json') self.value, actual = policy_module.json_read(self.path) require(actual == digest, 'migration approval digest differs') validate(self.value, launch, launch_digest, self.policy.digest) require(self.value['authorization']==self.policy.value['authorization'],'node migration authority differs from retired-writer policy') refuse_existing_legacy_anchor(launch) self.digest = digest self.launch = launch c01_network.require_private(os.getpid(), 'managed') manager = views.require_private_backing(launch['manager_backing']) os.close(manager) identity = Path(launch['manager_backing'])/'identity/nostr_pubkey' identity_fd = views._open_locator(identity, directory=False) try: # O_PATH pins the actual locator; open the pinned object read-only. with open(f'/proc/self/fd/{identity_fd}', 'rb') as stream: public = stream.read(129) require(hashlib.sha256(public).hexdigest() == self.value['source_public_sha256'], 'migration public identity changed') from c01_admission import require_public_identity require_public_identity(public, bytes(launch['binding']['local_identity'])) finally:os.close(identity_fd) source = self.value['source'] path_fd = views._open_locator(source['path'], directory=False) try:self.source_fd = os.open(f'/proc/self/fd/{path_fd}', os.O_RDONLY|os.O_CLOEXEC) finally:os.close(path_fd) try: self.source_identity = self.identity() self.source_pin = views.PinnedSource.capture(self.source_fd, source['path']) require(self.source_identity[:5] == (source['device'],source['inode'],source['uid'],source['gid'],source['bytes']), 'migration source appointment changed') with os.fdopen(os.dup(self.source_fd),'rb') as stream: require(hashlib.file_digest(stream,'sha256').hexdigest() == source['sha256'], 'migration source bytes changed') require(self.identity() == self.source_identity, 'migration source changed while hashing') # Hold the real access-owner lock through import/retirement. The # access service cannot begin serving against a half-imported store. self.journal = access.Journal(self.policy) except BaseException:self.close();raise def identity(self): m = os.fstat(self.source_fd) require(stat.S_ISREG(m.st_mode) and m.st_nlink == 1, 'migration source type/alias') return m.st_dev,m.st_ino,m.st_uid,m.st_gid,m.st_size,m.st_mtime_ns,m.st_ctime_ns,m.st_mode def check(self, owner): # Recheck the real retirement controls; a persisted retired label alone # is not writer exclusion. No runtime user process may exist in this # cold data-only phase, including a PAM/status/Podman helper. require(policy_module.json_read(self.path)[1] == self.digest, 'migration approval changed') require(policy_module.json_read(policy_module.CONFIG)[1] == self.policy.digest, 'migration access policy changed') current, _ = policy_module.json_read(policy_module.STATE/'journal.json') access.completed_history(current, self.policy) require(current['state'] == 'retired' and current['configuration_sha256'] == self.policy.digest and current['boot'] == Path('/proc/sys/kernel/random/boot_id').read_text().strip(), 'migration requires reconciled same-boot retirement') self.policy.verify_files() self.policy.verify_trees() self.policy.preserved() kernel.verify_private(self.policy) for cohort in self.policy.value['cohorts']: kernel.replacement_tree(cohort, self.policy.runtime) access.semantic_checks(self.policy) access.ssh_checks(self.policy) refuse_existing_legacy_anchor(self.launch) allowed = {record.pid for record in owner.children.children.values()} accounts = self.policy.value['accounts'] for process in kernel.all_processes(): require(accounts['runtime']['uid'] not in process['uids'], 'migration runtime writer still exists') if any(accounts[role]['uid'] in process['uids'] for role in ('manager','authority')): require(process['pid'] in allowed, 'migration foreign protected-role process') require(self.identity() == self.source_identity, 'migration source changed after exclusion') named=self.source_pin.reopen() os.close(named) def native_appointment(self): source = self.value['source'] return dict(source_device=source['device'],source_inode=source['inode'],source_uid=source['uid'], source_gid=source['gid'],source_sha256=list(bytes.fromhex(source['sha256'])), approval_sha256=list(bytes.fromhex(self.digest)),approved_watermark=self.value['approved_watermark']) def close(self): if self.source_fd is not None: os.close(self.source_fd);self.source_fd = None if self.journal is not None: self.journal.close();self.journal = None