Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
244 lines
9.8 KiB
Python
244 lines
9.8 KiB
Python
"""Outage download boundaries, failure cleanup and independently usable kit."""
|
|
import hashlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import pty
|
|
import select
|
|
import termios
|
|
import urllib.error
|
|
import zipfile
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load(name, root=ROOT):
|
|
spec = importlib.util.spec_from_file_location(name, root / 'tools/bootstrap-recovery' / (name + '.py'))
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
m = load('retrieve-identity-offline')
|
|
k = load('recovery-kit')
|
|
|
|
|
|
def test_private_prompt_with_real_controlling_terminal_hides_input():
|
|
import fcntl
|
|
master, slave = pty.openpty()
|
|
|
|
def terminal_session():
|
|
os.setsid()
|
|
fcntl.ioctl(0, termios.TIOCSCTTY, 0)
|
|
|
|
code = ("import importlib.util; "
|
|
f"s=importlib.util.spec_from_file_location('offline',{str(ROOT / 'tools/bootstrap-recovery/retrieve-identity-offline.py')!r}); "
|
|
"m=importlib.util.module_from_spec(s); s.loader.exec_module(m); "
|
|
"value=m.private_prompt('Private fixture prompt: '); "
|
|
"print('accepted' if value=='fixture-never-echo-me' else 'rejected')")
|
|
child = subprocess.Popen([sys.executable, '-c', code], stdin=slave, stdout=slave,
|
|
stderr=slave, preexec_fn=terminal_session)
|
|
os.close(slave)
|
|
captured = b''
|
|
try:
|
|
assert select.select([master], [], [], 10)[0], 'prompt timeout'
|
|
while b'Private fixture prompt: ' not in captured:
|
|
captured += os.read(master, 4096)
|
|
assert select.select([master], [], [], 10)[0] or b'Private fixture prompt: ' in captured
|
|
os.write(master, b'fixture-never-echo-me\n')
|
|
child.wait(timeout=10)
|
|
while select.select([master], [], [], 0)[0]:
|
|
try:
|
|
block = os.read(master, 4096)
|
|
except OSError:
|
|
break
|
|
if not block:
|
|
break
|
|
captured += block
|
|
assert child.returncode == 0 and b'accepted' in captured
|
|
assert b'fixture-never-echo-me' not in captured
|
|
finally:
|
|
if child.poll() is None:
|
|
child.kill()
|
|
child.wait()
|
|
os.close(master)
|
|
|
|
|
|
def test_private_prompt_refuses_redirected_input(monkeypatch):
|
|
monkeypatch.setattr(m.sys.stdin, 'isatty', lambda: False)
|
|
monkeypatch.setattr(m.getpass, 'getpass', lambda *args: pytest.fail('must not prompt'))
|
|
with pytest.raises(m.offsite.Refused):
|
|
m.private_prompt('Private: ')
|
|
|
|
|
|
def test_private_prompt_never_falls_back_to_echo(monkeypatch):
|
|
import warnings
|
|
monkeypatch.setattr(m.sys.stdin, 'isatty', lambda: True)
|
|
monkeypatch.setattr(m.sys.stderr, 'isatty', lambda: True)
|
|
|
|
def cannot_hide(*args):
|
|
warnings.warn('fixture cannot hide input', m.getpass.GetPassWarning)
|
|
pytest.fail('must stop before fallback reads input')
|
|
|
|
monkeypatch.setattr(m.getpass, 'getpass', cannot_hide)
|
|
with pytest.raises(m.getpass.GetPassWarning):
|
|
m.private_prompt('Private: ')
|
|
|
|
|
|
def test_http_failure_receipt_metadata_never_includes_private_response():
|
|
error = urllib.error.HTTPError('https://private.invalid/secret', 401,
|
|
'private response', {'Authorization': 'secret'}, None)
|
|
assert m.failure_metadata(error) == {'failure_kind': 'provider_http_error', 'http_status': 401}
|
|
|
|
|
|
@pytest.mark.parametrize('script', ['retrieve-identity-offline.py', 'recovery-kit.py'])
|
|
def test_cli_refuses_unattended_prompt_with_safe_stage_receipt(tmp_path, script):
|
|
receipt = tmp_path / 'receipt.json'
|
|
args = [sys.executable, str(ROOT / 'tools/bootstrap-recovery' / script),
|
|
'--receipt', str(receipt)]
|
|
if script == 'recovery-kit.py':
|
|
args += ['--output-dir', str(tmp_path / 'kit'), '--publish']
|
|
else:
|
|
args += ['--source-receipt', str(ROOT / 'docs/evidence/2026-10-16-identity-backup-live/actual-offsite.json')]
|
|
result = subprocess.run(args, input=b'', capture_output=True, timeout=10)
|
|
assert result.returncode == 1
|
|
record = json.loads(receipt.read_text())
|
|
assert record['failed_stage'] == 'private_provider_prompt'
|
|
assert record['failure_kind'] == 'scope_or_integrity_refusal'
|
|
assert 'Traceback' not in result.stderr.decode()
|
|
|
|
|
|
class Response(io.BytesIO):
|
|
def __init__(self, url, raw, etag='"original"', status=200):
|
|
super().__init__(raw)
|
|
self.url = url
|
|
self.status = status
|
|
self.headers = {'Content-Length': str(len(raw)), 'ETag': etag}
|
|
|
|
|
|
class Provider:
|
|
def __init__(self, body):
|
|
self.body = body
|
|
self.calls = []
|
|
self.tamper = False
|
|
self.etag = '"original"'
|
|
self.objects = {}
|
|
|
|
def open(self, request, timeout):
|
|
method = request.get_method()
|
|
self.calls.append((method, request.full_url, dict(request.headers)))
|
|
assert request.full_url.startswith((m.ACCOUNT_BASE, m.offsite.BASE))
|
|
if self.objects or method == 'PUT':
|
|
if method == 'PUT':
|
|
assert request.get_header('If-none-match') == '*'
|
|
assert request.full_url not in self.objects
|
|
self.objects[request.full_url] = request.data
|
|
return Response(request.full_url, b'', status=201)
|
|
if request.full_url not in self.objects:
|
|
raise urllib.error.HTTPError(request.full_url, 404, 'missing', {}, None)
|
|
return Response(request.full_url, self.objects[request.full_url])
|
|
raw = self.body if not self.tamper or method == 'HEAD' else b'X' * len(self.body)
|
|
return Response(request.full_url, raw, self.etag)
|
|
|
|
|
|
@pytest.fixture
|
|
def record():
|
|
return dict(schema='netkingdom.identity-offsite.v1', status='readback_verified',
|
|
provider='Storage Share', folder='Backup/railiance-backups/net-kingdom-identity',
|
|
artifact='net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age',
|
|
ciphertext_bytes=32, ciphertext_sha256=hashlib.sha256(b'A' * 32).hexdigest(),
|
|
etag='"original"')
|
|
|
|
|
|
@pytest.mark.parametrize('mode', ['account', 'folder-share'])
|
|
def test_offline_success_uses_only_fixed_provider_reads(record, mode):
|
|
provider = Provider(b'A' * 32)
|
|
record['untrusted_extra'] = 'must-not-export'
|
|
result = m.retrieve(record, 'fixtureSecret1234', mode, provider)
|
|
try:
|
|
assert Path(result['fetched_path']).read_bytes() == b'A' * 32
|
|
assert not result['openbao_contacted'] and not result['sso_contacted']
|
|
assert {c[0] for c in provider.calls} == {'HEAD', 'GET'}
|
|
assert 'fixtureSecret1234' not in json.dumps(result)
|
|
assert 'must-not-export' not in json.dumps(result)
|
|
assert provider.calls[1][2]['If-match'] == record['etag']
|
|
finally:
|
|
path = Path(result['fetched_path'])
|
|
path.unlink()
|
|
path.parent.rmdir()
|
|
|
|
|
|
@pytest.mark.parametrize('field,value', [
|
|
('artifact', '../another.tar.age'), ('folder', 'other'),
|
|
('ciphertext_sha256', 'x' * 64), ('ciphertext_bytes', True),
|
|
('provider', 'other'), ('etag', 'W/"weak"'),
|
|
])
|
|
def test_unreviewed_scope_refuses_before_contact(record, field, value):
|
|
record[field] = value
|
|
provider = Provider(b'A' * 32)
|
|
with pytest.raises(m.offsite.Refused):
|
|
m.retrieve(record, 'fixtureSecret1234', 'account', provider)
|
|
assert not provider.calls
|
|
|
|
|
|
def test_digest_failure_removes_incomplete_ciphertext(record, monkeypatch, tmp_path):
|
|
directory = tmp_path / 'download'
|
|
directory.mkdir()
|
|
monkeypatch.setattr(m.tempfile, 'mkdtemp', lambda **kwargs: str(directory))
|
|
provider = Provider(b'A' * 32)
|
|
provider.tamper = True
|
|
with pytest.raises(m.offsite.Refused):
|
|
m.retrieve(record, 'fixtureSecret1234', 'account', provider)
|
|
assert not directory.exists()
|
|
|
|
|
|
def test_changed_etag_refuses_before_download(record):
|
|
provider = Provider(b'A' * 32)
|
|
provider.etag = '"changed"'
|
|
with pytest.raises(m.offsite.Refused):
|
|
m.retrieve(record, 'fixtureSecret1234', 'account', provider)
|
|
assert len(provider.calls) == 1
|
|
|
|
|
|
def test_kit_contains_all_offline_imports_and_checksums(tmp_path):
|
|
files = k.build()
|
|
archive = next(value for name, value in files.items() if name.endswith('.zip'))
|
|
with zipfile.ZipFile(io.BytesIO(archive)) as kit:
|
|
kit.extractall(tmp_path)
|
|
for line in kit.read('SHA256SUMS').decode().splitlines():
|
|
digest, name = line.split(' ', 1)
|
|
assert hashlib.sha256((tmp_path / name).read_bytes()).hexdigest() == digest
|
|
for script in ('verify-identity.py', 'retrieve-identity-offline.py'):
|
|
result = subprocess.run([sys.executable, str(tmp_path / 'tools/bootstrap-recovery' / script), '--help'],
|
|
capture_output=True)
|
|
assert result.returncode == 0, result.stderr
|
|
assert k.build() == files
|
|
|
|
|
|
def test_kit_publication_conditional_and_exact_readback():
|
|
provider = Provider(b'')
|
|
# Ensure initial HEAD returns 404 instead of the download-fixture response.
|
|
provider.objects['unrelated'] = b'preserved'
|
|
files = k.build()
|
|
rows = k.publish(files, {'Authorization': 'fixture'}, provider)
|
|
assert len(rows) == 3 and all(row['readback_verified'] for row in rows)
|
|
assert len([c for c in provider.calls if c[0] == 'PUT']) == 3
|
|
k.publish(files, {'Authorization': 'fixture'}, provider)
|
|
assert len([c for c in provider.calls if c[0] == 'PUT']) == 3
|
|
assert provider.objects['unrelated'] == b'preserved'
|
|
|
|
|
|
def test_kit_conflicting_existing_object_never_overwritten():
|
|
provider = Provider(b'')
|
|
files = k.build()
|
|
first = next(iter(files))
|
|
provider.objects[m.ACCOUNT_BASE + first] = b'X' * len(files[first])
|
|
with pytest.raises(k.offsite.Refused):
|
|
k.publish(files, {'Authorization': 'fixture'}, provider)
|
|
assert not any(c[0] == 'PUT' for c in provider.calls)
|