88 lines
3.9 KiB
Python
88 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()
|