railiance-platform/scripts/prove-sitting-requester-exchange.py

174 lines
7.1 KiB
Python
Raw Normal View History

"""Silent create-only token-exchange proof; no sitting POST."""
import base64
import json
import os
import stat
from datetime import datetime, timezone
from pathlib import Path
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import ProxyHandler, Request, build_opener, HTTPRedirectHandler
import jwt
ROOT = Path('/home/worsch/railiance-platform')
RECEIPT = ROOT / 'docs/evidence/2026-09-15-sitting-requester-exchange.json'
POLICY = 'workload-kv-read-informed-decision-sitting-requester-client'
KV = 'platform/data/workloads/informed-decision/sitting-requester'
SIBLING = 'platform/data/workloads/secrets-engine/approval-requester'
PARENT = 'platform/metadata/workloads/informed-decision'
ISSUER = 'https://kc.coulomb.social'
CLIENT_ID = 'informed-decision-sitting-requester'
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
class Refused(Exception):
pass
def http(url, *, body=None, headers=None):
req = Request(url, data=body, headers=headers or {})
try:
with build_opener(ProxyHandler({}), NoRedirect()).open(req, timeout=20) as response:
content = response.read(1048577)
status = response.status
except HTTPError as error:
status = error.code
content = error.read(1048577)
error.close()
if len(content) > 1048576:
raise Refused('response_too_large')
if not content:
return status, {}
try:
return status, json.loads(content)
except json.JSONDecodeError:
raise Refused('non_json_http_response')
def bao(*args):
import subprocess
p = subprocess.run(['bao', *args], capture_output=True, text=True, timeout=20)
if p.returncode:
raise Refused('metadata_failed')
return json.loads(p.stdout)
def capabilities(result):
if isinstance(result, dict):
result = result.get('data', result).get('capabilities', result)
return result if isinstance(result, list) else []
def is_deny(result):
caps = set(capabilities(result))
return caps <= {'deny'} or not caps.intersection({'read', 'create', 'update', 'delete', 'list', 'patch', 'sudo'})
def check_identity(data):
policies = set(data.get('policies', [])) | set(data.get('identity_policies', []))
if POLICY not in policies or policies - {POLICY, 'default'}:
raise Refused('reader_identity_failed')
if not data.get('entity_id') or not 0 < int(data.get('ttl') or 0) <= 900:
raise Refused('reader_identity_failed')
def secret_from_read(payload):
body = payload.get('data', payload)
inner = body.get('data', body)
version = (body.get('metadata') or {}).get('version')
secret = inner.get('CLIENT_SECRET') if isinstance(inner, dict) else None
if version != 1 or not secret:
raise Refused('requester_delivery_failed')
return secret
def main(receipt):
if Path.home().parent.name != '.warden-attended-login' or os.getenv('BAO_TOKEN') or os.getenv('VAULT_TOKEN'):
raise Refused('attended_reader_required')
receipt['phase'] = 'envelope_ok'
check_identity(bao('token', 'lookup', '-format=json')['data'])
receipt['phase'] = 'identity_ok'
if capabilities(bao('token', 'capabilities', '-format=json', KV)) != ['read']:
raise Refused('reader_scope_failed:kv')
if not is_deny(bao('token', 'capabilities', '-format=json', SIBLING)):
raise Refused('reader_scope_failed:sibling')
if not is_deny(bao('token', 'capabilities', '-format=json', PARENT)):
raise Refused('reader_scope_failed:parent')
receipt['phase'] = 'scope_ok'
helper = Path.home() / '.vault-token'
info = helper.lstat()
if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o600 or info.st_uid != os.getuid():
raise Refused('private_helper_required')
secret = secret_from_read(bao('read', '-format=json', KV))
receipt['phase'] = 'secret_present'
receipt['secret_present'] = True
def exchange(scope, credential=secret):
auth = base64.b64encode((CLIENT_ID + ':' + credential).encode()).decode()
return http(ISSUER + '/token', body=urlencode({'grant_type': 'client_credentials', 'scope': scope}).encode(),
headers={'Authorization': 'Basic ' + auth, 'Content-Type': 'application/x-www-form-urlencoded'})
status, tokens = exchange('approval:create')
if status != 200 or 'access_token' not in tokens:
raise Refused('requester_exchange_failed')
receipt['phase'] = 'exchange_ok'
token = tokens['access_token']
status, jwks = http(ISSUER + '/jwks')
if status != 200:
raise Refused('jwks_failed')
header = jwt.get_unverified_header(token)
keys = [key for key in jwks['keys'] if key['kid'] == header.get('kid')]
if header.get('alg') != 'RS256' or len(keys) != 1:
raise Refused('signing_key_failed')
claims = jwt.decode(token, jwt.PyJWK.from_dict(keys[0]).key, algorithms=['RS256'], issuer=ISSUER,
audience='approval-engine', options={'strict_aud': True, 'require': ['sub', 'iat', 'exp', 'iss', 'aud']})
roles = claims.get('roles')
if isinstance(roles, str):
roles = [roles]
if claims.get('sub') != 'informed-decision':
raise Refused('requester_claims_failed:sub')
if claims.get('tenant') != 'tenant:platform':
raise Refused('requester_claims_failed:tenant')
if claims.get('principal_type') != 'service':
raise Refused('requester_claims_failed:principal_type')
if claims.get('scope') != 'approval:create':
raise Refused('requester_claims_failed:scope')
if roles != ['informed-decision-sitting-requester']:
raise Refused('requester_claims_failed:roles')
if claims.get('groups') not in (None, []):
raise Refused('requester_claims_failed:groups')
if int(claims['exp']) - int(claims['iat']) != 900:
raise Refused('requester_claims_failed:ttl')
receipt['phase'] = 'claims_ok'
for scope in ('approval:approve', 'approval:consume', 'approval:read'):
if exchange(scope)[0] != 400:
raise Refused('excess_scope_not_refused')
if exchange('approval:create', 'invalid-synthetic-credential')[0] != 401:
raise Refused('wrong_secret_not_refused')
receipt.update(status='verified', phase='requester_verified', signature_verified=True,
excess_scopes_refused=True, wrong_secret_refused=True, reader_scope_verified=True,
sitting_post=False)
if __name__ == '__main__':
if RECEIPT.exists():
try:
existing = json.loads(RECEIPT.read_text())
except Exception:
raise SystemExit(1)
if existing.get('status') == 'verified':
raise SystemExit(1)
receipt = {'observed_at': datetime.now(timezone.utc).isoformat(),
'status': 'failed', 'phase': 'preflight', 'credential_values_emitted': False, 'sitting_post': False}
try:
main(receipt)
except Exception as error:
receipt['failure'] = str(error) if isinstance(error, Refused) else 'contained_operation_failed'
raise SystemExit(1) from None
finally:
RECEIPT.write_text(json.dumps(receipt, indent=2) + '\n')