Rebrand SooSeF to FieldWitness

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>
This commit is contained in:
Aaron D. Lee
2026-04-02 15:05:13 -04:00
parent 6325e86873
commit 490f9d4a1d
188 changed files with 4588 additions and 2017 deletions

View File

@@ -1,4 +1,4 @@
"""
Admin routes are registered directly in app.py via _register_stegasoo_routes()
Admin routes are registered directly in app.py via _register_stego_routes()
alongside the auth routes (setup, login, logout, account, admin/users).
"""

View File

@@ -1,7 +1,7 @@
"""
Attestation blueprint — attest and verify images via Verisoo.
Attestation blueprint — attest and verify images via Attest.
Wraps verisoo's attestation and verification libraries to provide:
Wraps attest's attestation and verification libraries to provide:
- Image attestation: upload → hash → sign → store in append-only log
- Image verification: upload → hash → search log → display matches
- Verification receipt: same as verify but returns a downloadable JSON file
@@ -21,27 +21,27 @@ bp = Blueprint("attest", __name__)
def _get_storage():
"""Get verisoo LocalStorage pointed at soosef's attestation directory."""
from soosef.verisoo.storage import LocalStorage
"""Get attest LocalStorage pointed at fieldwitness's attestation directory."""
from fieldwitness.attest.storage import LocalStorage
from soosef.paths import ATTESTATIONS_DIR
from fieldwitness.paths import ATTESTATIONS_DIR
return LocalStorage(base_path=ATTESTATIONS_DIR)
def _get_private_key():
"""Load the Ed25519 private key from soosef identity directory."""
from soosef.verisoo.crypto import load_private_key
"""Load the Ed25519 private key from fieldwitness identity directory."""
from fieldwitness.attest.crypto import load_private_key
from soosef.paths import IDENTITY_PRIVATE_KEY
from fieldwitness.paths import IDENTITY_PRIVATE_KEY
if not IDENTITY_PRIVATE_KEY.exists():
return None
return load_private_key(IDENTITY_PRIVATE_KEY)
def _wrap_in_chain(verisoo_record, private_key, metadata: dict | None = None):
"""Wrap a Verisoo attestation record in the hash chain.
def _wrap_in_chain(attest_record, private_key, metadata: dict | None = None):
"""Wrap a Attest attestation record in the hash chain.
Returns the chain record, or None if chain is disabled.
"""
@@ -49,23 +49,23 @@ def _wrap_in_chain(verisoo_record, private_key, metadata: dict | None = None):
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from soosef.config import SoosefConfig
from soosef.federation.chain import ChainStore
from soosef.paths import CHAIN_DIR, IDENTITY_PRIVATE_KEY
from fieldwitness.config import FieldWitnessConfig
from fieldwitness.federation.chain import ChainStore
from fieldwitness.paths import CHAIN_DIR, IDENTITY_PRIVATE_KEY
config = SoosefConfig.load()
config = FieldWitnessConfig.load()
if not config.chain_enabled or not config.chain_auto_wrap:
return None
# Hash the verisoo record bytes as chain content
# Hash the attest record bytes as chain content
record_bytes = (
verisoo_record.to_bytes()
if hasattr(verisoo_record, "to_bytes")
else str(verisoo_record).encode()
attest_record.to_bytes()
if hasattr(attest_record, "to_bytes")
else str(attest_record).encode()
)
content_hash = hashlib.sha256(record_bytes).digest()
# Load Ed25519 key for chain signing (need the cryptography key, not verisoo's)
# Load Ed25519 key for chain signing (need the cryptography key, not attest's)
priv_pem = IDENTITY_PRIVATE_KEY.read_bytes()
chain_private_key = load_pem_private_key(priv_pem, password=None)
@@ -79,7 +79,7 @@ def _wrap_in_chain(verisoo_record, private_key, metadata: dict | None = None):
store = ChainStore(CHAIN_DIR)
return store.append(
content_hash=content_hash,
content_type="verisoo/attestation-v1",
content_type="attest/attestation-v1",
private_key=chain_private_key,
metadata=chain_metadata,
)
@@ -111,7 +111,7 @@ def attest():
if request.method == "POST":
if not has_identity:
flash(
"No identity configured. Run 'soosef init' or generate one from the Keys page.",
"No identity configured. Run 'fieldwitness init' or generate one from the Keys page.",
"error",
)
return redirect(url_for("attest.attest"))
@@ -151,7 +151,7 @@ def attest():
# Extract-then-classify: get evidentiary metadata before attestation
# so user can control what's included
if auto_exif and strip_device:
from soosef.metadata import extract_and_classify
from fieldwitness.metadata import extract_and_classify
extraction = extract_and_classify(image_data)
# Merge evidentiary fields (GPS, timestamp) but exclude
@@ -166,7 +166,7 @@ def attest():
metadata[f"exif_{key}"] = str(value)
# Create the attestation
from soosef.verisoo.attestation import create_attestation
from fieldwitness.attest.attestation import create_attestation
attestation = create_attestation(
image_data=image_data,
@@ -194,14 +194,14 @@ def attest():
# Save our own identity so we can look it up during verification
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from soosef.verisoo.models import Identity
from fieldwitness.attest.models import Identity
pub_key = private_key.public_key()
pub_bytes = pub_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
identity = Identity(
public_key=pub_bytes,
fingerprint=attestation.record.attestor_fingerprint,
metadata={"name": "SooSeF Local Identity"},
metadata={"name": "FieldWitness Local Identity"},
)
try:
storage.save_identity(identity)
@@ -246,11 +246,11 @@ def attest_batch():
"""
import hashlib
from soosef.verisoo.hashing import hash_image
from fieldwitness.attest.hashing import hash_image
private_key = _get_private_key()
if private_key is None:
return {"error": "No identity key. Run soosef init first."}, 400
return {"error": "No identity key. Run fieldwitness init first."}, 400
files = request.files.getlist("images")
if not files:
@@ -271,14 +271,14 @@ def attest_batch():
results.append({"file": filename, "status": "skipped", "reason": "already attested"})
continue
from soosef.verisoo.attestation import create_attestation
from fieldwitness.attest.attestation import create_attestation
attestation = create_attestation(image_data, private_key)
index = storage.append_record(attestation.record)
# Wrap in chain if enabled
chain_index = None
config = request.app.config.get("SOOSEF_CONFIG") if hasattr(request, "app") else None
config = request.app.config.get("FIELDWITNESS_CONFIG") if hasattr(request, "app") else None
if config and getattr(config, "chain_enabled", False) and getattr(config, "chain_auto_wrap", False):
try:
chain_record = _wrap_in_chain(attestation.record, private_key, {})
@@ -365,11 +365,11 @@ def _verify_image(image_data: bytes) -> dict:
"""Run the full verification pipeline against the attestation log.
Returns a dict with keys:
query_hashes — ImageHashes object from verisoo
query_hashes — ImageHashes object from fieldwitness.attest
matches — list of match dicts (record, match_type, distances, attestor_name)
record_count — total records searched
"""
from soosef.verisoo.hashing import compute_all_distances, hash_image, is_same_image
from fieldwitness.attest.hashing import compute_all_distances, hash_image, is_same_image
query_hashes = hash_image(image_data)
storage = _get_storage()
@@ -541,12 +541,12 @@ def verify_receipt():
# Chain position proof — look up this attestation in the hash chain
try:
from soosef.config import SoosefConfig
from soosef.federation.chain import ChainStore
from soosef.federation.serialization import compute_record_hash
from soosef.paths import CHAIN_DIR
from fieldwitness.config import FieldWitnessConfig
from fieldwitness.federation.chain import ChainStore
from fieldwitness.federation.serialization import compute_record_hash
from fieldwitness.paths import CHAIN_DIR
chain_config = SoosefConfig.load()
chain_config = FieldWitnessConfig.load()
if chain_config.chain_enabled:
chain_store = ChainStore(CHAIN_DIR)
# Search chain for a record whose content_hash matches this attestation

