coulomb-social/tests/test_recover_paired_backup.py
tegwick a2cb673e0a
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m5s
Prepare verified immutable paired backups and attended recovery
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a11538-05ea-7b63-b3c7-a2df5f38898d
2026-10-07 23:23:33 +02:00

113 lines
4.6 KiB
Python

"""Only an authenticated pair may leave the attended decryption boundary."""
import hashlib
import importlib.util
import io
import json
import subprocess
import tarfile
from pathlib import Path
import pytest
@pytest.fixture
def recovery():
spec = importlib.util.spec_from_file_location(
"recover_pair", Path(__file__).resolve().parents[1] / "scripts/recover-paired-backup.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def packet(path, bad=None):
contents = {"database.dump": b"PGDMPsynthetic", "content.tar.gz": b"synthetic assets"}
receipt = {
"status": "captured",
"writers_quiesced": True,
"app_resumed": True,
"database": "coulomb_social_db",
"started_at": "2026-10-07T20:00:00Z",
"image": "synthetic@sha256:fixture",
}
for name, key in [("database.dump", "database"), ("content.tar.gz", "content")]:
receipt[key + "_bytes"] = len(contents[name])
receipt[key + "_sha256"] = hashlib.sha256(contents[name]).hexdigest()
contents["receipt.json"] = json.dumps(receipt).encode()
with tarfile.open(path, "w") as bundle:
for name, data in contents.items():
entry = tarfile.TarInfo(name)
entry.size = len(data)
if bad == "link" and name == "content.tar.gz":
entry.type, entry.linkname, entry.size = tarfile.SYMTYPE, "/tmp/private", 0
bundle.addfile(entry, io.BytesIO(data))
if bad in {"duplicate", "traversal", "foreign"}:
entry = tarfile.TarInfo(
{"duplicate": "database.dump", "traversal": "../escaped", "foreign": "extra.env"}[
bad
]
)
entry.size = 1
bundle.addfile(entry, io.BytesIO(b"x"))
path.chmod(0o600)
return receipt
@pytest.mark.parametrize("bad", ["duplicate", "traversal", "foreign", "link"])
def test_rejects_entire_unsafe_packet_before_extraction(recovery, tmp_path, bad):
source = tmp_path / "packet.tar"
packet(source, bad)
destination = tmp_path / "out"
destination.mkdir(mode=0o700)
with pytest.raises(recovery.Refused, match="invalid_packet_members"):
recovery.unpack_packet(source, destination)
assert list(destination.iterdir()) == []
def test_real_age_roundtrip_checks_both_stores_and_capture_binding(recovery, tmp_path):
tmp_path.chmod(0o700)
source = tmp_path / "packet.tar"
inner = packet(source)
identity = tmp_path / "synthetic-identity"
generated = subprocess.run(["age-keygen", "-o", str(identity)], capture_output=True)
assert generated.returncode == 0
public = subprocess.run(["age-keygen", "-y", str(identity)], capture_output=True, check=True)
encrypted = tmp_path / "packet.tar.age"
subprocess.run(
["age", "-r", public.stdout.decode().strip(), "-o", str(encrypted), str(source)],
capture_output=True,
check=True,
)
encrypted.chmod(0o600)
transfer = {
"status": "offsite_fetched_verified",
"application": "coulomb-social",
"remote_retrieval_verified": True,
"version_id": "synthetic-version",
"destination": recovery.PREFIX + "synthetic.tar.age",
"captured_at": inner["started_at"],
"image": inner["image"],
"ciphertext_sha256": recovery.digest(encrypted),
"ciphertext_bytes": encrypted.stat().st_size,
}
receipt_path = tmp_path / "transfer.json"
receipt_path.write_text(json.dumps(transfer))
receipt_path.chmod(0o600)
destination = tmp_path / "recovered"
result = recovery.recover(encrypted, receipt_path, identity, destination)
assert result["status"] == "decrypted_pair_verified"
assert not result["application_restore_verified"]
assert (destination / "database.dump").read_bytes() == b"PGDMPsynthetic"
assert (destination / "content.tar.gz").read_bytes() == b"synthetic assets"
assert destination.stat().st_mode & 0o777 == 0o700
assert all(path.stat().st_mode & 0o777 == 0o600 for path in destination.iterdir())
assert not (destination / "packet.tar").exists()
transfer["captured_at"] = "different capture"
receipt_path.write_text(json.dumps(transfer))
with pytest.raises(recovery.Refused, match="capture_binding_mismatch"):
recovery.recover(encrypted, receipt_path, identity, tmp_path / "wrong-capture")
encrypted.write_bytes(b"tampered")
with pytest.raises(recovery.Refused, match="verified_transfer_required"):
recovery.recover(encrypted, receipt_path, identity, tmp_path / "corrupt")
assert not (tmp_path / "corrupt").exists()