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:
@@ -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).
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user