feat(admission): founder-signed trust and recovery-attempt admission (WP14 T02, WP15 T05)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

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:
tegwick 2026-10-21 00:30:59 +02:00
parent 497040c58c
commit ebd8e49aff
15 changed files with 469 additions and 29 deletions

View file

@ -29,3 +29,39 @@ def test_oversize_public_input_refused(tmp_path):
def test_symlink_public_input_refused(tmp_path):
p=tmp_path/'selector.json';p.write_text('{}');link=tmp_path/'link';link.symlink_to(p)
with pytest.raises(ValueError):m.read(link)
# Founder attempt admission (SECRETS-WP-0015-T05), gated-executor signature pattern.
from datetime import datetime, timezone
from secrets_engine.errors import DecisionError
from tests.signature_support import admit, FOUNDER
NOW=datetime(2026,10,21,12,0,0,tzinfo=timezone.utc)
ISSUER='https://kc.coulomb.social'
SELECTOR_RAW=json.dumps({'issuer':ISSUER,'consumer':SELECTOR}).encode()
ADMISSION={'schema':'secrets-engine.recovery-attempt-admission.v1','selector_sha256':'sha256:'+hashlib.sha256(SELECTOR_RAW).hexdigest(),
'source_sha256':DIGEST,'issuer':ISSUER,'case':'secrets15-nk49-readiness',
'not_before':'2026-10-21T11:00:00Z','not_after':'2026-10-21T13:00:00Z'}
def signed(tmp_path,payload=ADMISSION,namespace='secrets-engine-recovery-attempt'):
p=tmp_path/'attempt.json';p.write_text(json.dumps(payload));admit(p,namespace=namespace)
return p.read_bytes(),(tmp_path/'attempt.json.sig').read_bytes()
def attempt(data,sig,selector_raw=SELECTOR_RAW,now=NOW):
return m.verify_attempt(data,sig,selector_raw=selector_raw,issuer=ISSUER,source_sha256=DIGEST,now=now)
def test_founder_admitted_attempt_binds_exact_inputs(tmp_path):
result=attempt(*signed(tmp_path))
assert result['principal']==FOUNDER and result['case']=='secrets15-nk49-readiness'
assert result['namespace']=='secrets-engine-recovery-attempt'
@pytest.mark.parametrize('field,value',[('selector_sha256','sha256:'+'0'*64),('source_sha256','sha256:'+'0'*64),
('issuer','https://other'),('case','Bad Case'),('schema','other'),('not_after','2026-10-22T12:00:01Z'),
('not_before','2026-10-21T12:00:01Z'),('not_after','2026-10-21T12:00:00Z')])
def test_attempt_binding_mismatch_refused(tmp_path,field,value):
with pytest.raises((ValueError,DecisionError)):attempt(*signed(tmp_path,{**ADMISSION,field:value}))
def test_attempt_signature_negatives_refused(tmp_path):
data,sig=signed(tmp_path)
for bad in [(data.replace(b'secrets15',b'secrets16'),sig),signed(tmp_path,namespace='secrets-engine-pdp-trust'),
signed(tmp_path,namespace='gexec-approval'),(data,b'')]:
with pytest.raises((ValueError,DecisionError)):attempt(*bad)
def test_attempt_refuses_changed_selector(tmp_path):
with pytest.raises((ValueError,DecisionError)):attempt(*signed(tmp_path),selector_raw=SELECTOR_RAW+b' ')
def test_cli_admission_refusal_is_suppressed(tmp_path,capsys,monkeypatch):
sel=tmp_path/'selector.json';sel.write_bytes(SELECTOR_RAW);(tmp_path/'a.json').write_text('{}')
monkeypatch.setattr('sys.argv',['x','--selector',str(sel),'--source-sha256',DIGEST,'--admission',str(tmp_path/'a.json')])
assert m.main()==1 and 'refused' in capsys.readouterr().out