Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 4515@bnt-lap001 Assistant-Session: 0bf96127-6e04-41be-bfda-d11717c3f354
179 lines
8.4 KiB
Python
179 lines
8.4 KiB
Python
"""Break-glass bundle sealing and Storage Share publication on contained fixtures."""
|
|
from datetime import datetime,timezone
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tarfile
|
|
import urllib.error
|
|
import pytest
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
spec=importlib.util.spec_from_file_location('kingpath_offsite',ROOT/'tools/bootstrap-recovery/kingpath-offsite.py')
|
|
k=importlib.util.module_from_spec(spec);spec.loader.exec_module(k)
|
|
NOW=datetime(2026,10,27,22,0,tzinfo=timezone.utc)
|
|
|
|
|
|
class Response(io.BytesIO):
|
|
def __init__(self,url,data=b'',status=200,etag='"v1"'):
|
|
super().__init__(data);self.url=url;self.status=status
|
|
self.headers={'ETag':etag,'Content-Length':str(len(data))}
|
|
def __enter__(self):return self
|
|
def __exit__(self,*a):self.close()
|
|
|
|
|
|
class Share:
|
|
def __init__(self,folder_exists=False):
|
|
self.objects={};self.folder=folder_exists;self.calls=[];self.tamper=False;self.collide=False
|
|
def open(self,request,timeout=None):
|
|
method=request.get_method();url=request.full_url;self.calls.append((method,url))
|
|
assert request.headers['Authorization'].startswith('Basic ')
|
|
if method=='MKCOL':
|
|
if self.folder:raise urllib.error.HTTPError(url,405,'exists',{},None)
|
|
self.folder=True;return Response(url,status=201)
|
|
if method=='PUT':
|
|
assert request.headers['If-none-match']=='*'
|
|
self.objects[url]=request.data.read();return Response(url,status=201)
|
|
if self.collide and method=='HEAD':return _head(url,b'other')
|
|
if url not in self.objects:raise urllib.error.HTTPError(url,404,'missing',{},None)
|
|
data=self.objects[url]
|
|
if method=='HEAD':return _head(url,data)
|
|
if self.tamper:data=data[:-1]+bytes([data[-1]^1])
|
|
return Response(url,data)
|
|
|
|
|
|
def _head(url,data):
|
|
r=Response(url,b'');r.headers['Content-Length']=str(len(data));return r
|
|
|
|
|
|
@pytest.fixture
|
|
def key(tmp_path):
|
|
identity=tmp_path/'fixture.key'
|
|
subprocess.run(['age-keygen','-o',str(identity)],check=True,capture_output=True)
|
|
recipient=subprocess.run(['age-keygen','-y',str(identity)],check=True,capture_output=True,text=True).stdout.strip()
|
|
return identity,recipient
|
|
|
|
|
|
def private(tmp_path,name,data,mode=0o600):
|
|
path=tmp_path/name;path.write_bytes(data);path.chmod(mode);return path
|
|
|
|
|
|
def test_seals_uploads_and_reads_back(tmp_path,key):
|
|
identity,recipient=key;share=Share()
|
|
a=private(tmp_path,'kc',b'synthetic-kubeconfig');b=private(tmp_path,'ssh',b'synthetic-ssh-key')
|
|
receipt=tmp_path/'receipt.json'
|
|
record=k.run([f'kubeconfig={a}',f'tunnel-ssh-key={b}'],'synthetic-app-password',receipt,share,recipient,NOW)
|
|
assert record['status']=='readback_verified' and record['items']=={'kubeconfig':20,'tunnel-ssh-key':17}
|
|
assert record['artifact'].startswith('net-kingdom-kingpath-20261027T220000Z-')
|
|
assert oct(receipt.stat().st_mode&0o777)=='0o600'
|
|
text=receipt.read_text();assert 'synthetic' not in text
|
|
(stored,)=share.objects.values();assert stored.startswith(b'age-encryption.org/v1\n') and b'synthetic' not in stored
|
|
plain=subprocess.run(['age','-d','-i',str(identity)],input=stored,capture_output=True,check=True).stdout
|
|
with tarfile.open(fileobj=io.BytesIO(plain)) as archive:
|
|
assert {m.name:(archive.extractfile(m).read(),m.mtime,m.mode) for m in archive}=={
|
|
'kubeconfig':(b'synthetic-kubeconfig',0,0o600),'tunnel-ssh-key':(b'synthetic-ssh-key',0,0o600)}
|
|
assert [c[0] for c in share.calls]==['MKCOL','HEAD','PUT','HEAD','GET']
|
|
|
|
|
|
def test_existing_folder_is_accepted(tmp_path,key):
|
|
share=Share(folder_exists=True)
|
|
k.run([f'kubeconfig={private(tmp_path,"kc",b"x")}'],'pw',tmp_path/'r.json',share,key[1],NOW)
|
|
assert len(share.objects)==1
|
|
|
|
|
|
@pytest.mark.parametrize('mode',[0o640,0o604,0o644])
|
|
def test_refuses_group_or_world_readable_input(tmp_path,key,mode):
|
|
with pytest.raises(k.Refused):
|
|
k.run([f'kubeconfig={private(tmp_path,"kc",b"x",mode)}'],'pw',tmp_path/'r.json',Share(),key[1],NOW)
|
|
|
|
|
|
def test_refuses_symlink_bad_name_duplicate_and_empty(tmp_path,key):
|
|
real=private(tmp_path,'kc',b'x');link=tmp_path/'link';link.symlink_to(real)
|
|
for specs in ([f'kubeconfig={link}'],[f'Bad_Name={real}'],[f'a={real}',f'a={real}'],
|
|
[f'a={private(tmp_path,"empty",b"")}'],[],[f'a={tmp_path/"missing"}']):
|
|
with pytest.raises(k.Refused):k.run(specs,'pw',tmp_path/'r.json',Share(),key[1],NOW)
|
|
|
|
|
|
def test_refuses_existing_receipt_before_any_provider_call(tmp_path,key):
|
|
share=Share();receipt=private(tmp_path,'r.json',b'{}')
|
|
with pytest.raises(k.Refused):k.run([f'a={private(tmp_path,"kc",b"x")}'],'pw',receipt,share,key[1],NOW)
|
|
assert share.calls==[]
|
|
|
|
|
|
def test_refuses_tampered_readback_and_writes_no_receipt(tmp_path,key):
|
|
share=Share();share.tamper=True;receipt=tmp_path/'r.json'
|
|
with pytest.raises(k.Refused):k.run([f'a={private(tmp_path,"kc",b"x")}'],'pw',receipt,share,key[1],NOW)
|
|
assert not receipt.exists()
|
|
|
|
|
|
def test_refuses_overwriting_an_existing_object(tmp_path,key):
|
|
share=Share();share.collide=True
|
|
with pytest.raises(k.Refused):k.run([f'a={private(tmp_path,"kc",b"x")}'],'pw',tmp_path/'r.json',share,key[1],NOW)
|
|
assert 'PUT' not in [c[0] for c in share.calls]
|
|
|
|
|
|
def test_refuses_bad_credential_and_recipient(tmp_path,key):
|
|
item=private(tmp_path,'kc',b'x')
|
|
for pw in ('','a\nb'):
|
|
with pytest.raises(k.Refused):k.run([f'a={item}'],pw,tmp_path/'r.json',Share(),key[1],NOW)
|
|
with pytest.raises(k.Refused):k.run([f'a={item}'],'pw',tmp_path/'r.json',Share(),'not-a-recipient',NOW)
|
|
|
|
|
|
def test_production_recipient_is_the_registered_backup_recipient():
|
|
profile=json.loads((ROOT/'tools/bootstrap-recovery/identity-backup-profile.json').read_text())
|
|
assert k.RECIPIENT==profile['public_recipient']
|
|
assert k.ACCOUNT_BASE=='https://nx4069.your-storageshare.de/remote.php/dav/files/Backup/railiance-backups/net-kingdom-kingpath/'
|
|
|
|
|
|
KUBECONFIG=b'apiVersion: v1\nclusters: [{name: c, cluster: {server: "https://127.0.0.1:1"}}]\nusers: [{name: u, user: {}}]\n'
|
|
SSHKEY=b'-----BEGIN OPENSSH PRIVATE KEY-----\nsynthetic\n-----END OPENSSH PRIVATE KEY-----\n'
|
|
|
|
|
|
def uploaded(tmp_path,key,share):
|
|
items=[f'admin-kubeconfig={private(tmp_path,"kc",KUBECONFIG)}',f'tunnel-ssh-key={private(tmp_path,"ssh",SSHKEY)}']
|
|
return k.run(items,'pw',tmp_path/'upload.json',share,key[1],NOW)
|
|
|
|
|
|
def keyfd(path):return os.open(path,os.O_RDONLY)
|
|
|
|
|
|
def test_verify_fetches_decrypts_in_memory_and_checks_shapes(tmp_path,key):
|
|
share=Share();upload=uploaded(tmp_path,key,share);fd=keyfd(key[0])
|
|
try:record=k.verify(upload,'pw',fd,tmp_path/'verify.json',share)
|
|
finally:os.close(fd)
|
|
assert record['status']=='verified' and record['unrelated_key_refused'] and record['custody_key_matches_recipient']
|
|
assert record['items']=={'admin-kubeconfig':{'bytes':len(KUBECONFIG),'shape':'kubeconfig'},
|
|
'tunnel-ssh-key':{'bytes':len(SSHKEY),'shape':'ssh-private-key'}}
|
|
text=(tmp_path/'verify.json').read_text();assert 'synthetic' not in text and '127.0.0.1' not in text
|
|
|
|
|
|
def test_verify_refuses_a_custody_key_for_another_recipient(tmp_path,key):
|
|
share=Share();upload=uploaded(tmp_path,key,share)
|
|
other=tmp_path/'other.key';subprocess.run(['age-keygen','-o',str(other)],check=True,capture_output=True)
|
|
fd=keyfd(other)
|
|
try:
|
|
with pytest.raises(k.Refused):k.verify(upload,'pw',fd,tmp_path/'verify.json',share)
|
|
finally:os.close(fd)
|
|
assert [c[0] for c in share.calls].count('GET')==1 # only the upload readback; no fetch with the wrong key
|
|
|
|
|
|
@pytest.mark.parametrize('change',['etag','sha','items','artifact'])
|
|
def test_verify_refuses_a_changed_record_or_object(tmp_path,key,change):
|
|
share=Share();upload=uploaded(tmp_path,key,share);fd=keyfd(key[0])
|
|
if change=='etag':upload['etag']='"other"'
|
|
if change=='sha':upload['ciphertext_sha256']='0'*64
|
|
if change=='items':upload['items']={'admin-kubeconfig':1}
|
|
if change=='artifact':upload['artifact']='../escape.tar.age'
|
|
try:
|
|
with pytest.raises(k.Refused):k.verify(upload,'pw',fd,tmp_path/'verify.json',share)
|
|
finally:os.close(fd)
|
|
assert not (tmp_path/'verify.json').exists()
|
|
|
|
|
|
def test_verify_refuses_tampered_ciphertext(tmp_path,key):
|
|
share=Share();upload=uploaded(tmp_path,key,share);share.tamper=True;fd=keyfd(key[0])
|
|
try:
|
|
with pytest.raises(k.Refused):k.verify(upload,'pw',fd,tmp_path/'verify.json',share)
|
|
finally:os.close(fd)
|