Some checks failed
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
512 lines
28 KiB
Python
512 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""Owner-run T18 session assembly using the existing archive/containment pipeline.
|
|
|
|
Candidate only: native pinned-image tests and review are required before use.
|
|
Shared Docker local-driver tmpfs is required because Docker cp does not support
|
|
private tmpfs/mount paths, and the original KeyCape image is distroless.
|
|
"""
|
|
import argparse
|
|
import base64
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import re
|
|
import stat
|
|
import select
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import uuid
|
|
|
|
HERE=Path(__file__).resolve().parent
|
|
|
|
def load(name):
|
|
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
|
|
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
|
|
|
|
adapter=load('isolated-keycape')
|
|
local=load('isolated-lldap')
|
|
bundle=local.bundle
|
|
# Bind the reused preflight itself, not only this module's direct Docker calls.
|
|
local.command=adapter.command
|
|
_original_preflight=local.preflight
|
|
def preflight():
|
|
contexts=json.loads(adapter.command(['docker','context','inspect']))
|
|
if len(contexts)!=1 or contexts[0]['Endpoints']['docker']['Host']!=adapter.ENDPOINT:raise adapter.Refused()
|
|
result=_original_preflight();result['docker_endpoint']=adapter.ENDPOINT;return result
|
|
local.preflight=preflight
|
|
WORKER=local.CLIENT
|
|
STAGING_ROOT=Path('/dev/shm')
|
|
RUNROOTS=('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')
|
|
VOLUME_OPTIONS={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
|
|
|
|
IMPORT=r'''
|
|
import base64,json,os,sys
|
|
from pathlib import Path
|
|
os.umask(0o077)
|
|
p=json.load(sys.stdin)
|
|
allowed=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
|
|
for name,encoded in p.items():
|
|
f=Path(name)
|
|
if not name.startswith(allowed) or '..' in f.parts or any(x.is_symlink() for x in (f,*f.parents)):raise ValueError()
|
|
f.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
|
|
fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
with os.fdopen(fd,'wb') as out:out.write(base64.b64decode(encoded,validate=True))
|
|
'''
|
|
|
|
# Small transport inside the already pinned helper; it has only loopback routes.
|
|
# No token handling, auth bypass, dependency imitation, reset or notifier path.
|
|
TLS=r'''
|
|
import select,socket,socketserver,ssl,threading
|
|
context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
context.minimum_version=ssl.TLSVersion.TLSv1_2
|
|
context.load_cert_chain('/recovery-tls/server.pem','/recovery-tls/server-key.pem')
|
|
class Handler(socketserver.BaseRequestHandler):
|
|
def handle(self):
|
|
try:
|
|
with context.wrap_socket(self.request,server_side=True) as incoming:
|
|
with socket.create_connection(('127.0.0.1',8080),timeout=10) as outgoing:
|
|
incoming.settimeout(15);outgoing.settimeout(15)
|
|
while True:
|
|
ready,_,_=select.select([incoming,outgoing],[],[],15)
|
|
if not ready:return
|
|
for source in ready:
|
|
data=source.recv(65536)
|
|
if not data:return
|
|
(outgoing if source is incoming else incoming).sendall(data)
|
|
except Exception:pass
|
|
class Server(socketserver.ThreadingTCPServer):
|
|
allow_reuse_address=True
|
|
daemon_threads=True
|
|
with Server(('127.0.0.1',443),Handler) as server:server.serve_forever()
|
|
'''
|
|
|
|
|
|
def run(argv,**kwargs):return adapter.command(argv,**kwargs)
|
|
|
|
|
|
def plan_session():
|
|
run_id=uuid.uuid4().hex
|
|
stem='nk49-kc-'+run_id
|
|
worker=stem+'-transport';issuer=stem+'-issuer'
|
|
volumes={stem+'-v'+str(i):{'target':root,'options':dict(VOLUME_OPTIONS)} for i,root in enumerate(RUNROOTS)}
|
|
return {'run_id':run_id,'containers':[worker,issuer], 'worker':worker,'issuer':issuer,
|
|
'images':{worker:WORKER,issuer:adapter.IMAGE},'volumes':volumes,
|
|
'public_image_probe':stem+'-image-probe','docker_endpoint':adapter.ENDPOINT}
|
|
|
|
|
|
def verify_volume(name,spec,run_id):
|
|
obj=json.loads(run(['docker','volume','inspect',name]))
|
|
if (len(obj)!=1 or obj[0]['Name']!=name or obj[0]['Driver']!='local' or obj[0].get('Scope')!='local'
|
|
or obj[0].get('Options')!=VOLUME_OPTIONS or spec['options']!=VOLUME_OPTIONS
|
|
or obj[0].get('Labels',{}).get(adapter.LABEL)!=run_id):raise adapter.Refused()
|
|
|
|
|
|
def base_args(session,name,network):
|
|
return ['docker','run','--detach','--pull=never','--name',name,'--network',network,
|
|
'--label',adapter.LABEL+'='+session['run_id'],'--read-only','--cap-drop','ALL',
|
|
'--security-opt','no-new-privileges','--user','1000:1000','--memory','512m','--cpus','1',
|
|
'--pids-limit','64','--log-driver','none','--restart','no',
|
|
'--tmpfs','/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700']
|
|
|
|
|
|
def mounts(session,readonly=False):
|
|
args=[]
|
|
for name,spec in session['volumes'].items():
|
|
args+=['--mount','type=volume,source='+name+',target='+spec['target']+(',readonly' if readonly and spec['target']!='/var/lib/keycape-policy' else '')]
|
|
return args
|
|
|
|
|
|
PROVISION_PHASES=(
|
|
'volume_creation_and_verification','worker_launch','worker_containment',
|
|
'private_import','issuer_launch','issuer_containment','tls_transport_launch','complete')
|
|
|
|
|
|
def provision(session,payload,tls,issuer,phase=None):
|
|
"""Inspect containment and driver options before the first private import."""
|
|
from urllib.parse import urlsplit
|
|
address=urlsplit(issuer)
|
|
if address.scheme!='https' or not address.hostname or address.port not in (None,443) or address.path not in ('','/'):
|
|
raise adapter.Refused()
|
|
if phase is None:phase=lambda value:None
|
|
phase('volume_creation_and_verification')
|
|
for name,spec in session['volumes'].items():
|
|
run(['docker','volume','create','--driver','local','--label',adapter.LABEL+'='+session['run_id'],
|
|
'--opt','type=tmpfs','--opt','device=tmpfs','--opt','o='+VOLUME_OPTIONS['o'],name])
|
|
verify_volume(name,spec,session['run_id'])
|
|
phase('worker_launch')
|
|
args=base_args(session,session['worker'],'none')+mounts(session)
|
|
# This pinned helper declares an image VOLUME here. Mask it explicitly so
|
|
# Docker cannot allocate an unregistered disk-backed anonymous volume.
|
|
args+=['--tmpfs','/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700']
|
|
args+=['--sysctl','net.ipv4.ip_unprivileged_port_start=0','--add-host',address.hostname+':127.0.0.1',
|
|
'--entrypoint','python3',WORKER,'-c','import time;time.sleep(1800)']
|
|
run(args)
|
|
# Check helper and every private filesystem before touching the ciphertext's
|
|
# private contents. Issuer is created only after originals are present.
|
|
phase('worker_containment')
|
|
adapter.inspect_session({**session,'containers':[session['worker']],'issuer':session['worker']},worker_only=True)
|
|
phase('private_import')
|
|
files={**payload['files'],**{k:base64.b64encode(v).decode() for k,v in tls.items()}}
|
|
run(['docker','exec','-i',session['worker'],'python3','-c',IMPORT],input=json.dumps(files).encode())
|
|
anchor=run(['docker','container','inspect',session['worker'],'--format','{{.Id}}']).decode().strip()
|
|
phase('issuer_launch')
|
|
args=base_args(session,session['issuer'],'container:'+anchor)+mounts(session,readonly=True)
|
|
# CLI argv carries environment names only. Original credentials stay in the
|
|
# owner subprocess environment and Docker's private container configuration.
|
|
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':'unix:///var/run/docker.sock',**payload['env'],
|
|
'SSL_CERT_FILE':'/recovery-tls/trust.pem'}
|
|
for name in (*payload['env'],'SSL_CERT_FILE'):args+=['--env',name]
|
|
args+=[adapter.IMAGE]
|
|
run(args,env=env)
|
|
phase('issuer_containment')
|
|
adapter.inspect_session(session)
|
|
phase('tls_transport_launch')
|
|
run(['docker','exec','--detach',session['worker'],'python3','-c',TLS])
|
|
phase('complete')
|
|
|
|
|
|
def cleanup_all(session,receipt):
|
|
failed=False
|
|
try:adapter.cleanup({**session,'containers':session['containers']+[session['public_image_probe']]},receipt)
|
|
except Exception:failed=True
|
|
rows=[]
|
|
for name,spec in session['volumes'].items():
|
|
row={'volume':name,'status':'unverified'};rows.append(row)
|
|
try:
|
|
exists=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
|
|
if not exists:
|
|
row['status']='absence_verified'
|
|
continue
|
|
verify_volume(name,spec,session['run_id'])
|
|
run(['docker','volume','rm',name])
|
|
remaining=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
|
|
if remaining:row['status']='still_present';failed=True
|
|
else:row['status']='absence_verified'
|
|
except Exception:failed=True
|
|
receipt['tmpfs_volume_cleanup_obligations']=rows
|
|
receipt['container_cleanup_verified']=not failed
|
|
if failed:raise adapter.Refused()
|
|
|
|
def register_workdir(work, run_id):
|
|
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+run_id+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
|
|
if any(p.is_symlink() for p in (work,*work.parents)):raise adapter.Refused()
|
|
info=work.lstat()
|
|
if not stat.S_ISDIR(info.st_mode) or info.st_uid!=os.getuid() or stat.S_IMODE(info.st_mode)!=0o700:raise adapter.Refused()
|
|
return {'path':str(work),'run_id':run_id,'device':info.st_dev,'inode':info.st_ino,'owner_uid':info.st_uid}
|
|
|
|
|
|
def cleanup_workdir(staging, receipt):
|
|
"""Delete only the registered inode below actual /dev/shm, never a replacement."""
|
|
row={'path':staging['path'],'device':staging['device'],'inode':staging['inode'],'status':'unverified'}
|
|
receipt['plaintext_cleanup_obligation']=row
|
|
work=Path(staging['path'])
|
|
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+staging['run_id']+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
|
|
if not any(line.split()[1:3]==['/dev/shm','tmpfs'] for line in Path('/proc/mounts').read_text().splitlines()):raise adapter.Refused()
|
|
if any(p.is_symlink() for p in work.parents):raise adapter.Refused()
|
|
parentfd=os.open(work.parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
|
|
try:
|
|
try:info=os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
|
|
except FileNotFoundError:
|
|
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
|
|
if (not stat.S_ISDIR(info.st_mode) or info.st_uid!=staging['owner_uid'] or info.st_uid!=os.getuid()
|
|
or info.st_dev!=staging['device'] or info.st_ino!=staging['inode'] or stat.S_IMODE(info.st_mode)!=0o700):
|
|
row['status']='identity_mismatch_not_removed';raise adapter.Refused()
|
|
def remove(parent,name,expected):
|
|
fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent)
|
|
try:
|
|
current=os.fstat(fd)
|
|
if (current.st_dev,current.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
|
|
for child in os.listdir(fd):
|
|
before=os.stat(child,dir_fd=fd,follow_symlinks=False)
|
|
if before.st_uid!=staging['owner_uid'] or before.st_dev!=staging['device']:raise adapter.Refused()
|
|
if stat.S_ISDIR(before.st_mode):remove(fd,child,before)
|
|
elif stat.S_ISREG(before.st_mode) and before.st_nlink==1:os.unlink(child,dir_fd=fd)
|
|
else:raise adapter.Refused() # symlinks/special files/hardlinks are uncertain, never followed
|
|
finally:os.close(fd)
|
|
after=os.stat(name,dir_fd=parent,follow_symlinks=False)
|
|
if (after.st_dev,after.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
|
|
os.rmdir(name,dir_fd=parent)
|
|
remove(parentfd,work.name,info)
|
|
try:os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
|
|
except FileNotFoundError:
|
|
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
|
|
raise adapter.Refused()
|
|
finally:os.close(parentfd)
|
|
|
|
|
|
def recover_obligations(session, staging, receipt):
|
|
failed=False
|
|
try:cleanup_all(session,receipt)
|
|
except Exception:failed=True
|
|
# The plaintext obligation is attempted even when Docker is unreachable.
|
|
try:cleanup_workdir(staging,receipt)
|
|
except Exception:failed=True
|
|
if failed:raise adapter.Refused()
|
|
|
|
|
|
def process_facts(pid):
|
|
raw=Path('/proc')/str(pid)/'stat'
|
|
try:
|
|
fields=raw.read_text().rsplit(')',1)[1].split()
|
|
return {'pid':pid,'state':fields[0],'pgid':int(fields[2]),'sid':int(fields[3]),
|
|
'start_ticks':int(fields[19]),'uid':raw.parent.stat().st_uid}
|
|
except FileNotFoundError:return None
|
|
|
|
|
|
def isolate_execution():
|
|
# Called by main before reading TLS/key inputs. Never reuse an inherited
|
|
# shell, custody-helper, user or unrelated process group.
|
|
os.setsid()
|
|
pid=os.getpid();facts=process_facts(pid)
|
|
if os.getsid(0)!=pid or os.getpgrp()!=pid or facts['sid']!=pid or facts['pgid']!=pid:raise adapter.Refused()
|
|
return {k:facts[k] for k in ('pid','pgid','sid','start_ticks','uid')}|{'deadline':time.monotonic()+1800}
|
|
|
|
|
|
def verify_execution(execution):
|
|
if execution['pid']!=execution['sid'] or execution['pid']!=execution['pgid'] or execution['uid']!=os.getuid():raise adapter.Refused()
|
|
current=process_facts(execution['pid'])
|
|
if current is not None and any(current[k]!=execution[k] for k in ('pid','pgid','sid','start_ticks','uid')):
|
|
# Numeric leader PID reuse is not authority to signal a new group.
|
|
raise adapter.Refused()
|
|
|
|
|
|
def group_members(execution):
|
|
verify_execution(execution)
|
|
members=[]
|
|
for path in Path('/proc').iterdir():
|
|
if not path.name.isdecimal():continue
|
|
facts=process_facts(int(path.name))
|
|
if facts is None or facts['pgid']!=execution['pgid']:continue
|
|
if facts['sid']!=execution['sid'] or facts['uid']!=execution['uid'] or facts['start_ticks']<execution['start_ticks']:raise adapter.Refused()
|
|
if facts['state'] not in ('Z','X'):members.append(facts)
|
|
return members
|
|
|
|
|
|
def pidfd_dead(pidfd):
|
|
poller=select.poll();poller.register(pidfd,select.POLLIN)
|
|
return bool(poller.poll(0))
|
|
|
|
|
|
def quiesce_execution(execution,parent_pidfd,receipt):
|
|
"""Stop only this fresh execution session; cleanup needs no live creator.
|
|
|
|
Exact parent pidfd prevents leader reuse. Each remaining session member gets
|
|
its own pidfd after metadata comparison; no inherited/user-group kill occurs.
|
|
Unknown membership or uninterruptible survivors refuse cleanup altogether.
|
|
"""
|
|
verify_execution(execution)
|
|
if not pidfd_dead(parent_pidfd):
|
|
signal.pidfd_send_signal(parent_pidfd,signal.SIGTERM)
|
|
poller=select.poll();poller.register(parent_pidfd,select.POLLIN);poller.poll(2000)
|
|
if not pidfd_dead(parent_pidfd):signal.pidfd_send_signal(parent_pidfd,signal.SIGKILL)
|
|
for attempt in range(50):
|
|
members=group_members(execution)
|
|
if pidfd_dead(parent_pidfd) and not members:
|
|
receipt['execution_quiescence_verified']=True;return
|
|
for original in members:
|
|
try:fd=os.pidfd_open(original['pid'])
|
|
except ProcessLookupError:continue
|
|
try:
|
|
current=process_facts(original['pid'])
|
|
if current is None or pidfd_dead(fd):continue
|
|
if current!=original: # state may naturally change; bind stable identity below
|
|
if any(current[k]!=original[k] for k in ('pid','pgid','sid','start_ticks','uid')):raise adapter.Refused()
|
|
verify_execution(execution)
|
|
signal.pidfd_send_signal(fd,signal.SIGKILL)
|
|
except ProcessLookupError:pass
|
|
finally:os.close(fd)
|
|
time.sleep(.05)
|
|
receipt['execution_quiescence_verified']=False
|
|
raise adapter.Refused()
|
|
|
|
|
|
def daemon_uncertain(staging):
|
|
return any(Path(staging['path']+suffix).exists() or Path(staging['path']+suffix).is_symlink() for suffix in ('-docker-inflight','-docker-uncertain'))
|
|
|
|
|
|
def reaper_main(session,staging,execution,parent_pidfd):
|
|
receipt={'status':'failed','execution_quiescence_verified':False}
|
|
verify_execution(execution)
|
|
if pidfd_dead(parent_pidfd):raise adapter.Refused()
|
|
print('registered',flush=True)
|
|
poller=select.poll();poller.register(parent_pidfd,select.POLLIN)
|
|
remaining=max(0,int((execution['deadline']-time.monotonic())*1000))
|
|
parent_lost=bool(poller.poll(remaining))
|
|
receipt['cleanup_trigger']='parent_exit' if parent_lost else 'execution_deadline'
|
|
try:
|
|
# No deletion races a live creator: first stop/wait this exact owner
|
|
# session and its children. A daemon request already accepted can still
|
|
# complete after CLI loss; retain that uncertainty even after readback.
|
|
quiesce_execution(execution,parent_pidfd,receipt)
|
|
uncertain=daemon_uncertain(staging)
|
|
receipt['in_flight_daemon_request_uncertain']=uncertain
|
|
recover_obligations(session,staging,receipt)
|
|
receipt['status']='bounded_cleanup_uncertain' if uncertain else 'bounded_cleanup_verified'
|
|
except Exception:receipt['status']='bounded_cleanup_uncertain'
|
|
finally:
|
|
os.close(parent_pidfd)
|
|
try:
|
|
fd=os.open(staging['path']+'-reaper.json',os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
with os.fdopen(fd,'w') as out:json.dump(receipt,out)
|
|
except Exception:pass
|
|
|
|
|
|
def start_reaper(session, staging, execution):
|
|
adapter.docker_environment()
|
|
verify_execution(execution)
|
|
if session.get('docker_endpoint')!=adapter.ENDPOINT or staging.get('run_id')!=session['run_id']:raise adapter.Refused()
|
|
if register_workdir(Path(staging['path']),session['run_id'])!=staging:raise adapter.Refused()
|
|
parent_pidfd=os.pidfd_open(execution['pid'])
|
|
code="""import importlib.util,json,sys
|
|
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
|
m.reaper_main(json.loads(sys.argv[2]),json.loads(sys.argv[3]),json.loads(sys.argv[4]),int(sys.argv[5]))
|
|
"""
|
|
try:
|
|
child=subprocess.Popen([sys.executable,'-c',code,str(Path(__file__).resolve()),json.dumps(session),json.dumps(staging),json.dumps(execution),str(parent_pidfd)],
|
|
stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
|
|
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':adapter.ENDPOINT},
|
|
close_fds=True,pass_fds=(parent_pidfd,),start_new_session=True)
|
|
finally:os.close(parent_pidfd)
|
|
ready,_,_=select.select([child.stdout],[],[],10)
|
|
if not ready or child.stdout.readline()!=b'registered\n':
|
|
child.terminate();child.wait(timeout=5);raise adapter.Refused()
|
|
child.stdout.close()
|
|
return child
|
|
|
|
|
|
def execute(transfer,admission,tls,receipt,execution,checkpoint=None):
|
|
"""Existing private custody/transfer/archive APIs; no second archive format."""
|
|
adapter.manifest_binding_mode(admission,transfer)
|
|
verify_execution(execution)
|
|
if execution['pid']!=os.getpid():raise adapter.Refused()
|
|
target=local.preflight()
|
|
for image in (adapter.IMAGE,):run(['docker','image','inspect',image])
|
|
local.offline.validate(transfer)
|
|
if transfer.get('independent_owner_retrieval_verified') is not True:raise adapter.Refused()
|
|
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
|
|
if (path.stat().st_size!=transfer['ciphertext_bytes'] or local.offline.offsite.digest(path)!=transfer['ciphertext_sha256']
|
|
or admission['ciphertext_sha256']!=transfer['ciphertext_sha256']):raise adapter.Refused()
|
|
if hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest()!=admission['trust_sha256']:raise adapter.Refused()
|
|
if hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest()!=admission['tls_cert_sha256']:raise adapter.Refused()
|
|
# Validate exact private certificate/key pair and original issuer name before
|
|
# opening service containers; do not generate replacement trust or keys.
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives import serialization
|
|
cert=x509.load_pem_x509_certificate(tls['/recovery-tls/server.pem'])
|
|
key=serialization.load_pem_private_key(tls['/recovery-tls/server-key.pem'],password=None)
|
|
if cert.public_key().public_numbers()!=key.public_key().public_numbers():raise adapter.Refused()
|
|
keyfd=local.verify.custody.private_key_fd();work=None;session=None;reaper=None;staging=None
|
|
receipt.update(status='failed',target=target,session_source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
|
source_files_sha256={name:hashlib.sha256((HERE/name).read_bytes()).hexdigest() for name in ('isolated-keycape.py','isolated-keycape-session.py','run-isolated-keycape.sh','isolated-lldap.py','identity-bundle.py','verify-identity.py','retrieve-identity-offline.py','identity-offsite.py')},
|
|
temporary_plaintext_cleanup_verified=False,container_cleanup_verified=False,
|
|
downstream_consumer_reconnect_proven=False,human_mfa_proven=False)
|
|
try:
|
|
if run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()!=bundle.profile()['public_recipient'].encode():raise adapter.Refused()
|
|
session=plan_session()
|
|
work=Path(tempfile.mkdtemp(prefix='nk49-keycape-'+session['run_id']+'-',dir='/dev/shm'))
|
|
staging=register_workdir(work,session['run_id'])
|
|
reaper=start_reaper(session,staging,execution)
|
|
adapter.EXECUTION={**execution,'staging':str(work)}
|
|
receipt.update(execution_binding=execution,bounded_cleanup_reaper_started=True,plaintext_cleanup_obligation={**staging,'status':'registered_before_plaintext'},docker_endpoint=adapter.ENDPOINT,
|
|
reaper_outcome_path=staging['path']+'-reaper.json',
|
|
registered_cleanup={'containers':session['containers']+[session['public_image_probe']],'tmpfs_volumes':list(session['volumes'])})
|
|
if checkpoint is not None:checkpoint(receipt)
|
|
archive=work/'archive.tar'
|
|
with archive.open('xb') as output:
|
|
p=adapter.process(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,stderr=subprocess.DEVNULL,pass_fds=(keyfd,),timeout=180)
|
|
if p.returncode or archive.stat().st_size>bundle.LIMIT+4*1024*1024:raise adapter.Refused()
|
|
root=work/'restored';root.mkdir(mode=0o700)
|
|
manifest,_=bundle.restore(archive,root)
|
|
# Data binding comes from exact reviewed ciphertext, never a new grant.
|
|
admission=adapter.private_manifest_admission(admission,manifest)
|
|
payload=adapter.private_payload(root,manifest,admission['manifest_sha256'])
|
|
import yaml
|
|
cfg=yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml']))
|
|
if cfg.get('port')!=8080 or cfg.get('issuer')!=admission['issuer']:raise adapter.Refused()
|
|
# Public executable readback uses the daemon archive API only on the
|
|
# original image rootfs, never a tmpfs or private user-created mount.
|
|
probe=session['public_image_probe']
|
|
try:
|
|
run(['docker','create','--pull=never','--name',probe,'--network','none','--read-only','--label',adapter.LABEL+'='+session['run_id'],adapter.IMAGE])
|
|
binary=adapter.container_file(probe,'/keycape')
|
|
if hashlib.sha256(binary).hexdigest()!=admission['verifier_sha256']:raise adapter.Refused()
|
|
finally:
|
|
receipt['public_image_probe_cleanup_obligations']=[{'container':probe,'status':'unverified'}]
|
|
run(['docker','rm',probe])
|
|
if run(['docker','container','ls','--all','--filter','name=^/'+probe+'$','--format','{{.Names}}']).strip():raise adapter.Refused()
|
|
receipt['public_image_probe_cleanup_obligations'][0]['status']='absence_verified'
|
|
def phase(value):
|
|
# Public enum only: no private payload, exception text or daemon logs.
|
|
if value not in PROVISION_PHASES:raise adapter.Refused()
|
|
receipt['provision_phase']=value
|
|
if checkpoint is not None:checkpoint(receipt)
|
|
provision(session,payload,tls,admission['issuer'],phase)
|
|
receipt.update(ciphertext_sha256=transfer['ciphertext_sha256'],all_manifest_hashes_verified=True,manifest_file_count=len(manifest['files']))
|
|
checked_admission={**admission,'session':session,'trust_path':'/recovery-tls/trust.pem'}
|
|
# Native checker consumes no service secret argument or new grant.
|
|
# Retry only bounded public readiness; no account/factor attempt.
|
|
for attempt in range(40):
|
|
try:
|
|
numbers=serialization.load_pem_private_key(base64.b64decode(payload['files']['/etc/keycape/key.pem']),password=None).public_key().public_numbers()
|
|
adapter.check_jwks(session,checked_admission,numbers);break
|
|
except Exception:
|
|
if attempt==39:raise
|
|
time.sleep(.25)
|
|
adapter.native_checks(root,manifest,checked_admission,receipt,cleanup_session=False)
|
|
finally:
|
|
os.close(keyfd)
|
|
try:
|
|
if session is not None and staging is not None:
|
|
uncertain=daemon_uncertain(staging)
|
|
receipt['in_flight_daemon_request_uncertain']=uncertain
|
|
recover_obligations(session,staging,receipt)
|
|
if uncertain:raise adapter.Refused()
|
|
if reaper is not None:
|
|
reaper.terminate();reaper.wait(timeout=10)
|
|
except Exception:
|
|
receipt['status']='failed'
|
|
raise
|
|
finally:
|
|
adapter.EXECUTION=None
|
|
if work is not None and not work.exists():receipt['temporary_plaintext_cleanup_verified']=True
|
|
|
|
|
|
def persist_receipt(fd,receipt):
|
|
raw=(json.dumps(receipt,indent=2)+'\n').encode()
|
|
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0)
|
|
with os.fdopen(os.dup(fd),'wb') as output:
|
|
output.write(raw);output.flush();os.fsync(output.fileno())
|
|
|
|
|
|
def main():
|
|
p=argparse.ArgumentParser(description=__doc__)
|
|
for name in ('transfer-receipt','admission','admission-sha256','tls-cert','tls-key','trust','receipt'):p.add_argument('--'+name,required=True)
|
|
args=p.parse_args();os.umask(0o077)
|
|
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
receipt={'schema':'netkingdom.isolated-keycape-recovery.v1','status':'failed','production_contacted':False,'secret_values_exported':False}
|
|
try:
|
|
execution=isolate_execution()
|
|
raw=Path(args.admission).read_bytes()
|
|
if hashlib.sha256(raw).hexdigest()!=args.admission_sha256:raise adapter.Refused()
|
|
admission=json.loads(raw)
|
|
tls={}
|
|
for name,path in (('server.pem',args.tls_cert),('server-key.pem',args.tls_key),('trust.pem',args.trust)):
|
|
source=Path(path);bundle.private(source,max_bytes=1024*1024);tls['/recovery-tls/'+name]=source.read_bytes()
|
|
execute(json.loads(Path(args.transfer_receipt).read_text()),admission,tls,receipt,execution,checkpoint=lambda r:persist_receipt(fd,r))
|
|
except (Exception,KeyboardInterrupt):receipt.update(status='failed',error='isolated_keycape_failed_private_diagnostics_suppressed')
|
|
finally:
|
|
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
|
|
try:persist_receipt(fd,receipt)
|
|
finally:os.close(fd)
|
|
print('OK: isolated KeyCape native checks and cleanup passed.' if receipt['status']=='isolated_keycape_native_registration_checked' else 'ERROR: isolated KeyCape stopped; review safe receipt.')
|
|
return int(receipt['status']!='isolated_keycape_native_registration_checked')
|
|
|
|
if __name__=='__main__':
|
|
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,local.offline.interrupted)
|
|
raise SystemExit(main())
|