Some checks failed
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
478 lines
26 KiB
Python
478 lines
26 KiB
Python
#!/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):
|
|
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()
|
|
# Reading YAML is data parsing only; no archived executable is evaluated.
|
|
import yaml
|
|
cfg = yaml.safe_load(files['/etc/keycape/config.yaml'])
|
|
if cfg['issuer'] != admission['issuer']:
|
|
raise Refused()
|
|
consumer = admission['consumer']
|
|
client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id'])
|
|
if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential'
|
|
or 'client_credentials' not in client['grantTypes'] or 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
|