net-kingdom/sso-mfa/k8s/keycape/openbao-negative-node-wrapper.py
tegwick 7ceac4372d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Publish reviewed bounded JWT ready handoff and retained owner history
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
2026-10-11 18:31:04 +02:00

183 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Founder-admitted node wrapper: private local child-ready pipe -> SSH control stdout.
No token read/copy, login, tunnel or approval authentication. Parent must capture
stdout over its admitted authenticated SSH peer; never use the silent warden lane.
"""
import argparse
import contextlib
import hashlib
import json
import math
import os
from pathlib import Path
import signal
import stat
import subprocess
import sys
import threading
import time
SCHEMA='key39-private-input-ready/v1'
PROPOSAL='KEY-WP-0039-T02/attended-lifecycle-v9-ready-input'
TARGET='sso/keycape'
MAX_READY=4096
MAX_ENVELOPE=65536
READY_TOTAL_SECONDS=5
class Refused(Exception):pass
def need(ok,code):
if not ok:raise Refused(code)
def finite(value):
try:return type(value) in (int,float) and math.isfinite(value)
except OverflowError:return False
def hash_bytes(raw):return hashlib.sha256(raw).hexdigest()
def hex_pin(value):return isinstance(value,str) and len(value)==64 and all(c in '0123456789abcdef' for c in value)
@contextlib.contextmanager
def deadline(seconds):
need(finite(seconds) and 0<seconds<=1200,'wrapper_budget_invalid')
need(threading.current_thread() is threading.main_thread()
and signal.getitimer(signal.ITIMER_REAL)==(0.0,0.0),'wrapper_timer_required')
handler=signal.getsignal(signal.SIGALRM)
def expired(*_):raise Refused('wrapper_timeout')
signal.signal(signal.SIGALRM,expired);signal.setitimer(signal.ITIMER_REAL,seconds)
try:yield
finally:signal.setitimer(signal.ITIMER_REAL,0);signal.signal(signal.SIGALRM,handler)
def decode(raw):
def unique(pairs):
result={}
for key,value in pairs:
need(key not in result,'wrapper_duplicate_key');result[key]=value
return result
try:
return json.loads(raw,object_pairs_hook=unique,
parse_constant=lambda _:(_ for _ in ()).throw(Refused('wrapper_nonfinite_json')))
except Refused:raise
except Exception:raise Refused('wrapper_json_invalid') from None
def pinned_file(path,pin,limit,private=False):
p=Path(path);need(p.is_absolute() and p.resolve()==p,'wrapper_absolute_pin_required')
need(hex_pin(pin),'wrapper_pin_required')
fd=os.open(p,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
try:
before=os.fstat(fd)
need(stat.S_ISREG(before.st_mode) and 0<before.st_size<=limit,'wrapper_file_invalid')
if private:
need(before.st_uid==os.getuid() and stat.S_IMODE(before.st_mode)==0o600
and before.st_nlink==1,'wrapper_private_envelope_required')
raw=os.read(fd,limit+1);after=os.fstat(fd)
need(len(raw)<=limit and hash_bytes(raw)==pin,'wrapper_source_changed')
need((before.st_dev,before.st_ino,before.st_size,before.st_mtime_ns,before.st_ctime_ns)==
(after.st_dev,after.st_ino,after.st_size,after.st_mtime_ns,after.st_ctime_ns),
'wrapper_file_changed')
return raw
finally:os.close(fd)
def validate_marker(raw,*,nonce,executor_pin,envelope_pin,envelope):
need(isinstance(raw,bytes) and 0<len(raw)<=MAX_READY and raw.endswith(b'\n')
and raw.count(b'\n')==1,'wrapper_ready_invalid')
marker=decode(raw)
expected={'schema','session_nonce','proposal_id','target','executor_sha256','envelope_sha256',
'bao_health_identity_sha256','valid_until_epoch','receive_budget_seconds'}
need(isinstance(marker,dict) and set(marker)==expected,'wrapper_ready_invalid')
need(marker['schema']==SCHEMA and marker['session_nonce']==nonce
and marker['proposal_id']==PROPOSAL and marker['target']==TARGET
and marker['executor_sha256']==executor_pin and marker['envelope_sha256']==envelope_pin
and marker['bao_health_identity_sha256']==envelope['bao_health_identity_sha256']
and marker['valid_until_epoch']==envelope['valid_until_epoch'], 'wrapper_ready_binding_changed')
need(finite(marker['valid_until_epoch']) and time.time()<marker['valid_until_epoch']
and finite(marker['receive_budget_seconds']) and 0<marker['receive_budget_seconds']<=15,
'wrapper_ready_window_expired')
return marker
def read_ready(fd):
raw=b''
while len(raw)<=MAX_READY:
chunk=os.read(fd,MAX_READY+1-len(raw))
if not chunk:break
raw+=chunk
need(len(raw)<=MAX_READY,'wrapper_ready_oversized')
return raw
def forward_ready(raw):
# Wrapper stdout is a captured control pipe. Never child stdout or token data.
current=os.fstat(1)
need(stat.S_ISFIFO(current.st_mode),'wrapper_private_control_pipe_required')
need((current.st_dev,current.st_ino)!=(os.fstat(0).st_dev,os.fstat(0).st_ino),
'wrapper_control_input_alias_refused')
offset=0
while offset<len(raw):
written=os.write(1,raw[offset:]);need(type(written) is int and written>0,'wrapper_control_write_failed')
offset+=written
def run(args):
proc=None;read_fd=None;write_fd=None
try:
need(hex_pin(args.session_nonce),'wrapper_nonce_required')
with deadline(READY_TOTAL_SECONDS):
child=pinned_file(args.child,args.child_sha256,256*1024)
envelope_raw=pinned_file(args.envelope,args.envelope_sha256,MAX_ENVELOPE,private=True)
envelope=decode(envelope_raw)
need(envelope.get('proposal_id')==PROPOSAL and envelope.get('target')==TARGET
and envelope.get('disposition')=='approved' and envelope.get('founder_decision_id'),
'wrapper_exact_owner_envelope_required')
need(envelope.get('source_hashes',{}).get(str(Path(args.child)))==args.child_sha256
and envelope.get('source_hashes',{}).get(str(Path(__file__).resolve()))==hash_bytes(Path(__file__).read_bytes()),
'wrapper_child_and_wrapper_pins_required')
expiry=envelope['valid_until_epoch'];maximum=envelope['maximum_session_seconds']
need(finite(expiry) and type(maximum) is int and 420<=maximum<=900
and 0<expiry-time.time()<=maximum,'wrapper_action_window_invalid')
absolute_end=time.monotonic()+expiry-time.time()+300
read_fd,write_fd=os.pipe()
proc=subprocess.Popen([sys.executable,args.child,'execute','--envelope',args.envelope,
'--receipt',args.receipt,'--ready-fd',str(write_fd),'--session-nonce',args.session_nonce,
'--ready-envelope-sha256',args.envelope_sha256],stdin=sys.stdin.buffer,
stdout=subprocess.PIPE,stderr=subprocess.PIPE,pass_fds=(write_fd,),close_fds=True)
os.close(write_fd);write_fd=None
raw=read_ready(read_fd);os.close(read_fd);read_fd=None
validate_marker(raw,nonce=args.session_nonce,executor_pin=args.child_sha256,
envelope_pin=args.envelope_sha256,envelope=envelope)
forward_ready(raw)
remaining=absolute_end-time.monotonic();need(remaining>0,'wrapper_session_window_expired')
with deadline(remaining):
rc=proc.wait(timeout=remaining)
# Bounded captures: even accidental child output is never forwarded.
unexpected=proc.stdout.read(MAX_READY+1) or proc.stderr.read(MAX_READY+1)
need(not unexpected,'wrapper_child_output_refused')
return 0 if rc==0 else 1
except BaseException:
return 1
finally:
for fd in (read_fd,write_fd):
if fd is not None:
try:os.close(fd)
except OSError:pass
if proc is not None:
if proc.poll() is None:
# Unknown/partial native outcomes remain owner recovery, never retry.
try:
with deadline(5):
proc.terminate();proc.wait(timeout=5)
except Exception:
try:
with deadline(2):
proc.kill();proc.wait(timeout=2)
except Exception:pass
for stream in (proc.stdout,proc.stderr):
if stream is not None:
try:
with deadline(0.02):stream.close()
except Exception:pass
def main():
parser=argparse.ArgumentParser(description=__doc__)
for name in ('child','child-sha256','envelope','envelope-sha256','receipt','session-nonce'):
parser.add_argument('--'+name,required=True)
return run(parser.parse_args())
if __name__=='__main__':raise SystemExit(main())