NK-WP-0055-T13: kingpath-offsite --verify restores and checks the break-glass copy
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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
This commit is contained in:
tegwick 2026-10-27 23:47:21 +01:00
parent fd64fc8478
commit 317d578193
3 changed files with 134 additions and 1 deletions

View file

@ -125,3 +125,55 @@ 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)