net-kingdom/tests/run_isolated_keycape_native_fixture.py

222 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Prepare/run the exact reviewed T18 candidate on disposable synthetic material.
Default --plan performs no Docker calls and generates no key/material. Execution
requires the reviewed fixture manifest hash and source closure. This runner calls
the candidate itself; hooks only pause or inject a post-response transport fault.
"""
import argparse
import base64
from datetime import datetime,timedelta,timezone
import hashlib
import importlib.util
import io
import json
import os
import re
from pathlib import Path
import signal
import subprocess
import sys
import tarfile
import tempfile
import time
import uuid
ROOT=Path(__file__).resolve().parents[1]
MANIFEST=ROOT/'tests/isolated-keycape-native-fixture-manifest.json'
ISSUER='https://nk49-fixture.invalid'
CASES=('positive','hard-parent-loss','deadline','nonzero-after-accept')
def load(name,path):
spec=importlib.util.spec_from_file_location(name,path)
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
s=load('session',ROOT/'tools/bootstrap-recovery/isolated-keycape-session.py')
def require(condition):
if not condition:raise RuntimeError('synthetic_fixture_refused')
def reviewed_manifest(expected):
raw=MANIFEST.read_bytes();require(hashlib.sha256(raw).hexdigest()==expected)
manifest=json.loads(raw)
for name,digest in manifest['source_sha256'].items():
require(isinstance(digest,str) and re.fullmatch('sha256:[a-f0-9]{64}',digest))
require('sha256:'+hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==digest)
return manifest
def private_write(path,value):
path.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(value)
def materials(root):
"""Reuse the existing full archive fixture factory, no real archive accepted."""
from cryptography import x509
from cryptography.hazmat.primitives import hashes,serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import yaml
existing=load('existing_bundle_fixture',ROOT/'tests/test_identity_bundle.py')
payload=existing.stage.__wrapped__(root)
config={'issuer':ISSUER,'port':8080,'privateKeyPem':'/etc/keycape/key.pem','environment':'dev','tokenLifetime':'5m',
'lldap':{'url':'ldap://127.0.0.1:1','bindDN':'cn=fixture','bindPW':'synthetic-only','baseDN':'dc=fixture'},
'authelia':{'baseURL':'http://127.0.0.1:1','clientId':'synthetic','clientSecret':'synthetic','redirectURI':ISSUER+'/authorize/callback'},
'privacyidea':{'baseURL':'http://127.0.0.1:1'},
'clients':[{'clientId':'nk49-fixture-consumer','clientType':'confidential','audience':'nk49-fixture',
'grantTypes':['client_credentials'],'allowedScopes':['fixture:read'],'secretRef':'env:KEYCAPE_FIXTURE_CLIENT_SECRET',
'serviceSubject':'service:nk49-fixture','tenant':'tenant:fixture','roles':['fixture-reader']}]+
[{'clientId':name,'displayName':name,'clientType':'public','grantTypes':['authorization_code'],
'redirectUris':['https://nk49-fixture.invalid/callback'],'allowedScopes':['openid'],'mfaRequired':True}
for name in ('vergabe-demo-company','user-engine-portal')]}
secret_path=payload/'resources/sso/secret/keycape-config.json';secret=json.loads(secret_path.read_text())
secret['data']['config.yaml']=base64.b64encode(yaml.safe_dump(config).encode()).decode();secret_path.write_text(json.dumps(secret))
client_path=payload/'resources/sso/secret/keycape-rapp-qonto-client.json';client=json.loads(client_path.read_text())
client['data']={'client-secret':base64.b64encode(b'synthetic-fixture-client-secret').decode()};client_path.write_text(json.dumps(client))
factor_path=payload/'resources/sso/secret/keycape-factor-read.json';factor=json.loads(factor_path.read_text())
factor['data']={'admin-token':base64.b64encode(b'synthetic-unused-factor-reader').decode()};factor_path.write_text(json.dumps(factor))
dep_path=payload/'resources/sso/deployment/keycape.json';dep=json.loads(dep_path.read_text())
dep['spec']={'template':{'spec':{'containers':[{'name':'keycape','image':s.adapter.IMAGE,'env':[
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
{'name':'KEYCAPE_FIXTURE_CLIENT_SECRET','valueFrom':{'secretKeyRef':{'name':'keycape-rapp-qonto-client','key':'client-secret'}}}]}]}}};dep_path.write_text(json.dumps(dep))
policy={'Revision':1,'Modes':{k:'mandatory' for k in ('vergabe-demo-company','user-engine-portal')},'History':[
{'reference':'synthetic-retained-receipt','actor':'service:fixture','client':'user-engine-portal','before':'mandatory','after':'mandatory','revision':1,'at':1}]}
# JSON tags in the owner store are lowercase; retain an existing lock inode
# representation as part of the archived tree, never synthesize it after import.
policy={k.lower():v for k,v in policy.items()};(payload/'data/keycape/policy.json').write_text(json.dumps(policy))
private_write(payload/'data/keycape/policy.json.lock',b'')
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'nk49-fixture.invalid')]);now=datetime.now(timezone.utc)
cert=(x509.CertificateBuilder().subject_name(subject).issuer_name(subject).public_key(key.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(now-timedelta(minutes=5)).not_valid_after(now+timedelta(days=1))
.add_extension(x509.SubjectAlternativeName([x509.DNSName('nk49-fixture.invalid')]),critical=False)
.add_extension(x509.BasicConstraints(ca=True,path_length=None),critical=True).sign(key,hashes.SHA256()))
tls={'/recovery-tls/server.pem':cert.public_bytes(serialization.Encoding.PEM),
'/recovery-tls/server-key.pem':key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()),
'/recovery-tls/trust.pem':cert.public_bytes(serialization.Encoding.PEM)}
for path,value in tls.items():private_write(root/Path(path).name,value)
age=root/'synthetic-age.key';s.run(['age-keygen','-o',str(age)])
recipient=s.run(['age-keygen','-y',str(age)]).decode().strip();profile=s.bundle.profile();profile['public_recipient']=recipient
s.bundle.profile=lambda:profile
artifact=root/'net-kingdom-identity-20261016T063159Z-0123456789ab.tar.age';s.bundle.seal(payload,artifact,{'synthetic_native_keycape_fixture':True})
raw=s.run(['age','-d','-i',str(age),str(artifact)],max_output=s.bundle.LIMIT)
with tarfile.open(fileobj=io.BytesIO(raw)) as archive:manifest=json.load(archive.extractfile('manifest.json'))
transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified','provider':'Storage Share',
'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':artifact.name,'ciphertext_bytes':artifact.stat().st_size,
'ciphertext_sha256':hashlib.sha256(artifact.read_bytes()).hexdigest(),'etag':'"synthetic"','fetched_path':str(artifact),
'independent_owner_retrieval_verified':True}
# Derive public executable identity only from the exact cached admitted image.
probe=s.plan_session();name=probe['public_image_probe']
try:
s.run(['docker','create','--pull=never','--name',name,'--network','none','--read-only','--label',s.adapter.LABEL+'='+probe['run_id'],s.adapter.IMAGE])
binary=s.adapter.container_file(name,'/keycape')
finally:s.adapter.cleanup({**probe,'containers':[name]}, {})
admission={'issuer':ISSUER,'ciphertext_sha256':transfer['ciphertext_sha256'],
'manifest_sha256':hashlib.sha256(json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest(),
'verifier_sha256':hashlib.sha256(binary).hexdigest(),'trust_sha256':hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest(),
'tls_cert_sha256':hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest(),
'consumer':{'client_id':'nk49-fixture-consumer','secret_env':'KEYCAPE_FIXTURE_CLIENT_SECRET','audience':'nk49-fixture','scope':'fixture:read',
'subject':'service:nk49-fixture','tenant':'tenant:fixture','roles':'fixture-reader','deny_scope':'fixture:write'}}
private_write(root/'fixture.json',json.dumps({'schema':'nk49.generated-keycape-fixture.v1','transfer':transfer,'admission':admission,'recipient':recipient}).encode())
def worker(root,case):
execution=s.isolate_execution();execution['deadline']=time.monotonic()+(20 if case=='deadline' else 120)
data=json.loads((root/'fixture.json').read_text());require(data['schema']=='nk49.generated-keycape-fixture.v1')
profile=s.bundle.profile();profile['public_recipient']=data['recipient'];s.bundle.profile=lambda:profile
s.local.verify.custody.private_key_fd=lambda:os.open(root/'synthetic-age.key',os.O_RDONLY|os.O_NOFOLLOW)
tls={'/recovery-tls/'+name:(root/name).read_bytes() for name in ('server.pem','server-key.pem','trust.pem')}
receipt={};output=root/(case+'.json');checkpoint=lambda r:output.write_text(json.dumps(r))
provision=s.provision
def paused(*args):
provision(*args)
if case in ('hard-parent-loss','deadline'):
private_write(root/(case+'.barrier'),b'private-import-and-default-launch-complete')
time.sleep(150)
s.provision=paused
if case=='nonzero-after-accept':
original=s.adapter.subprocess.run;injected=False
def fault(argv,**kwargs):
nonlocal injected
result=original(argv,**kwargs)
if argv[:3]==['docker','--host',s.adapter.ENDPOINT] and 'run' in argv and not injected and result.returncode==0:
injected=True
return subprocess.CompletedProcess(argv,1,b'',b'')
return result
s.adapter.subprocess.run=fault
try:s.execute(data['transfer'],data['admission'],tls,receipt,execution,checkpoint=checkpoint)
except BaseException:receipt['status']='failed'
finally:output.write_text(json.dumps(receipt))
return int(receipt.get('status')!='isolated_keycape_native_registration_checked')
def run_cases(root,summary,cases):
for case in cases:
child=subprocess.Popen([sys.executable,str(Path(__file__).resolve()),'--worker-root',str(root),'--worker-case',case,
'--execute-reviewed-fixture','--expected-manifest-sha256',summary['manifest_sha256']],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
try:
end=time.monotonic()+150
if case=='hard-parent-loss':
while not (root/(case+'.barrier')).exists() and child.poll() is None and time.monotonic()<end:time.sleep(.05)
require((root/(case+'.barrier')).exists())
execution=json.loads((root/(case+'.json')).read_text())['execution_binding'];require(execution['pid']==child.pid)
fd=os.pidfd_open(child.pid)
try:signal.pidfd_send_signal(fd,signal.SIGKILL)
finally:os.close(fd)
child.wait(timeout=max(1,end-time.monotonic()))
receipt=json.loads((root/(case+'.json')).read_text())
summary['receipts'][case]=receipt
if case=='deadline':require((root/(case+'.barrier')).exists())
if case=='positive':
require(receipt.get('status')=='isolated_keycape_native_registration_checked' and receipt.get('container_cleanup_verified') and receipt.get('temporary_plaintext_cleanup_verified'))
require(receipt.get('native_service_registration_verified') and receipt.get('original_public_signing_key_in_live_jwks'))
summary['actual_issuer_original_hostname_tls_verified']=True
else:
require(receipt.get('status')!='isolated_keycape_native_registration_checked')
outcome=Path(receipt['reaper_outcome_path'])
while not outcome.exists() and time.monotonic()<end:time.sleep(.05)
require(outcome.exists());cleanup=json.loads(outcome.read_text())
require(cleanup.get('execution_quiescence_verified') and cleanup.get('temporary_plaintext_cleanup_verified'))
if case=='nonzero-after-accept':require(cleanup.get('status')=='bounded_cleanup_uncertain' and cleanup.get('in_flight_daemon_request_uncertain'))
else:require(cleanup.get('status')=='bounded_cleanup_verified')
summary['cases'][case]='passed'
finally:
if child.poll() is None:
fd=os.pidfd_open(child.pid)
try:signal.pidfd_send_signal(fd,signal.SIGKILL)
finally:os.close(fd)
child.wait(timeout=5)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--plan',action='store_true')
p.add_argument('--execute-reviewed-fixture',action='store_true');p.add_argument('--expected-manifest-sha256')
p.add_argument('--worker-root');p.add_argument('--worker-case',choices=CASES);p.add_argument('--case',choices=(*CASES,'all'),default='positive');args=p.parse_args()
if not args.execute_reviewed_fixture:
print(MANIFEST.read_text());return 0
require(not args.plan);manifest=reviewed_manifest(args.expected_manifest_sha256);os.umask(0o077)
if args.worker_root:
root=Path(args.worker_root);require(root.parent==Path('/dev/shm') and root.name.startswith('nk49-keycape-fixture-'))
s.bundle.private(root,True);return worker(root,args.worker_case)
require(not args.worker_case)
s.local.preflight();s.run(['docker','image','inspect',s.adapter.IMAGE,'--format','{{.Id}}'])
safe=Path(tempfile.mkdtemp(prefix='nk49-keycape-native-fixture-review-',dir='/tmp'))
summary={'schema':'nk49.synthetic-keycape-native-fixture.v1','status':'failed','manifest_sha256':args.expected_manifest_sha256,
'cases':{},'receipts':{},'actual_credentials_used':False,'actual_recovery_proven':False,'docker_executed':True}
print('Safe synthetic fixture result: '+str(safe/'result.json'),flush=True)
try:
with tempfile.TemporaryDirectory(prefix='nk49-keycape-fixture-',dir='/dev/shm') as temporary:
root=Path(temporary);materials(root);run_cases(root,summary,CASES if args.case=='all' else (args.case,))
summary['status']='synthetic_native_fixture_passed'
finally:(safe/'result.json').write_text(json.dumps(summary,indent=2)+'\n')
print('Safe synthetic fixture result: '+str(safe/'result.json'))
return int(summary['status']!='synthetic_native_fixture_passed')
if __name__=='__main__':raise SystemExit(main())