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
68 lines
2.7 KiB
Python
68 lines
2.7 KiB
Python
"""Founder admission of the responder trust file (SECRETS-WP-0014-T02)."""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from secrets_engine import founder_admission
|
|
from secrets_engine.decision_signature import load_trust
|
|
from secrets_engine.errors import DecisionError
|
|
from tests.signature_support import PUBLIC, FOUNDER, admit
|
|
|
|
TRUST = {'schema':'secrets-engine.pdp-trust.v1','evaluator':'stub/local','mode':'standalone',
|
|
'policy_package':'p','policy_version':'v','policy_package_digest':'sha256:'+'1'*64,
|
|
'keys':{'testdata-ed25519':PUBLIC}}
|
|
|
|
|
|
@pytest.fixture
|
|
def trust(tmp_path):
|
|
path=tmp_path/'pdp-trust.json';path.write_text(json.dumps(TRUST));return path
|
|
|
|
|
|
def test_admitted_trust_carries_founder_identity_only(trust):
|
|
loaded=load_trust(admit(trust))
|
|
assert dict(loaded)==TRUST
|
|
assert loaded.admission['principal']==FOUNDER
|
|
assert loaded.admission['key_fingerprint'].startswith('SHA256:')
|
|
assert loaded.admission['namespace']=='secrets-engine-pdp-trust'
|
|
|
|
|
|
@pytest.mark.parametrize('change',['unsigned','edited','other-namespace','gexec-namespace','garbage-signature'])
|
|
def test_unadmitted_trust_refuses(trust,change):
|
|
if change=='unsigned':pass
|
|
elif change=='edited':
|
|
admit(trust);trust.write_text(json.dumps({**TRUST,'keys':{'attacker':PUBLIC}}))
|
|
elif change=='other-namespace':admit(trust,namespace='secrets-engine-recovery-attempt')
|
|
elif change=='gexec-namespace':admit(trust,namespace='gexec-approval')
|
|
else:(trust.parent/(trust.name+'.sig')).write_text('-----BEGIN SSH SIGNATURE-----\nAAAA\n')
|
|
with pytest.raises(DecisionError):load_trust(trust)
|
|
|
|
|
|
def test_key_outside_anchor_refuses(trust,tmp_path):
|
|
key=tmp_path/'intruder'
|
|
subprocess.run(['ssh-keygen','-q','-t','ed25519','-N','','-f',str(key)],check=True,timeout=30)
|
|
with pytest.raises(DecisionError):load_trust(admit(trust,key=key))
|
|
|
|
|
|
def test_runtime_writable_anchor_refuses(trust,monkeypatch,founder_anchor):
|
|
admit(trust)
|
|
# The anchor is owned by the test's own uid, so the production rule must refuse.
|
|
monkeypatch.setattr(founder_admission,'_writable_by_runtime',founder_anchor['real_rule'])
|
|
assert os.stat(founder_anchor['anchor']).st_uid==os.geteuid()!=0
|
|
with pytest.raises(DecisionError):load_trust(trust)
|
|
|
|
|
|
def test_group_writable_anchor_refuses(trust,founder_anchor):
|
|
admit(trust);signers=founder_anchor['anchor']/'allowed_signers'
|
|
signers.chmod(0o664)
|
|
try:
|
|
with pytest.raises(DecisionError):load_trust(trust)
|
|
finally:signers.chmod(0o644)
|
|
|
|
|
|
def test_relocated_anchor_without_signers_refuses(trust,tmp_path,monkeypatch):
|
|
admit(trust);empty=tmp_path/'empty';empty.mkdir()
|
|
monkeypatch.setenv('SECRETS_ENGINE_ADMISSION_ANCHOR',str(empty))
|
|
with pytest.raises(DecisionError):load_trust(trust)
|
|
|