fieldwitness/tests/test_killswitch.py
Aaron D. Lee 51c9b0a99a Fix 14 bugs and add features from power-user security audit
Critical fixes:
- Fix admin_delete_user missing current_user_id argument (TypeError on every delete)
- Fix self-signed cert OOM: bytes(2130706433) → IPv4Address("127.0.0.1")
- Add @login_required to attestation routes (attest, log); verify stays public
- Add auth guards to fieldkit (@admin_required on killswitch) and keys blueprints
- Fix cleanup_temp_files NameError in generate() route

Security hardening:
- Unify temp storage to ~/.soosef/temp/ so killswitch purge covers web uploads
- Replace Path.unlink() with secure deletion (shred fallback) in temp_storage
- Add structured audit log (audit.jsonl) for admin, key, and killswitch actions

New features:
- Dead man's switch background enforcement thread in serve + check-deadman CLI
- Key rotation: soosef keys rotate-identity/rotate-channel with archiving
- Batch attestation: soosef attest batch <dir> with progress and error handling
- Geofence CLI: set/check/clear commands with config persistence
- USB CLI: snapshot/check commands against device whitelist
- Verification receipt download (/verify/receipt JSON endpoint + UI button)
- IdentityInfo.created_at populated from sidecar meta.json (mtime fallback)

Data layer:
- ChainStore.get() now O(1) via byte-offset index built during state rebuild
- Add federation module (chain, models, serialization, entropy)

Includes 45+ new tests across chain, deadman, key rotation, killswitch, and
serialization modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:06:33 -04:00

135 lines
4.6 KiB
Python

"""Tests for killswitch — verifies emergency purge destroys all sensitive data."""
from __future__ import annotations
import hashlib
from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
Encoding,
NoEncryption,
PrivateFormat,
PublicFormat,
)
@pytest.fixture()
def populated_soosef(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Create a populated ~/.soosef directory with identity, chain, attestations, etc."""
import soosef.paths as paths
data_dir = tmp_path / ".soosef"
data_dir.mkdir()
monkeypatch.setattr(paths, "BASE_DIR", data_dir)
# Create identity
identity_dir = data_dir / "identity"
identity_dir.mkdir()
key = Ed25519PrivateKey.generate()
priv_pem = key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
(identity_dir / "private.pem").write_bytes(priv_pem)
pub_pem = key.public_key().public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
(identity_dir / "public.pem").write_bytes(pub_pem)
# Create channel key
stegasoo_dir = data_dir / "stegasoo"
stegasoo_dir.mkdir()
(stegasoo_dir / "channel.key").write_text("test-channel-key")
# Create chain data
from soosef.federation.chain import ChainStore
chain_dir = data_dir / "chain"
store = ChainStore(chain_dir)
for i in range(3):
store.append(hashlib.sha256(f"c-{i}".encode()).digest(), "test/plain", key)
# Create attestation dir with a dummy file
att_dir = data_dir / "attestations"
att_dir.mkdir()
(att_dir / "log.bin").write_bytes(b"dummy attestation data")
# Create other dirs
(data_dir / "auth").mkdir()
(data_dir / "auth" / "soosef.db").write_bytes(b"dummy db")
(data_dir / "temp").mkdir()
(data_dir / "temp" / "file.tmp").write_bytes(b"tmp")
(data_dir / "instance").mkdir()
(data_dir / "instance" / ".secret_key").write_bytes(b"secret")
(data_dir / "config.json").write_text("{}")
return data_dir
def test_purge_all_destroys_chain_data(populated_soosef: Path):
"""CRITICAL: execute_purge(ALL) must destroy chain directory."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
chain_dir = populated_soosef / "chain"
assert chain_dir.exists()
assert (chain_dir / "chain.bin").exists()
result = execute_purge(PurgeScope.ALL, reason="test")
assert not chain_dir.exists(), "Chain directory must be destroyed by killswitch"
assert "destroy_chain_data" in result.steps_completed
def test_purge_all_destroys_identity(populated_soosef: Path):
"""execute_purge(ALL) must destroy identity keys."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
assert (populated_soosef / "identity" / "private.pem").exists()
result = execute_purge(PurgeScope.ALL, reason="test")
assert not (populated_soosef / "identity").exists()
assert "destroy_identity_keys" in result.steps_completed
def test_purge_all_destroys_attestation_log(populated_soosef: Path):
"""execute_purge(ALL) must destroy the Verisoo attestation log."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
result = execute_purge(PurgeScope.ALL, reason="test")
assert not (populated_soosef / "attestations").exists()
assert "destroy_attestation_log" in result.steps_completed
def test_purge_keys_only_preserves_chain(populated_soosef: Path):
"""KEYS_ONLY purge destroys keys but preserves chain and attestation data."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
result = execute_purge(PurgeScope.KEYS_ONLY, reason="test")
# Keys gone
assert not (populated_soosef / "identity").exists()
assert "destroy_identity_keys" in result.steps_completed
# Chain and attestations preserved (KEYS_ONLY doesn't touch data)
assert (populated_soosef / "chain" / "chain.bin").exists()
assert (populated_soosef / "attestations" / "log.bin").exists()
def test_purge_reports_all_steps(populated_soosef: Path):
"""execute_purge(ALL) reports all expected steps including chain."""
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
result = execute_purge(PurgeScope.ALL, reason="test")
expected_steps = [
"destroy_identity_keys",
"destroy_channel_key",
"destroy_flask_secret",
"destroy_auth_db",
"destroy_attestation_log",
"destroy_chain_data",
"destroy_temp_files",
"destroy_config",
]
for step in expected_steps:
assert step in result.steps_completed, f"Missing purge step: {step}"