feat(admission): founder-signed trust and recovery-attempt admission (WP14 T02, WP15 T05)
Reuse the gated-executor approval pattern: ssh-keygen -Y signatures over exact bytes, verified against a runtime-unwritable allowed_signers anchor with a fixed namespace per purpose. load_trust now refuses responder trust without a founder admission; the KeyCape selector preflight gains --admission for one exact, bounded recovery attempt. WP14 re-pointed to CUST-WP-0082-T06 and split founder key admission into human-gated T04; WP15 T04 now requires the signed admission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 809600@bnt-lap001 Assistant-Session: 40ba228f-c8a0-4ec9-bb29-3129951afdc4
This commit is contained in:
parent
497040c58c
commit
ebd8e49aff
15 changed files with 469 additions and 29 deletions
|
|
@ -3,9 +3,16 @@
|
|||
|
||||
Checks current consumer source constants and the producer's already-deployed
|
||||
public env/denial selector. This is source metadata, not runtime/owner assent.
|
||||
|
||||
With --admission, additionally requires a founder SSH signature (gated-executor
|
||||
pattern, namespace secrets-engine-recovery-attempt) over an attempt admission
|
||||
that binds these exact selector bytes, source hash, issuer, case and a bounded
|
||||
window. That is checkable founder authority for one attempt, still not native
|
||||
acceptance; single-use consumption stays with the bounded NK49 execution.
|
||||
"""
|
||||
import argparse
|
||||
import ast
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
|
@ -15,6 +22,8 @@ import stat
|
|||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
LIMIT=128*1024
|
||||
ADMISSION_FIELDS={'schema','selector_sha256','source_sha256','issuer','case','not_before','not_after'}
|
||||
MAX_WINDOW=86400
|
||||
|
||||
def read(path):
|
||||
path=Path(os.path.abspath(path))
|
||||
|
|
@ -51,13 +60,38 @@ def verify(selector,source,source_sha256):
|
|||
return {'schema':'secrets-engine.recovery-selector-preflight.v1','status':'source_selector_matches_not_native_assent',
|
||||
'source_sha256':source_sha256,'consumer':expected,'credential_source_exercised':False,'network_contacted':False}
|
||||
|
||||
def ts(value):
|
||||
if not isinstance(value,str) or not re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z',value):raise ValueError()
|
||||
return datetime.strptime(value,'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
|
||||
|
||||
def verify_attempt(admission,signature,*,selector_raw,issuer,source_sha256,now=None):
|
||||
"""Founder-signed exact attempt binding; the signature covers the admission bytes."""
|
||||
import sys;sys.path.insert(0,str(ROOT/'src'))
|
||||
from secrets_engine.founder_admission import verify_admission
|
||||
payload=json.loads(admission,object_pairs_hook=unique)
|
||||
if not isinstance(payload,dict) or set(payload)!=ADMISSION_FIELDS:raise ValueError()
|
||||
if payload['schema']!='secrets-engine.recovery-attempt-admission.v1':raise ValueError()
|
||||
if payload['selector_sha256']!='sha256:'+hashlib.sha256(selector_raw).hexdigest():raise ValueError()
|
||||
if payload['source_sha256']!=source_sha256 or payload['issuer']!=issuer:raise ValueError()
|
||||
if not isinstance(payload['case'],str) or not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,127}',payload['case']):raise ValueError()
|
||||
start,end=ts(payload['not_before']),ts(payload['not_after']);now=now or datetime.now(timezone.utc)
|
||||
if not start<=now<end or (end-start).total_seconds()>MAX_WINDOW:raise ValueError()
|
||||
signer=verify_admission(admission,signature,namespace='secrets-engine-recovery-attempt')
|
||||
return {'case':payload['case'],'not_after':payload['not_after'],**signer}
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--selector',required=True);p.add_argument('--source-sha256',required=True)
|
||||
p.add_argument('--admission',help='founder-signed attempt admission JSON; signature at <admission>.sig')
|
||||
args=p.parse_args()
|
||||
try:
|
||||
proposal=json.loads(read(args.selector),object_pairs_hook=unique)
|
||||
selector_raw=read(args.selector)
|
||||
proposal=json.loads(selector_raw,object_pairs_hook=unique)
|
||||
if not isinstance(proposal,dict) or not isinstance(proposal.get('consumer'),dict):raise ValueError()
|
||||
result=verify(proposal['consumer'],read(ROOT/'src/secrets_engine/service_auth.py'),args.source_sha256)
|
||||
if args.admission:
|
||||
result['attempt_admission']=verify_attempt(read(args.admission),read(args.admission+'.sig'),
|
||||
selector_raw=selector_raw,issuer=proposal.get('issuer'),source_sha256=args.source_sha256)
|
||||
result['status']='founder_admitted_attempt_not_native_acceptance'
|
||||
except Exception:print('{"status":"source_selector_refused","private_diagnostics_suppressed":true}');return 1
|
||||
print(json.dumps(result,indent=2));return 0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue