net-kingdom/tests/test_bootstrap_recovery_transport.py
tegwick d9557ba90e
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Harden remaining bootstrap custody callers and prepare isolated recovery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
2026-10-15 22:57:14 +02:00

87 lines
3.9 KiB
Python

"""Actual age/archive checks using disposable custody, never estate material."""
import hashlib
import io
import json
import os
from pathlib import Path
import shutil
import subprocess
import tarfile
import pytest
ROOT=Path(__file__).resolve().parents[1]
@pytest.fixture
def transport(tmp_path):
checkout=tmp_path/'checkout'
for relative in ['tools/bootstrap-recovery/verify-bundle.py', 'sso-mfa/bootstrap/sops-private.py']:
target=checkout/relative;target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(ROOT/relative,target)
key=tmp_path/'fixture.key'
subprocess.run(['age-keygen','-o',str(key)],capture_output=True,check=True)
recipient=subprocess.check_output(['age-keygen','-y',str(key)],text=True).strip()
(checkout/'keys').mkdir();(checkout/'keys/age.pub').write_text(recipient+'\n')
env={'PATH':os.environ['PATH'],'HOME':str(tmp_path),'NETKINGDOM_CUSTODY_AGE_KEY_FILE':str(key),
'SOPS_AGE_KEY':'fixture-ambient-ignored','TMPDIR':str(checkout)}
return checkout,key,recipient,env,tmp_path
def archive_bytes(members):
stream=io.BytesIO()
with tarfile.open(fileobj=stream,mode='w') as tar:
for name,content,kind in members:
info=tarfile.TarInfo(name)
if kind=='link':info.type=tarfile.SYMTYPE;info.linkname='/outside'
elif kind=='hardlink':info.type=tarfile.LNKTYPE;info.linkname='secrets/lldap/secrets.env'
else:info.size=len(content)
tar.addfile(info,io.BytesIO(content) if kind=='file' else None)
return stream.getvalue()
def verify(transport, members, *, required='lldap/secrets.env', sha=None):
checkout,_,recipient,env,root=transport
cipher=subprocess.check_output(['age','-r',recipient],input=archive_bytes(members))
bundle=root/'input.tar.age';bundle.write_bytes(cipher)
receipt=root/'receipt.json'
result=subprocess.run(['python3',str(checkout/'tools/bootstrap-recovery/verify-bundle.py'),
'--bundle',str(bundle),'--bundle-sha256',sha or hashlib.sha256(cipher).hexdigest(),
'--required-file',required,'--receipt',str(receipt)],capture_output=True,env=env,timeout=20)
assert b'fixture-private-payload' not in result.stdout+result.stderr
assert not list(checkout.glob('netkingdom-recovery-*'))
return result,receipt
def test_native_ciphertext_is_restored_checked_and_removed_with_explicit_limits(transport):
r,p=verify(transport,[('secrets/lldap/secrets.env',b'VALUE=fixture-private-payload\n','file')])
assert r.returncode==0
receipt=json.loads(p.read_text())
assert receipt['wrong_key_refused'] and receipt['registered_recipient_match']
assert receipt['isolated_restore_readback_passed'] and receipt['temporary_plaintext_cleanup_verified']
assert not receipt['native_consumer_recovery_proven']
assert not receipt['independent_custody_proven']
assert receipt['restored_files']==['lldap/secrets.env']
@pytest.mark.parametrize('members',[
[('../escape',b'fixture-private-payload','file')],
[('secrets/lldap/secrets.env',b'', 'link')],
[('secrets/lldap/secrets.env',b'', 'hardlink')],
[('secrets/lldap/secrets.env',b'AGE-SECRET-KEY-fixture-private-payload','file')],
[('secrets/lldap/secrets.env',b'one','file'),('secrets/lldap/secrets.env',b'two','file')],
[('secrets/unreviewed/secrets.env',b'fixture-private-payload','file')],
])
def test_unsafe_archives_never_publish_acceptance(transport,members):
r,p=verify(transport,members)
assert r.returncode!=0 and not p.exists()
def test_ciphertext_digest_is_bound_before_custody_use(transport):
r,p=verify(transport,[('secrets/lldap/secrets.env',b'fixture-private-payload','file')],sha='0'*64)
assert r.returncode!=0 and not p.exists()
def test_missing_required_service_file_refuses_acceptance(transport):
r,p=verify(transport,[('secrets/lldap/secrets.env',b'fixture-private-payload','file')],required='privacyidea/pi.enc')
assert r.returncode!=0 and not p.exists()