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>
135 lines
4.6 KiB
Python
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}"
|