View File

@@ -1,7 +1,7 @@
"""
Source drop box blueprint — anonymous, token-gated file submission.
Provides a SecureDrop-like intake that lives inside SooSeF:
Provides a SecureDrop-like intake that lives inside FieldWitness:
- Admin creates a time-limited upload token
- Source opens the token URL in a browser (no account needed)
- Files are uploaded, EXIF-stripped, and auto-attested on receipt
@@ -21,8 +21,8 @@ from pathlib import Path
from auth import admin_required, login_required
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
from soosef.audit import log_action
from soosef.paths import AUTH_DIR, TEMP_DIR
from fieldwitness.audit import log_action
from fieldwitness.paths import AUTH_DIR, TEMP_DIR
bp = Blueprint("dropbox", __name__, url_prefix="/dropbox")
@@ -188,7 +188,7 @@ def upload(token):
# 1. Extract EXIF into attestation metadata (evidentiary fields)
# 2. Attest the ORIGINAL bytes (hash matches what source submitted)
# 3. Strip metadata from the stored copy (protect source device info)
from soosef.metadata import extract_strip_pipeline
from fieldwitness.metadata import extract_strip_pipeline
extraction, stripped_data = extract_strip_pipeline(raw_data)
@@ -204,7 +204,7 @@ def upload(token):
# is preserved in the attestation metadata; dangerous fields
# (device serial) are excluded.
try:
from soosef.verisoo.attestation import create_attestation
from fieldwitness.attest.attestation import create_attestation
from blueprints.attest import _get_private_key, _get_storage
@@ -240,7 +240,7 @@ def upload(token):
# insufficient), making valid receipts unforgeable.
import hmac
from soosef.paths import SECRET_KEY_FILE
from fieldwitness.paths import SECRET_KEY_FILE
server_secret = SECRET_KEY_FILE.read_bytes() if SECRET_KEY_FILE.exists() else token.encode()
receipt_code = hmac.new(
@@ -279,7 +279,7 @@ def upload(token):
return Response(receipt_text, content_type="text/plain")
# GET — show upload form with client-side SHA-256 hashing
# Minimal page, no SooSeF branding (source safety)
# Minimal page, no FieldWitness branding (source safety)
remaining = token_data["max_files"] - token_data["used"]
return f"""<!DOCTYPE html>
<html><head><title>Secure Upload</title>

