Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
262 lines
20 KiB
Python
262 lines
20 KiB
Python
import base64
|
|
import hashlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape.py'
|
|
spec=importlib.util.spec_from_file_location('adapter',SOURCE)
|
|
a=importlib.util.module_from_spec(spec);spec.loader.exec_module(a)
|
|
|
|
class ArchiveTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
|
|
self.root=Path(self.tmp.name)
|
|
config={'data':{k:base64.b64encode(v).decode() for k,v in {'config.yaml':b'issuer: https://issuer.example','key.pem':b'fixture-key'}.items()}}
|
|
self.values={'resources/sso/secret/keycape-config.json':json.dumps(config).encode(),
|
|
'resources/sso/deployment/keycape.json':json.dumps({'spec':{'template':{'spec':{'containers':[{'name':'keycape','image':a.IMAGE,'env':[
|
|
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
|
|
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
|
|
{'name':'KEYCAPE_ORIGINAL_SECRET','valueFrom':{'secretKeyRef':{'name':'client','key':'secret'}}}]}]}}}}).encode(),
|
|
'resources/sso/secret/client.json':json.dumps({'data':{'secret':base64.b64encode(b'fixture-private').decode()}}).encode(),
|
|
'data/keycape/policy.json':b'{}','data/keycape/receipts/one.json':b'{"original":true}'}
|
|
for name,value in self.values.items():
|
|
p=self.root/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value);p.chmod(0o600)
|
|
self.manifest={'profile':{'components':[{'namespace':'sso','deployment':'keycape','secret_refs':['keycape-config','client'],'configmap_refs':[]}]},
|
|
'files':[{'path':k,'bytes':len(v),'sha256':hashlib.sha256(v).hexdigest()} for k,v in self.values.items()]}
|
|
def payload(self):
|
|
digest=hashlib.sha256(json.dumps(self.manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
return a.private_payload(self.root,self.manifest,digest)
|
|
def test_original_state_and_secret_delivery_retained(self):
|
|
p=self.payload();self.assertEqual(p['env']['KEYCAPE_ORIGINAL_SECRET'],'fixture-private')
|
|
self.assertEqual(base64.b64decode(p['files']['/var/lib/keycape-policy/receipts/one.json']),b'{"original":true}')
|
|
def test_archive_digest_refused(self):
|
|
with self.assertRaises(a.Refused):a.private_payload(self.root,self.manifest,'0'*64)
|
|
def test_changed_file_refused(self):
|
|
(self.root/'data/keycape/policy.json').write_text('{"changed":true}')
|
|
with self.assertRaises(a.Refused):self.payload()
|
|
def test_symlink_refused(self):
|
|
p=self.root/'data/keycape/policy.json';p.unlink();p.symlink_to('/etc/hostname')
|
|
with self.assertRaises(Exception):self.payload()
|
|
def test_duplicate_manifest_refused(self):
|
|
self.manifest['files'].append(self.manifest['files'][0])
|
|
with self.assertRaises(a.Refused):self.payload()
|
|
def test_image_mismatch_refused(self):
|
|
d=json.loads(self.values['resources/sso/deployment/keycape.json']);d['spec']['template']['spec']['containers'][0]['image']='keycape:latest'
|
|
raw=json.dumps(d).encode();p=self.root/'resources/sso/deployment/keycape.json';p.write_bytes(raw)
|
|
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):
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
self.key=rsa.generate_private_key(public_exponent=65537,key_size=2048).private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())
|
|
self.cfg=b'issuer: https://issuer.example\nclients:\n- clientId: existing\n clientType: confidential\n secretRef: env:KEYCAPE_ORIGINAL_SECRET\n grantTypes: [client_credentials]\n'
|
|
self.files={'/etc/keycape/config.yaml':self.cfg,'/etc/keycape/key.pem':self.key,'/var/lib/keycape-policy/policy.json':b'{}'}
|
|
self.payload={'env':{'KEYCAPE_ORIGINAL_SECRET':'fixture-private'},'files':{k:base64.b64encode(v).decode() for k,v in self.files.items()}}
|
|
self.admission={'issuer':'https://issuer.example','verifier_sha256':hashlib.sha256(b'binary').hexdigest(),
|
|
'trust_path':'/trust.pem','trust_sha256':hashlib.sha256(b'original-trust').hexdigest(),
|
|
'consumer':{'client_id':'existing','secret_env':'KEYCAPE_ORIGINAL_SECRET','scope':'read','audience':'consumer',
|
|
'subject':'service:existing','tenant':'tenant:platform','roles':'reader','deny_scope':'write'}}
|
|
self.session={'issuer':'isolated','volumes':{x:{'target':x} for x in ('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')}}
|
|
self.output=''.join('PASS '+x+'\n' for x in ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings','principal_type is service','sub matches the registration','tenant matches the registration','roles present','granted scope carries no excess grant','predecessor secret refused','excess scope refused: write'])
|
|
def file(self,container,path):
|
|
return {**self.files,'/etc/hosts':b'127.0.0.1 issuer.example\n','/keycape':b'binary','/trust.pem':b'original-trust'}[path]
|
|
def command(self,argv,**kwargs):
|
|
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem']).encode()
|
|
output=io.BytesIO()
|
|
with tarfile.open(fileobj=output,mode='w') as t:
|
|
m=tarfile.TarInfo('policy.json');m.size=2;t.addfile(m,io.BytesIO(b'{}'))
|
|
return output.getvalue()
|
|
def verify(self,code=0,output=None):
|
|
with patch.object(a,'check_jwks'),patch.object(a,'runtime_file',return_value=b'original-trust'),patch.object(a,'runtime_files',side_effect=lambda session,paths=(),tree=False:({k:v for k,v in self.files.items() if k.startswith('/var/lib/keycape-policy/')} if tree else dict(self.files))),patch.object(a,'container_file',side_effect=self.file),patch.object(a,'command',side_effect=self.command),patch.object(a.subprocess,'run',return_value=subprocess.CompletedProcess([],code,b'',(self.output if output is None else output).encode())) as run:
|
|
result=a.run_verifier(self.session,self.admission,self.payload)
|
|
argv=run.call_args.args[0]
|
|
self.assertNotIn('fixture-private',' '.join(argv))
|
|
self.assertIn('-previous-secret-env',argv)
|
|
return result
|
|
def test_original_registration_and_unrelated_secret(self):
|
|
r=self.verify();self.assertTrue(r['wrong_unrelated_secret_explicitly_refused']);self.assertNotIn('fixture-private',json.dumps(r))
|
|
def test_transport_failure_never_counts_as_wrong_secret(self):
|
|
with self.assertRaises(a.Refused):self.verify(1,'FAIL discovery and endpoint origin: TLS failure\n')
|
|
def test_missing_explicit_rejection_never_counts(self):
|
|
with self.assertRaises(a.Refused):self.verify(0,self.output.replace('PASS predecessor secret refused\n',''))
|
|
def test_fail_line_refused_even_zero_exit(self):
|
|
with self.assertRaises(a.Refused):self.verify(0,self.output+'FAIL transport\n')
|
|
def test_security_environment_override_refused(self):
|
|
original=self.command
|
|
def altered(argv,**kwargs):
|
|
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem','SSL_CERT_DIR=/unreviewed']).encode()
|
|
return original(argv,**kwargs)
|
|
with patch.object(self,'command',side_effect=altered):
|
|
with self.assertRaises(a.Refused):self.verify()
|
|
def test_http_issuer_refused(self):
|
|
self.admission['issuer']='http://issuer.example'
|
|
with self.assertRaises(a.Refused):self.verify()
|
|
def test_changed_trust_refused(self):
|
|
self.admission['trust_sha256']='0'*64
|
|
with self.assertRaises(a.Refused):self.verify()
|
|
def test_config_drift_refused(self):
|
|
self.payload['files']['/etc/keycape/config.yaml']=base64.b64encode(b'changed').decode()
|
|
with self.assertRaises(a.Refused):self.verify()
|
|
|
|
class PublicReadinessTransportTests(unittest.TestCase):
|
|
def test_provider_probe_failure_is_closed_false_data_not_failed_docker_exec(self):
|
|
output=io.StringIO()
|
|
with patch('sys.stdin',io.StringIO('{}')),patch('sys.stdout',output):
|
|
exec(compile(a.JWKS_TRANSPORT,'<public-readiness>','exec',optimize=2),{})
|
|
self.assertEqual(json.loads(output.getvalue()),{'original_public_signing_key_in_live_jwks':False})
|
|
|
|
class BoundaryTests(unittest.TestCase):
|
|
def test_remote_docker_refused_before_info(self):
|
|
session={'run_id':'a'*32,'containers':['nk49-kc-'+('a'*32)+'-worker'],'worker':'nk49-kc-'+('a'*32)+'-worker','issuer':'nk49-kc-'+('a'*32)+'-worker'}
|
|
with patch.object(a,'command',return_value=b'[{"Endpoints":{"docker":{"Host":"tcp://remote:2375"}}}]') as command:
|
|
with self.assertRaises(a.Refused):a.inspect_session(session)
|
|
self.assertEqual(command.call_count,1)
|
|
def test_failed_verifier_cleanup_is_attempted(self):
|
|
receipt={};admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
|
|
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',side_effect=a.Refused),patch.object(a,'cleanup') as cleanup:
|
|
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
|
|
cleanup.assert_called_once();self.assertTrue(receipt['container_cleanup_verified']);self.assertEqual(receipt['status'],'failed')
|
|
|
|
def test_cleanup_failure_overrides_success(self):
|
|
admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
|
|
result={x:True for x in ('native_service_registration_verified','wrong_unrelated_secret_explicitly_refused','denied_scopes_refused','original_config_registry_policy_receipts_equal','original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks')}
|
|
result['public_signing_spki_sha256']='e'*64;receipt={}
|
|
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',return_value=result),patch.object(a,'cleanup',side_effect=a.Refused):
|
|
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
|
|
self.assertEqual(receipt['status'],'failed');self.assertFalse(receipt['container_cleanup_verified'])
|
|
|
|
class ContainmentTests(unittest.TestCase):
|
|
def inspect(self,overrides=None,launch=None,mounts=None):
|
|
name='nk49-kc-'+('a'*32)+'-issuer'
|
|
session={'run_id':'a'*32,'containers':[name],'issuer':name,'worker':name,'images':{name:a.IMAGE}}
|
|
host={'NetworkMode':'none','ReadonlyRootfs':True,'Privileged':False,'CapDrop':['ALL'],'SecurityOpt':['no-new-privileges']}
|
|
host.update(overrides or {})
|
|
def command(argv,**kwargs):
|
|
if argv[1:3]==['context','inspect']:return b'[{"Endpoints":{"docker":{"Host":"unix:///var/run/docker.sock"}}}]'
|
|
if argv[1]=='info':return b'{"ID":"74f93783-6cef-43a8-a3c4-f62586cd3bc1","Name":"bnt-lap001","OSType":"linux"}'
|
|
if argv[-1]=='{{.Id}}':return b'anchor-id'
|
|
if '"path"' in argv[-1]:return json.dumps(launch or {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}).encode()
|
|
return json.dumps({'host':host,'mounts':mounts or [],'labels':{a.LABEL:'a'*32},'image':a.IMAGE,'user':'1000:1000'}).encode()
|
|
with patch.object(a,'command',side_effect=command),patch.dict('os.environ',{'DOCKER_HOST':'unix:///var/run/docker.sock'}):a.inspect_session(session)
|
|
def test_minimal_network_none_admitted(self):self.inspect()
|
|
def test_image_declared_anonymous_disk_volume_remains_refused(self):
|
|
with self.assertRaises(a.Refused):
|
|
self.inspect(mounts=[{'Type':'volume','Name':'anonymous-fixture','Destination':'/privacyidea/etc/persistent','RW':True}])
|
|
|
|
def test_alternate_same_image_command_refused(self):
|
|
with self.assertRaises(a.Refused):self.inspect(launch={'entrypoint':['/keycape'],'cmd':['check-config'],'path':'/keycape','args':['check-config']})
|
|
def test_cap_add_refused(self):
|
|
with self.assertRaises(a.Refused):self.inspect({'CapAdd':['NET_ADMIN']})
|
|
def test_devices_refused(self):
|
|
with self.assertRaises(a.Refused):self.inspect({'Devices':[{'PathOnHost':'/dev/mem'}]})
|
|
def test_device_requests_refused(self):
|
|
with self.assertRaises(a.Refused):self.inspect({'DeviceRequests':[{'Driver':'nvidia'}]})
|
|
def test_host_namespace_refused(self):
|
|
for field in ('PidMode','IpcMode','UTSMode','UsernsMode','CgroupnsMode'):
|
|
with self.subTest(field=field),self.assertRaises(a.Refused):self.inspect({field:'host'})
|
|
def test_security_override_refused(self):
|
|
with self.assertRaises(a.Refused):self.inspect({'SecurityOpt':['no-new-privileges','seccomp=unconfined']})
|
|
|
|
class CleanupTests(unittest.TestCase):
|
|
def test_partial_cleanup_attempts_every_owned_container(self):
|
|
session={'run_id':'a'*32,'containers':['first','second','third']};receipt={}
|
|
removed=[]
|
|
def command(argv,**kwargs):
|
|
if argv[1:3]==['container','inspect']:return ('a'*32).encode()
|
|
if argv[1]=='rm':
|
|
removed.append(argv[-1])
|
|
if argv[-1]=='third':raise a.Refused()
|
|
return b''
|
|
if argv[1:3]==['container','ls']:
|
|
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
|
|
return (name+'\n').encode() if name=='third' or name not in removed else b''
|
|
return b''
|
|
with patch.object(a,'command',side_effect=command):
|
|
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
|
|
self.assertEqual(removed,['third','second','first'])
|
|
self.assertEqual([r['status'] for r in receipt['container_cleanup_obligations']],['still_present','absence_verified','absence_verified'])
|
|
def test_ownership_mismatch_does_not_remove_and_continues(self):
|
|
session={'run_id':'a'*32,'containers':['first','second']};receipt={};removed=[]
|
|
def command(argv,**kwargs):
|
|
if argv[1:3]==['container','inspect']:return (('b' if argv[3]=='second' else 'a')*32).encode()
|
|
if argv[1]=='rm':removed.append(argv[-1])
|
|
if argv[1:3]==['container','ls']:
|
|
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
|
|
return (name+'\n').encode() if name not in removed else b''
|
|
return b''
|
|
with patch.object(a,'command',side_effect=command):
|
|
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
|
|
self.assertEqual(removed,['first'])
|
|
self.assertEqual(receipt['container_cleanup_obligations'][0]['status'],'ownership_mismatch_not_removed')
|
|
def test_inspect_failure_retains_uncertain_obligation(self):
|
|
receipt={}
|
|
with patch.object(a,'command',side_effect=a.Refused):
|
|
with self.assertRaises(a.Refused):a.cleanup({'run_id':'a'*32,'containers':['exact-owned-name']},receipt)
|
|
self.assertEqual(receipt['container_cleanup_obligations'],[{'container':'exact-owned-name','status':'unverified'}])
|
|
|
|
class JWKSTests(unittest.TestCase):
|
|
def execute(self,n='123',endpoint='https://issuer.example/jwks',resolved='127.0.0.1',extra=False,kid='key-1'):
|
|
request={'issuer':'https://issuer.example','trust':'public-certificate','n':'123','e':'65537'}
|
|
def enc(i):return base64.urlsafe_b64encode(int(i).to_bytes((int(i).bit_length()+7)//8,'big')).decode().rstrip('=')
|
|
responses=[{'issuer':'https://issuer.example','jwks_uri':endpoint},{'keys':[{'kty':'RSA','kid':kid,'use':'sig','alg':'RS256','n':enc(n),'e':enc(65537)}]}]
|
|
if extra:responses[1]['keys'].append(dict(responses[1]['keys'][0],kid='key-2'))
|
|
class Response:
|
|
def __init__(self,value):self.value=value
|
|
def __enter__(self):return self
|
|
def __exit__(self,*args):pass
|
|
def read(self,*args):return json.dumps(self.value).encode()
|
|
class Opener:
|
|
def open(self,*args,**kwargs):return Response(responses.pop(0))
|
|
with patch('sys.stdin',io.StringIO(json.dumps(request))),patch('sys.stdout',io.StringIO()),patch('socket.getaddrinfo',return_value=[(2,1,6,'',(resolved,443))]),patch('ssl.create_default_context'),patch('urllib.request.build_opener',return_value=Opener()):
|
|
exec(compile(a.JWKS_PROBE,'<jwks-probe>','exec',optimize=2),{})
|
|
def test_exact_original_public_key(self):self.execute()
|
|
def test_extra_signing_key_refused_under_optimization(self):
|
|
with self.assertRaises(ValueError):self.execute(extra=True)
|
|
def test_wrong_kid_refused_under_optimization(self):
|
|
with self.assertRaises(ValueError):self.execute(kid='replacement')
|
|
def test_wrong_live_signing_key_refused(self):
|
|
with self.assertRaises(ValueError):self.execute(n='124')
|
|
def test_cross_origin_jwks_refused(self):
|
|
with self.assertRaises(ValueError):self.execute(endpoint='https://production.example/jwks')
|
|
def test_production_dns_refused_before_fetch(self):
|
|
with self.assertRaises(ValueError):self.execute(resolved='192.0.2.1')
|
|
|
|
if __name__=='__main__':unittest.main()
|