the-custodian/docs/changes/netkingdom-target-2026-10-17/native-identity-r1/selector-uniqueness.patch
codex a6eee04d42
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Reject generated-only commits / generated-commit-guard (push) Successful in 8s
Record native security preparation and owner checkpoints (2/7)
2026-10-17 07:51:36 +02:00

80 lines
4.6 KiB
Diff

--- a/tools/bootstrap-recovery/isolated-keycape.py
+++ b/tools/bootstrap-recovery/isolated-keycape.py
@@ -322,6 +322,23 @@
def run_verifier(session, admission, payload):
+ # Select from the already-restored config before further private/runtime reads.
+ # This is not public preflight before archive decryption or its key prompt.
+ import yaml
+ cfg = yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml'],validate=True))
+ if not isinstance(cfg,dict) or cfg.get('issuer') != admission['issuer']:
+ raise Refused()
+ consumer = admission['consumer']
+ clients = cfg.get('clients')
+ if not isinstance(clients,list) or any(not isinstance(client,dict) for client in clients):
+ raise Refused()
+ matches = [client for client in clients if client.get('clientId') == consumer['client_id']]
+ if len(matches) != 1:
+ raise Refused()
+ client = matches[0]
+ if (client.get('secretRef') != 'env:'+consumer['secret_env'] or client.get('clientType') != 'confidential'
+ or 'client_credentials' not in client.get('grantTypes',[])):
+ raise Refused()
issuer = session['issuer']
if {v['target'] for v in session.get('volumes',{}).values()}!={'/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls'}:raise Refused()
from urllib.parse import urlsplit
@@ -352,15 +369,7 @@
if durable!={k:v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}:
raise Refused()
equal()
- # Reading YAML is data parsing only; no archived executable is evaluated.
- import yaml
- cfg = yaml.safe_load(files['/etc/keycape/config.yaml'])
- if cfg['issuer'] != admission['issuer']:
- raise Refused()
- consumer = admission['consumer']
- client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id'])
- if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential'
- or 'client_credentials' not in client['grantTypes'] or not env.get(consumer['secret_env'])):
+ if not env.get(consumer['secret_env']):
raise Refused()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
--- a/tests/test_isolated_keycape_recovery.py
+++ b/tests/test_isolated_keycape_recovery.py
@@ -53,6 +53,34 @@
for row in self.manifest['files']:
if row['path'].endswith('/deployment/keycape.json'):row.update(bytes=len(raw),sha256=hashlib.sha256(raw).hexdigest())
with self.assertRaises(a.Refused):self.payload()
+
+
+class OriginalSelectorTests(unittest.TestCase):
+ """Already-restored public fixture config; no archive/key/native action."""
+ def assert_refused_before_further_inputs(self,clients):
+ import yaml
+ cfg={'issuer':'https://issuer.example','clients':clients}
+ payload={'files':{'/etc/keycape/config.yaml':base64.b64encode(yaml.safe_dump(cfg).encode()).decode(),
+ '/etc/keycape/key.pem':'deliberately-not-base64'}}
+ admission={'issuer':'https://issuer.example','consumer':{'client_id':'existing','secret_env':'KEYCAPE_ORIGINAL_SECRET'}}
+ # Empty session/no env and an undecodable key also expose early private access.
+ from contextlib import ExitStack
+ with ExitStack() as stack:
+ blocked=[stack.enter_context(patch.object(a,name,side_effect=AssertionError('later input or native call')))
+ for name in ('container_file','runtime_file','runtime_files','command','check_jwks','process')]
+ with self.assertRaises(a.Refused):a.run_verifier({},admission,payload)
+ for call in blocked:call.assert_not_called()
+ def client(self):
+ return {'clientId':'existing','clientType':'confidential','secretRef':'env:KEYCAPE_ORIGINAL_SECRET',
+ 'grantTypes':['client_credentials']}
+ def test_absent_original_selector_refused_before_further_inputs(self):
+ self.assert_refused_before_further_inputs([dict(self.client(),clientId='unrelated')])
+ def test_identical_duplicate_selector_refused_before_further_inputs(self):
+ self.assert_refused_before_further_inputs([self.client(),self.client()])
+ def test_conflicting_duplicate_selector_refused_before_further_inputs(self):
+ self.assert_refused_before_further_inputs([self.client(),dict(self.client(),secretRef='env:OTHER')])
+ def test_empty_registry_refused_before_further_inputs(self):
+ self.assert_refused_before_further_inputs([])
class NativeTests(unittest.TestCase):
def setUp(self):