View File

@@ -12,7 +12,7 @@ bp = Blueprint("federation", __name__, url_prefix="/federation")
@login_required
def status():
"""Federation status dashboard."""
from soosef.verisoo.peer_store import PeerStore
from fieldwitness.attest.peer_store import PeerStore
store = PeerStore()
peers = store.list_peers()
@@ -21,9 +21,9 @@ def status():
# Get local node info
node_info = {"root": None, "size": 0}
try:
from soosef.verisoo.storage import LocalStorage
from fieldwitness.attest.storage import LocalStorage
import soosef.paths as _paths
import fieldwitness.paths as _paths
storage = LocalStorage(_paths.ATTESTATIONS_DIR)
stats = storage.get_stats()
@@ -48,7 +48,7 @@ def status():
@admin_required
def peer_add():
"""Add a federation peer."""
from soosef.verisoo.peer_store import PeerStore
from fieldwitness.attest.peer_store import PeerStore
url = request.form.get("url", "").strip()
fingerprint = request.form.get("fingerprint", "").strip()
@@ -67,7 +67,7 @@ def peer_add():
@admin_required
def peer_remove():
"""Remove a federation peer."""
from soosef.verisoo.peer_store import PeerStore
from fieldwitness.attest.peer_store import PeerStore
url = request.form.get("url", "").strip()
store = PeerStore()

View File

@@ -5,7 +5,7 @@ Fieldkit blueprint — killswitch, dead man's switch, status dashboard.
from auth import admin_required, get_username, login_required
from flask import Blueprint, flash, redirect, render_template, request, url_for
from soosef.audit import log_action
from fieldwitness.audit import log_action
bp = Blueprint("fieldkit", __name__, url_prefix="/fieldkit")
@@ -14,7 +14,7 @@ bp = Blueprint("fieldkit", __name__, url_prefix="/fieldkit")
@login_required
def status():
"""Fieldkit status dashboard — all monitors and system health."""
from soosef.fieldkit.deadman import DeadmanSwitch
from fieldwitness.fieldkit.deadman import DeadmanSwitch
deadman = DeadmanSwitch()
return render_template(
@@ -39,7 +39,7 @@ def killswitch():
flash("Killswitch requires password confirmation.", "danger")
return render_template("fieldkit/killswitch.html")
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
actor = username
result = execute_purge(PurgeScope.ALL, reason="web_ui")
@@ -71,7 +71,7 @@ def killswitch():
@login_required
def deadman_checkin():
"""Record a dead man's switch check-in."""
from soosef.fieldkit.deadman import DeadmanSwitch
from fieldwitness.fieldkit.deadman import DeadmanSwitch
deadman = DeadmanSwitch()
deadman.checkin()

View File

@@ -5,7 +5,7 @@ Key management blueprint — unified view of all key material.
from auth import get_username, login_required
from flask import Blueprint, flash, redirect, render_template, url_for
from soosef.audit import log_action
from fieldwitness.audit import log_action
bp = Blueprint("keys", __name__, url_prefix="/keys")
@@ -14,7 +14,7 @@ bp = Blueprint("keys", __name__, url_prefix="/keys")
@login_required
def index():
"""Key management dashboard."""
from soosef.keystore import KeystoreManager
from fieldwitness.keystore import KeystoreManager
ks = KeystoreManager()
return render_template("fieldkit/keys.html", keystore=ks.status())
@@ -24,7 +24,7 @@ def index():
@login_required
def generate_channel():
"""Generate a new channel key."""
from soosef.keystore import KeystoreManager
from fieldwitness.keystore import KeystoreManager
ks = KeystoreManager()
try:
@@ -54,7 +54,7 @@ def generate_channel():
@login_required
def generate_identity():
"""Generate a new Ed25519 identity."""
from soosef.keystore import KeystoreManager
from fieldwitness.keystore import KeystoreManager
ks = KeystoreManager()
try:

View File

@@ -1,8 +1,8 @@
"""
Steganography routes are registered directly in app.py via _register_stegasoo_routes()
rather than as a blueprint, because the stegasoo route logic (3,600+ lines) uses
Steganography routes are registered directly in app.py via _register_stego_routes()
rather than as a blueprint, because the stego route logic (3,600+ lines) uses
module-level state (ThreadPoolExecutor, jobs dict, subprocess_stego instance)
that doesn't translate cleanly to a blueprint.
The stego templates are in templates/stego/ and extend the soosef base.html.
The stego templates are in templates/stego/ and extend the fieldwitness base.html.
"""