#!/usr/bin/env python3 """T18 native adapter for an owner-admitted, already restored isolated session. Called after identity-bundle.restore, never with an unvalidated directory. This module does not provision TLS, grant credentials, or claim downstream acceptance. The existing decrypt/tmpfs wrapper remains responsible for archive cleanup. """ import base64 import hashlib import importlib.util import json import io import os import secrets import tarfile import time import re import subprocess from pathlib import Path HERE = Path(__file__).resolve().parent IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010' LABEL = 'net-kingdom.recovery-run' class Refused(Exception): pass def load_bundle(): spec = importlib.util.spec_from_file_location('identity_bundle', HERE / 'identity-bundle.py') module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def manifest_binding_mode(admission,transfer): """Versioned private-manifest binding; legacy explicit digests stay valid.""" mode=admission.get('manifest_binding_mode','explicit-digest-v1') if mode not in ('explicit-digest-v1','ciphertext-derived-v1'):raise Refused() if (not isinstance(admission.get('ciphertext_sha256'),str) or not re.fullmatch('[a-f0-9]{64}',admission['ciphertext_sha256']) or admission['ciphertext_sha256']!=transfer.get('ciphertext_sha256') or transfer.get('independent_owner_retrieval_verified') is not True):raise Refused() if mode=='explicit-digest-v1': if not isinstance(admission.get('manifest_sha256'),str) or not re.fullmatch('[a-f0-9]{64}',admission['manifest_sha256']):raise Refused() elif 'manifest_sha256' in admission:raise Refused() return mode def private_manifest_admission(admission,manifest): # Only called after the fixed ciphertext bytes were hashed, authenticated # decryption succeeded and bundle.restore checked the complete archive. # Never return this enriched private object in a safe receipt/checkpoint. digest=hashlib.sha256(json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest() mode=admission.get('manifest_binding_mode','explicit-digest-v1') if mode=='explicit-digest-v1': if digest!=admission['manifest_sha256']:raise Refused() elif mode!='ciphertext-derived-v1' or 'manifest_sha256' in admission:raise Refused() return {**admission,'manifest_sha256':digest} def private_payload(root, manifest, expected_manifest_sha256): """Bind the complete validated archive before selecting KeyCape material. Private hashes never enter the public receipt. The digest is supplied by the existing private archive admission, not inferred from an untrusted manifest. """ encoded = json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode() if hashlib.sha256(encoded).hexdigest() != expected_manifest_sha256: raise Refused() entries = manifest['files'] if len(entries) != len({e['path'] for e in entries}): raise Refused() values = {} bundle = load_bundle() for entry in entries: if not bundle.allowed(entry['path'], manifest['profile']): raise Refused() path = root / entry['path'] bundle.private(path) value = path.read_bytes() if len(value) != entry['bytes'] or hashlib.sha256(value).hexdigest() != entry['sha256']: raise Refused() values[entry['path']] = value deployment = json.loads(values['resources/sso/deployment/keycape.json']) main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'keycape') if main.get('command') or main.get('args') or main.get('envFrom'): raise Refused() if main['image'] != IMAGE: # Mutable-tag/historical mismatches need a separate explicit owner admission. raise Refused() secret = json.loads(values['resources/sso/secret/keycape-config.json'])['data'] files = {'/etc/keycape/' + name: base64.b64decode(secret[name], validate=True) for name in ('config.yaml', 'key.pem')} for path, value in values.items(): if path.startswith('data/keycape/'): relative = path.removeprefix('data/keycape/') files['/var/lib/keycape-policy/' + relative] = value if '/var/lib/keycape-policy/policy.json' not in files: raise Refused() if 'resources/sso/secret/keycape-factor-read.json' in values: factor=json.loads(values['resources/sso/secret/keycape-factor-read.json'])['data'] files['/etc/keycape-factor/admin-token']=base64.b64decode(factor['admin-token'],validate=True) # Resolve original delivered environment, including all referenced secrets. env = {} for item in main.get('env', []): if not re.fullmatch(r'KEYCAPE_[A-Z0-9_]+',item['name']):raise Refused() if item['name'] in env: raise Refused() if 'value' in item: env[item['name']] = item['value'] else: ref = item['valueFrom']['secretKeyRef'] source = json.loads(values['resources/sso/secret/' + ref['name'] + '.json'])['data'] env[item['name']] = base64.b64decode(source[ref['key']], validate=True).decode() if env.get('KEYCAPE_CONFIG') != '/etc/keycape/config.yaml' or env.get('KEYCAPE_POLICY_PATH') != '/var/lib/keycape-policy/policy.json': raise Refused() return {'files': {p: base64.b64encode(v).decode() for p, v in files.items()}, 'env': env} ENDPOINT = 'unix:///var/run/docker.sock' EXECUTION = None def docker_environment(environment=None): # Even an admitted daemon exposed by another Unix socket is outside this # exact session binding. Conflicting ambient routing is refused, not ignored. for source in (os.environ, environment or {}): if source.get('DOCKER_HOST') not in (None,'',ENDPOINT):raise Refused() if source.get('DOCKER_CONTEXT') not in (None,'','default'):raise Refused() if any(source.get(k) for k in ('DOCKER_TLS_VERIFY','DOCKER_CERT_PATH')):raise Refused() env=dict(os.environ if environment is None else environment) for key in ('DOCKER_CONTEXT','DOCKER_TLS_VERIFY','DOCKER_CERT_PATH'):env.pop(key,None) env['DOCKER_HOST']=ENDPOINT return env def process(argv, **kwargs): marker=None if EXECUTION is not None and (kwargs.get('start_new_session') or kwargs.get('process_group') not in (None,-1)):raise Refused() if argv[0]=='docker': argv=['docker','--host',ENDPOINT,*argv[1:]] kwargs['env']=docker_environment(kwargs.get('env')) if EXECUTION is not None: if (os.getpid()!=EXECUTION['pid'] or os.getsid(0)!=EXECUTION['sid'] or os.getpgrp()!=EXECUTION['pgid'] or time.monotonic()>=EXECUTION['deadline']):raise Refused() marker=Path(EXECUTION['staging']+'-docker-inflight') fd=os.open(marker,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) os.close(fd) try: result=subprocess.run(argv,**kwargs) if marker is not None and result.returncode!=0: fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd) return result except BaseException: # Parent loss leaves .docker-inflight; caught timeout/transport failures # retain a sticky uncertainty marker even if later cleanup can run. if marker is not None: fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd) raise finally: if marker is not None: try:marker.unlink() except FileNotFoundError:pass def command(argv, **kwargs): maximum = kwargs.pop('max_output', 32768) result = process(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=kwargs.pop('timeout', 180), **kwargs) if result.returncode or len(result.stdout) > maximum: raise Refused() return result.stdout def inspect_session(session, worker_only=False): """No credential environment is read by Docker inspect or returned to caller.""" if not re.fullmatch(r'[a-f0-9]{32}', session['run_id']): raise Refused() names = session['containers'] if not names or len(names) > 12 or len(set(names)) != len(names): raise Refused() for name in names: if not re.fullmatch(r'nk49-kc-' + session['run_id'] + r'-[a-z0-9-]{1,24}', name): raise Refused() if session['worker'] not in names or session['issuer'] not in names: raise Refused() import os contexts=json.loads(command(['docker','context','inspect'])) endpoints=[contexts[0]['Endpoints']['docker']['Host'],os.environ.get('DOCKER_HOST') or ENDPOINT] if not all(x==ENDPOINT for x in endpoints):raise Refused() info = json.loads(command(['docker', 'info', '--format', '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'])) if info != {'ID': '74f93783-6cef-43a8-a3c4-f62586cd3bc1', 'Name': 'bnt-lap001', 'OSType': 'linux'}: raise Refused() # The worker is the anchor; all dependencies/TLS/issuer share its network-none namespace. anchor = command(['docker', 'container', 'inspect', session['worker'], '--format', '{{.Id}}']).decode().strip() for name,spec in session.get('volumes',{}).items(): volume=json.loads(command(['docker','volume','inspect',name])) expected={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'} if len(volume)!=1 or volume[0]['Driver']!='local' or volume[0].get('Scope')!='local' or volume[0].get('Options')!=expected or spec['options']!=expected or volume[0].get('Labels',{}).get(LABEL)!=session['run_id']:raise Refused() for name in names: obj = json.loads(command(['docker', 'container', 'inspect', name, '--format', '{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"labels":{{json .Config.Labels}},"image":{{json .Config.Image}},"user":{{json .Config.User}},"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}}}'])) host = obj['host'] mounts=obj['mounts'] destinations=[m['Destination'] for m in mounts] if len(destinations)!=len(set(destinations)):raise Refused() for volume,expected_spec in session.get('volumes',{}).items(): matches=[m for m in mounts if m['Destination']==expected_spec['target']] if len(matches)!=1 or matches[0]['Type']!='volume' or matches[0]['Name']!=volume or matches[0]['RW']!=(name==session['worker'] or expected_spec['target']=='/var/lib/keycape-policy'):raise Refused() if (obj['labels'].get(LABEL) != session['run_id'] or not obj['user'] or obj['user'].split(':')[0] in ('0', 'root') or not host['ReadonlyRootfs'] or host['Privileged'] or host.get('Binds') or host.get('PortBindings') or host.get('PidMode') or host.get('IpcMode') == 'host' or host.get('UTSMode') or host.get('UsernsMode') == 'host' or host.get('CgroupnsMode') == 'host' or host.get('Sysctls',{}) not in ({},{'net.ipv4.ip_unprivileged_port_start':'0'} if name==session['worker'] else {}) or host.get('CapAdd') or host.get('Devices') or host.get('DeviceRequests') or host.get('VolumesFrom') or host.get('CapDrop') != ['ALL'] or host.get('SecurityOpt') not in (['no-new-privileges'], ['no-new-privileges:true']) or any(m['Type']!='tmpfs' and not (m['Type']=='volume' and m['Name'] in session.get('volumes',{}) and m['Destination']==session['volumes'][m['Name']]['target']) for m in obj['mounts']) or host['NetworkMode'] != ('none' if name == session['worker'] else 'container:' + anchor) or obj['image'] != session['images'][name] or '@sha256:' not in obj['image']): raise Refused() if worker_only:return if session['images'][session['issuer']] != IMAGE: raise Refused() # Known admitted archive has no command/args override; image default /keycape # must be the actual issuer executable, not an alternate same-image command. launch=json.loads(command(['docker','container','inspect',session['issuer'],'--format', '{"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}},"path":{{json .Path}},"args":{{json .Args}}}'])) if launch != {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}: raise Refused() def container_file(container, path): # Docker streams one tar member into memory; never materialize private files. raw = command(['docker', 'cp', container + ':' + path, '-'], max_output=128*1024*1024) with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as archive: members = archive.getmembers() if len(members) != 1 or not members[0].isfile() or members[0].size > 96*1024*1024: raise Refused() return archive.extractfile(members[0]).read() JWKS_PROBE = r""" import base64,ipaddress,json,socket,ssl,sys,urllib.request from urllib.parse import urlsplit p=json.load(sys.stdin);issuer=p['issuer'];origin=urlsplit(issuer) def require(condition): if not condition:raise ValueError('public_jwks_binding_refused') require(origin.scheme=='https') resolved=socket.getaddrinfo(origin.hostname,origin.port or 443,type=socket.SOCK_STREAM) require(bool(resolved) and all(ipaddress.ip_address(x[4][0]).is_loopback for x in resolved)) class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self,*args,**kwargs):return None opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect(), urllib.request.HTTPSHandler(context=ssl.create_default_context(cadata=p['trust']))) def get(url): u=urlsplit(url) require((u.scheme,u.hostname,u.port or 443)==(origin.scheme,origin.hostname,origin.port or 443)) require(not u.username and not u.password and not u.fragment) with opener.open(url,timeout=10) as response: raw=response.read(1024*1024+1);require(len(raw)<=1024*1024) return json.loads(raw) d=get(issuer.rstrip('/')+'/.well-known/openid-configuration');require(d['issuer']==issuer) keys=get(d['jwks_uri'])['keys'] def integer(value):return int.from_bytes(base64.urlsafe_b64decode(value+'='*(-len(value)%4)),'big') # The exact admitted owner release constructs one key, kid key-1. Reject every # extra key, including irrelevant keys: a native successful exchange can then # only have used the original signer through the existing verifier. require(isinstance(keys,list) and len(keys)==1) k=keys[0] require(k.get('kty')=='RSA' and k.get('kid')=='key-1' and k.get('use')=='sig' and k.get('alg')=='RS256') require(integer(k['n'])==int(p['n']) and integer(k['e'])==int(p['e'])) print('{"original_public_signing_key_in_live_jwks":true}') """ # A failed public readiness probe is closed data, not a failing Docker RPC. # Transport/CLI nonzero remains uncertain; no deliberately failing exec is # retried as if daemon acceptance had been established. JWKS_TRANSPORT = ("import contextlib,io\ntry:\n" " with contextlib.redirect_stdout(io.StringIO()):\n" " exec(" + repr(JWKS_PROBE) + ")\n" "except Exception:\n print('{\"original_public_signing_key_in_live_jwks\":false}')\n" "else:\n print('{\"original_public_signing_key_in_live_jwks\":true}')\n") def check_jwks(session, admission, public_numbers): trust=runtime_file(session,admission['trust_path']).decode() request={'issuer':admission['issuer'],'trust':trust,'n':str(public_numbers.n),'e':str(public_numbers.e)} result=json.loads(command(['docker','exec','-i',session['worker'],'python3','-c',JWKS_TRANSPORT],input=json.dumps(request).encode())) if result != {'original_public_signing_key_in_live_jwks': True}:raise Refused() RUNTIME_FILES = r""" import base64,json,sys from pathlib import Path p=json.load(sys.stdin) roots=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/') result={} paths=[] if p.get('tree'): paths=list(Path('/var/lib/keycape-policy').rglob('*')) else:paths=[Path(x) for x in p['paths']] for f in paths: if f.is_dir():continue if not str(f).startswith(roots) or not f.is_file() or any(x.is_symlink() for x in (f,*f.parents)) or f.stat().st_size>8*1024*1024:raise ValueError() result[str(f)]=base64.b64encode(f.read_bytes()).decode() if len(result)>4096:raise ValueError() print(json.dumps(result)) """ def runtime_files(session, paths=(), tree=False): raw=command(['docker','exec','-i',session['worker'],'python3','-c',RUNTIME_FILES], input=json.dumps({'paths':list(paths),'tree':tree}).encode(),max_output=48*1024*1024) return {k:base64.b64decode(v,validate=True) for k,v in json.loads(raw).items()} def runtime_file(session,path): return runtime_files(session,[path])[path] def run_verifier(session, admission, payload): # Select from the already-restored config before further private/runtime reads. # This is not public preflight before archive decryption or its key prompt. import yaml cfg = yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml'],validate=True)) if not isinstance(cfg,dict) or cfg.get('issuer') != admission['issuer']: raise Refused() consumer = admission['consumer'] clients = cfg.get('clients') if not isinstance(clients,list) or any(not isinstance(client,dict) for client in clients): raise Refused() matches = [client for client in clients if client.get('clientId') == consumer['client_id']] if len(matches) != 1: raise Refused() client = matches[0] if (client.get('secretRef') != 'env:'+consumer['secret_env'] or client.get('clientType') != 'confidential' or 'client_credentials' not in client.get('grantTypes',[])): raise Refused() issuer = session['issuer'] if {v['target'] for v in session.get('volumes',{}).values()}!={'/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls'}:raise Refused() from urllib.parse import urlsplit import ipaddress address = urlsplit(admission['issuer']) if (address.scheme != 'https' or not address.hostname or address.username or address.password or address.query or address.fragment): raise Refused() if hashlib.sha256(container_file(issuer, '/keycape')).hexdigest() != admission['verifier_sha256']: raise Refused() if hashlib.sha256(runtime_file(session, admission['trust_path'])).hexdigest() != admission['trust_sha256']: raise Refused() # Capture private environment only in owner process memory, never receipt. environment = json.loads(command(['docker','container','inspect',issuer,'--format','{{json .Config.Env}}'])) env = dict(item.split('=',1) for item in environment) if len(env)!=len(environment):raise Refused() if set(env)-set(payload['env'])-{'PATH','HOME','HOSTNAME','SSL_CERT_FILE'}:raise Refused() if env.get('SSL_CERT_FILE') != admission['trust_path'] or any(k.lower().endswith('_proxy') and v for k,v in env.items()): raise Refused() if {k:v for k,v in env.items() if k.startswith('KEYCAPE_')}!={k:v for k,v in payload['env'].items() if k.startswith('KEYCAPE_')}:raise Refused() for name,value in payload['env'].items(): if env.get(name) != value: raise Refused() files = {k:base64.b64decode(v,validate=True) for k,v in payload['files'].items()} def equal(): if runtime_files(session,files)!=files:raise Refused() durable=runtime_files(session,tree=True) if durable!={k:v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}: raise Refused() equal() if not env.get(consumer['secret_env']): raise Refused() from cryptography.hazmat.primitives import serialization key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None) check_jwks(session,admission,key.public_key().public_numbers()) wrong = secrets.token_urlsafe(48) if wrong == env[consumer['secret_env']]:raise Refused() args = ['docker','exec','--env','NK49_UNRELATED_NEGATIVE_SECRET='+wrong,issuer,'/keycape','verify-client', '-issuer',admission['issuer'],'-previous-secret-env','NK49_UNRELATED_NEGATIVE_SECRET'] flags = {'client_id':'client-id','secret_env':'secret-env','audience':'audience','scope':'scope', 'subject':'expect-subject','tenant':'expect-tenant','roles':'expect-roles','deny_scope':'deny-scope'} for name,flag in flags.items():args+=['-'+flag,consumer[name]] # Native command reports on stderr. Do not return or print its raw response. result = process(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=150) raw = (result.stdout+result.stderr).decode() required = ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings', 'principal_type is service','sub matches the registration','tenant matches the registration','roles present', 'granted scope carries no excess grant','predecessor secret refused'] required += ['excess scope refused: '+scope for scope in consumer['deny_scope'].split()] if result.returncode or any('PASS '+name+'\n' not in raw for name in required) or 'FAIL ' in raw: raise Refused() equal() from cryptography.hazmat.primitives import serialization key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None) check_jwks(session,admission,key.public_key().public_numbers()) public=key.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo) return {'native_service_registration_verified':True,'wrong_unrelated_secret_explicitly_refused':True, 'denied_scopes_refused':True,'original_config_registry_policy_receipts_equal':True, 'public_signing_spki_sha256':hashlib.sha256(public).hexdigest(), 'original_https_issuer_and_trust_preserved':True,'original_public_signing_key_in_live_jwks':True} def cleanup(session, receipt=None): """Attempt every owned cleanup even when one obligation fails. Preserve exact safe container names and outcomes so the owner can recover an uncertain/partial cleanup. An inspect/daemon failure never means absence. """ obligations = [] failed = False for name in reversed(session['containers']): row = {'container': name, 'status': 'unverified'} obligations.append(row) try: present=command(['docker','container','ls','--all','--filter','name=^/'+name+'$','--format','{{.Names}}']).decode().splitlines() if not present: row['status']='absence_verified' continue label = command(['docker', 'container', 'inspect', name, '--format', '{{index .Config.Labels "' + LABEL + '"}}']).decode().strip() if label != session['run_id']: row['status'] = 'ownership_mismatch_not_removed' failed = True continue # A failed remove is followed by a successful explicit inventory; # interrupted remove may actually have completed. try: command(['docker', 'rm', '--force', '--volumes', name]) except Exception: pass remaining = command(['docker','container','ls','--all','--filter', 'name=^/' + name + '$','--format','{{.Names}}']).decode().splitlines() if remaining: row['status'] = 'still_present' failed = True else: row['status'] = 'absence_verified' except Exception: failed = True if receipt is not None: receipt['container_cleanup_obligations'] = obligations if failed: raise Refused() def native_checks(root, manifest, admission, receipt, cleanup_session=True): """Owner wrapper provides a reviewed session after existing restore pipeline. Admission must bind ciphertext, canonical private manifest digest, executable and trust bytes, exact image/target list, original issuer and selected consumer. None of these admission fields grant authority or carry a client secret. """ session = admission['session'] # Validate ownership/containment before permitting either exec or cleanup. inspect_session(session) receipt.update(status='failed', source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), ciphertext_sha256=admission['ciphertext_sha256'], production_contacted=False, human_mfa_proven=False, downstream_consumer_reconnect_proven=False, container_cleanup_verified=False) try: for field in ('ciphertext_sha256', 'manifest_sha256', 'verifier_sha256', 'trust_sha256'): if not re.fullmatch('[a-f0-9]{64}', admission[field]): raise Refused() for field in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope'): if not isinstance(admission['consumer'].get(field), str) or not admission['consumer'][field].strip(): raise Refused() payload = private_payload(root, manifest, admission['manifest_sha256']) result = run_verifier(session, admission, payload) required = {'native_service_registration_verified','wrong_unrelated_secret_explicitly_refused', 'denied_scopes_refused','original_config_registry_policy_receipts_equal', 'original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks'} if any(result.get(k) is not True for k in required) or not re.fullmatch('[a-f0-9]{64}', result.get('public_signing_spki_sha256','')): raise Refused() # Closed receipt allowlist: never copy arbitrary worker output. receipt.update({k: True for k in required}) receipt['public_signing_spki_sha256'] = result['public_signing_spki_sha256'] receipt['status'] = 'isolated_keycape_native_registration_checked' finally: try: if cleanup_session: cleanup(session, receipt) receipt['container_cleanup_verified'] = True except Exception: receipt['status'] = 'failed' raise