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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue