railiance-platform/scripts/railiance_clock_custody.py
codex e70ef2f32a
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Implement attended Railiance Clock host key delivery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6
2026-09-15 23:00:20 +02:00

132 lines
7 KiB
Python

#!/usr/bin/env python3
"""Attended CCR-2026-0028 initial custody and host delivery. Silent, resumable."""
from pathlib import Path
import argparse
import hashlib
import json
import os
import shlex
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[1]
CCR = ROOT / 'credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml'
KV = 'platform/data/workloads/railiance-clock/authority-signing'
KID = 'railiance01-clock-20260915-v1'
TARGET = '/var/lib/railiance-clock/signing.pem'
def validate_contract(ccr):
errors=[]
expected={'mount':'platform','kv_path':'platform/workloads/railiance-clock/authority-signing',
'fields':['PRIVATE_KEY_PEM','KEY_ID'],'auth':{'method':'attended-oidc','mount':'netkingdom','role':'platform-admin'},
'runtime_access':False,'initial_cas':0}
delivery={'surface':'attended-host-file','host':'92.205.62.239','ssh_user':'tegwick',
'target':TARGET,'owner':'railiance-clock','mode':'0600','directory_mode':'0700',
'procedure':'scripts/railiance_clock_custody.py'}
if ccr.get('id')!='CCR-2026-0028' or ccr.get('request_type')!='attended-host-key-delivery':
errors.append('unsupported attended host contract')
if ccr.get('openbao')!=expected or ccr.get('delivery')!=delivery:
errors.append('exact Clock custody and host binding required')
if ccr.get('access_frontdoor',{}).get('resolvable') is not False:
errors.append('runtime credential frontdoor must remain disabled')
if ccr.get('status') in {'approved','applied','verified','active'}:
comments=ccr.get('review',{}).get('comments',[])
for role in ['platform-operator','railiance-clock-owner']:
if not any(c.get('reviewer')==f'User ({role})' and c.get('decision')=='approved' for c in comments):
errors.append('explicit role approval missing: '+role)
for section,fields in {'verification':['positive','negative','activation_conditions'],
'lifecycle':['deactivate','rotate','compromised']}.items():
for field in fields:
if not ccr.get(section,{}).get(field):errors.append(section+'.'+field+' required')
return errors
class CustodyError(Exception): pass
def run_cmd(argv,payload=None):
r=subprocess.run(argv,input=payload,capture_output=True,timeout=90)
if r.returncode:raise CustodyError('contained_command_failed')
return r.stdout
def bao(args,payload=None):
return json.loads(run_cmd(['bao',*args],None if payload is None else json.dumps(payload).encode()))
REMOTE = '''import os,sys,stat,pwd,subprocess,json,hashlib
p='/var/lib/railiance-clock/signing.pem'
u=pwd.getpwnam('railiance-clock')
d=os.lstat(os.path.dirname(p))
assert stat.S_ISDIR(d.st_mode) and d.st_uid==u.pw_uid and stat.S_IMODE(d.st_mode)==0o700
key=sys.stdin.buffer.read(8193)
assert 0<len(key)<=8192
# Publish the complete key atomically; initial delivery never overwrites a key.
if not os.path.lexists(p):
tmp=p+'.admission-'+str(os.getpid())
fd=os.open(tmp,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
try:
os.fchown(fd,u.pw_uid,u.pw_gid)
with os.fdopen(fd,'wb') as f:
f.write(key);f.flush();os.fsync(f.fileno())
os.link(tmp,p)
finally:
os.unlink(tmp)
fd=os.open(p,os.O_RDONLY|os.O_NOFOLLOW)
with os.fdopen(fd,'rb') as f:
st=os.fstat(f.fileno())
assert stat.S_ISREG(st.st_mode) and st.st_uid==u.pw_uid and stat.S_IMODE(st.st_mode)==0o600
assert f.read(8193)==key
r=subprocess.run(['openssl','pkey','-pubout'],input=key,capture_output=True,check=True)
print(json.dumps({'public_key_sha256':hashlib.sha256(r.stdout).hexdigest(),'owner':'railiance-clock','mode':'0600'}))
'''
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--receipt',required=True)
p.add_argument('--resume-version',type=int)
a=p.parse_args()
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
receipt={'schema':'platform.railiance-clock-host-custody.v1','status':'failed','stage':'contract','ccr':'CCR-2026-0028'}
try:
import yaml
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
ccr=yaml.safe_load(CCR.read_text())
if validate_contract(ccr) or ccr['status'] not in {'approved','applied','verified','active'}:
raise CustodyError('approved_exact_contract_required')
receipt['stage']='attended_identity'
who=bao(['token','lookup','-format=json'])['data']
if 'platform-admin' not in who['policies'] or 'root' in who['policies'] or not who['path'].startswith('auth/netkingdom/'):
raise CustodyError('attended_platform_admin_required')
if not bao(['audit','list','-format=json']):raise CustodyError('audit_required')
receipt['stage']='custody'
if a.resume_version is None:
key=ec.generate_private_key(ec.SECP256R1())
pem=key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()).decode()
result=bao(['write','-format=json',KV,'-'],{'options':{'cas':0},'data':{'PRIVATE_KEY_PEM':pem,'KEY_ID':KID}})
version=result['data']['version']
else:
if a.resume_version!=1:raise CustodyError('only_initial_version_resume_supported')
version=a.resume_version
receipt['kv_version']=version
native=bao(['read','-format=json',KV])['data']
if native['metadata']['version']!=version or version!=1:raise CustodyError('custody_version_changed')
values=native['data']
if set(values)!={'PRIVATE_KEY_PEM','KEY_ID'} or values['KEY_ID']!=KID:raise CustodyError('custody_shape_mismatch')
pem=values['PRIVATE_KEY_PEM'].encode()
key=serialization.load_pem_private_key(pem,password=None)
if not isinstance(key,ec.EllipticCurvePrivateKey) or not isinstance(key.curve,ec.SECP256R1):raise CustodyError('es256_key_required')
public=key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo)
receipt.update(kid=KID,public_key_pem=public.decode(),public_key_sha256=hashlib.sha256(public).hexdigest())
receipt['stage']='host_delivery'
remote=json.loads(run_cmd(['ssh','-o','BatchMode=yes','-o','StrictHostKeyChecking=yes','-o','ConnectTimeout=10',
'tegwick@92.205.62.239','sudo -n python3 -c '+shlex.quote(REMOTE)],pem))
if remote['public_key_sha256']!=receipt['public_key_sha256']:raise CustodyError('host_public_key_mismatch')
receipt['host']=remote
receipt['status']='delivered_pending_authority_acceptance'
receipt['stage']='complete'
except Exception as e:
receipt['error']=str(e) if isinstance(e,CustodyError) else 'internal_error'
finally:
with os.fdopen(fd,'w') as f:json.dump(receipt,f,indent=2);f.write('\n')
return 0 if receipt['status']!='failed' else 1
if __name__=='__main__':sys.exit(main())