67 lines
3.1 KiB
Python
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
|