Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6
132 lines
7 KiB
Python
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())
|