Files
archy/scripts/c01_provision.py
T

234 lines
14 KiB
Python

"""Offline data-only store coordinator. Fixed native capabilities, no clock grant.
Run as an independently appointed root process in a delegated supervisor cgroup.
It never installs policy, generates keys, admits the application, deletes partial
stores or retries uncertainty. Inspection recovers existing authenticated storage
with retained anchors. Legacy import is a separate explicit root appointment,
requires live cold exclusion, and cannot substitute for encrypted-store recovery.
"""
import hashlib
import itertools
import json
import os
from pathlib import Path
import socket
import sys
import c01_access_generation as generation
import c01_children as children
import c01_supervisor as supervisor
import c01_views as views
def validate_anchor(anchor, binding, initial):
"""Validate the independently authenticated native receipt, never reset it."""
require = children.require
require(type(anchor) is dict and set(anchor) == {'incarnation', 'revision', 'watermark', 'digest'},
'authenticated anchor fields')
for name in ('incarnation', 'digest'):
value = anchor[name]
require(type(value) is list and len(value) == 32 and
all(type(n) is int and 0 <= n <= 255 for n in value) and any(value),
'authenticated anchor encoding')
require(anchor['incarnation'] == binding['incarnation'], 'authenticated anchor incarnation differs')
require(type(anchor['revision']) is int and 1 <= anchor['revision'] < 2**64 and
type(anchor['watermark']) is int and 0 <= anchor['watermark'] < 2**64,
'authenticated anchor counters')
if initial:
require(anchor['revision'] == 1 and anchor['watermark'] == 0,
'initial authenticated anchor differs')
def run(inspect=False, epoch_mode=None, wait_for_power=False, legacy_approval=None):
require = children.require
require(os.getresuid() == (0, 0, 0) and os.getresgid() == (0, 0, 0), 'root provisioning coordinator required')
require(epoch_mode in (None, 'provision', 'recover'), 'fixed epoch operation required')
require(epoch_mode is None or inspect, 'epoch enablement cannot initialize an empty snapshot')
require(not wait_for_power or epoch_mode == 'recover', 'data-only epoch lifetime mode')
require(legacy_approval is None or (not inspect and epoch_mode is None and not wait_for_power),
'legacy import cannot inspect/reinitialize existing state or grant time')
value, _ = supervisor.root_json(supervisor.CONFIG)
generation.validate_launch(value) # structural bindings, NOT clock qualification
app = supervisor.principal(value['application_user'])
authority = supervisor.principal(value['authority_user'])
require(len({app.pw_uid, authority.pw_uid, value['runtime_uid'], 0}) == 4, 'provisioning principals overlap')
binary = os.open('/usr/local/bin/archipelago', os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC)
descriptors = [binary]
owner = None
migration = None
lock = supervisor.LaunchLock()
try:
meta = os.fstat(binary)
require(meta.st_uid == 0 and not meta.st_mode & 0o022, 'provisioning executable protection')
with os.fdopen(os.dup(binary), 'rb') as stream:
require(hashlib.file_digest(stream, 'sha256').hexdigest() == value['native_sha256'], 'provisioning native digest')
if not inspect and legacy_approval is None:
# Empty protected directories do not prove an existing manager has
# no retained history. This initializer supports only a new identity
# root; existing manager state needs a separately admitted migration.
manager = views.require_private_backing(value['manager_backing'])
descriptors.append(manager)
readable = os.open('.', os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC, dir_fd=manager)
descriptors.append(readable)
require(views.identity(manager)[2:] == (value['runtime_uid'], value['runtime_gid'], 0o700),
'new manager backing ownership')
with os.scandir(readable) as entries:
require({entry.name for entry in itertools.islice(entries, 2)} == {'identity'},
'existing manager state requires separately admitted migration')
supervisor.private_identity(readable, value['runtime_uid'])
identity = os.open('identity', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=readable)
descriptors.append(identity)
with os.scandir(identity) as entries:
require({entry.name for entry in itertools.islice(entries, 2)} == {'nostr_secret'},
'existing identity history requires separately admitted migration')
keys = {}
sources = {}
for role in ('snapshot', 'authority'):
keys[role] = supervisor.root_capability(value[role + '_key'], 32)
backing = views.require_private_backing(value[role + '_backing'])
descriptors.extend((keys[role], backing))
require(views.identity(backing)[2:] == (value['runtime_uid'], value['runtime_gid'], 0o700), 'original store ownership')
sources[role] = views.PinnedSource.capture(backing, value[role + '_backing'])
if not inspect:
require(not os.listdir(value[role + '_backing']), 'new-store provisioning refuses existing state')
require(supervisor.key_binding(keys['snapshot']) != supervisor.key_binding(keys['authority']), 'provisioning keys alias')
epoch = None
if epoch_mode is not None:
epoch = supervisor.load_epoch_configuration(value)
keys['epoch'] = supervisor.root_capability(epoch['key'], 32)
backing = views.require_private_backing(epoch['backing'])
descriptors.extend((keys['epoch'], backing))
require(views.identity(backing)[2:] == (value['runtime_uid'], value['runtime_gid'], 0o700), 'epoch backing ownership')
sources['epoch'] = views.PinnedSource.capture(backing, epoch['backing'])
require(len({tuple(supervisor.key_binding(fd).values()) for fd in keys.values()}) == 3, 'epoch key alias')
owner = supervisor.Supervisor(lock)
if legacy_approval is not None:
import c01_migration
migration = c01_migration.LegacyImport(legacy_approval, value)
migration.check(owner)
if wait_for_power:
supervisor.acquire_admission_leases(owner,value,app,authority)
owner.power_lease = supervisor.c01_power.Lease(value)
parent = os.getpid()
address = '\0c01-provision-' + os.urandom(16).hex()
deadline = supervisor.now() + 30_000_000_000
def child(control, role, account):
mounts = views.PrivateMounts()
mounts.cover('/run', 0o755)
target = Path('/run/c01-store')
target.mkdir(mode=0o700)
mounts.idmapped_directory(sources[role], target, value['runtime_uid'], account.pw_uid,
value['runtime_gid'], account.pw_gid)
keep = {binary, control.fileno()}
if epoch is not None and role == 'authority':
Path('/run/c01-epoch').mkdir(mode=0o700)
mounts.idmapped_directory(sources['epoch'], Path('/run/c01-epoch'), value['runtime_uid'], account.pw_uid,
value['runtime_gid'], account.pw_gid)
keep.add(keys['epoch'])
if epoch is None or role == 'authority': keep.add(keys[role])
if migration is not None and role == 'snapshot':keep.add(migration.source_fd)
supervisor.close_except(keep)
children.drop_credentials(account.pw_uid, account.pw_gid, parent)
channel = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
if role == 'authority':
channel.bind(address)
channel.listen(1)
supervisor.packet(control, b'LISTENING')
connected, _ = channel.accept()
channel.close()
channel = connected
else:
channel.connect(address)
supervisor.packet(control, b'CONNECTED')
if epoch is not None and role == 'snapshot':
alias = channel.dup()
if wait_for_power:
require(control.recv(32) == b'VERIFY_REVOKED', 'epoch lifetime peer control')
else:
supervisor.child_wait(control, b'VERIFY_REVOKED', deadline)
channel.settimeout(2); alias.settimeout(2)
require(channel.recv(1) == b'' and alias.recv(1) == b'', 'epoch socket aliases not revoked')
supervisor.packet(control, b'REVOKED')
supervisor.child_wait(control, b'RELEASE', deadline)
return
supervisor.child_wait(control, b'EXEC', deadline)
mapping = {3: control.fileno(), 4: channel.fileno(), 9: keys[role]}
if migration is not None and role == 'snapshot':mapping[14] = migration.source_fd
if epoch is not None: mapping[13] = keys['epoch']
supervisor.native(binary, 'c01-provision-store-v1',
mapping, {})
def guarded(control, role, account):
try:
child(control, role, account)
except Exception as error:
# This setup path reads no key bytes; preserve the concrete
# mount/identity failure rather than only a generic EOF barrier.
print('C01_STORE_SETUP_FAILURE:', str(error), file=sys.stderr, flush=True)
raise
service = owner.children.spawn('authority', lambda c: guarded(c, 'authority', authority), descriptors)
owner.wait(service, b'LISTENING', authority.pw_uid, authority.pw_gid, deadline)
client = owner.children.spawn('application', lambda c: guarded(c, 'snapshot', app),
descriptors + ([migration.source_fd] if migration is not None else []))
for record, account in ((service, authority), (client, app)):
owner.wait(record, b'CONNECTED', account.pw_uid, account.pw_gid, deadline)
def grant(record, role, operation, peer, account):
supervisor.packet(record.channel, b'EXEC')
request = dict(version=1, process_id=record.pid,
operation=operation, store_dir='/run/c01-store', binding=value['binding'],
key_file=supervisor.key_binding(keys[role]),
peer=dict(pid=peer.pid, uid=account.pw_uid, gid=account.pw_gid))
if epoch is not None:
request['epoch'] = dict(store_dir='/run/c01-epoch', key_file=supervisor.key_binding(keys['epoch']), delegation=epoch['delegation'])
if migration is not None and role == 'snapshot':
request.update(version=2,legacy=migration.native_appointment())
supervisor.packet(record.channel, request)
if epoch is not None:
grant(service, 'authority', 'epoch_lifetime' if wait_for_power else 'provision_epoch' if epoch_mode == 'provision' else 'recover_epoch', client, app)
context = json.loads(owner.receive(service, authority.pw_uid, authority.pw_gid, deadline))
require(context['binding'] == value['binding'] and context['delegation_id'] == epoch['delegation']['id'] and
bytes(context['boot_id']).hex() == supervisor.recovery.boot_id(), 'native epoch context binding')
if wait_for_power:
owner.authority_record=service
owner.authority_credentials=(service.pid,authority.pw_uid,authority.pw_gid)
owner.power_command=b'REVOKE'
print('C01_NON_ADMITTING_EPOCH_LIFETIME_HELD',flush=True)
while owner.tick():
supervisor.time.sleep(.02)
return {'epoch_context':context,'clock_authorized':False}
supervisor.packet(service.channel, b'REVOKE')
owner.wait(service, b'EPOCH_REVOKED', authority.pw_uid, authority.pw_gid, deadline)
supervisor.packet(client.channel, b'VERIFY_REVOKED')
owner.wait(client, b'REVOKED', app.pw_uid, app.pw_gid, deadline)
owner.completed_one_shots.update(('application','authority'))
supervisor.packet(service.channel, b'RELEASE')
supervisor.packet(client.channel, b'RELEASE')
return {'epoch_context':context, 'revoked_aliases':True, 'clock_authorized':False}
grant(service, 'authority', 'recover_authority' if inspect else 'provision_authority', client, app)
owner.wait(service, b'STORE_READY', authority.pw_uid, authority.pw_gid, deadline)
grant(client, 'snapshot', 'import_legacy_snapshot' if migration is not None else
'inspect_snapshot' if inspect else 'provision_snapshot', service, authority)
anchor = json.loads(owner.receive(client, app.pw_uid, app.pw_gid, deadline))
validate_anchor(anchor, value['binding'], initial=not inspect and migration is None)
if migration is not None:
require(anchor['revision'] == 1 and anchor['watermark'] == migration.value['approved_watermark'],
'imported authenticated anchor differs from approved floor')
owner.completed_one_shots.add('application')
supervisor.packet(client.channel, b'RELEASE')
if migration is not None:migration.check(owner)
result = {'operation': 'import-legacy' if migration is not None else 'inspect' if inspect else 'provision',
'anchor': anchor, 'clock_authorized': False, 'native_sha256': value['native_sha256']}
if migration is not None:
result.update(approval_sha256=migration.digest, source_sha256=migration.value['source']['sha256'],
source_preserved=True, source_has_no_protected_anchor=True)
return result
finally:
if owner is not None:
owner.retire()
else:
lock.close()
if migration is not None:migration.close()
for fd in descriptors:
os.close(fd)