Files
archy/scripts/c01_access_client.py

67 lines
3.1 KiB
Python

"""Fixed supervisor-side access capability. Embed alongside the C01 supervisor.
Acquire before launch and retain on root Supervisor through complete retirement.
Never inherit this connection into role children; they do not own admission.
The existing runtime broker uses runtime-access-v1; this client owns the distinct
exclusive supervisor-access-v1 lifetime bound to the protected launch digest.
"""
import json
import os
from pathlib import Path
import select
import socket
import stat
import struct
SOCKET = Path('/run/archipelago-c01/runtime-access.sock')
class AccessLease:
def __init__(self, launch_sha256):
if not (type(launch_sha256) is str and len(launch_sha256) == 64 and
all(c in '0123456789abcdef' for c in launch_sha256)):
raise ValueError('protected launch digest required')
self.channel = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET | socket.SOCK_CLOEXEC)
self.pidfd = None
try:
for path in reversed(SOCKET.parents):
meta = path.lstat()
if not stat.S_ISDIR(meta.st_mode) or meta.st_uid != 0 or meta.st_mode & 0o022:
raise ValueError('unprotected access socket ancestry')
meta = SOCKET.lstat()
if not stat.S_ISSOCK(meta.st_mode) or (meta.st_uid, meta.st_gid) != (0, 0) or meta.st_mode & 0o077:
raise ValueError('unprotected access socket')
# Candidate admission waits for an in-flight obsolete scan and a
# new generation check; the owner's absolute evidence fence is 5s.
self.channel.settimeout(6)
self.channel.connect(str(SOCKET))
pid, uid, gid = struct.unpack('3i', self.channel.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12))
if pid <= 1 or (uid, gid) != (0, 0):
raise ValueError('access owner credentials refused')
self.pidfd = os.pidfd_open(pid)
request = {'protocol': 'supervisor-access-v1', 'configuration_sha256': launch_sha256,
'nonce': os.urandom(32).hex()}
raw = json.dumps(request, separators=(',', ':')).encode()
if self.channel.send(raw) != len(raw):
raise ValueError('access challenge delivery failed')
# Exact bytes are unnecessary (provider JSON formatting can differ),
# but fields, nonce and digest must match exactly. No FD transfer.
response, ancillary, flags, _ = self.channel.recvmsg(1025, 0)
if flags or ancillary or len(response) > 1024 or json.loads(response) != request:
raise ValueError('access capability refused')
self.channel.setblocking(False)
self.check()
except BaseException:
self.close()
raise
def check(self):
if self.pidfd is None or select.select([self.channel, self.pidfd], [], [], 0)[0]:
raise ValueError('access enforcement ownership lost')
def close(self):
self.channel.close()
if self.pidfd is not None:
os.close(self.pidfd)
self.pidfd = None