Complete project rebrand for better positioning in the press freedom and digital security space. FieldWitness communicates both field deployment and evidence testimony — appropriate for the target audience of journalists, NGOs, and human rights organizations. Rename mapping: - soosef → fieldwitness (package, CLI, all imports) - soosef.stegasoo → fieldwitness.stego - soosef.verisoo → fieldwitness.attest - ~/.soosef/ → ~/.fwmetadata/ (innocuous data dir name) - SOOSEF_DATA_DIR → FIELDWITNESS_DATA_DIR - SoosefConfig → FieldWitnessConfig - SoosefError → FieldWitnessError Also includes: - License switch from MIT to GPL-3.0 - C2PA bridge module (Phase 0-2 MVP): cert.py, export.py, vendor_assertions.py - README repositioned to lead with provenance/federation, stego backgrounded - Threat model skeleton at docs/security/threat-model.md - Planning docs: docs/planning/c2pa-integration.md, docs/planning/gtm-feasibility.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
135 lines
4.7 KiB
Python
135 lines
4.7 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_fieldwitness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
"""Create a populated ~/.fieldwitness directory with identity, chain, attestations, etc."""
|
|
import fieldwitness.paths as paths
|
|
|
|
data_dir = tmp_path / ".fieldwitness"
|
|
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
|
|
stego_dir = data_dir / "stego"
|
|
stego_dir.mkdir()
|
|
(stego_dir / "channel.key").write_text("test-channel-key")
|
|
|
|
# Create chain data
|
|
from fieldwitness.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" / "fieldwitness.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_fieldwitness: Path):
|
|
"""CRITICAL: execute_purge(ALL) must destroy chain directory."""
|
|
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
|
|
|
|
chain_dir = populated_fieldwitness / "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_fieldwitness: Path):
|
|
"""execute_purge(ALL) must destroy identity keys."""
|
|
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
|
|
|
|
assert (populated_fieldwitness / "identity" / "private.pem").exists()
|
|
|
|
result = execute_purge(PurgeScope.ALL, reason="test")
|
|
|
|
assert not (populated_fieldwitness / "identity").exists()
|
|
assert "destroy_identity_keys" in result.steps_completed
|
|
|
|
|
|
def test_purge_all_destroys_attestation_log(populated_fieldwitness: Path):
|
|
"""execute_purge(ALL) must destroy the Attest attestation log."""
|
|
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
|
|
|
|
result = execute_purge(PurgeScope.ALL, reason="test")
|
|
|
|
assert not (populated_fieldwitness / "attestations").exists()
|
|
assert "destroy_attestation_log" in result.steps_completed
|
|
|
|
|
|
def test_purge_keys_only_preserves_chain(populated_fieldwitness: Path):
|
|
"""KEYS_ONLY purge destroys keys but preserves chain and attestation data."""
|
|
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
|
|
|
|
result = execute_purge(PurgeScope.KEYS_ONLY, reason="test")
|
|
|
|
# Keys gone
|
|
assert not (populated_fieldwitness / "identity").exists()
|
|
assert "destroy_identity_keys" in result.steps_completed
|
|
|
|
# Chain and attestations preserved (KEYS_ONLY doesn't touch data)
|
|
assert (populated_fieldwitness / "chain" / "chain.bin").exists()
|
|
assert (populated_fieldwitness / "attestations" / "log.bin").exists()
|
|
|
|
|
|
def test_purge_reports_all_steps(populated_fieldwitness: Path):
|
|
"""execute_purge(ALL) reports all expected steps including chain."""
|
|
from fieldwitness.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}"
|