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

@ -15,7 +15,7 @@ jobs:
run: apt-get update && apt-get install -y --no-install-recommends git
- name: Checkout
run: |
git clone --depth=1 --branch="${GITHUB_REF_NAME}" https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE" || git clone --depth=1 https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE"
git clone --depth=1 --branch="${GITHUB_REF_NAME}" https://git.golfcards.club/alee/fieldwitness.git "$GITHUB_WORKSPACE" || git clone --depth=1 https://git.golfcards.club/alee/fieldwitness.git "$GITHUB_WORKSPACE"
- run: pip install ruff black
- name: Check formatting
run: black --check --target-version py312 src/ tests/ frontends/
@ -31,12 +31,12 @@ jobs:
run: apt-get update && apt-get install -y --no-install-recommends git
- name: Checkout
run: |
git clone --depth=1 --branch="${GITHUB_REF_NAME}" https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE" || git clone --depth=1 https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE"
git clone --depth=1 --branch="${GITHUB_REF_NAME}" https://git.golfcards.club/alee/fieldwitness.git "$GITHUB_WORKSPACE" || git clone --depth=1 https://git.golfcards.club/alee/fieldwitness.git "$GITHUB_WORKSPACE"
- run: pip install mypy
- name: Typecheck
run: mypy src/
# TODO: Re-enable once stegasoo/verisoo are available from git.golfcards.club
# TODO: Re-enable once stego/attest are available from git.golfcards.club
# test:
# runs-on: ubuntu-latest
# container:
@ -46,8 +46,8 @@ jobs:
# run: apt-get update && apt-get install -y --no-install-recommends git
# - name: Checkout
# run: |
# git clone --depth=1 --branch="${GITHUB_REF_NAME}" https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE" || git clone --depth=1 https://git.golfcards.club/alee/soosef.git "$GITHUB_WORKSPACE"
# git clone --depth=1 --branch="${GITHUB_REF_NAME}" https://git.golfcards.club/alee/fieldwitness.git "$GITHUB_WORKSPACE" || git clone --depth=1 https://git.golfcards.club/alee/fieldwitness.git "$GITHUB_WORKSPACE"
# - name: Install dependencies
# run: pip install -e ".[dev]"
# - name: Run tests
# run: pytest --cov=soosef --cov-report=term-missing
# run: pytest --cov=fieldwitness --cov-report=term-missing

View File

@ -1,14 +1,19 @@
# SooSeF -- Claude Code Project Guide
# FieldWitness -- Claude Code Project Guide
SooSeF (Soo Security Fieldkit) is an offline-first security toolkit for journalists, NGOs,
and at-risk organizations. Monorepo consolidating Stegasoo and Verisoo as subpackages.
FieldWitness (FieldWitness) is an offline-first provenance attestation and gossip
federation system for journalists, NGOs, and at-risk organizations. It establishes
cryptographic chain-of-custody over evidence in airgapped and resource-constrained
environments, syncs attestations across organizational boundaries via a gossip protocol
with Merkle consistency proofs, and produces court-ready evidence packages with standalone
verification. Steganography (Stego) and provenance attestation (Attest) are included
as subpackages in this monorepo.
Version 0.2.0 · Python >=3.11 · MIT License
Version 0.2.0 · Python >=3.11 · GPL-3.0 License
## Quick commands
```bash
# Development install (single command -- stegasoo and verisoo are inlined subpackages)
# Development install (single command -- stego and attest are inlined subpackages)
pip install -e ".[dev]"
pytest # Run tests
@ -25,20 +30,20 @@ mypy src/ # Type check
## Architecture
```
src/soosef/ Core library
src/fieldwitness/ Core library
__init__.py Package init, __version__ (0.2.0)
_availability.py Runtime checks for optional subpackages (has_stegasoo, has_verisoo)
api.py Optional unified FastAPI app (uvicorn soosef.api:app)
audit.py Append-only JSON-lines audit log (~/.soosef/audit.jsonl)
cli.py Click CLI entry point (soosef command)
paths.py All ~/.soosef/* path constants (single source of truth, lazy resolution)
config.py Unified config loader (SoosefConfig dataclass + JSON)
exceptions.py SoosefError, ChainError, ChainIntegrityError, ChainAppendError, KeystoreError
_availability.py Runtime checks for optional subpackages (has_stego, has_attest)
api.py Optional unified FastAPI app (uvicorn fieldwitness.api:app)
audit.py Append-only JSON-lines audit log (~/.fieldwitness/audit.jsonl)
cli.py Click CLI entry point (fieldwitness command)
paths.py All ~/.fieldwitness/* path constants (single source of truth, lazy resolution)
config.py Unified config loader (FieldWitnessConfig dataclass + JSON)
exceptions.py FieldWitnessError, ChainError, ChainIntegrityError, ChainAppendError, KeystoreError
metadata.py Extract-then-strip EXIF pipeline with field classification
evidence.py Self-contained evidence package export (ZIP with verify.py)
archive.py Cold archive export for long-term preservation (OAIS-aligned)
stegasoo/ Steganography engine (inlined from stegasoo v4.3.0)
stego/ Steganography engine (inlined from fieldwitness.stego v4.3.0)
encode.py / decode.py Core encode/decode API
generate.py Cover image generation
crypto.py AES-256-GCM encryption, channel fingerprints
@ -55,14 +60,14 @@ src/soosef/ Core library
validation.py Input validation
models.py Data models
constants.py Magic bytes, version constants, AUDIO_ENABLED, VIDEO_ENABLED
cli.py Stegasoo-specific CLI commands
api.py / api_auth.py Stegasoo REST API + auth
cli.py Stego-specific CLI commands
api.py / api_auth.py Stego REST API + auth
carrier_tracker.py Carrier image reuse tracking (warns on reuse)
platform_presets.py Social-media-aware encoding presets
image_utils.py / audio_utils.py / video_utils.py
keygen.py / qr_utils.py / recovery.py / debug.py / utils.py
verisoo/ Provenance attestation engine (inlined from verisoo v0.1.0)
attest/ Provenance attestation engine (inlined from fieldwitness.attest v0.1.0)
attestation.py Core attestation creation + EXIF extraction
verification.py Attestation verification
crypto.py Ed25519 signing
@ -75,9 +80,9 @@ src/soosef/ Core library
federation.py GossipNode, HttpTransport, PeerInfo, SyncStatus
peer_store.py SQLite-backed peer persistence for federation
models.py Attestation, AttestationRecord, ImageHashes, Identity
exceptions.py VerisooError, AttestationError, VerificationError, FederationError
cli.py Verisoo-specific CLI commands
api.py Verisoo REST API + federation endpoints
exceptions.py AttestError, AttestationError, VerificationError, FederationError
cli.py Attest-specific CLI commands
api.py Attest REST API + federation endpoints
federation/ Federated attestation chain system
chain.py ChainStore -- append-only hash chain with key rotation/recovery/delivery-ack
@ -103,7 +108,7 @@ frontends/web/ Unified Flask web UI
app.py App factory (create_app()), ~36k -- mounts all blueprints
auth.py SQLite3 multi-user auth with lockout + rate limiting
temp_storage.py File-based temp storage with expiry
subprocess_stego.py Crash-safe subprocess isolation for stegasoo
subprocess_stego.py Crash-safe subprocess isolation for stego
stego_worker.py Background stego processing
stego_routes.py Stego route helpers (~87k)
ssl_utils.py Self-signed HTTPS cert generation (cover_name support)
@ -119,7 +124,7 @@ frontends/web/ Unified Flask web UI
dropbox/admin.html Drop box admin panel
federation/status.html Federation peer dashboard
frontends/cli/ CLI package init (main entry point is src/soosef/cli.py)
frontends/cli/ CLI package init (main entry point is src/fieldwitness/cli.py)
deploy/ Deployment artifacts
docker/ Dockerfile (multi-stage: builder, relay, server) + docker-compose.yml
@ -135,7 +140,7 @@ docs/ Documentation
architecture/
federation.md System architecture overview (threat model, layers, key domains)
chain-format.md Chain record spec (CBOR, entropy witnesses, serialization)
export-bundle.md Export bundle spec (SOOSEFX1 binary format, envelope encryption)
export-bundle.md Export bundle spec (FIELDWITNESSX1 binary format, envelope encryption)
federation-protocol.md Federation server protocol (CT-inspired, gossip, storage tiers)
training/
reporter-quickstart.md One-page reporter quick-start for Tier 1 USB (print + laminate)
@ -160,40 +165,40 @@ Reporter in the field Newsroom / NGO office Friendly jurisdiction
## Dependency model
Stegasoo and Verisoo are inlined subpackages, not separate pip packages:
- `from soosef.stegasoo import encode` for steganography
- `from soosef.verisoo import Attestation` for provenance attestation
- Never `import stegasoo` or `import verisoo` directly
- `_availability.py` provides `has_stegasoo()` / `has_verisoo()` for graceful degradation
Stego and Attest are inlined subpackages, not separate pip packages:
- `from fieldwitness.stego import encode` for steganography
- `from fieldwitness.attest import Attestation` for provenance attestation
- Never `import fieldwitness.stego` or `import fieldwitness.attest` directly
- `_availability.py` provides `has_stego()` / `has_attest()` for graceful degradation
when optional extras are not installed
## Key design decisions
- **Two key domains, never merged**: Stegasoo AES-256-GCM (derived from factors) and
Verisoo Ed25519 (signing identity) are separate security concerns
- **Two key domains, never merged**: Stego AES-256-GCM (derived from factors) and
Attest Ed25519 (signing identity) are separate security concerns
- **Extract-then-strip model**: Stego strips all EXIF (carrier is vessel); attestation
extracts evidentiary EXIF (GPS, timestamp) then strips dangerous fields (device serial)
- **subprocess_stego.py copies verbatim** from stegasoo -- it's a crash-safety boundary
- **All state under ~/.soosef/** -- one directory to back up, one to destroy.
`SOOSEF_DATA_DIR` env var relocates everything (cover mode, USB mode)
- **subprocess_stego.py copies verbatim** from fieldwitness.stego -- it's a crash-safety boundary
- **All state under ~/.fieldwitness/** -- one directory to back up, one to destroy.
`FIELDWITNESS_DATA_DIR` env var relocates everything (cover mode, USB mode)
- **Offline-first**: All static assets vendored, no CDN. pip wheels bundled for airgap install
- **Flask blueprints**: stego, attest, fieldkit, keys, admin, dropbox, federation
- **Flask-WTF**: CSRF protection on all form endpoints; drop box is CSRF-exempt (sources
don't have sessions)
- **Client-side SHA-256**: Drop box upload page uses SubtleCrypto for pre-upload hashing
- **Waitress**: Production WSGI server (replaces dev-only Flask server)
- **FastAPI option**: `soosef.api` provides a REST API alternative to the Flask web UI
- **FastAPI option**: `fieldwitness.api` provides a REST API alternative to the Flask web UI
- **Pluggable backends**: Stego backends (LSB, DCT) registered via `backends/registry.py`
- **ImageHashes generalized**: phash/dhash now optional, enabling non-image attestation
- **Lazy path resolution**: All paths in paths.py resolve lazily via `__getattr__` from
`BASE_DIR` so that runtime overrides (--data-dir, SOOSEF_DATA_DIR) propagate correctly
- **Two-way federation**: Delivery acknowledgment records (`soosef/delivery-ack-v1`)
`BASE_DIR` so that runtime overrides (--data-dir, FIELDWITNESS_DATA_DIR) propagate correctly
- **Two-way federation**: Delivery acknowledgment records (`fieldwitness/delivery-ack-v1`)
enable handshake proof
- **Chain record types** (in federation/chain.py):
- `CONTENT_TYPE_KEY_ROTATION = "soosef/key-rotation-v1"` -- signed by OLD key
- `CONTENT_TYPE_KEY_RECOVERY = "soosef/key-recovery-v1"` -- signed by NEW key
- `CONTENT_TYPE_DELIVERY_ACK = "soosef/delivery-ack-v1"` -- signed by receiver
- **Gossip federation** (verisoo/federation.py): GossipNode with async peer sync,
- `CONTENT_TYPE_KEY_ROTATION = "fieldwitness/key-rotation-v1"` -- signed by OLD key
- `CONTENT_TYPE_KEY_RECOVERY = "fieldwitness/key-recovery-v1"` -- signed by NEW key
- `CONTENT_TYPE_DELIVERY_ACK = "fieldwitness/delivery-ack-v1"` -- signed by receiver
- **Gossip federation** (attest/federation.py): GossipNode with async peer sync,
consistency proofs, HttpTransport over aiohttp. PeerStore for SQLite-backed persistence
- **Threat level presets**: deploy/config-presets/ with low/medium/high/critical configs
- **Selective disclosure**: Chain records can be exported with non-selected records
@ -203,18 +208,18 @@ Stegasoo and Verisoo are inlined subpackages, not separate pip packages:
- **Transport-aware stego**: --transport whatsapp|signal|telegram auto-selects DCT/JPEG
and pre-resizes carrier for platform survival
## Data directory layout (`~/.soosef/`)
## Data directory layout (`~/.fieldwitness/`)
```
~/.soosef/
config.json Unified configuration (SoosefConfig dataclass)
~/.fieldwitness/
config.json Unified configuration (FieldWitnessConfig dataclass)
audit.jsonl Append-only audit trail (JSON-lines)
carrier_history.json Carrier reuse tracking database
identity/ Ed25519 keypair (private.pem, public.pem, identity.meta.json)
archived/ Timestamped old keypairs from rotations
stegasoo/ Channel key (channel.key)
stego/ Channel key (channel.key)
archived/ Timestamped old channel keys from rotations
attestations/ Verisoo attestation store
attestations/ Attest attestation store
log.bin Binary attestation log
index/ LMDB index
peers.json Legacy peer file
@ -223,7 +228,7 @@ Stegasoo and Verisoo are inlined subpackages, not separate pip packages:
chain/ Hash chain (chain.bin, state.cbor)
anchors/ External timestamp anchors (JSON files)
auth/ Web UI auth databases
soosef.db User accounts
fieldwitness.db User accounts
dropbox.db Drop box tokens + receipts
certs/ Self-signed TLS certificates (cert.pem, key.pem)
fieldkit/ Fieldkit state
@ -253,4 +258,4 @@ pytest tests/test_chain.py # Chain-specific
Test files: `test_chain.py`, `test_chain_security.py`, `test_deadman_enforcement.py`,
`test_key_rotation.py`, `test_killswitch.py`, `test_serialization.py`,
`test_stegasoo_audio.py`, `test_stegasoo.py`, `test_verisoo_hashing.py`
`test_stego_audio.py`, `test_stego.py`, `test_attest_hashing.py`

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

519
README.md
View File

@ -1,42 +1,54 @@
# SooSeF -- Soo Security Fieldkit
# FieldWitness -- FieldWitness
**Offline-first security toolkit for journalists, NGOs, and at-risk organizations.**
**Offline-first provenance attestation with gossip federation for journalists, NGOs, and at-risk organizations.**
<!-- badges -->
![Version](https://img.shields.io/badge/version-0.2.0-blue)
![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)
![License](https://img.shields.io/badge/license-MIT-green)
![License](https://img.shields.io/badge/license-GPL--3.0-blue)
---
## What is SooSeF?
## What is FieldWitness?
SooSeF combines steganography, provenance attestation, and field security tools into a
single package designed for airgapped and resource-constrained environments. It lets you:
FieldWitness is a field-deployable evidence integrity system. It lets journalists, human rights
documenters, and NGOs establish cryptographic chain-of-custody over photos, documents, and
sensor data -- in airgapped environments, across organizational boundaries, and in adversarial
conditions where data may need to be destroyed on demand.
- **Hide messages** in images, audio, and video using multiple steganographic techniques, with transport-aware encoding for lossy channels (WhatsApp, Signal, Telegram)
- **Prove authenticity** of photos, documents, and arbitrary files with Ed25519 signatures, Merkle-style hash chains, and RFC 3161 trusted timestamps
- **Protect data in the field** with a killswitch (including deep forensic scrub and self-uninstall), dead man's switch with webhook warnings, tamper detection, USB device whitelisting, and GPS geofencing
- **Manage cryptographic keys** with identity rotation, channel key generation, encrypted key bundle export/import, QR code sharing, trust store management, and identity recovery from chain
- **Federate attestations** across organizations with signed exchange bundles, delivery acknowledgments, selective disclosure for legal discovery, and investigation namespaces
- **Accept anonymous submissions** through a SecureDrop-style source drop box with token-gated uploads, client-side SHA-256 hashing, and automatic EXIF extraction/stripping
- **Preserve evidence long-term** with self-contained evidence packages and OAIS-aligned cold archives that include standalone verification scripts and algorithm documentation
The core claim: a file attested with FieldWitness can be handed to a court or partner organization
with proof that it has not been altered, proof of when it was captured (anchored externally
via RFC 3161), and proof of who held it -- without requiring the verifying party to install
FieldWitness or trust any central authority.
Stegasoo (steganography, v4.3.0) and Verisoo (attestation, v0.1.0) are included as
subpackages (`import soosef.stegasoo`, `import soosef.verisoo`). Everything ships as one
install: `pip install soosef`.
**Three-tier deployment model:**
```
Tier 1: Field Device Tier 2: Org Server Tier 3: Federation Relay
(Bootable USB + laptop) (Docker on mini PC / VPS) (Docker on VPS)
Reporter in the field Newsroom / NGO office Friendly jurisdiction
Amnesic, LUKS-encrypted Persistent storage Attestation sync only
Pull USB = zero trace Web UI + federation API Zero knowledge of keys
\ | /
\_____ sneakernet ____+____ gossip API ____/
```
Stego (steganography, v4.3.0) and Attest (attestation, v0.1.0) are included as
subpackages (`import fieldwitness.stego`, `import fieldwitness.attest`). Everything ships as one
install: `pip install fieldwitness`.
---
## Quick Start
```bash
pip install "soosef[web,cli]"
soosef init
soosef serve
pip install "fieldwitness[web,cli]"
fieldwitness init
fieldwitness serve
```
This creates the `~/.soosef/` directory structure, generates an Ed25519 identity and
This creates the `~/.fieldwitness/` directory structure, generates an Ed25519 identity and
channel key, writes a default config, and starts an HTTPS web UI on
`https://127.0.0.1:5000`.
@ -44,58 +56,46 @@ channel key, writes a default config, and starts an HTTPS web UI on
## Features
### Steganography (Stegasoo)
- **LSB encoding** -- bit-level message hiding in PNG images
- **DCT encoding** -- frequency-domain hiding in JPEG images (requires `stego-dct` extra)
- **Audio steganography** -- hide data in WAV/FLAC audio (requires `stego-audio` extra)
- **Video steganography** -- frame-level encoding
- **Transport-aware encoding** -- `--transport whatsapp|signal|telegram|discord|email|direct` auto-selects the right encoding mode and carrier resolution for lossy messaging platforms. WhatsApp/Signal/Telegram force DCT/JPEG mode and pre-resize the carrier to survive recompression
- **Carrier reuse tracking** -- warns when a carrier image has been used before, since comparing two versions of the same carrier trivially reveals steganographic modification
- AES-256-GCM encryption with Argon2id key derivation
- EXIF stripping on encode to prevent metadata leakage
- Compression support (zstandard, optional LZ4)
### Attestation (Verisoo)
### Attestation and Provenance (Attest)
- Ed25519 digital signatures for images and arbitrary files (CSV, documents, sensor data)
- Perceptual hashing (pHash, dHash) for tamper-evident photo attestation; SHA-256-only mode for non-image files
- Perceptual hashing (pHash, dHash) for tamper-evident photo attestation -- identifies
re-uploaded or re-compressed copies. SHA-256-only mode for non-image files
- Append-only hash chain (CBOR-encoded) with Merkle tree verification -- every attestation
is chained to all prior attestations, making retroactive tampering detectable
- LMDB-backed attestation storage
- Append-only hash chain (CBOR-encoded) with Merkle tree verification
- Batch attestation for directories
- **Investigation namespaces** -- tag and filter attestations by case or project
- **Derived work lineage** -- parent-child attestation tracking for editorial workflows
- **Chain position proof** -- verification receipts include the record's position in the hash chain
- **Chain position proof** -- verification receipts include the record's position in the
hash chain
### Extract-Then-Strip EXIF Pipeline
Resolves the tension between steganography (strip everything to protect sources) and
attestation (preserve everything to prove provenance):
Resolves the tension between protecting sources (strip everything) and proving provenance
(preserve everything):
1. Extract all EXIF metadata from the original image bytes
2. Classify fields as **evidentiary** (GPS coordinates, timestamp -- valuable for provenance) or **dangerous** (device serial number, firmware version -- could identify the source)
2. Classify fields as **evidentiary** (GPS coordinates, timestamp -- valuable for
provenance) or **dangerous** (device serial number, firmware version -- could identify
the source)
3. Preserve evidentiary fields in the attestation record
4. Strip all metadata from the stored/display copy
### Cross-Organization Federation
### Gossip Federation
- **Attestation exchange** -- export signed bundles of attestation records and chain data for offline transfer to partner organizations
- **Delivery acknowledgments** -- when an organization imports a bundle, a `soosef/delivery-ack-v1` chain record is signed and can be shared back, creating a two-way federation handshake
- **Trust store** -- import collaborator Ed25519 public keys; only records signed by trusted keys are imported during federation
- **Investigation filtering** -- export/import only records tagged with a specific investigation
### External Timestamp Anchoring
Two mechanisms to externally prove that the chain head existed before a given time:
- **RFC 3161 TSA** -- automated submission to any RFC 3161 Timestamping Authority (e.g., FreeTSA). The signed timestamp token is saved alongside the chain
- **Manual anchors** -- export the chain head hash as a compact string for manual submission to any external witness (blockchain transaction, newspaper classified, tweet, email to a TSA)
A single anchor for the chain head implicitly timestamps every record that preceded it, because the chain is append-only with hash linkage.
### Selective Disclosure
Produce verifiable proofs for specific chain records while keeping others redacted. Selected records are included in full; non-selected records appear only as hashes. A third party can verify that the disclosed records are part of an unbroken chain without seeing the contents of other records. Designed for legal discovery, court orders, and FOIA responses.
- **Gossip protocol** -- nodes periodically exchange Merkle roots with peers. Divergence
triggers a consistency proof request and incremental record fetch. No central
coordinator, no consensus, no leader election -- just append-only logs that converge
- **Attestation exchange** -- export signed bundles of attestation records and chain data
for offline transfer (sneakernet) to partner organizations
- **Delivery acknowledgments** -- when an organization imports a bundle, a
`fieldwitness/delivery-ack-v1` chain record is signed and can be shared back, creating a
two-way federation handshake
- **Trust store** -- import collaborator Ed25519 public keys; only records signed by
trusted keys are imported during federation
- **Investigation filtering** -- export/import only records tagged with a specific
investigation
### Evidence Packages
@ -105,12 +105,14 @@ Self-contained ZIP bundles for handing evidence to lawyers, courts, or archives:
- Attestation records with full Ed25519 signatures
- Chain segment with hash linkage
- Signer's public key
- `verify.py` -- standalone verification script that requires only Python 3.11+ and the `cryptography` pip package (no SooSeF installation needed)
- `verify.py` -- standalone verification script that requires only Python 3.11+ and the
`cryptography` pip package (no FieldWitness installation needed)
- Human-readable README
### Cold Archive
Full-state export for long-term evidence preservation (10+ year horizon), aligned with OAIS (ISO 14721):
Full-state export for long-term evidence preservation (10+ year horizon), aligned with
OAIS (ISO 14721):
- Raw chain binary and state checkpoint
- Attestation log and LMDB index
@ -121,40 +123,95 @@ Full-state export for long-term evidence preservation (10+ year horizon), aligne
- `verify.py` standalone verifier
- `manifest.json` with SHA-256 integrity hashes of key files
### External Timestamp Anchoring
Two mechanisms to prove the chain head existed before a given time:
- **RFC 3161 TSA** -- automated submission to any RFC 3161 Timestamping Authority (e.g.,
FreeTSA). The signed timestamp token is saved alongside the chain
- **Manual anchors** -- export the chain head hash as a compact string for manual
submission to any external witness (blockchain transaction, newspaper classified, email
to a TSA)
A single anchor for the chain head implicitly timestamps every record that preceded it,
because the chain is append-only with hash linkage.
### Selective Disclosure
Produce verifiable proofs for specific chain records while keeping others redacted.
Selected records are included in full; non-selected records appear only as hashes. A third
party can verify that the disclosed records are part of an unbroken chain without seeing
the contents of other records. Designed for legal discovery, court orders, and FOIA
responses.
### Field Security (Fieldkit)
- **Killswitch** -- emergency destruction of all data under `~/.fieldwitness/`, ordered by
sensitivity (keys first, then data, then logs). Includes:
- **Deep forensic scrub** -- removes `__pycache__`, `.pyc`, pip `dist-info`, pip
download cache, and scrubs shell history entries containing "fieldwitness"
- **Self-uninstall** -- runs `pip uninstall -y fieldwitness` as the final step
- **System log clearing** -- best-effort journald vacuum on Linux
- **Dead man's switch** -- automated purge if check-in is missed, with a configurable
grace period. During the grace period, a **webhook warning** is sent (POST to a
configured URL) and a local warning file is written before the killswitch fires
- **Tamper detection** -- file integrity monitoring with baseline snapshots
- **USB whitelist** -- block or alert on unauthorized USB devices (Linux/pyudev)
- **Geofence** -- GPS boundary enforcement with configurable radius. Supports live GPS via
**gpsd** (`get_current_location()` connects to `127.0.0.1:2947`)
- **Hardware killswitch** -- GPIO pin monitoring for Raspberry Pi physical button
(configurable pin and hold duration)
### Source Drop Box
SecureDrop-style anonymous intake built into the SooSeF web UI:
SecureDrop-style anonymous intake built into the FieldWitness web UI:
- Admin creates a time-limited upload token with a configurable file limit
- Source opens the token URL (no account or SooSeF branding -- source safety)
- **Client-side SHA-256** via SubtleCrypto runs in the browser before upload, so the source can independently verify what they submitted
- Source opens the token URL (no account or FieldWitness branding -- source safety)
- **Client-side SHA-256** via SubtleCrypto runs in the browser before upload, so the
source can independently verify what they submitted
- Files are run through the extract-then-strip EXIF pipeline and auto-attested on receipt
- Source receives HMAC-derived receipt codes that prove delivery
- Tokens and receipts are stored in SQLite; tokens auto-expire
### Key Rotation and Recovery
- **Key rotation** -- both identity (Ed25519) and channel (AES) keys can be rotated. The chain records the rotation as a `soosef/key-rotation-v1` record signed by the OLD key, creating a cryptographic trust chain
- **Identity recovery** -- after device loss, a new key can be introduced via a `soosef/key-recovery-v1` chain record. The record carries the old fingerprint and optional cosigner fingerprints for audit
- **Channel key only export** -- share just the channel key (not identity keys) with collaborators via encrypted file or QR code (`soosef-channel:` URI scheme)
- **Key rotation** -- both identity (Ed25519) and channel (AES) keys can be rotated. The
chain records the rotation as a `fieldwitness/key-rotation-v1` record signed by the OLD key,
creating a cryptographic trust chain
- **Identity recovery** -- after device loss, a new key can be introduced via a
`fieldwitness/key-recovery-v1` chain record. The record carries the old fingerprint and
optional cosigner fingerprints for audit
- **Channel key only export** -- share just the channel key (not identity keys) with
collaborators via encrypted file or QR code (`fieldwitness-channel:` URI scheme)
- **Backup tracking** -- records when the last backup was taken and warns when overdue
### Fieldkit
- **Killswitch** -- emergency destruction of all data under `~/.soosef/`, ordered by sensitivity (keys first, then data, then logs). Includes:
- **Deep forensic scrub** -- removes `__pycache__`, `.pyc`, pip `dist-info`, pip download cache, and scrubs shell history entries containing "soosef"
- **Self-uninstall** -- runs `pip uninstall -y soosef` as the final step
- **System log clearing** -- best-effort journald vacuum on Linux
- **Dead man's switch** -- automated purge if check-in is missed, with a configurable grace period. During the grace period, a **webhook warning** is sent (POST to a configured URL) and a local warning file is written before the killswitch fires
- **Tamper detection** -- file integrity monitoring with baseline snapshots
- **USB whitelist** -- block or alert on unauthorized USB devices (Linux/pyudev)
- **Geofence** -- GPS boundary enforcement with configurable radius. Supports live GPS via **gpsd** (`get_current_location()` connects to `127.0.0.1:2947`)
- **Hardware killswitch** -- GPIO pin monitoring for Raspberry Pi physical button (configurable pin and hold duration)
### Cover / Duress Mode
- **Configurable certificate CN** -- set `cover_name` in config to replace "SooSeF Local" in the self-signed TLS certificate
- **Portable data directory** -- set `SOOSEF_DATA_DIR` to relocate all state to an arbitrary path (e.g., an innocuously named directory on a USB stick). All paths resolve lazily from `BASE_DIR`, so runtime overrides propagate correctly
- **Configurable certificate CN** -- set `cover_name` in config to replace "FieldWitness Local"
in the self-signed TLS certificate
- **Portable data directory** -- set `FIELDWITNESS_DATA_DIR` to relocate all state to an
arbitrary path (e.g., an innocuously named directory on a USB stick). All paths resolve
lazily from `BASE_DIR`, so runtime overrides propagate correctly
### Additional Tools: Steganography (Stego)
For situations requiring covert channels -- hiding communications or small payloads inside
ordinary media files:
- **LSB encoding** -- bit-level message hiding in PNG images
- **DCT encoding** -- frequency-domain hiding in JPEG images (requires `stego-dct` extra)
- **Audio steganography** -- hide data in WAV/FLAC audio (requires `stego-audio` extra)
- **Video steganography** -- frame-level encoding
- **Transport-aware encoding** -- `--transport whatsapp|signal|telegram|discord|email|direct`
auto-selects the right encoding mode and carrier resolution for lossy messaging
platforms. WhatsApp/Signal/Telegram force DCT/JPEG mode and pre-resize the carrier to
survive recompression
- **Carrier reuse tracking** -- warns when a carrier image has been used before, since
comparing two versions of the same carrier trivially reveals steganographic modification
- AES-256-GCM encryption with Argon2id key derivation
- EXIF stripping on encode to prevent metadata leakage
- Compression support (zstandard, optional LZ4)
---
@ -163,15 +220,15 @@ SecureDrop-style anonymous intake built into the SooSeF web UI:
### Basic install (core library only)
```bash
pip install soosef
pip install fieldwitness
```
### With extras
```bash
pip install "soosef[web,cli]" # Web UI + CLI (most common)
pip install "soosef[all]" # Everything except dev tools
pip install "soosef[dev]" # All + pytest, black, ruff, mypy
pip install "fieldwitness[web,cli]" # Web UI + CLI (most common)
pip install "fieldwitness[all]" # Everything except dev tools
pip install "fieldwitness[dev]" # All + pytest, black, ruff, mypy
```
### Available extras
@ -197,35 +254,24 @@ Bundle wheels on a networked machine, then install offline:
```bash
# On networked machine
pip download "soosef[web,cli]" -d ./wheels
pip download "fieldwitness[web,cli]" -d ./wheels
# Transfer ./wheels to target via USB
# On airgapped machine
pip install --no-index --find-links=./wheels "soosef[web,cli]"
soosef init
soosef serve --host 0.0.0.0
pip install --no-index --find-links=./wheels "fieldwitness[web,cli]"
fieldwitness init
fieldwitness serve --host 0.0.0.0
```
---
## Deployment
SooSeF uses a three-tier deployment model designed for field journalism, organizational
FieldWitness uses a three-tier deployment model designed for field journalism, organizational
evidence management, and cross-organization federation.
```
Tier 1: Field Device Tier 2: Org Server Tier 3: Federation Relay
(Bootable USB + laptop) (Docker on mini PC / VPS) (Docker on VPS)
Reporter in the field Newsroom / NGO office Friendly jurisdiction
Amnesic, LUKS-encrypted Persistent storage Attestation sync only
Pull USB = zero trace Web UI + federation API Zero knowledge of keys
\ | /
\_____ sneakernet ____+____ gossip API ____/
```
**Tier 1 -- Field Device.** A bootable Debian Live USB stick. Boots into a minimal desktop
with Firefox pointed at the local SooSeF web UI. LUKS-encrypted persistent partition. Pull
with Firefox pointed at the local FieldWitness web UI. LUKS-encrypted persistent partition. Pull
the USB and the host machine retains nothing.
**Tier 2 -- Org Server.** A Docker deployment on a mini PC or trusted VPS. Runs the full
@ -251,7 +297,7 @@ cd deploy/docker && docker compose up relay -d
### Threat level configuration presets
SooSeF ships four configuration presets at `deploy/config-presets/`:
FieldWitness ships four configuration presets at `deploy/config-presets/`:
| Preset | Session | Killswitch | Dead Man | Cover Name |
|---|---|---|---|---|
@ -261,7 +307,7 @@ SooSeF ships four configuration presets at `deploy/config-presets/`:
| `critical-threat.json` | 3 min | On | 6h / 1h grace | "System Statistics" |
```bash
cp deploy/config-presets/high-threat.json ~/.soosef/config.json
cp deploy/config-presets/high-threat.json ~/.fieldwitness/config.json
```
See [docs/deployment.md](docs/deployment.md) for the full deployment guide including
@ -271,22 +317,22 @@ security hardening, Kubernetes manifests, systemd services, and operational secu
## CLI Reference
All commands accept `--data-dir PATH` to override the default `~/.soosef` directory,
All commands accept `--data-dir PATH` to override the default `~/.fieldwitness` directory,
and `--json` for machine-readable output.
```
soosef [--data-dir PATH] [--json] COMMAND
fieldwitness [--data-dir PATH] [--json] COMMAND
```
### Core commands
| Command | Description |
|---|---|
| `soosef init` | Create directory structure, generate identity + channel key, write default config |
| `soosef serve` | Start the web UI (default: `https://127.0.0.1:5000`) |
| `soosef status` | Show instance status: identity, keys, chain, fieldkit, config |
| `fieldwitness init` | Create directory structure, generate identity + channel key, write default config |
| `fieldwitness serve` | Start the web UI (default: `https://127.0.0.1:5000`) |
| `fieldwitness status` | Show instance status: identity, keys, chain, fieldkit, config |
### `soosef serve` options
### `fieldwitness serve` options
| Option | Default | Description |
|---|---|---|
@ -296,9 +342,70 @@ soosef [--data-dir PATH] [--json] COMMAND
| `--debug` | off | Use Flask dev server instead of Waitress |
| `--workers` | `4` | Number of Waitress/Gunicorn worker threads |
### Steganography commands (`soosef stego`)
### Attestation commands (`fieldwitness attest`)
Stegasoo uses multi-factor authentication: a **reference photo** (shared image both
```bash
# Attest an image (sign with Ed25519 identity)
fieldwitness attest IMAGE photo.jpg
fieldwitness attest IMAGE photo.jpg --caption "Field report" --location "Istanbul"
# Batch attest a directory
fieldwitness attest batch ./photos/ --caption "Field report"
# Verify an image against the attestation log
fieldwitness attest verify photo.jpg
# View attestation log
fieldwitness attest log --limit 20
```
### Chain commands (`fieldwitness chain`)
```bash
fieldwitness chain status # Show chain head, length, integrity
fieldwitness chain verify # Verify entire chain integrity (hashes + signatures)
fieldwitness chain show INDEX # Show a specific chain record
fieldwitness chain log --count 20 # Show recent chain entries
fieldwitness chain backfill # Backfill existing attestations into chain
# Evidence export
fieldwitness chain export --start 0 --end 100 -o chain.zip
# Selective disclosure (for legal discovery / court orders)
fieldwitness chain disclose -i 5,12,47 -o disclosure.json
# External timestamp anchoring
fieldwitness chain anchor # Manual anchor (prints hash for tweet/email/blockchain)
fieldwitness chain anchor --tsa https://freetsa.org/tsr # RFC 3161 automated anchor
```
### Fieldkit commands (`fieldwitness fieldkit`)
```bash
fieldwitness fieldkit status # Show fieldkit state
fieldwitness fieldkit checkin # Reset dead man's switch timer
fieldwitness fieldkit check-deadman # Check if deadman timer has expired
fieldwitness fieldkit purge --confirm # Activate killswitch (destroys all data)
fieldwitness fieldkit geofence set --lat 48.8566 --lon 2.3522 --radius 1000
fieldwitness fieldkit geofence check --lat 48.8600 --lon 2.3500
fieldwitness fieldkit geofence clear
fieldwitness fieldkit usb snapshot # Snapshot current USB devices as whitelist
fieldwitness fieldkit usb check # Check for unauthorized USB devices
```
### Key management commands (`fieldwitness keys`)
```bash
fieldwitness keys show # Display current key info
fieldwitness keys export -o backup.enc # Export encrypted key bundle
fieldwitness keys import -b backup.enc # Import key bundle
fieldwitness keys rotate-identity # Generate new Ed25519 identity (records rotation in chain)
fieldwitness keys rotate-channel # Generate new channel key
```
### Steganography commands (`fieldwitness stego`)
Stego uses multi-factor authentication: a **reference photo** (shared image both
parties have), a **passphrase** (4+ words), and a **PIN** (6-9 digits). All three are
required to encode or decode. The passphrase and PIN are prompted interactively
(hidden input) if not provided via options.
@ -306,134 +413,73 @@ required to encode or decode. The passphrase and PIN are prompted interactively
```bash
# Encode a text message into an image
# CARRIER is the image to hide data in, -r is the shared reference photo
soosef stego encode cover.png -r shared_photo.jpg -m "Secret message"
fieldwitness stego encode cover.png -r shared_photo.jpg -m "Secret message"
# Passphrase: **** (prompted, hidden)
# PIN: **** (prompted, hidden)
# -> writes encoded PNG to current directory
# Encode with explicit output path
soosef stego encode cover.png -r shared_photo.jpg -m "Secret" -o stego_output.png
fieldwitness stego encode cover.png -r shared_photo.jpg -m "Secret" -o stego_output.png
# Encode a file instead of text
soosef stego encode cover.png -r shared_photo.jpg -f document.pdf
fieldwitness stego encode cover.png -r shared_photo.jpg -f document.pdf
# Transport-aware encoding (auto-selects DCT/JPEG and resizes for the platform)
soosef stego encode cover.jpg -r shared.jpg -m "Secret" --transport whatsapp
soosef stego encode cover.jpg -r shared.jpg -m "Secret" --transport signal
soosef stego encode cover.jpg -r shared.jpg -m "Secret" --transport telegram
soosef stego encode cover.jpg -r shared.jpg -m "Secret" --transport email
soosef stego encode cover.jpg -r shared.jpg -m "Secret" --transport direct
fieldwitness stego encode cover.jpg -r shared.jpg -m "Secret" --transport whatsapp
fieldwitness stego encode cover.jpg -r shared.jpg -m "Secret" --transport signal
fieldwitness stego encode cover.jpg -r shared.jpg -m "Secret" --transport telegram
fieldwitness stego encode cover.jpg -r shared.jpg -m "Secret" --transport email
fieldwitness stego encode cover.jpg -r shared.jpg -m "Secret" --transport direct
# Dry run -- check capacity without encoding
soosef stego encode cover.png -r shared_photo.jpg -m "Secret" --dry-run
fieldwitness stego encode cover.png -r shared_photo.jpg -m "Secret" --dry-run
# Decode a message from a stego image (same reference + passphrase + PIN)
soosef stego decode stego_output.png -r shared_photo.jpg
fieldwitness stego decode stego_output.png -r shared_photo.jpg
# Passphrase: ****
# PIN: ****
# -> prints decoded message or saves decoded file
# Decode and save file payload to specific path
soosef stego decode stego_output.png -r shared_photo.jpg -o recovered.pdf
fieldwitness stego decode stego_output.png -r shared_photo.jpg -o recovered.pdf
# DCT mode for JPEG (survives social media compression)
soosef stego encode cover.jpg -r shared_photo.jpg -m "Secret" --platform telegram
fieldwitness stego encode cover.jpg -r shared_photo.jpg -m "Secret" --platform telegram
# Audio steganography
soosef stego audio-encode audio.wav -r shared_photo.jpg -m "Hidden in audio"
soosef stego audio-decode stego.wav -r shared_photo.jpg
fieldwitness stego audio-encode audio.wav -r shared_photo.jpg -m "Hidden in audio"
fieldwitness stego audio-decode stego.wav -r shared_photo.jpg
# Generate credentials
soosef stego generate # Generate passphrase + PIN
soosef stego generate --pin-length 8 # Longer PIN
fieldwitness stego generate # Generate passphrase + PIN
fieldwitness stego generate --pin-length 8 # Longer PIN
# Channel key management
soosef stego channel status # Show current channel key
soosef stego channel generate # Generate new channel key
fieldwitness stego channel status # Show current channel key
fieldwitness stego channel generate # Generate new channel key
# Image info and capacity
soosef stego info cover.png # Image details + LSB/DCT capacity
```
### Attestation commands (`soosef attest`)
```bash
# Attest an image (sign with Ed25519 identity)
soosef attest IMAGE photo.jpg
soosef attest IMAGE photo.jpg --caption "Field report" --location "Istanbul"
# Batch attest a directory
soosef attest batch ./photos/ --caption "Field report"
# Verify an image against the attestation log
soosef attest verify photo.jpg
# View attestation log
soosef attest log --limit 20
```
### Fieldkit commands (`soosef fieldkit`)
```bash
soosef fieldkit status # Show fieldkit state
soosef fieldkit checkin # Reset dead man's switch timer
soosef fieldkit check-deadman # Check if deadman timer has expired
soosef fieldkit purge --confirm # Activate killswitch (destroys all data)
soosef fieldkit geofence set --lat 48.8566 --lon 2.3522 --radius 1000
soosef fieldkit geofence check --lat 48.8600 --lon 2.3500
soosef fieldkit geofence clear
soosef fieldkit usb snapshot # Snapshot current USB devices as whitelist
soosef fieldkit usb check # Check for unauthorized USB devices
```
### Key management commands (`soosef keys`)
```bash
soosef keys show # Display current key info
soosef keys export -o backup.enc # Export encrypted key bundle
soosef keys import -b backup.enc # Import key bundle
soosef keys rotate-identity # Generate new Ed25519 identity (records rotation in chain)
soosef keys rotate-channel # Generate new channel key
```
### Chain commands (`soosef chain`)
```bash
soosef chain status # Show chain head, length, integrity
soosef chain verify # Verify entire chain integrity (hashes + signatures)
soosef chain show INDEX # Show a specific chain record
soosef chain log --count 20 # Show recent chain entries
soosef chain backfill # Backfill existing attestations into chain
# Evidence export
soosef chain export --start 0 --end 100 -o chain.zip
# Selective disclosure (for legal discovery / court orders)
soosef chain disclose -i 5,12,47 -o disclosure.json
# External timestamp anchoring
soosef chain anchor # Manual anchor (prints hash for tweet/email/blockchain)
soosef chain anchor --tsa https://freetsa.org/tsr # RFC 3161 automated anchor
fieldwitness stego info cover.png # Image details + LSB/DCT capacity
```
---
## Web UI
Start with `soosef serve`. The web UI provides authenticated access to all features
Start with `fieldwitness serve`. The web UI provides authenticated access to all features
through Flask blueprints. Served by **Waitress** (production WSGI server) by default.
### Routes
| Blueprint | Routes | Description |
|---|---|---|
| stego | `/encode`, `/decode`, `/generate` | Steganography operations |
| attest | `/attest`, `/verify` | Attestation signing and verification |
| federation | `/federation/*` | Federation peer dashboard, peer add/remove |
| fieldkit | `/fieldkit/*` | Killswitch, dead man's switch, status dashboard |
| keys | `/keys/*` | Key management, rotation, export/import |
| admin | `/admin/*` | User management (multi-user auth via SQLite) |
| dropbox | `/dropbox/admin`, `/dropbox/upload/<token>` | Source drop box: token creation (admin), anonymous upload (source), receipt verification |
| federation | `/federation/*` | Federation peer dashboard, peer add/remove |
| stego | `/encode`, `/decode`, `/generate` | Steganography operations |
| health | `/health` | Capability reporting endpoint (see API section) |
<!-- TODO: screenshots -->
@ -442,8 +488,8 @@ through Flask blueprints. Served by **Waitress** (production WSGI server) by def
## Configuration
SooSeF loads configuration from `~/.soosef/config.json`. All fields have sensible defaults.
`soosef init` writes the default config file.
FieldWitness loads configuration from `~/.fieldwitness/config.json`. All fields have sensible defaults.
`fieldwitness init` writes the default config file.
### Config fields
@ -457,7 +503,7 @@ SooSeF loads configuration from `~/.soosef/config.json`. All fields have sensibl
| `session_timeout_minutes` | int | `15` | Session expiry |
| `login_lockout_attempts` | int | `5` | Failed logins before lockout |
| `login_lockout_minutes` | int | `15` | Lockout duration |
| `default_embed_mode` | string | `auto` | Stegasoo encoding mode |
| `default_embed_mode` | string | `auto` | Stego encoding mode |
| `killswitch_enabled` | bool | `false` | Enable killswitch functionality |
| `deadman_enabled` | bool | `false` | Enable dead man's switch |
| `deadman_interval_hours` | int | `24` | Check-in interval |
@ -468,7 +514,7 @@ SooSeF loads configuration from `~/.soosef/config.json`. All fields have sensibl
| `chain_enabled` | bool | `true` | Enable attestation hash chain |
| `chain_auto_wrap` | bool | `true` | Auto-wrap attestations in chain records |
| `backup_reminder_days` | int | `7` | Days before backup reminder |
| `cover_name` | string | `""` | If set, used for SSL cert CN instead of "SooSeF Local" (cover/duress mode) |
| `cover_name` | string | `""` | If set, used for SSL cert CN instead of "FieldWitness Local" (cover/duress mode) |
| `gpio_killswitch_pin` | int | `17` | Raspberry Pi GPIO pin for hardware killswitch |
| `gpio_killswitch_hold_seconds` | float | `5.0` | Hold duration to trigger hardware killswitch |
@ -476,7 +522,7 @@ SooSeF loads configuration from `~/.soosef/config.json`. All fields have sensibl
| Variable | Description |
|---|---|
| `SOOSEF_DATA_DIR` | Override the data directory (default: `~/.soosef`). Enables portable USB mode and cover/duress directory naming |
| `FIELDWITNESS_DATA_DIR` | Override the data directory (default: `~/.fieldwitness`). Enables portable USB mode and cover/duress directory naming |
---
@ -485,19 +531,16 @@ SooSeF loads configuration from `~/.soosef/config.json`. All fields have sensibl
### Source layout
```
src/soosef/
src/fieldwitness/
__init__.py Package init, __version__
cli.py Click CLI (entry point: soosef)
cli.py Click CLI (entry point: fieldwitness)
paths.py All path constants (lazy resolution from BASE_DIR)
config.py Unified config loader (dataclass + JSON)
exceptions.py SoosefError base exception
exceptions.py FieldWitnessError base exception
metadata.py Extract-then-strip EXIF pipeline
evidence.py Self-contained evidence package export
archive.py Cold archive for long-term preservation (OAIS-aligned)
stegasoo/ Steganography engine (subpackage)
encode.py Transport-aware encoding (--transport flag)
carrier_tracker.py Carrier reuse tracking and warnings
verisoo/ Attestation engine (subpackage)
attest/ Attestation engine (subpackage)
models.py ImageHashes (images + arbitrary files), AttestationRecord
keystore/
manager.py Key material management (channel + identity + trust store + backup)
@ -513,6 +556,9 @@ src/soosef/
tamper.py File integrity monitoring
usb_monitor.py USB device whitelist (Linux/pyudev)
geofence.py GPS boundary enforcement (gpsd integration)
stego/ Steganography engine (subpackage)
encode.py Transport-aware encoding (--transport flag)
carrier_tracker.py Carrier reuse tracking and warnings
frontends/web/
app.py Flask app factory (create_app())
@ -521,13 +567,13 @@ frontends/web/
subprocess_stego.py Crash-safe subprocess isolation
ssl_utils.py Self-signed HTTPS cert generation
blueprints/
stego.py /encode, /decode, /generate
attest.py /attest, /verify
federation.py /federation/* (peer dashboard)
fieldkit.py /fieldkit/*
keys.py /keys/*
admin.py /admin/*
dropbox.py /dropbox/* (source drop box)
federation.py /federation/* (peer dashboard)
stego.py /encode, /decode, /generate
deploy/ Deployment artifacts
docker/ Dockerfile (multi-stage: builder, relay, server) + compose
@ -536,18 +582,18 @@ deploy/ Deployment artifacts
config-presets/ Threat level presets (low/medium/high/critical)
```
### Data directory (`~/.soosef/`)
### Data directory (`~/.fieldwitness/`)
```
~/.soosef/
~/.fieldwitness/
config.json Unified configuration
audit.jsonl Append-only audit trail
carrier_history.json Carrier reuse tracking database
identity/ Ed25519 keypair (private.pem, public.pem, identity.meta.json)
stegasoo/ Channel key (channel.key)
attestations/ Verisoo attestation store (log.bin, index/, peers.json)
stego/ Channel key (channel.key)
attestations/ Attest attestation store (log.bin, index/, peers.json)
chain/ Hash chain (chain.bin, state.cbor, anchors/)
auth/ Web UI auth database (soosef.db, dropbox.db)
auth/ Web UI auth database (fieldwitness.db, dropbox.db)
certs/ Self-signed TLS certificates
fieldkit/ Fieldkit state (deadman.json, tamper/, usb/, geofence.json)
temp/ Ephemeral file storage (dropbox uploads)
@ -562,15 +608,15 @@ Sensitive directories (`identity/`, `auth/`, `certs/`, and the root) are created
## Security Model
**Two key domains, never merged.** Stegasoo uses AES-256-GCM with keys derived via
Argon2id from user-supplied factors. Verisoo uses Ed25519 for signing. These serve
**Two key domains, never merged.** Stego uses AES-256-GCM with keys derived via
Argon2id from user-supplied factors. Attest uses Ed25519 for signing. These serve
different security purposes and are kept strictly separate.
**Killswitch priority.** The killswitch destroys all data under `~/.soosef/`, including
**Killswitch priority.** The killswitch destroys all data under `~/.fieldwitness/`, including
the audit log. This is intentional -- in a field compromise scenario, data destruction
takes precedence over audit trail preservation. The deep forensic scrub extends beyond
the data directory to remove Python bytecache, pip metadata, pip download cache, shell
history entries, and the soosef package itself.
history entries, and the fieldwitness package itself.
**Offline-first.** All static assets are vendored (no CDN calls). Pip wheels can be
bundled for fully airgapped installation. No network access is required for any core
@ -598,16 +644,30 @@ specific records without accessing the full chain.
## Cross-Domain Applications
While SooSeF was designed for journalist and NGO field security, the attestation chain,
While FieldWitness was designed for journalist and NGO field security, the attestation chain,
federation, and evidence packaging capabilities apply to a range of domains:
- **Human rights documentation** -- field workers attest photos and videos of incidents with GPS and timestamps, federate evidence to international partners, and produce court-ready evidence packages
- **Research integrity** -- researchers attest datasets (CSV, sensor readings) at collection time, creating a tamper-evident chain of custody. `ImageHashes.from_file()` supports arbitrary file types via SHA-256
- **Election monitoring** -- observers attest ballot images and tally sheets with location metadata, anchor the chain to an RFC 3161 TSA for independent time proof, and use selective disclosure for audit requests
- **Supply chain verification** -- attest inspection photos, sensor data, and certificates of origin at each stage. Federation enables multi-party chains across organizations
- **Art authentication** -- attest high-resolution photographs of artworks with device and location metadata, creating provenance records that survive format conversion via perceptual hashing
- **Corporate whistleblowing** -- the source drop box accepts anonymous uploads with client-side hashing. Cover mode (`cover_name`, `SOOSEF_DATA_DIR`) disguises the installation. The killswitch provides emergency destruction if the instance is compromised
- **Environmental monitoring** -- attest sensor data, satellite imagery, and field photographs. Cold archives with `ALGORITHMS.txt` ensure evidence remains verifiable decades later
- **Human rights documentation** -- field workers attest photos and videos of incidents
with GPS and timestamps, federate evidence to international partners, and produce
court-ready evidence packages
- **Research integrity** -- researchers attest datasets (CSV, sensor readings) at
collection time, creating a tamper-evident chain of custody. `ImageHashes.from_file()`
supports arbitrary file types via SHA-256
- **Election monitoring** -- observers attest ballot images and tally sheets with location
metadata, anchor the chain to an RFC 3161 TSA for independent time proof, and use
selective disclosure for audit requests
- **Supply chain verification** -- attest inspection photos, sensor data, and certificates
of origin at each stage. Federation enables multi-party chains across organizations
- **Art authentication** -- attest high-resolution photographs of artworks with device and
location metadata, creating provenance records that survive format conversion via
perceptual hashing
- **Corporate whistleblowing** -- the source drop box accepts anonymous uploads with
client-side hashing. Cover mode (`cover_name`, `FIELDWITNESS_DATA_DIR`) disguises the
installation. The killswitch provides emergency destruction if the instance is
compromised
- **Environmental monitoring** -- attest sensor data, satellite imagery, and field
photographs. Cold archives with `ALGORITHMS.txt` ensure evidence remains verifiable
decades later
---
@ -632,10 +692,10 @@ Useful for monitoring and for clients to discover which extras are installed.
Install the `api` extra for a standalone FastAPI REST interface:
```bash
pip install "soosef[api]"
pip install "fieldwitness[api]"
```
This provides `soosef.api` with a FastAPI application served by uvicorn, suitable for
This provides `fieldwitness.api` with a FastAPI application served by uvicorn, suitable for
programmatic integration.
---
@ -645,8 +705,8 @@ programmatic integration.
### Setup
```bash
git clone https://github.com/alee/soosef.git
cd soosef
git clone https://github.com/alee/fieldwitness.git
cd fieldwitness
pip install -e ".[dev]"
```
@ -680,6 +740,7 @@ Python 3.11, 3.12, 3.13, and 3.14.
| [docs/federation.md](docs/federation.md) | System administrators | Gossip protocol, peer setup, offline bundles, federation API |
| [docs/evidence-guide.md](docs/evidence-guide.md) | Investigators, legal teams | Evidence packages, cold archives, selective disclosure, anchoring |
| [docs/source-dropbox.md](docs/source-dropbox.md) | Administrators | Source drop box setup, EXIF pipeline, receipt codes |
| [docs/security/threat-model.md](docs/security/threat-model.md) | Security reviewers, contributors | Threat model, adversary model, trust boundaries, cryptographic primitives |
| [docs/training/reporter-quickstart.md](docs/training/reporter-quickstart.md) | Field reporters | One-page quick-start card for Tier 1 USB users |
| [docs/training/emergency-card.md](docs/training/emergency-card.md) | All users | Laminated wallet card: emergency destruction, dead man's switch |
| [docs/training/admin-reference.md](docs/training/admin-reference.md) | Administrators | CLI cheat sheet, hardening checklist, troubleshooting |
@ -697,4 +758,4 @@ Architecture documents (design-level, for contributors):
## License
MIT License. See [LICENSE](LICENSE) for details.
GPL-3.0 License. See [LICENSE](LICENSE) for details.

View File

@ -1,7 +1,7 @@
# SooSeF Threat Level Configuration Presets
# FieldWitness Threat Level Configuration Presets
Select a preset based on your operational environment. Copy the appropriate
JSON file to `~/.soosef/config.json` (or let the setup wizard choose one).
JSON file to `~/.fwmetadata/config.json` (or let the setup wizard choose one).
## Presets
@ -40,7 +40,7 @@ Specific journalist or org targeted by state actor (Pegasus-level).
```bash
# Copy preset to config location
cp deploy/config-presets/high-threat.json ~/.soosef/config.json
cp deploy/config-presets/high-threat.json ~/.fwmetadata/config.json
# Or via CLI (future: soosef init --threat-level high)
# Or via CLI (future: fieldwitness init --threat-level high)
```

View File

@ -1,13 +1,13 @@
# SooSeF Federation Server
# FieldWitness Federation Server
# Multi-stage build for minimal image size.
#
# Tier 2: Org server (full features — web UI, attestation, federation, stego)
# docker build -t soosef-server .
# docker run -v soosef-data:/data -p 5000:5000 -p 8000:8000 soosef-server
# docker build -t fieldwitness-server .
# docker run -v fieldwitness-data:/data -p 5000:5000 -p 8000:8000 fieldwitness-server
#
# Tier 3: Federation relay (attestation + federation only, no stego, no web UI)
# docker build --target relay -t soosef-relay .
# docker run -v relay-data:/data -p 8000:8000 soosef-relay
# docker build --target relay -t fieldwitness-relay .
# docker run -v relay-data:/data -p 8000:8000 fieldwitness-relay
# === Stage 1: Build dependencies ===
FROM python:3.12-slim-bookworm AS builder
@ -22,8 +22,8 @@ WORKDIR /build
COPY . .
# Install into a virtual environment for clean copying
RUN python -m venv /opt/soosef-env \
&& /opt/soosef-env/bin/pip install --no-cache-dir \
RUN python -m venv /opt/fieldwitness-env \
&& /opt/fieldwitness-env/bin/pip install --no-cache-dir \
".[web,cli,attest,stego-dct,api,federation]"
# === Stage 2: Federation relay (minimal) ===
@ -32,21 +32,21 @@ FROM python:3.12-slim-bookworm AS relay
RUN apt-get update && apt-get install -y --no-install-recommends \
libjpeg62-turbo libopenblas0 \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -m -s /bin/bash soosef
&& useradd -m -s /bin/bash fieldwitness
COPY --from=builder /opt/soosef-env /opt/soosef-env
COPY --from=builder /opt/fieldwitness-env /opt/fieldwitness-env
ENV PATH="/opt/soosef-env/bin:$PATH" \
SOOSEF_DATA_DIR=/data \
ENV PATH="/opt/fieldwitness-env/bin:$PATH" \
FIELDWITNESS_DATA_DIR=/data \
PYTHONUNBUFFERED=1
VOLUME /data
EXPOSE 8000
USER soosef
USER fieldwitness
# Federation relay: only the verisoo API with federation endpoints
CMD ["uvicorn", "soosef.verisoo.api:app", "--host", "0.0.0.0", "--port", "8000"]
# Federation relay: only the attest API with federation endpoints
CMD ["uvicorn", "fieldwitness.attest.api:app", "--host", "0.0.0.0", "--port", "8000"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
@ -57,25 +57,25 @@ FROM python:3.12-slim-bookworm AS server
RUN apt-get update && apt-get install -y --no-install-recommends \
libjpeg62-turbo libopenblas0 \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -m -s /bin/bash soosef
&& useradd -m -s /bin/bash fieldwitness
COPY --from=builder /opt/soosef-env /opt/soosef-env
COPY --from=builder /opt/fieldwitness-env /opt/fieldwitness-env
# Copy frontend templates and static assets
COPY frontends/ /opt/soosef-env/lib/python3.12/site-packages/frontends/
COPY frontends/ /opt/fieldwitness-env/lib/python3.12/site-packages/frontends/
ENV PATH="/opt/soosef-env/bin:$PATH" \
SOOSEF_DATA_DIR=/data \
ENV PATH="/opt/fieldwitness-env/bin:$PATH" \
FIELDWITNESS_DATA_DIR=/data \
PYTHONUNBUFFERED=1
VOLUME /data
EXPOSE 5000 8000
USER soosef
USER fieldwitness
# Init on first run, then start web UI (HTTPS by default with self-signed cert).
# Use --no-https explicitly if running behind a TLS-terminating reverse proxy.
CMD ["sh", "-c", "soosef init 2>/dev/null; soosef serve --host 0.0.0.0"]
CMD ["sh", "-c", "fieldwitness init 2>/dev/null; fieldwitness serve --host 0.0.0.0"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"

View File

@ -1,4 +1,4 @@
# SooSeF Docker Compose — Three-Tier Deployment
# FieldWitness Docker Compose — Three-Tier Deployment
#
# Tier 2 (Org Server): Full web UI + attestation + federation
# Tier 3 (Federation Relay): Lightweight attestation API only
@ -10,7 +10,7 @@
services:
# === Tier 2: Organizational Server ===
# Full SooSeF instance with web UI, stego, attestation, federation.
# Full FieldWitness instance with web UI, stego, attestation, federation.
# Deploy on a mini PC in the newsroom or a trusted VPS.
server:
build:
@ -23,8 +23,8 @@ services:
volumes:
- server-data:/data
environment:
- SOOSEF_DATA_DIR=/data
- VERISOO_GOSSIP_INTERVAL=60
- FIELDWITNESS_DATA_DIR=/data
- FIELDWITNESS_GOSSIP_INTERVAL=60
restart: unless-stopped
# === Tier 3: Federation Relay ===
@ -41,7 +41,7 @@ services:
volumes:
- relay-data:/data
environment:
- SOOSEF_DATA_DIR=/data
- FIELDWITNESS_DATA_DIR=/data
restart: unless-stopped
volumes:

View File

@ -1,4 +1,4 @@
# SooSeF Kubernetes Deployment
# FieldWitness Kubernetes Deployment
## Architecture
@ -29,8 +29,8 @@
```bash
# Build images
docker build -t soosef-server --target server -f deploy/docker/Dockerfile .
docker build -t soosef-relay --target relay -f deploy/docker/Dockerfile .
docker build -t fieldwitness-server --target server -f deploy/docker/Dockerfile .
docker build -t fieldwitness-relay --target relay -f deploy/docker/Dockerfile .
# Deploy to Kubernetes
kubectl apply -f deploy/kubernetes/namespace.yaml
@ -41,7 +41,7 @@ kubectl apply -f deploy/kubernetes/relay-deployment.yaml
## Notes
- **Single writer**: Both deployments use `replicas: 1` with `Recreate` strategy.
SooSeF uses SQLite and append-only binary logs that require single-writer access.
FieldWitness uses SQLite and append-only binary logs that require single-writer access.
Do not scale horizontally.
- **PVCs**: Both deployments require persistent volumes. The server needs 10Gi,
the relay needs 5Gi. Adjust based on expected attestation volume.

View File

@ -1,6 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: soosef
name: fieldwitness
labels:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness

View File

@ -1,13 +1,13 @@
# SooSeF Federation Relay — Lightweight attestation sync relay.
# FieldWitness Federation Relay — Lightweight attestation sync relay.
# Deploy on a VPS in a favorable jurisdiction for geographic redundancy.
# Stores only attestation records — zero knowledge of encryption keys.
apiVersion: apps/v1
kind: Deployment
metadata:
name: soosef-relay
namespace: soosef
name: fieldwitness-relay
namespace: fieldwitness
labels:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: relay
spec:
replicas: 1
@ -15,12 +15,12 @@ spec:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: relay
template:
metadata:
labels:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: relay
spec:
securityContext:
@ -29,12 +29,12 @@ spec:
fsGroup: 1000
containers:
- name: relay
image: soosef-relay:latest
image: fieldwitness-relay:latest
ports:
- containerPort: 8000
name: federation
env:
- name: SOOSEF_DATA_DIR
- name: FIELDWITNESS_DATA_DIR
value: /data
volumeMounts:
- name: data
@ -61,7 +61,7 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: relay-data
namespace: soosef
namespace: fieldwitness
spec:
accessModes:
- ReadWriteOnce
@ -72,11 +72,11 @@ spec:
apiVersion: v1
kind: Service
metadata:
name: soosef-relay
namespace: soosef
name: fieldwitness-relay
namespace: fieldwitness
spec:
selector:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: relay
ports:
- name: federation

View File

@ -1,12 +1,12 @@
# SooSeF Org Server — Full deployment with persistent storage.
# FieldWitness Org Server — Full deployment with persistent storage.
# For newsroom or trusted infrastructure deployment.
apiVersion: apps/v1
kind: Deployment
metadata:
name: soosef-server
namespace: soosef
name: fieldwitness-server
namespace: fieldwitness
labels:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: server
spec:
replicas: 1 # Single writer — do not scale horizontally
@ -14,12 +14,12 @@ spec:
type: Recreate # Not RollingUpdate — SQLite + append-only logs need single writer
selector:
matchLabels:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: server
template:
metadata:
labels:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: server
spec:
securityContext:
@ -27,17 +27,17 @@ spec:
runAsGroup: 1000
fsGroup: 1000
containers:
- name: soosef
image: soosef-server:latest
- name: fieldwitness
image: fieldwitness-server:latest
ports:
- containerPort: 5000
name: web
- containerPort: 8000
name: federation
env:
- name: SOOSEF_DATA_DIR
- name: FIELDWITNESS_DATA_DIR
value: /data
- name: VERISOO_GOSSIP_INTERVAL
- name: FIELDWITNESS_GOSSIP_INTERVAL
value: "60"
volumeMounts:
- name: data
@ -64,13 +64,13 @@ spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: soosef-data
claimName: fieldwitness-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: soosef-data
namespace: soosef
name: fieldwitness-data
namespace: fieldwitness
spec:
accessModes:
- ReadWriteOnce
@ -81,11 +81,11 @@ spec:
apiVersion: v1
kind: Service
metadata:
name: soosef-server
namespace: soosef
name: fieldwitness-server
namespace: fieldwitness
spec:
selector:
app.kubernetes.io/name: soosef
app.kubernetes.io/name: fieldwitness
app.kubernetes.io/component: server
ports:
- name: web

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build a bootable Debian Live USB image with SooSeF pre-installed.
# Build a bootable Debian Live USB image with FieldWitness pre-installed.
#
# Prerequisites:
# apt install live-build
@ -12,10 +12,10 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SOOSEF_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
FIELDWITNESS_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
echo "=== SooSeF Live USB Image Builder ==="
echo "Source: $SOOSEF_ROOT"
echo "=== FieldWitness Live USB Image Builder ==="
echo "Source: $FIELDWITNESS_ROOT"
echo
cd "$SCRIPT_DIR"

View File

@ -1,26 +1,26 @@
#!/bin/bash
# Install SooSeF and all dependencies into the live image.
# Install FieldWitness and all dependencies into the live image.
# This runs inside the chroot during image build.
set -euo pipefail
echo "=== Installing SooSeF ==="
echo "=== Installing FieldWitness ==="
# Create soosef user
useradd -m -s /bin/bash -G sudo soosef
echo "soosef:soosef" | chpasswd
# Create fieldwitness user
useradd -m -s /bin/bash -G sudo fieldwitness
echo "fieldwitness:fieldwitness" | chpasswd
# Create virtual environment
python3 -m venv /opt/soosef-env
source /opt/soosef-env/bin/activate
python3 -m venv /opt/fieldwitness-env
source /opt/fieldwitness-env/bin/activate
# Install soosef with all extras (pre-built wheels from PyPI)
pip install --no-cache-dir "soosef[web,cli,attest,stego-dct,stego-audio,fieldkit]"
# Install fieldwitness with all extras (pre-built wheels from PyPI)
pip install --no-cache-dir "fieldwitness[web,cli,attest,stego-dct,stego-audio,fieldkit]"
# Verify installation
python3 -c "import soosef; print(f'SooSeF {soosef.__version__} installed')"
python3 -c "from soosef.stegasoo import encode; print('stegasoo OK')"
python3 -c "from soosef.verisoo import Attestation; print('verisoo OK')"
python3 -c "import fieldwitness; print(f'FieldWitness {fieldwitness.__version__} installed')"
python3 -c "from fieldwitness.stego import encode; print('stego OK')"
python3 -c "from fieldwitness.attest import Attestation; print('attest OK')"
deactivate
echo "=== SooSeF installation complete ==="
echo "=== FieldWitness installation complete ==="

View File

@ -6,17 +6,17 @@ echo "=== Applying security hardening ==="
# Disable core dumps (Python doesn't zero memory — core dumps leak keys)
echo "* hard core 0" >> /etc/security/limits.conf
echo "fs.suid_dumpable = 0" >> /etc/sysctl.d/99-soosef.conf
echo "kernel.core_pattern=|/bin/false" >> /etc/sysctl.d/99-soosef.conf
echo "fs.suid_dumpable = 0" >> /etc/sysctl.d/99-fieldwitness.conf
echo "kernel.core_pattern=|/bin/false" >> /etc/sysctl.d/99-fieldwitness.conf
# Disable swap (keys persist in swap pages)
systemctl mask swap.target || true
echo "vm.swappiness = 0" >> /etc/sysctl.d/99-soosef.conf
echo "vm.swappiness = 0" >> /etc/sysctl.d/99-fieldwitness.conf
# Enable UFW with deny-all + allow web UI
ufw default deny incoming
ufw default allow outgoing
ufw allow 5000/tcp comment "SooSeF Web UI"
ufw allow 5000/tcp comment "FieldWitness Web UI"
ufw allow 22/tcp comment "SSH"
ufw --force enable || true
@ -25,14 +25,14 @@ systemctl disable bluetooth.service 2>/dev/null || true
systemctl disable avahi-daemon.service 2>/dev/null || true
systemctl disable cups.service 2>/dev/null || true
# Enable SooSeF service
# Enable FieldWitness service
systemctl enable soosef.service
# Auto-login to openbox (so the browser opens without login prompt)
mkdir -p /etc/lightdm/lightdm.conf.d
cat > /etc/lightdm/lightdm.conf.d/50-autologin.conf << 'EOF'
[Seat:*]
autologin-user=soosef
autologin-user=fieldwitness
autologin-user-timeout=0
EOF

View File

@ -1,17 +1,17 @@
[Unit]
Description=SooSeF Security Fieldkit
Description=FieldWitness
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=soosef
Group=soosef
WorkingDirectory=/home/soosef
Environment=PATH=/opt/soosef-env/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=SOOSEF_DATA_DIR=/home/soosef/.soosef
ExecStartPre=/opt/soosef-env/bin/soosef init --no-identity --no-channel
ExecStart=/opt/soosef-env/bin/soosef serve --host 0.0.0.0 --no-https
User=fieldwitness
Group=fieldwitness
WorkingDirectory=/home/fieldwitness
Environment=PATH=/opt/fieldwitness-env/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=FIELDWITNESS_DATA_DIR=/home/fieldwitness/.fwmetadata
ExecStartPre=/opt/fieldwitness-env/bin/fieldwitness init --no-identity --no-channel
ExecStart=/opt/fieldwitness-env/bin/fieldwitness serve --host 0.0.0.0 --no-https
Restart=on-failure
RestartSec=5
@ -19,7 +19,7 @@ RestartSec=5
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/soosef/.soosef
ReadWritePaths=/home/fieldwitness/.fwmetadata
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes

View File

@ -1,4 +1,4 @@
# SooSeF Live USB — auto-open web UI in Firefox
# Wait for the SooSeF server to start, then open the browser
# FieldWitness Live USB — auto-open web UI in Firefox
# Wait for the FieldWitness server to start, then open the browser
sleep 5
firefox-esr --kiosk http://127.0.0.1:5000 &

View File

@ -12,7 +12,7 @@ libssl-dev
gfortran
libopenblas-dev
## SooSeF runtime dependencies
## FieldWitness runtime dependencies
gpsd
gpsd-clients
cryptsetup

View File

@ -1,15 +1,9 @@
# SooSeF Docker Image
#
# Requires stegasoo and verisoo source directories alongside soosef:
# Sources/
# ├── stegasoo/
# ├── verisoo/
# └── soosef/ ← build context is parent (Sources/)
# FieldWitness Docker Image
#
# Build:
# docker build -t soosef -f soosef/docker/Dockerfile .
# docker build -t fieldwitness -f docker/Dockerfile .
#
# Or use docker-compose from soosef/docker/:
# Or use docker-compose from docker/:
# docker compose up
FROM python:3.12-slim
@ -33,35 +27,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
WORKDIR /app
# ── Install stegasoo ─────────────────────────────────────────────
COPY stegasoo/pyproject.toml stegasoo/pyproject.toml
COPY stegasoo/README.md stegasoo/README.md
COPY stegasoo/src/ stegasoo/src/
COPY stegasoo/data/ stegasoo/data/
COPY stegasoo/frontends/ stegasoo/frontends/
RUN pip install --no-cache-dir /app/stegasoo[web,dct,audio,cli]
# ── Install verisoo ──────────────────────────────────────────────
COPY verisoo/pyproject.toml verisoo/pyproject.toml
COPY verisoo/README.md verisoo/README.md
COPY verisoo/src/ verisoo/src/
RUN pip install --no-cache-dir /app/verisoo[cli]
# ── Install soosef ───────────────────────────────────────────────
COPY soosef/pyproject.toml soosef/pyproject.toml
COPY soosef/README.md soosef/README.md
COPY soosef/src/ soosef/src/
COPY soosef/frontends/ soosef/frontends/
RUN pip install --no-cache-dir /app/soosef[web,cli]
# ── Install fieldwitness ─────────────────────────────────────────
COPY pyproject.toml pyproject.toml
COPY README.md README.md
COPY src/ src/
COPY frontends/ frontends/
RUN pip install --no-cache-dir /app[web,cli]
# ── Runtime setup ────────────────────────────────────────────────
RUN mkdir -p /root/.soosef
RUN mkdir -p /root/.fwmetadata
COPY soosef/docker/entrypoint.sh /app/entrypoint.sh
COPY docker/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENV SOOSEF_DATA_DIR=/root/.soosef
WORKDIR /app/soosef
ENV FIELDWITNESS_DATA_DIR=/root/.fwmetadata
WORKDIR /app
EXPOSE 35811

View File

@ -1,19 +1,19 @@
services:
soosef:
fieldwitness:
build:
context: ../.. # Sources/ directory (contains stegasoo/, verisoo/, soosef/)
dockerfile: soosef/docker/Dockerfile
container_name: soosef
context: ../.. # Sources/ directory (contains stego/, attest/, fieldwitness/)
dockerfile: fieldwitness/docker/Dockerfile
container_name: fieldwitness
ports:
- "35811:35811"
environment:
SOOSEF_DATA_DIR: /root/.soosef
SOOSEF_PORT: "35811"
SOOSEF_WORKERS: "2"
SOOSEF_HTTPS_ENABLED: "${SOOSEF_HTTPS_ENABLED:-false}"
STEGASOO_CHANNEL_KEY: "${STEGASOO_CHANNEL_KEY:-}"
FIELDWITNESS_DATA_DIR: /root/.fwmetadata
FIELDWITNESS_PORT: "35811"
FIELDWITNESS_WORKERS: "2"
FIELDWITNESS_HTTPS_ENABLED: "${FIELDWITNESS_HTTPS_ENABLED:-false}"
FIELDWITNESS_CHANNEL_KEY: "${FIELDWITNESS_CHANNEL_KEY:-}"
volumes:
- soosef-data:/root/.soosef
- fieldwitness-data:/root/.fwmetadata
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fs", "--max-time", "3", "http://localhost:35811/"]
@ -29,5 +29,5 @@ services:
memory: 512M
volumes:
soosef-data:
fieldwitness-data:
driver: local

View File

@ -2,24 +2,24 @@
set -e
# Initialize if needed (generates identity + channel key + config)
if [ ! -f "$SOOSEF_DATA_DIR/config.json" ]; then
echo "First run — initializing SooSeF..."
soosef init
if [ ! -f "$FIELDWITNESS_DATA_DIR/config.json" ]; then
echo "First run — initializing FieldWitness..."
fieldwitness init
echo "Initialization complete."
fi
# Determine HTTPS mode
HTTPS_FLAG=""
if [ "${SOOSEF_HTTPS_ENABLED:-true}" = "false" ]; then
if [ "${FIELDWITNESS_HTTPS_ENABLED:-true}" = "false" ]; then
HTTPS_FLAG="--no-https"
fi
echo "Starting SooSeF on port ${SOOSEF_PORT:-35811}..."
echo "Starting FieldWitness on port ${FIELDWITNESS_PORT:-35811}..."
# Run with gunicorn for production
exec gunicorn \
--bind "0.0.0.0:${SOOSEF_PORT:-35811}" \
--workers "${SOOSEF_WORKERS:-2}" \
--bind "0.0.0.0:${FIELDWITNESS_PORT:-35811}" \
--workers "${FIELDWITNESS_WORKERS:-2}" \
--timeout 180 \
--access-logfile - \
--error-logfile - \

View File

@ -10,7 +10,7 @@ The attestation chain is an append-only sequence of signed records stored locall
offline device. Each record includes a hash of the previous record, forming a tamper-evident
chain analogous to git commits or blockchain blocks.
The chain wraps existing Verisoo attestation records. A Verisoo record's serialized bytes
The chain wraps existing Attest attestation records. A Attest record's serialized bytes
become the input to `content_hash`, preserving the original attestation while adding
ordering, entropy witnesses, and chain integrity guarantees.
@ -24,8 +24,8 @@ ordering, entropy witnesses, and chain integrity guarantees.
| `record_id` | 1 | byte string | 16 bytes | UUID v7 (RFC 9562). Time-ordered unique identifier. |
| `chain_index` | 2 | unsigned int | 8 bytes max | Monotonically increasing, 0-based. Genesis record is index 0. |
| `prev_hash` | 3 | byte string | 32 bytes | SHA-256 of `canonical_bytes(previous_record)`. Genesis: `0x00 * 32`. |
| `content_hash` | 4 | byte string | 32 bytes | SHA-256 of the wrapped content (e.g., Verisoo record bytes). |
| `content_type` | 5 | text string | variable | MIME-like type identifier. `"verisoo/attestation-v1"` for Verisoo records. |
| `content_hash` | 4 | byte string | 32 bytes | SHA-256 of the wrapped content (e.g., Attest record bytes). |
| `content_type` | 5 | text string | variable | MIME-like type identifier. `"attest/attestation-v1"` for Attest records. |
| `metadata` | 6 | CBOR map | variable | Extensible key-value map. See §2.1. |
| `claimed_ts` | 7 | integer | 8 bytes max | Unix timestamp in microseconds (µs). Signed integer to handle pre-epoch dates. |
| `entropy_witnesses` | 8 | CBOR map | variable | System entropy snapshot. See §3. |
@ -41,7 +41,7 @@ The `metadata` field is an open CBOR map with text string keys. Defined keys:
| `"backfilled"` | bool | `true` if this record was created by the backfill migration |
| `"caption"` | text | Human-readable description of the attested content |
| `"location"` | text | Location name associated with the attestation |
| `"original_ts"` | integer | Original Verisoo timestamp (µs) if different from `claimed_ts` |
| `"original_ts"` | integer | Original Attest timestamp (µs) if different from `claimed_ts` |
| `"tags"` | array of text | User-defined classification tags |
Applications may add custom keys. Unknown keys must be preserved during serialization.
@ -214,24 +214,24 @@ This file is a performance optimization — the canonical state is always deriva
### 6.3 File Locations
```
~/.soosef/chain/
~/.fwmetadata/chain/
chain.bin Append-only record log
state.cbor Chain state checkpoint
```
Paths are defined in `src/soosef/paths.py`.
Paths are defined in `src/fieldwitness/paths.py`.
## 7. Migration from Verisoo-Only Attestations
## 7. Migration from Attest-Only Attestations
Existing Verisoo attestations in `~/.soosef/attestations/` are not modified. The chain
is a parallel structure. Migration is performed by the `soosef chain backfill` command:
Existing Attest attestations in `~/.fwmetadata/attestations/` are not modified. The chain
is a parallel structure. Migration is performed by the `fieldwitness chain backfill` command:
1. Iterate all records in Verisoo's `LocalStorage` (ordered by timestamp)
1. Iterate all records in Attest's `LocalStorage` (ordered by timestamp)
2. For each record, compute `content_hash = SHA-256(record.to_bytes())`
3. Create a chain record with:
- `content_type = "verisoo/attestation-v1"`
- `claimed_ts` set to the original Verisoo timestamp
- `metadata = {"backfilled": true, "original_ts": <verisoo_timestamp>}`
- `content_type = "attest/attestation-v1"`
- `claimed_ts` set to the original Attest timestamp
- `metadata = {"backfilled": true, "original_ts": <attest_timestamp>}`
- Entropy witnesses collected at migration time (not original time)
4. Append to chain
@ -245,8 +245,8 @@ The `content_type` field identifies what was hashed into `content_hash`. Defined
| Content Type | Description |
|---|---|
| `verisoo/attestation-v1` | Verisoo `AttestationRecord` serialized bytes |
| `soosef/raw-file-v1` | Raw file bytes (for non-image attestations, future) |
| `soosef/metadata-only-v1` | No file content; metadata-only attestation (future) |
| `attest/attestation-v1` | Attest `AttestationRecord` serialized bytes |
| `fieldwitness/raw-file-v1` | Raw file bytes (for non-image attestations, future) |
| `fieldwitness/metadata-only-v1` | No file content; metadata-only attestation (future) |
New content types may be added without changing the record format version.

View File

@ -22,7 +22,7 @@ version, structured binary payload.
```
Offset Size Field
────── ───────── ──────────────────────────────────────
0 8 magic: b"SOOSEFX1"
0 8 magic: b"FIELDWITNESSX1"
8 1 version: uint8 (1)
9 4 summary_len: uint32 BE
13 var chain_summary: CBOR (see §3)
@ -39,7 +39,7 @@ All multi-byte integers are big-endian. The total bundle size is:
### Parsing Without Decryption
To audit a bundle without decryption, read:
1. Magic (8 bytes) — verify `b"SOOSEFX1"`
1. Magic (8 bytes) — verify `b"FIELDWITNESSX1"`
2. Version (1 byte) — verify `1`
3. Summary length (4 bytes BE) — read the next N bytes as CBOR
4. Chain summary — verify signature, inspect metadata
@ -132,7 +132,7 @@ For each recipient, the DEK is wrapped using X25519 ECDH + HKDF + AES-256-GCM:
2. derived_key = HKDF-SHA256(
ikm=shared_secret,
salt=bundle_id, # binds to this specific bundle
info=b"soosef-dek-wrap-v1",
info=b"fieldwitness-dek-wrap-v1",
length=32
)
3. wrapped_dek = AES-256-GCM_Encrypt(
@ -185,7 +185,7 @@ A recipient decrypts a bundle:
2. Find own pubkey in recipients array
3. shared_secret = X25519_ECDH(recipient_x25519_private, sender_x25519_public)
(sender_x25519_public derived from summary.signer_pubkey)
4. derived_key = HKDF-SHA256(shared_secret, salt=bundle_id, info=b"soosef-dek-wrap-v1")
4. derived_key = HKDF-SHA256(shared_secret, salt=bundle_id, info=b"fieldwitness-dek-wrap-v1")
5. dek = AES-256-GCM_Decrypt(derived_key, wrap_nonce, wrapped_dek, aad=bundle_id)
6. compressed = AES-256-GCM_Decrypt(dek, nonce, ciphertext, aad=summary_bytes)
7. records_cbor = zstd.decompress(compressed)
@ -240,11 +240,11 @@ These are two different trees:
## 6. Steganographic Embedding
Bundles can optionally be embedded in JPEG images using stegasoo's DCT steganography:
Bundles can optionally be embedded in JPEG images using stego's DCT steganography:
```
1. bundle_bytes = create_export_bundle(chain, start, end, private_key, recipients)
2. stego_image = stegasoo.encode(
2. stego_image = stego.encode(
carrier=carrier_image,
reference=reference_image,
file_data=bundle_bytes,
@ -256,14 +256,14 @@ Bundles can optionally be embedded in JPEG images using stegasoo's DCT steganogr
Extraction:
```
1. result = stegasoo.decode(
1. result = stego.decode(
carrier=stego_image,
reference=reference_image,
passphrase=passphrase,
channel_key=channel_key
)
2. bundle_bytes = result.file_data
3. assert bundle_bytes[:8] == b"SOOSEFX1"
3. assert bundle_bytes[:8] == b"FIELDWITNESSX1"
```
### 6.1 Capacity Considerations
@ -299,7 +299,7 @@ recipient, the creator needs only their public key (no shared secret setup requi
Recipients' Ed25519 public keys can be obtained via:
- Direct exchange (QR code, USB transfer, verbal fingerprint verification)
- Federation server identity registry (when available)
- Verisoo's existing `peers.json` file
- Attest's existing `peers.json` file
### 7.3 Self-Encryption
@ -310,7 +310,7 @@ This allows them to decrypt their own exports (e.g., when restoring from backup)
| Error | Cause | Response |
|---|---|---|
| Bad magic | Not a SOOSEFX1 bundle | Reject with `ExportError("not a SooSeF export bundle")` |
| Bad magic | Not a FIELDWITNESSX1 bundle | Reject with `ExportError("not a FieldWitness export bundle")` |
| Bad version | Unsupported format version | Reject with `ExportError("unsupported bundle version")` |
| Signature invalid | Tampered summary or wrong signer | Reject with `ExportError("bundle signature verification failed")` |
| No matching recipient | Decryptor's key not in recipients list | Reject with `ExportError("not an authorized recipient")` |

View File

@ -19,7 +19,7 @@ the underlying attestation data.
| Term | Definition |
|---|---|
| **Bundle** | An encrypted export bundle (SOOSEFX1 format) containing chain records |
| **Bundle** | An encrypted export bundle (FIELDWITNESSX1 format) containing chain records |
| **STH** | Signed Tree Head — a server's signed commitment to its current Merkle tree state |
| **Receipt** | A server-signed proof that a bundle was included in its log at a specific time |
| **Inclusion proof** | Merkle path from a leaf (bundle hash) to the tree root |
@ -106,7 +106,7 @@ POST /v1/submit
**Request body**: Raw bundle bytes (application/octet-stream)
**Processing**:
1. Verify magic bytes `b"SOOSEFX1"` and version
1. Verify magic bytes `b"FIELDWITNESSX1"` and version
2. Parse chain summary
3. Verify `bundle_sig` against `signer_pubkey`
4. Compute `bundle_hash = SHA-256(0x00 || bundle_bytes)`
@ -215,7 +215,7 @@ GET /v1/entries?start={s}&end={e}
0: tree_index, # uint
1: bundle_hash, # bytes[32]
2: chain_summary, # CBOR map (from bundle, unencrypted)
3: encrypted_blob, # bytes — full SOOSEFX1 bundle
3: encrypted_blob, # bytes — full FIELDWITNESSX1 bundle
4: receipt_ts, # int — Unix µs when received
}
```
@ -488,8 +488,8 @@ CREATE INDEX idx_bundles_receipt_ts ON bundles(receipt_ts);
"server_id": "my-server.example.org",
"host": "0.0.0.0",
"port": 8443,
"data_dir": "/var/lib/soosef-federation",
"identity_key_path": "/etc/soosef-federation/identity/private.pem",
"data_dir": "/var/lib/fieldwitness-federation",
"identity_key_path": "/etc/fieldwitness-federation/identity/private.pem",
"peers": [
{
"url": "https://peer1.example.org:8443",

View File

@ -6,7 +6,7 @@
## 1. Problem Statement
SooSeF operates offline-first: devices create Ed25519-signed attestations without network
FieldWitness operates offline-first: devices create Ed25519-signed attestations without network
access. This creates two fundamental challenges:
1. **Timestamp credibility** — An offline device's clock is untrusted. An adversary with
@ -55,7 +55,7 @@ protecting content confidentiality even from the distribution infrastructure.
────────────── ─────── ────────
┌──────────┐ ┌──────────────┐ ┌──────────┐ USB/SD ┌──────────┐ ┌────────────┐
Verisoo │────>│ Hash Chain │────>│ Export │───────────────>│ Loader │────>│ Federation │
Attest │────>│ Hash Chain │────>│ Export │───────────────>│ Loader │────>│ Federation │
│ Attest │ │ (Layer 1) │ │ Bundle │ │ (App) │ │ Server │
└──────────┘ └──────────────┘ │ (Layer 2)│ └────┬─────┘ └─────┬──────┘
└──────────┘ │ │
@ -80,8 +80,8 @@ Each attestation is wrapped in a chain record that includes:
- Entropy witnesses (system uptime, kernel state) that make timestamp fabrication expensive
- An Ed25519 signature over the entire record
The chain lives on the offline device at `~/.soosef/chain/`. It wraps existing Verisoo
attestation records — the Verisoo record's bytes become the `content_hash` input.
The chain lives on the offline device at `~/.fwmetadata/chain/`. It wraps existing Attest
attestation records — the Attest record's bytes become the `content_hash` input.
**See**: [chain-format.md](chain-format.md)
@ -94,7 +94,7 @@ A range of chain records is packaged into a portable bundle:
4. An unencrypted `chain_summary` (record count, hash range, Merkle root, signature) allows
auditing without decryption
Bundles can optionally be embedded in JPEG images via stegasoo's DCT steganography,
Bundles can optionally be embedded in JPEG images via stego's DCT steganography,
making them indistinguishable from normal photos on a USB stick.
**See**: [export-bundle.md](export-bundle.md)
@ -123,13 +123,13 @@ The loader never needs signing keys — bundles are already signed. It is a tran
## 4. Key Domains
SooSeF maintains strict separation between two cryptographic domains:
FieldWitness maintains strict separation between two cryptographic domains:
| Domain | Algorithm | Purpose | Key Location |
|---|---|---|---|
| **Signing** | Ed25519 | Attestation signatures, chain records, bundle summaries | `~/.soosef/identity/` |
| **Signing** | Ed25519 | Attestation signatures, chain records, bundle summaries | `~/.fwmetadata/identity/` |
| **Encryption** | X25519 + AES-256-GCM | Bundle payload encryption (envelope) | Derived from Ed25519 via birational map |
| **Steganography** | AES-256-GCM (from factors) | Stegasoo channel encryption | `~/.soosef/stegasoo/channel.key` |
| **Steganography** | AES-256-GCM (from factors) | Stego channel encryption | `~/.fwmetadata/stego/channel.key` |
The signing and encryption domains share a key lineage (Ed25519 → X25519 derivation) but
serve different purposes. The steganography domain remains fully independent — it protects
@ -215,7 +215,7 @@ more credible timestamps. Frequent USB sync trips shrink the window.
## 10. File Layout
```
src/soosef/federation/
src/fieldwitness/federation/
__init__.py
models.py Chain record and state dataclasses
serialization.py CBOR canonical encoding
@ -241,7 +241,7 @@ src/soosef/federation/
permissions.py Access control
config.py Server configuration
~/.soosef/
~/.fwmetadata/
chain/ Local hash chain
chain.bin Append-only record log
state.cbor Chain state checkpoint

View File

@ -1,10 +1,10 @@
# SooSeF Deployment Guide
# FieldWitness Deployment Guide
Three-tier deployment model for field journalism, organizational evidence management,
and cross-organization federation.
This guide is for field deployers: IT staff at NGOs, technically competent journalists,
and anyone setting up SooSeF for operational use. Read the tier descriptions first, then
and anyone setting up FieldWitness for operational use. Read the tier descriptions first, then
jump to the section that matches your deployment.
---
@ -28,7 +28,7 @@ LUKS-encrypted persistent partition for keys, config, and attestations. Amnesic
pull the USB and the host machine retains nothing.
**Tier 2 -- Org Server.** A persistent Docker deployment on a mini PC in the newsroom or a
trusted VPS. Runs the full SooSeF web UI (port 5000) and the federation API (port 8000).
trusted VPS. Runs the full FieldWitness web UI (port 5000) and the federation API (port 8000).
Stores attestations, manages keys, and synchronizes with federation relays.
**Tier 3 -- Federation Relay.** A lightweight Docker container on a VPS in a jurisdiction
@ -48,8 +48,8 @@ actionable.
common, replaceable, good Linux support -- but any laptop that boots from USB works)
- A build machine with `live-build` installed (any Debian/Ubuntu system)
The resulting USB image is a Debian Live system with SooSeF pre-installed. No pip, no
terminal, no manual setup. The reporter boots it and gets a working SooSeF instance.
The resulting USB image is a Debian Live system with FieldWitness pre-installed. No pip, no
terminal, no manual setup. The reporter boots it and gets a working FieldWitness instance.
### 1.2 Building the USB image
@ -85,29 +85,29 @@ When a reporter boots from the USB:
1. GRUB loads the Debian Live system
2. A minimal desktop environment starts (no login prompt)
3. Firefox opens automatically, pointed at `https://127.0.0.1:5000`
4. The SooSeF web UI prompts for first-user setup (on first boot) or login
4. The FieldWitness web UI prompts for first-user setup (on first boot) or login
No terminal interaction required for normal operation.
### 1.4 Persistent encrypted storage
The USB image includes a LUKS-encrypted persistent partition. On first boot, the reporter
sets a passphrase. All SooSeF state lives on this partition:
sets a passphrase. All FieldWitness state lives on this partition:
```
/persistent/
.soosef/ Keys, config, attestations, chain data, auth
.fwmetadata/ Keys, config, attestations, chain data, auth
```
On subsequent boots, the system prompts for the LUKS passphrase to unlock the persistent
partition. If the passphrase is not entered (or wrong), SooSeF starts in a fresh,
partition. If the passphrase is not entered (or wrong), FieldWitness starts in a fresh,
ephemeral state -- useful for crossing borders with a "clean" appearance.
### 1.5 Amnesic operation
The live system runs from RAM. When the USB is removed:
- The host laptop retains zero SooSeF data (no files, no swap traces, no browser cache)
- The host laptop retains zero FieldWitness data (no files, no swap traces, no browser cache)
- The host's own storage is never written to
- RAM contents are gone on power-off
@ -162,17 +162,17 @@ cd deploy/docker
docker compose up server -d
```
This starts the full SooSeF server with:
This starts the full FieldWitness server with:
- **Port 5000**: Web UI (Flask/Waitress) -- stego, attestation, key management, admin
- **Port 8000**: Federation API (FastAPI/uvicorn) -- cross-org attestation sync
The Docker image is a multi-stage build (`deploy/docker/Dockerfile`, target: `server`).
It installs SooSeF into a Python 3.12 virtualenv, copies frontend assets, and runs
`soosef init` on first start followed by `soosef serve`.
It installs FieldWitness into a Python 3.12 virtualenv, copies frontend assets, and runs
`fieldwitness init` on first start followed by `fieldwitness serve`.
Data is persisted in a Docker volume (`server-data`) mounted at `/data` inside the
container. The `SOOSEF_DATA_DIR` environment variable points SooSeF at this volume.
container. The `FIELDWITNESS_DATA_DIR` environment variable points FieldWitness at this volume.
### 2.3 Docker Compose reference
@ -187,8 +187,8 @@ services:
volumes:
- server-data:/data
environment:
- SOOSEF_DATA_DIR=/data
- VERISOO_GOSSIP_INTERVAL=60
- FIELDWITNESS_DATA_DIR=/data
- FIELDWITNESS_GOSSIP_INTERVAL=60
relay: # Tier 3: Federation relay
ports:
@ -196,7 +196,7 @@ services:
volumes:
- relay-data:/data
environment:
- SOOSEF_DATA_DIR=/data
- FIELDWITNESS_DATA_DIR=/data
```
Adjust port mappings and volume drivers as needed for your environment.
@ -207,8 +207,8 @@ For organizations already running Kubernetes:
```bash
# Build images
docker build -t soosef-server --target server -f deploy/docker/Dockerfile .
docker build -t soosef-relay --target relay -f deploy/docker/Dockerfile .
docker build -t fieldwitness-server --target server -f deploy/docker/Dockerfile .
docker build -t fieldwitness-relay --target relay -f deploy/docker/Dockerfile .
# Deploy
kubectl apply -f deploy/kubernetes/namespace.yaml
@ -218,7 +218,7 @@ kubectl apply -f deploy/kubernetes/server-deployment.yaml
Important constraints from `deploy/kubernetes/README.md`:
- **Single writer only.** Both deployments use `replicas: 1` with `Recreate` strategy.
SooSeF uses SQLite and append-only binary logs that require single-writer access. Do not
FieldWitness uses SQLite and append-only binary logs that require single-writer access. Do not
scale horizontally.
- **Persistent volumes required.** The server needs 10Gi, the relay needs 5Gi. Adjust
based on expected attestation volume.
@ -234,21 +234,21 @@ be handled by:
- A cloud load balancer (if on a VPS)
For a simple mini-PC newsroom setup without a reverse proxy, override the CMD to remove
`--no-https` and let SooSeF generate a self-signed certificate.
`--no-https` and let FieldWitness generate a self-signed certificate.
### 2.6 Backups
The Docker volume contains all SooSeF state. Back it up:
The Docker volume contains all FieldWitness state. Back it up:
```bash
# Stop the container, snapshot the volume, restart
docker compose stop server
docker run --rm -v server-data:/data -v /backup:/backup \
busybox tar czf /backup/soosef-$(date +%Y%m%d).tar.gz -C /data .
busybox tar czf /backup/fieldwitness-$(date +%Y%m%d).tar.gz -C /data .
docker compose start server
```
Or use `soosef keys export` from inside the container for key-only backups.
Or use `fieldwitness keys export` from inside the container for key-only backups.
---
@ -279,7 +279,7 @@ This starts the relay with:
- Data volume: `relay-data` at `/data`
The Docker image uses the `relay` target from the same multi-stage Dockerfile. The relay
runs only `uvicorn soosef.verisoo.api:app` -- the minimal federation endpoint.
runs only `uvicorn fieldwitness.attest.api:app` -- the minimal federation endpoint.
### 3.3 Kubernetes deployment
@ -320,11 +320,11 @@ attestations.
## 4. Threat Level Configuration Presets
SooSeF ships four configuration presets at `deploy/config-presets/`. Choose one based on
FieldWitness ships four configuration presets at `deploy/config-presets/`. Choose one based on
your operational threat environment and copy it to your config location.
```bash
cp deploy/config-presets/high-threat.json ~/.soosef/config.json
cp deploy/config-presets/high-threat.json ~/.fwmetadata/config.json
```
### 4.1 Low Threat -- Press Freedom Country
@ -407,34 +407,34 @@ measures that are outside the scope of this software.
### 4.5 Customizing presets
The presets are starting points. Override individual settings in `~/.soosef/config.json`
The presets are starting points. Override individual settings in `~/.fwmetadata/config.json`
after copying. The full configuration reference is in Section 8.
---
## 5. Initial Setup (All Tiers)
### 5.1 Initialize SooSeF
### 5.1 Initialize FieldWitness
On Tier 1 (USB), initialization happens automatically on first boot. On Tier 2/3 (Docker),
the container runs `soosef init` on first start. For manual installs:
the container runs `fieldwitness init` on first start. For manual installs:
```bash
soosef init
fieldwitness init
```
This creates the `~/.soosef/` directory structure:
This creates the `~/.fwmetadata/` directory structure:
```
~/.soosef/
~/.fwmetadata/
config.json Unified configuration
identity/ Ed25519 signing keypair (verisoo)
identity/ Ed25519 signing keypair (attest)
private.pem
public.pem
identity.meta.json
stegasoo/ Stegasoo state
stego/ Stego state
channel.key AES-256-GCM channel key
attestations/ Verisoo attestation log and index
attestations/ Attest attestation log and index
chain/ Hash chain data
anchors/ External timestamp anchors
auth/ Web UI user database (SQLite)
@ -448,7 +448,7 @@ This creates the `~/.soosef/` directory structure:
The `identity/` and `auth/` directories are created with mode 0700.
`soosef init` generates:
`fieldwitness init` generates:
- An Ed25519 identity keypair (for signing attestations)
- A channel key (for steganographic encoding)
@ -457,7 +457,7 @@ The `identity/` and `auth/` directories are created with mode 0700.
### 5.2 Apply a threat level preset
```bash
cp deploy/config-presets/<level>-threat.json ~/.soosef/config.json
cp deploy/config-presets/<level>-threat.json ~/.fwmetadata/config.json
```
See Section 4 for preset descriptions.
@ -467,7 +467,7 @@ See Section 4 for preset descriptions.
Start the server and create the first admin user through the web UI:
```bash
soosef serve --host 0.0.0.0 --no-https
fieldwitness serve --host 0.0.0.0 --no-https
```
Navigate to `http://<host-ip>:5000` from a device on the same network. The web UI will
@ -510,12 +510,12 @@ Adjust the device path to match your partition layout.
### 6.2 Disable core dumps
A core dump from the SooSeF process would contain key material in plaintext.
A core dump from the FieldWitness process would contain key material in plaintext.
```bash
echo "* hard core 0" | sudo tee -a /etc/security/limits.conf
echo "kernel.core_pattern=/dev/null" | sudo tee -a /etc/sysctl.d/99-soosef.conf
sudo sysctl -p /etc/sysctl.d/99-soosef.conf
echo "kernel.core_pattern=/dev/null" | sudo tee -a /etc/sysctl.d/99-fieldwitness.conf
sudo sysctl -p /etc/sysctl.d/99-fieldwitness.conf
```
### 6.3 Firewall
@ -525,7 +525,7 @@ sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 5000/tcp # SooSeF web UI
sudo ufw allow 5000/tcp # FieldWitness web UI
sudo ufw allow 8000/tcp # Federation API (Tier 2 only)
sudo ufw enable
```
@ -553,39 +553,39 @@ Adjust based on what your system has running. The goal is to minimize attack sur
```bash
# LAN-only, no HTTPS (acceptable if the network is physically isolated)
soosef serve --host 0.0.0.0 --no-https
fieldwitness serve --host 0.0.0.0 --no-https
# With self-signed HTTPS (recommended)
soosef serve --host 0.0.0.0
fieldwitness serve --host 0.0.0.0
# Custom port
soosef serve --host 0.0.0.0 --port 8443
fieldwitness serve --host 0.0.0.0 --port 8443
```
On first HTTPS start, SooSeF auto-generates a self-signed certificate at
`~/.soosef/certs/cert.pem`. Browsers will show a certificate warning -- this is expected
On first HTTPS start, FieldWitness auto-generates a self-signed certificate at
`~/.fwmetadata/certs/cert.pem`. Browsers will show a certificate warning -- this is expected
for self-signed certs. Instruct users to accept the warning or distribute the cert file
to client devices.
SooSeF uses Waitress (pure Python, no C dependencies) as its production server with 4
FieldWitness uses Waitress (pure Python, no C dependencies) as its production server with 4
worker threads by default. Adjust with `--workers`.
### 7.2 systemd service (bare metal Tier 2)
Create `/etc/systemd/system/soosef.service`:
Create `/etc/systemd/system/fieldwitness.service`:
```ini
[Unit]
Description=SooSeF Security Fieldkit
Description=FieldWitness Security Fieldkit
After=network.target
[Service]
Type=simple
User=soosef
Group=soosef
WorkingDirectory=/home/soosef
Environment="PATH=/home/soosef/soosef-env/bin:/usr/bin"
ExecStart=/home/soosef/soosef-env/bin/soosef serve --host 0.0.0.0 --workers 4
User=fieldwitness
Group=fieldwitness
WorkingDirectory=/home/fieldwitness
Environment="PATH=/home/fieldwitness/fieldwitness-env/bin:/usr/bin"
ExecStart=/home/fieldwitness/fieldwitness-env/bin/fieldwitness serve --host 0.0.0.0 --workers 4
Restart=on-failure
RestartSec=5
@ -593,7 +593,7 @@ RestartSec=5
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/soosef/.soosef
ReadWritePaths=/home/fieldwitness/.fieldwitness
PrivateTmp=yes
[Install]
@ -604,8 +604,8 @@ Enable and start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now soosef
sudo journalctl -u soosef -f # Watch logs
sudo systemctl enable --now fieldwitness
sudo journalctl -u fieldwitness -f # Watch logs
```
Add `--no-https` to `ExecStart` if running on a physically isolated LAN where TLS is
@ -615,7 +615,7 @@ unnecessary.
## 8. Configuration Reference
Configuration lives at `~/.soosef/config.json`. Edit it directly or use the web admin
Configuration lives at `~/.fwmetadata/config.json`. Edit it directly or use the web admin
panel. All fields have sensible defaults -- you only need to set what you want to change.
| Field | Default | Description |
@ -628,7 +628,7 @@ panel. All fields have sensible defaults -- you only need to set what you want t
| `session_timeout_minutes` | `15` | Idle session expiry. Lower is safer. |
| `login_lockout_attempts` | `5` | Failed logins before lockout. |
| `login_lockout_minutes` | `15` | Lockout duration after exceeding failed login attempts. |
| `default_embed_mode` | `auto` | Default steganographic embedding mode for Stegasoo. |
| `default_embed_mode` | `auto` | Default steganographic embedding mode for Stego. |
| `killswitch_enabled` | `false` | Enable software killswitch. |
| `deadman_enabled` | `false` | Enable dead man's switch. |
| `deadman_interval_hours` | `24` | Hours between required check-ins. |
@ -637,8 +637,8 @@ panel. All fields have sensible defaults -- you only need to set what you want t
| `usb_monitoring_enabled` | `false` | Monitor for unauthorized USB devices. |
| `tamper_monitoring_enabled` | `false` | File integrity monitoring. |
| `chain_enabled` | `true` | Wrap attestations in a hash chain. |
| `chain_auto_wrap` | `true` | Automatically chain verisoo attestations. |
| `backup_reminder_days` | `7` | Warn if no backup in this many days. `soosef status` reports overdue backups. |
| `chain_auto_wrap` | `true` | Automatically chain attest attestations. |
| `backup_reminder_days` | `7` | Warn if no backup in this many days. `fieldwitness status` reports overdue backups. |
| `cover_name` | `""` | If set, used as the CN in the self-signed SSL certificate instead of "localhost". See Section 15 (Cover/Duress Mode). |
Example minimal config for a high-threat field deployment:
@ -669,14 +669,14 @@ deployment type. On Tier 1 (USB), the killswitch destroys the LUKS key. On Tier
### 9.1 Dead man's switch
The dead man's switch requires periodic check-ins. If you miss a check-in, SooSeF sends
The dead man's switch requires periodic check-ins. If you miss a check-in, FieldWitness sends
a warning during the grace period. If the grace period expires without a check-in, the
killswitch fires automatically and destroys all key material and data.
Arm it:
```bash
soosef fieldkit deadman arm --interval 12 --grace 1
fieldwitness fieldkit deadman arm --interval 12 --grace 1
```
This requires a check-in every 12 hours, with a 1-hour grace period.
@ -684,7 +684,7 @@ This requires a check-in every 12 hours, with a 1-hour grace period.
Check in:
```bash
soosef fieldkit checkin
fieldwitness fieldkit checkin
```
You can also check in through the web UI at `/fieldkit`.
@ -692,10 +692,10 @@ You can also check in through the web UI at `/fieldkit`.
Check status:
```bash
soosef status
fieldwitness status
```
The dead man's switch enforcement loop runs as a background thread inside `soosef serve`,
The dead man's switch enforcement loop runs as a background thread inside `fieldwitness serve`,
checking every 60 seconds. It will send a webhook warning (if configured via
`deadman_warning_webhook`) during the grace period, then execute a full purge if the grace
period expires. The webhook must be a public URL -- SSRF protection blocks private/internal
@ -704,7 +704,7 @@ IP ranges.
For cron-based enforcement outside the web server (e.g., on a headless node), use:
```bash
soosef fieldkit check-deadman
fieldwitness fieldkit check-deadman
```
Exit codes: 0 = not armed or not overdue, 1 = unexpected error, 2 = killswitch fired.
@ -712,16 +712,16 @@ Exit codes: 0 = not armed or not overdue, 1 = unexpected error, 2 = killswitch f
Disarm:
```bash
soosef fieldkit deadman disarm
fieldwitness fieldkit deadman disarm
```
### 9.2 Geofence
If you have a USB GPS module, you can set a geographic boundary. SooSeF will trigger the
If you have a USB GPS module, you can set a geographic boundary. FieldWitness will trigger the
killswitch if the device moves outside the fence.
```bash
soosef fieldkit geofence set --lat 50.4501 --lon 30.5234 --radius 5000
fieldwitness fieldkit geofence set --lat 50.4501 --lon 30.5234 --radius 5000
```
Coordinates are in decimal degrees, radius in meters. Most useful on Tier 1 field devices.
@ -731,10 +731,10 @@ Coordinates are in decimal degrees, radius in meters. Most useful on Tier 1 fiel
Record currently connected USB devices as the trusted baseline:
```bash
soosef fieldkit usb snapshot
fieldwitness fieldkit usb snapshot
```
When monitoring is enabled, SooSeF will alert (or trigger killswitch, depending on config)
When monitoring is enabled, FieldWitness will alert (or trigger killswitch, depending on config)
if an unknown USB device is connected.
### 9.4 Tamper baseline
@ -742,10 +742,10 @@ if an unknown USB device is connected.
Record file integrity baselines for critical files:
```bash
soosef fieldkit tamper baseline
fieldwitness fieldkit tamper baseline
```
SooSeF monitors for unexpected changes to tracked files when tamper monitoring is enabled.
FieldWitness monitors for unexpected changes to tracked files when tamper monitoring is enabled.
### 9.5 Killswitch
@ -760,9 +760,9 @@ maximize what is gone before any interruption:
5. **Attestation log and chain data**
6. **Temp files and audit log**
7. **Configuration**
8. **System journal entries** for the soosef unit
8. **System journal entries** for the fieldwitness unit
9. **Deep forensic scrub** (see below)
10. **Self-uninstall** of the soosef pip package
10. **Self-uninstall** of the fieldwitness pip package
On Tier 1 (USB), steps 1-10 are replaced by LUKS header destruction, which is faster and
more reliable on flash media (see Section 1.6).
@ -770,25 +770,25 @@ more reliable on flash media (see Section 1.6).
Trigger manually:
```bash
soosef fieldkit purge --confirm CONFIRM-PURGE
fieldwitness fieldkit purge --confirm CONFIRM-PURGE
```
**Deep forensic scrub (Tier 2 bare metal only).** When the killswitch fires with `ALL`
scope on a non-USB deployment, it performs a deep forensic scrub that removes traces of
SooSeF beyond the `~/.soosef/` directory:
FieldWitness beyond the `~/.fwmetadata/` directory:
- **Python bytecache**: removes all `__pycache__` directories and `.pyc` files for
soosef, stegasoo, and verisoo from site-packages
fieldwitness, stego, and attest from site-packages
- **pip dist-info**: removes package metadata directories that would reveal what was
installed
- **pip download cache**: removes cached wheels and source distributions under
`~/.cache/pip/` matching soosef/stegasoo/verisoo
`~/.cache/pip/` matching fieldwitness/stego/attest
- **Shell history**: rewrites `~/.bash_history`, `~/.zsh_history`, and fish history to
remove all lines containing "soosef"
- **Self-uninstall**: runs `pip uninstall -y soosef` to remove the package from the
remove all lines containing "fieldwitness"
- **Self-uninstall**: runs `pip uninstall -y fieldwitness` to remove the package from the
virtual environment
After a full purge, the system will show minimal evidence that SooSeF was ever installed.
After a full purge, the system will show minimal evidence that FieldWitness was ever installed.
Note that this is best-effort -- filesystem journal entries, inode metadata, and flash
wear-leveling remnants may still exist. For complete deniability on Tier 2, use full-disk
encryption (LUKS) and physically destroy the storage media. On Tier 1, LUKS header
@ -798,11 +798,11 @@ destruction handles this.
## 10. Key Management
SooSeF manages two separate key domains:
FieldWitness manages two separate key domains:
- **Ed25519 identity key** (`~/.soosef/identity/`) -- used for signing attestations.
- **Ed25519 identity key** (`~/.fwmetadata/identity/`) -- used for signing attestations.
This is your provenance identity.
- **AES-256-GCM channel key** (`~/.soosef/stegasoo/channel.key`) -- used for
- **AES-256-GCM channel key** (`~/.fwmetadata/stego/channel.key`) -- used for
steganographic encoding/decoding. Shared with anyone who needs to read your
stego messages.
@ -810,11 +810,11 @@ These are separate security concerns and are never merged.
### 10.1 Backup
Back up keys regularly. SooSeF warns if no backup has been taken within the
Back up keys regularly. FieldWitness warns if no backup has been taken within the
`backup_reminder_days` window (default: 7 days).
```bash
soosef keys export /media/usb/soosef-backup.enc
fieldwitness keys export /media/usb/fieldwitness-backup.enc
```
This creates an encrypted bundle. You will be prompted for a passphrase. Store the USB
@ -826,7 +826,7 @@ keys are gone. Back up to a second USB drive and store it in a separate location
### 10.2 Restore
```bash
soosef keys import /media/usb/soosef-backup.enc
fieldwitness keys import /media/usb/fieldwitness-backup.enc
```
### 10.3 Key rotation
@ -834,13 +834,13 @@ soosef keys import /media/usb/soosef-backup.enc
Rotate the identity keypair (old key is archived, not destroyed):
```bash
soosef keys rotate-identity
fieldwitness keys rotate-identity
```
Rotate the channel key:
```bash
soosef keys rotate-channel
fieldwitness keys rotate-channel
```
After rotating keys, take a fresh backup immediately. Notify all collaborators of the
@ -851,7 +851,7 @@ new identity fingerprint so they can update their trusted-key lists.
Import a collaborator's public key so you can verify their attestations:
```bash
soosef keys trust --import /media/usb/collaborator-pubkey.pem
fieldwitness keys trust --import /media/usb/collaborator-pubkey.pem
```
Verify the fingerprint out-of-band (in person, over a secure channel) before trusting.
@ -861,7 +861,7 @@ Verify the fingerprint out-of-band (in person, over a secure channel) before tru
## 11. Source Drop Box
The source drop box provides a SecureDrop-like anonymous file intake that runs inside
SooSeF. Sources do not need a SooSeF account -- they receive a one-time upload URL and
FieldWitness. Sources do not need a FieldWitness account -- they receive a one-time upload URL and
submit files through their browser.
### 11.1 Creating tokens
@ -908,7 +908,7 @@ the receipt is valid.
### 11.5 Operational security for the drop box
- **No SooSeF branding**: the upload page is a minimal HTML form with no identifying
- **No FieldWitness branding**: the upload page is a minimal HTML form with no identifying
marks, styled generically
- **No authentication required**: the source never creates an account or reveals
identity information
@ -918,21 +918,21 @@ the receipt is valid.
- **Tor compatibility**: the upload page is a self-contained HTML page with inline
JavaScript (SubtleCrypto only) and no external resources. It works over Tor Browser
with JavaScript enabled. No CDN, no fonts, no analytics
- **No IP logging**: SooSeF does not log source IP addresses. Ensure your reverse proxy
- **No IP logging**: FieldWitness does not log source IP addresses. Ensure your reverse proxy
(if any) also does not log access. If running behind Tor, the source's real IP is never
visible to the server
- **Receipt codes are deterministic**: the receipt is an HMAC of the file's SHA-256 keyed
by the token, so the source can independently verify it corresponds to their file
If operating in a high-risk environment, consider running SooSeF as a Tor hidden service
If operating in a high-risk environment, consider running FieldWitness as a Tor hidden service
(`.onion` address). Configure a torrc hidden service pointing to `127.0.0.1:5000` and
share the `.onion` URL instead of a LAN address.
### 11.6 Drop box file storage
Uploaded files are stored in `~/.soosef/temp/dropbox/` with filenames derived from the
Uploaded files are stored in `~/.fwmetadata/temp/dropbox/` with filenames derived from the
SHA-256 prefix. This directory has mode 0700. Token metadata and receipts are stored in a
SQLite database at `~/.soosef/auth/dropbox.db`.
SQLite database at `~/.fwmetadata/auth/dropbox.db`.
---
@ -957,11 +957,11 @@ If the device has internet access (even temporarily), submit the chain head to a
Timestamping Authority:
```bash
soosef chain anchor --tsa https://freetsa.org/tsr
fieldwitness chain anchor --tsa https://freetsa.org/tsr
```
This sends the chain head digest to the TSA, receives a signed timestamp token, and saves
both the anchor and the TSA response as a JSON file under `~/.soosef/chain/anchors/`.
both the anchor and the TSA response as a JSON file under `~/.fwmetadata/chain/anchors/`.
The TSA token is a cryptographically signed proof from a third party that the hash existed
at the timestamp. This is legally stronger than a self-asserted timestamp.
@ -971,7 +971,7 @@ at the timestamp. This is legally stronger than a self-asserted timestamp.
Without `--tsa`, the command exports the anchor hash for manual external submission:
```bash
soosef chain anchor
fieldwitness chain anchor
```
This prints a compact text block containing the chain ID, head index, record count, and
@ -989,7 +989,7 @@ The anchor file is saved locally regardless of whether a TSA was used.
For Tier 1 (USB) and other airgapped deployments:
1. Run `soosef chain anchor` on the airgapped device
1. Run `fieldwitness chain anchor` on the airgapped device
2. Copy the printed anchor text to a USB drive (text file, photograph of screen, or
paper transcription)
3. On an internet-connected device, publish the anchor text to one or more external
@ -1002,7 +1002,7 @@ For Tier 1 (USB) and other airgapped deployments:
To verify that the current chain state matches a previously created anchor:
```bash
soosef chain verify
fieldwitness chain verify
```
This checks all hash linkage and signatures in the chain. If the chain has been tampered
@ -1012,7 +1012,7 @@ with since the anchor was created, verification will fail.
## 13. Cross-Organization Federation
Federation allows multiple SooSeF instances to exchange attestation records for
Federation allows multiple FieldWitness instances to exchange attestation records for
collaborative investigations. Bundles are self-authenticating: each record carries the
signer's public key, so the importer can verify signatures against their trust store.
@ -1031,14 +1031,14 @@ On Organization A:
```bash
# Export public key
cp ~/.soosef/identity/public.pem /media/usb/org-a-pubkey.pem
cp ~/.fwmetadata/identity/public.pem /media/usb/org-a-pubkey.pem
```
On Organization B:
```bash
# Import Org A's key and verify fingerprint
soosef keys trust --import /media/usb/org-a-pubkey.pem
fieldwitness keys trust --import /media/usb/org-a-pubkey.pem
```
Always verify fingerprints out-of-band (in person, over a known-secure voice channel).
@ -1049,13 +1049,13 @@ Repeat in both directions so each organization trusts the other.
Export a JSON bundle containing attestation records and chain data:
```bash
soosef chain export --output /media/usb/investigation-bundle.zip
fieldwitness chain export --output /media/usb/investigation-bundle.zip
```
To export only records from a specific index range:
```bash
soosef chain export --start 100 --end 200 --output /media/usb/partial-bundle.zip
fieldwitness chain export --start 100 --end 200 --output /media/usb/partial-bundle.zip
```
The export includes:
@ -1068,7 +1068,7 @@ The export includes:
### 13.3 Importing attestation bundles
On the receiving organization's SooSeF instance:
On the receiving organization's FieldWitness instance:
- Records are imported into the local attestation log with a `federated_from` metadata tag
- Records signed by untrusted fingerprints are rejected (unless trust-on-first-use is used)
@ -1077,7 +1077,7 @@ On the receiving organization's SooSeF instance:
### 13.4 Delivery acknowledgments
When a bundle is imported and the receiving instance has a chain store and private key,
SooSeF automatically creates a delivery acknowledgment record in the local chain. This
FieldWitness automatically creates a delivery acknowledgment record in the local chain. This
records the bundle hash, sender fingerprint, and count of records received. The
acknowledgment provides a cryptographic receipt that the bundle was delivered and ingested.
@ -1087,7 +1087,7 @@ To produce evidence for a court order or legal discovery request without reveali
entire chain:
```bash
soosef chain disclose --indices 42,43,44 --output disclosure.json
fieldwitness chain disclose --indices 42,43,44 --output disclosure.json
```
This exports a proof bundle where the selected records are shown in full and all other
@ -1110,7 +1110,7 @@ Tier 1 field devices.
## 14. Evidence Packages and Cold Archives
SooSeF provides two export formats for preserving evidence outside the running instance.
FieldWitness provides two export formats for preserving evidence outside the running instance.
### 14.1 Evidence packages
@ -1127,21 +1127,21 @@ Contents of an evidence package:
`cryptography`)
- `README.txt` -- human-readable instructions
The package is self-contained. No SooSeF installation is required to verify the evidence.
The package is self-contained. No FieldWitness installation is required to verify the evidence.
The standalone `verify.py` script checks image SHA-256 hashes against attestation records
and verifies chain hash linkage.
**When to create evidence packages:**
- Before handing evidence to a legal team
- When sharing with a partner organization that does not run SooSeF
- When sharing with a partner organization that does not run FieldWitness
- For court submission (the self-contained verifier is the key feature)
- Before any action that might destroy the running instance (travel through hostile
checkpoints, anticipated raids)
### 14.2 Cold archives
A cold archive is a full snapshot of the entire SooSeF evidence store, designed for
A cold archive is a full snapshot of the entire FieldWitness evidence store, designed for
long-term preservation. It follows OAIS (ISO 14721) alignment: the archive is
self-describing, includes its own verification code, and documents the cryptographic
algorithms used.
@ -1149,7 +1149,7 @@ algorithms used.
Contents of a cold archive:
- `chain/` -- raw append-only hash chain binary, state checkpoint, and anchor files
- `attestations/` -- full verisoo attestation log and LMDB index
- `attestations/` -- full attest attestation log and LMDB index
- `keys/public.pem` -- signer's public key
- `keys/bundle.enc` -- encrypted key bundle (optional, password-protected)
- `keys/trusted/` -- trusted collaborator public keys
@ -1157,13 +1157,13 @@ Contents of a cold archive:
- `verify.py` -- standalone verification script
- `ALGORITHMS.txt` -- documents all cryptographic algorithms and formats used (Ed25519,
SHA-256, AES-256-GCM, Argon2id, CBOR, etc.) so the archive remains verifiable even if
SooSeF no longer exists
FieldWitness no longer exists
- `README.txt` -- human-readable description
To restore a cold archive on a fresh SooSeF instance:
To restore a cold archive on a fresh FieldWitness instance:
```bash
soosef archive import <archive.zip>
fieldwitness archive import <archive.zip>
```
**When to create cold archives:**
@ -1178,13 +1178,13 @@ soosef archive import <archive.zip>
For legal discovery and court proceedings:
1. Use `soosef chain disclose` for selective disclosure (Section 13.5) when you must
1. Use `fieldwitness chain disclose` for selective disclosure (Section 13.5) when you must
respond to a specific request without revealing the full chain
2. Use evidence packages for handing specific images and their attestations to counsel
3. Use cold archives when full preservation is required
All three formats include standalone verification scripts so that the receiving party does
not need to install SooSeF.
not need to install FieldWitness.
### 14.4 Long-term archival best practices
@ -1202,24 +1202,24 @@ not need to install SooSeF.
## 15. Cover/Duress Mode
Cover mode disguises a SooSeF installation so that casual inspection of the device does
Cover mode disguises a FieldWitness installation so that casual inspection of the device does
not immediately reveal it as a security toolkit.
### 15.1 Renaming the data directory
By default, SooSeF stores everything under `~/.soosef/`. To use an inconspicuous name,
set the `SOOSEF_DATA_DIR` environment variable:
By default, FieldWitness stores everything under `~/.fwmetadata/`. To use an inconspicuous name,
set the `FIELDWITNESS_DATA_DIR` environment variable:
```bash
export SOOSEF_DATA_DIR=~/.local/share/inventory
soosef init
export FIELDWITNESS_DATA_DIR=~/.local/share/inventory
fieldwitness init
```
All SooSeF commands respect this variable. Add it to the soosef user's shell profile or
All FieldWitness commands respect this variable. Add it to the fieldwitness user's shell profile or
the systemd service file:
```ini
Environment="SOOSEF_DATA_DIR=/home/soosef/.local/share/inventory"
Environment="FIELDWITNESS_DATA_DIR=/home/fieldwitness/.local/share/inventory"
```
In Docker deployments, set this in the environment section of the compose file. The
@ -1237,7 +1237,7 @@ a browser inspector sees a plausible-looking certificate:
}
```
Delete `~/.soosef/certs/cert.pem` and restart the server to regenerate the certificate
Delete `~/.fwmetadata/certs/cert.pem` and restart the server to regenerate the certificate
with the new CN.
The threat level presets (Section 4) include appropriate cover names for each level.
@ -1245,7 +1245,7 @@ The threat level presets (Section 4) include appropriate cover names for each le
### 15.3 Portable USB operation (Tier 1)
The Tier 1 bootable USB is the primary cover mechanism. When the USB is not inserted, the
host laptop shows no trace of SooSeF. The USB itself is a LUKS-encrypted partition that
host laptop shows no trace of FieldWitness. The USB itself is a LUKS-encrypted partition that
reveals nothing without the passphrase.
For additional cover, the USB can be labeled generically (e.g., "DEBIAN LIVE") and the
@ -1255,19 +1255,19 @@ LUKS partition does not advertise its contents.
## 16. Operational Security Notes
SooSeF is a tool, not a shield. Understand what it cannot do.
FieldWitness is a tool, not a shield. Understand what it cannot do.
### What SooSeF does not protect against
### What FieldWitness does not protect against
- **Physical coercion.** If someone forces you to unlock the device or reveal passwords,
no software can help. The killswitch is for situations where you can act before
interception, not during.
- **Social engineering.** SooSeF cannot prevent users from being tricked into revealing
- **Social engineering.** FieldWitness cannot prevent users from being tricked into revealing
credentials or disabling security features.
- **Leaving the browser open.** The session timeout helps, but if someone walks up to an
unlocked browser session, they have access. Train users to close the browser or lock the
screen.
- **Compromised client devices.** SooSeF secures the server. If a user's laptop has
- **Compromised client devices.** FieldWitness secures the server. If a user's laptop has
malware, their browser session is compromised regardless of what the server does.
- **Tier 3 relay compromise.** If the relay is seized, attestation metadata (hashes,
signatures, timestamps) is exposed. This reveals that certain public keys attested
@ -1285,7 +1285,7 @@ reliable** because:
- Wear leveling distributes writes across the flash, meaning the original block may be
preserved.
SooSeF's defense against this is **cryptographic erasure**: destroy the keys first, then
FieldWitness's defense against this is **cryptographic erasure**: destroy the keys first, then
the data. Even if fragments of encrypted data survive on flash, they are useless without
the keys. The killswitch destroys keys before anything else, and keys are small enough to
fit in a single flash block.
@ -1318,7 +1318,7 @@ This is a fundamental limitation of Python-based security tools.
### Health check
SooSeF exposes a `/health` endpoint on the web UI. Hit it to verify the server is running:
FieldWitness exposes a `/health` endpoint on the web UI. Hit it to verify the server is running:
```bash
# Tier 1 or bare metal
@ -1336,7 +1336,7 @@ The `-k` flag skips certificate verification for self-signed certs.
### System status
```bash
soosef status
fieldwitness status
```
This checks identity key, channel key, trusted keys, dead man's switch state, geofence,
@ -1348,7 +1348,7 @@ chain status, and backup status. Use `--json` for machine-readable output.
1. Check that `host` is set to `0.0.0.0` in config, not `127.0.0.1`
2. Check firewall: `sudo ufw status` -- port 5000 must be allowed
3. Check the service is running: `sudo systemctl status soosef` (bare metal) or
3. Check the service is running: `sudo systemctl status fieldwitness` (bare metal) or
`docker compose ps` (Docker)
4. Check the machine's IP: `ip addr show`
@ -1361,13 +1361,13 @@ docker compose logs server
```
Common causes: port conflict (5000 or 8000 already in use), volume permission issues,
or missing initialization. The container runs `soosef init` on first start, which requires
or missing initialization. The container runs `fieldwitness init` on first start, which requires
write access to the `/data` volume.
**Certificate warnings in browser**
Expected with self-signed certificates. Users must click through the warning. To avoid
this, distribute `~/.soosef/certs/cert.pem` to client devices and install it as a
this, distribute `~/.fwmetadata/certs/cert.pem` to client devices and install it as a
trusted certificate.
**Dead man's switch fires unexpectedly**
@ -1380,16 +1380,16 @@ start. Make sure the systemd service is set to `Restart=on-failure` (bare metal)
If you need to perform maintenance, disarm the switch first:
```bash
soosef fieldkit deadman disarm
fieldwitness fieldkit deadman disarm
```
Re-arm when maintenance is complete.
**Permission errors on ~/.soosef/**
**Permission errors on ~/.fwmetadata/**
The `identity/`, `auth/`, and `certs/` directories are mode 0700. If running under a
different user than the one who ran `soosef init`, you will get permission denied errors.
Always run SooSeF as the same user. In Docker, the container runs as the `soosef` user
different user than the one who ran `fieldwitness init`, you will get permission denied errors.
Always run FieldWitness as the same user. In Docker, the container runs as the `fieldwitness` user
created during image build.
**Drop box tokens expire immediately**
@ -1410,7 +1410,7 @@ sudo date -s "2026-04-01 12:00:00"
**Chain anchor TSA submission fails**
TSA submission requires network access. On Tier 1 (USB) or other airgapped devices, use
manual anchoring instead (`soosef chain anchor` without `--tsa`). If the TSA URL is
manual anchoring instead (`fieldwitness chain anchor` without `--tsa`). If the TSA URL is
unreachable, the anchor is still saved locally -- only the external timestamp token is
missing.
@ -1420,8 +1420,8 @@ If you set `cover_name` after the certificate was already generated, delete the
certificate and restart:
```bash
rm ~/.soosef/certs/cert.pem ~/.soosef/certs/key.pem
sudo systemctl restart soosef
rm ~/.fwmetadata/certs/cert.pem ~/.fwmetadata/certs/key.pem
sudo systemctl restart fieldwitness
```
**Account lockout after repeated failed logins**
@ -1452,15 +1452,15 @@ python verify.py
**Kubernetes pod stuck in CrashLoopBackOff**
Check logs with `kubectl logs -n soosef <pod-name>`. Common cause: the PersistentVolumeClaim
is not bound. Verify with `kubectl get pvc -n soosef`. The server needs 10Gi and the relay
Check logs with `kubectl logs -n fieldwitness <pod-name>`. Common cause: the PersistentVolumeClaim
is not bound. Verify with `kubectl get pvc -n fieldwitness`. The server needs 10Gi and the relay
needs 5Gi.
---
## Appendix A: Legacy Raspberry Pi Deployment
The Raspberry Pi was the original deployment target for SooSeF. It remains a viable
The Raspberry Pi was the original deployment target for FieldWitness. It remains a viable
option for fixed installations (e.g., a permanently installed newsroom server that does
not need to be portable). The three-tier model supersedes the RPi as the primary
deployment for most use cases because:
@ -1477,7 +1477,7 @@ If you still want to use a Raspberry Pi for a fixed Tier 2 server:
libjpeg62-turbo-dev zlib1g-dev libffi-dev libssl-dev gfortran libopenblas-dev`
- Follow the Tier 2 bare metal instructions (systemd service, hardening) in this guide
- For GPIO hardware killswitch support, install the `rpi` extra:
`pip install "soosef[rpi]"` (includes `gpiozero`)
`pip install "fieldwitness[rpi]"` (includes `gpiozero`)
- Wire a momentary push button between GPIO 17 and 3.3V with a 10k pull-down to GND.
Default: 5-second hold to trigger. Configurable via `gpio_killswitch_hold_seconds`

View File

@ -1,20 +1,20 @@
# Evidence Guide
**Audience**: Journalists, investigators, and legal teams who need to create, export, and
verify evidence packages from SooSeF.
verify evidence packages from FieldWitness.
**Prerequisites**: A running SooSeF instance with at least one attested image or file.
**Prerequisites**: A running FieldWitness instance with at least one attested image or file.
Familiarity with basic CLI commands.
---
## Overview
SooSeF provides three mechanisms for preserving and sharing evidence outside a running
FieldWitness provides three mechanisms for preserving and sharing evidence outside a running
instance: evidence packages (for handing specific files to third parties), cold archives
(full-state preservation for 10+ year horizons), and selective disclosure (proving specific
records without revealing the rest of the chain). All three include standalone verification
scripts that require no SooSeF installation.
scripts that require no FieldWitness installation.
---
@ -37,24 +37,24 @@ verification of specific attested images or files.
```bash
# Package specific images with their attestation records
$ soosef evidence export photo1.jpg photo2.jpg --output evidence_package.zip
$ fieldwitness evidence export photo1.jpg photo2.jpg --output evidence_package.zip
# Filter by investigation tag
$ soosef evidence export photo1.jpg --investigation "case-2026-001" \
$ fieldwitness evidence export photo1.jpg --investigation "case-2026-001" \
--output evidence_case001.zip
```
### When to create evidence packages
- Before handing evidence to a legal team or court
- When sharing with a partner organization that does not run SooSeF
- When sharing with a partner organization that does not run FieldWitness
- Before crossing a hostile checkpoint (create the package, send it to a trusted party,
then activate the killswitch if needed)
- When an investigation is complete and files must be archived independently
### Verifying an evidence package
The recipient does not need SooSeF. They need only Python 3.11+ and the `cryptography`
The recipient does not need FieldWitness. They need only Python 3.11+ and the `cryptography`
pip package:
```bash
@ -75,9 +75,9 @@ The verification script checks:
## Cold Archives
A cold archive is a full snapshot of the entire SooSeF evidence store, designed for
A cold archive is a full snapshot of the entire FieldWitness evidence store, designed for
long-term preservation aligned with OAIS (ISO 14721). It is self-describing and includes
everything needed to verify the evidence decades later, even if SooSeF no longer exists.
everything needed to verify the evidence decades later, even if FieldWitness no longer exists.
### What is inside a cold archive
@ -86,7 +86,7 @@ everything needed to verify the evidence decades later, even if SooSeF no longer
| `chain/chain.bin` | Raw append-only hash chain binary |
| `chain/state.cbor` | Chain state checkpoint |
| `chain/anchors/` | External timestamp anchor files (RFC 3161 tokens, manual anchors) |
| `attestations/log.bin` | Full verisoo attestation log |
| `attestations/log.bin` | Full attest attestation log |
| `attestations/index/` | LMDB index files |
| `keys/public.pem` | Signer's Ed25519 public key |
| `keys/bundle.enc` | Encrypted key bundle (optional, password-protected) |
@ -100,10 +100,10 @@ everything needed to verify the evidence decades later, even if SooSeF no longer
```bash
# Full archive without encrypted key bundle
$ soosef archive export --output archive_20260401.zip
$ fieldwitness archive export --output archive_20260401.zip
# Include encrypted key bundle (will prompt for passphrase)
$ soosef archive export --include-keys --output archive_20260401.zip
$ fieldwitness archive export --include-keys --output archive_20260401.zip
```
> **Warning:** If you include the encrypted key bundle, store the passphrase separately
@ -119,11 +119,11 @@ $ soosef archive export --include-keys --output archive_20260401.zip
### Restoring from a cold archive
On a fresh SooSeF instance:
On a fresh FieldWitness instance:
```bash
$ soosef init
$ soosef archive import archive_20260401.zip
$ fieldwitness init
$ fieldwitness archive import archive_20260401.zip
```
### Long-term archival best practices
@ -133,7 +133,7 @@ $ soosef archive import archive_20260401.zip
3. Include the encrypted key bundle in the archive with a strong passphrase
4. Periodically verify archive integrity: unzip and run `python verify.py`
5. The `ALGORITHMS.txt` file documents every algorithm and parameter used, so a verifier
can be written from scratch even if SooSeF no longer exists
can be written from scratch even if FieldWitness no longer exists
### The ALGORITHMS.txt file
@ -143,7 +143,7 @@ This file documents every cryptographic algorithm, parameter, and format used:
- **Hashing**: SHA-256 for content and chain linkage; pHash and dHash for perceptual image matching
- **Encryption (key bundle)**: AES-256-GCM with Argon2id key derivation (time_cost=4, memory_cost=256MB, parallelism=4)
- **Chain format**: Append-only binary log with uint32 BE length prefixes and CBOR (RFC 8949) records
- **Attestation log**: Verisoo binary log format
- **Attestation log**: Attest binary log format
---
@ -163,7 +163,7 @@ are part of an unbroken chain without seeing the contents of other records.
```bash
# Disclose records at chain indices 5, 12, and 47
$ soosef chain disclose --indices 5,12,47 --output disclosure.json
$ fieldwitness chain disclose --indices 5,12,47 --output disclosure.json
```
### Disclosure output format
@ -180,7 +180,7 @@ $ soosef chain disclose --indices 5,12,47 --output disclosure.json
{
"chain_index": 5,
"content_hash": "...",
"content_type": "verisoo/attestation-v1",
"content_type": "attest/attestation-v1",
"prev_hash": "...",
"record_hash": "...",
"signer_pubkey": "...",
@ -230,11 +230,11 @@ preceded it, because the chain is append-only with hash linkage.
If the device has internet access (even temporarily):
```bash
$ soosef chain anchor --tsa https://freetsa.org/tsr
$ fieldwitness chain anchor --tsa https://freetsa.org/tsr
```
This sends the chain head digest to a Timestamping Authority, receives a signed timestamp
token, and saves both as a JSON file under `~/.soosef/chain/anchors/`. The TSA token is a
token, and saves both as a JSON file under `~/.fwmetadata/chain/anchors/`. The TSA token is a
cryptographically signed proof from a third party that the hash existed at the stated time.
This is legally stronger than a self-asserted timestamp.
@ -243,7 +243,7 @@ This is legally stronger than a self-asserted timestamp.
Without `--tsa`:
```bash
$ soosef chain anchor
$ fieldwitness chain anchor
```
This prints a compact text block. Publish it to any external witness:
@ -268,16 +268,16 @@ This prints a compact text block. Publish it to any external witness:
For responding to a court order, subpoena, or legal discovery request:
1. **Selective disclosure** (`soosef chain disclose`) when the request specifies particular
1. **Selective disclosure** (`fieldwitness chain disclose`) when the request specifies particular
records and you must not reveal the full chain
2. **Evidence package** when the request requires original images with verification
capability
3. **Cold archive** when full preservation is required (e.g., an entire investigation)
All three formats include standalone verification scripts so the receiving party does not
need SooSeF installed. The verification scripts require only Python 3.11+ and the
need FieldWitness installed. The verification scripts require only Python 3.11+ and the
`cryptography` pip package.
> **Note:** Consult with legal counsel before producing evidence from SooSeF. The selective
> **Note:** Consult with legal counsel before producing evidence from FieldWitness. The selective
> disclosure mechanism is designed to support legal privilege and proportionality, but its
> application depends on your jurisdiction and the specific legal context.

View File

@ -1,16 +1,16 @@
# Federation Guide
**Audience**: System administrators and technical leads setting up cross-organization
attestation sync between SooSeF instances.
attestation sync between FieldWitness instances.
**Prerequisites**: A running SooSeF instance (Tier 2 org server or Tier 3 relay), familiarity
**Prerequisites**: A running FieldWitness instance (Tier 2 org server or Tier 3 relay), familiarity
with the CLI, and trusted public keys from partner organizations.
---
## Overview
SooSeF federation synchronizes attestation records between organizations using a gossip
FieldWitness federation synchronizes attestation records between organizations using a gossip
protocol. Nodes periodically exchange Merkle roots, detect divergence, and fetch missing
records. The system is eventually consistent with no central coordinator, no leader
election, and no consensus protocol -- just append-only logs that converge.
@ -20,7 +20,7 @@ Federation operates at two levels:
1. **Offline bundles** -- JSON export/import via sneakernet (USB drive). Works on all tiers
including fully airgapped Tier 1 field devices.
2. **Live gossip** -- HTTP-based periodic sync between Tier 2 org servers and Tier 3
federation relays. Requires the `federation` extra (`pip install soosef[federation]`).
federation relays. Requires the `federation` extra (`pip install fieldwitness[federation]`).
> **Warning:** Federation shares attestation records (image hashes, Ed25519 signatures,
> timestamps, and signer public keys). It never shares encryption keys, plaintext messages,
@ -76,16 +76,16 @@ Each peer tracks:
Unhealthy peers are skipped during gossip rounds but remain registered. They are retried
on the next full gossip round. Peer state persists in SQLite at
`~/.soosef/attestations/federation/peers.db`.
`~/.fwmetadata/attestations/federation/peers.db`.
### Gossip interval
The default gossip interval is 60 seconds, configurable via the `VERISOO_GOSSIP_INTERVAL`
The default gossip interval is 60 seconds, configurable via the `FIELDWITNESS_GOSSIP_INTERVAL`
environment variable. In Docker Compose, set it in the environment section:
```yaml
environment:
- VERISOO_GOSSIP_INTERVAL=60
- FIELDWITNESS_GOSSIP_INTERVAL=60
```
Lower intervals mean faster convergence but more network traffic.
@ -102,13 +102,13 @@ Always verify fingerprints out-of-band (in person or over a known-secure voice c
On Organization A:
```bash
$ cp ~/.soosef/identity/public.pem /media/usb/org-a-pubkey.pem
$ cp ~/.fwmetadata/identity/public.pem /media/usb/org-a-pubkey.pem
```
On Organization B:
```bash
$ soosef keys trust --import /media/usb/org-a-pubkey.pem
$ fieldwitness keys trust --import /media/usb/org-a-pubkey.pem
```
Repeat in both directions so each organization trusts the other.
@ -123,7 +123,7 @@ Through the web UI at `/federation`, or via the peer store directly:
```bash
# On Org A's server, register Org B's federation endpoint
$ soosef federation peer add \
$ fieldwitness federation peer add \
--url https://orgb.example.org:8000 \
--fingerprint a1b2c3d4e5f6...
```
@ -143,7 +143,7 @@ security groups, etc.).
For manual one-time sync:
```bash
$ soosef federation sync --peer https://orgb.example.org:8000
$ fieldwitness federation sync --peer https://orgb.example.org:8000
```
### Step 4: Monitor sync status
@ -163,19 +163,19 @@ For Tier 1 field devices and airgapped environments, use offline bundles.
### Exporting a bundle
```bash
$ soosef chain export --output /media/usb/bundle.zip
$ fieldwitness chain export --output /media/usb/bundle.zip
```
To export only records from a specific investigation:
```bash
$ soosef chain export --investigation "case-2026-001" --output /media/usb/bundle.zip
$ fieldwitness chain export --investigation "case-2026-001" --output /media/usb/bundle.zip
```
To export a specific index range:
```bash
$ soosef chain export --start 100 --end 200 --output /media/usb/partial.zip
$ fieldwitness chain export --start 100 --end 200 --output /media/usb/partial.zip
```
### Importing a bundle
@ -183,7 +183,7 @@ $ soosef chain export --start 100 --end 200 --output /media/usb/partial.zip
On the receiving instance:
```bash
$ soosef chain import /media/usb/bundle.zip
$ fieldwitness chain import /media/usb/bundle.zip
```
During import:
@ -191,12 +191,12 @@ During import:
- Records signed by untrusted fingerprints are rejected
- Duplicate records (matching SHA-256) are skipped
- Imported records are tagged with `federated_from` metadata
- A delivery acknowledgment record (`soosef/delivery-ack-v1`) is automatically appended
- A delivery acknowledgment record (`fieldwitness/delivery-ack-v1`) is automatically appended
to the local chain
### Delivery acknowledgments
When a bundle is imported, SooSeF signs a `soosef/delivery-ack-v1` chain record that
When a bundle is imported, FieldWitness signs a `fieldwitness/delivery-ack-v1` chain record that
contains:
- The SHA-256 of the imported bundle file
@ -208,7 +208,7 @@ bundle was delivered and ingested. It creates a two-way federation handshake.
```bash
# On receiving org: export the acknowledgment back
$ soosef chain export --start <ack_index> --end <ack_index> \
$ fieldwitness chain export --start <ack_index> --end <ack_index> \
--output /media/usb/delivery-ack.zip
```
@ -293,7 +293,7 @@ Records are rejected if the signer's fingerprint is not in the local trust store
the sender's public key first:
```bash
$ soosef keys trust --import /path/to/sender-pubkey.pem
$ fieldwitness keys trust --import /path/to/sender-pubkey.pem
```
**Consistency proof failure**
@ -302,7 +302,7 @@ A consistency proof failure means the peer's log is not a valid extension of the
This indicates a potential fork -- the peer may have a different chain history. Investigate
before proceeding:
1. Compare chain heads: `soosef chain status` on both instances
1. Compare chain heads: `fieldwitness chain status` on both instances
2. If a fork is confirmed, one instance's records must be exported and re-imported into a
fresh chain
@ -311,7 +311,7 @@ before proceeding:
The gossip loop requires the `federation` extra:
```bash
$ pip install "soosef[federation]"
$ pip install "fieldwitness[federation]"
```
This installs `aiohttp` for async HTTP communication.

View File

@ -1,4 +1,4 @@
# SooSeF Documentation
# FieldWitness Documentation
## For Reporters and Field Users
@ -30,5 +30,5 @@
|---|---|
| [Federation Architecture](architecture/federation.md) | System design: threat model, layers (chain, bundles, federation), key domains, permission tiers. |
| [Chain Format Spec](architecture/chain-format.md) | CBOR record format, entropy witnesses, serialization, storage format, content types. |
| [Export Bundle Spec](architecture/export-bundle.md) | SOOSEFX1 binary format, envelope encryption (X25519 + AES-256-GCM), Merkle trees. |
| [Export Bundle Spec](architecture/export-bundle.md) | FIELDWITNESSX1 binary format, envelope encryption (X25519 + AES-256-GCM), Merkle trees. |
| [Federation Protocol Spec](architecture/federation-protocol.md) | CT-inspired server protocol: API endpoints, gossip, storage tiers, receipts, security model. |

View File

@ -0,0 +1,240 @@
# C2PA Integration Plan
**Audience:** FieldWitness developers and maintainers
**Status:** Planning (pre-implementation)
**Last updated:** 2026-04-01
## Overview
FieldWitness needs C2PA (Coalition for Content Provenance and Authenticity) export/import
capability. C2PA is the emerging industry standard for content provenance, backed by
Adobe, Microsoft, Google, and the BBC. ProofMode, Guardian Project, and Starling Lab
have all adopted C2PA. FieldWitness must speak C2PA to remain relevant in the provenance
space.
---
## C2PA Spec Essentials
- JUMBF-based provenance standard embedded in media files
- Core structures: **Manifest Store > Manifest > Claim + Assertions + Ingredients + Signature**
- Claims are CBOR maps with assertion references, signing algorithm, `claim_generator`,
and timestamps
- Standard assertions:
- `c2pa.actions` -- edit history
- `c2pa.hash.data` -- hard binding (byte-range)
- `c2pa.location.broad` -- city/region location
- `c2pa.exif` -- EXIF metadata
- `c2pa.creative.work` -- title, description, authorship
- `c2pa.training-mining` -- AI training/mining consent
- Vendor-specific assertions under reverse-DNS (e.g., `org.fieldwitness.*`)
- Signing uses **COSE_Sign1** (RFC 9052)
- Supported algorithms: Ed25519 (OKP), ES256/ES384/ES512 (ECDSA), PS256/PS384/PS512 (RSA-PSS)
- **X.509 certificate chain required** -- embedded in COSE unprotected header; raw public
keys are not sufficient
- Offline validation works with pre-installed trust anchors; self-signed certs work in
"local trust anchor" mode
## Python Library: c2pa-python
- Canonical binding from C2PA org (PyPI: `c2pa-python`, GitHub: `contentauth/c2pa-python`)
- Rust extension (`c2pa-rs` via PyO3), not pure Python
- Version ~0.6.x, API not fully stable
- Platform wheels: manylinux2014 x86_64/aarch64, macOS, Windows
- **No armv6/armv7 wheels** -- affects Tier 1 Raspberry Pi deployments
- Core API: `c2pa.Reader`, `c2pa.Builder`, `builder.sign()`, `c2pa.create_signer()`
- `create_signer` takes a callback, algorithm, certs PEM, optional timestamp URL
- `timestamp_url=None` skips RFC 3161 timestamping (acceptable for offline use)
---
## Concept Mapping: FieldWitness to C2PA
### Clean mappings
| FieldWitness | C2PA |
|--------|------|
| `AttestationRecord` | C2PA Manifest |
| `attestor_fingerprint` | Signer cert subject (wrapped in X.509) |
| `AttestationRecord.timestamp` | Claim `created` (ISO 8601) |
| `CaptureMetadata.captured_at` | `c2pa.exif` DateTimeOriginal |
| `CaptureMetadata.location` | `c2pa.location.broad` |
| `CaptureMetadata.device` | `c2pa.exif` Make/Model |
| `CaptureMetadata.caption` | `c2pa.creative.work` description |
| `ImageHashes.sha256` | `c2pa.hash.data` (hard binding) |
| Ed25519 private key | COSE_Sign1 signing key (needs X.509 wrapper) |
### FieldWitness has, C2PA does not
- Perceptual hashes (phash, dhash) -- map to vendor assertion `org.fieldwitness.perceptual-hashes`
- Merkle log inclusion proofs -- map to vendor assertion `org.fieldwitness.merkle-proof`
- Chain records with entropy witnesses -- map to vendor assertion `org.fieldwitness.chain-record`
- Delivery acknowledgment records (entirely FieldWitness-specific)
- Cross-org gossip federation
- Perceptual matching for verification (survives recompression)
- Selective disclosure / redaction
### C2PA has, FieldWitness does not
- Hard file binding (byte-range exclusion zones)
- X.509 certificate trust chains
- Actions history (`c2pa.actions`: crop, rotate, AI-generate, etc.)
- AI training/mining consent
- Ingredient DAG (content derivation graph)
---
## Privacy Design
Three tiers of identity disclosure:
1. **Org-level cert (preferred):** One self-signed X.509 cert per organization, not per
person. Subject is org name. Individual reporters do not appear in the manifest.
2. **Pseudonym cert:** Subject is pseudonym or random UUID. Valid C2PA but unrecognized
by external trust anchors.
3. **No C2PA export:** For critical-threat presets, evidence stays in FieldWitness format until
reaching Tier 2.
### GPS handling
C2PA's `c2pa.location.broad` is city/region level. FieldWitness captures precise GPS. On
export, downsample to city-level unless the operator explicitly opts in. Precise GPS
stays in FieldWitness record only.
### Metadata handling
Strip all EXIF from the output file except what is intentionally placed in the
`c2pa.exif` assertion.
---
## Offline-First Constraints
- **Tier 1 (field, no internet):** C2PA manifests without RFC 3161 timestamp. FieldWitness
chain record provides timestamp anchoring via vendor assertion.
- **Tier 2 (org server, may have internet):** Optionally contact TSA at export time.
Connects to existing `anchors.py` infrastructure.
- Entropy witnesses embedded as vendor assertions provide soft timestamp evidence.
- Evidence packages include org cert PEM alongside C2PA manifest for offline verification.
- `c2pa-python` availability gated behind `has_c2pa()` -- not all hardware can run it.
---
## Architecture
### New module: `src/fieldwitness/c2pa_bridge/`
```
src/fieldwitness/c2pa_bridge/
__init__.py # Public API: export, import, has_c2pa()
cert.py # Self-signed X.509 cert generation from Ed25519 key
export.py # AttestationRecord -> C2PA manifest
importer.py # C2PA manifest -> AttestationRecord (best-effort)
vendor_assertions.py # org.fieldwitness.* assertion schemas
cli.py # CLI subcommands: fieldwitness c2pa export / verify / import
```
### Module relationships
- `export.py` reads from `attest/models.py`, `federation/chain.py`,
`keystore/manager.py`; calls `cert.py` and `vendor_assertions.py`
- `importer.py` reads image bytes, writes `AttestationRecord` via
`attest/attestation.py`, parses vendor assertions
### Web UI
New routes in the `attest.py` blueprint:
- `GET /attest/<record_id>/c2pa` -- download C2PA-embedded image
- `POST /attest/import-c2pa` -- upload and import C2PA manifest
### Evidence packages
`evidence.py` gains `include_c2pa=True` option. Adds C2PA-embedded file variants and
org cert to the ZIP.
### pyproject.toml extra
```toml
c2pa = ["c2pa-python>=0.6.0", "fieldwitness[attest]"]
```
---
## Implementation Phases
### Phase 0 -- Prerequisites (~1h)
- `has_c2pa()` in `_availability.py`
- `c2pa` extra in `pyproject.toml`
### Phase 1 -- Certificate management (~3h)
- `c2pa_bridge/cert.py`
- Self-signed X.509 from Ed25519 identity key
- Configurable subject (org name default, pseudonym for high-threat)
- Store at `~/.fwmetadata/identity/c2pa_cert.pem`
- Regenerate on key rotation
### Phase 2 -- Export path (~6h)
- `c2pa_bridge/export.py` + `vendor_assertions.py`
- Core function `export_c2pa()` takes image data, `AttestationRecord`, key, cert, options
- Builds assertions: `c2pa.actions`, `c2pa.hash.data`, `c2pa.exif`, `c2pa.creative.work`,
`org.fieldwitness.perceptual-hashes`, `org.fieldwitness.chain-record`, `org.fieldwitness.attestation-id`
- Vendor assertion schemas versioned (v1)
### Phase 3 -- Import path (~5h)
- `c2pa_bridge/importer.py`
- `import_c2pa()` reads C2PA manifest, produces `AttestationRecord`
- Maps C2PA fields to FieldWitness model
- Returns `C2PAImportResult` with `trust_status`
- Creates new FieldWitness attestation record over imported data
### Phase 4 -- CLI integration (~4h)
- `fieldwitness c2pa export/verify/import/show` subcommands
- Gated on `has_c2pa()`
### Phase 5 -- Web UI + evidence packages (~5h)
- Blueprint routes for export/import
- Evidence package C2PA option
### Phase 6 -- Threat-level presets (~2h)
- Add `c2pa` config block to each preset (`export_enabled`, `privacy_level`,
`include_precise_gps`, `timestamp_url`)
- `C2PAConfig` sub-dataclass in `FieldWitnessConfig`
### MVP scope
**Phases 0-2 (~10h):** Produces C2PA-compatible images viewable in Adobe Content
Credentials and any C2PA verifier.
---
## Key Decisions (Before Coding)
1. **Use existing Ed25519 identity key for cert** (not a separate key) -- preserves
single-key-domain design.
2. **Cert stored at `~/.fwmetadata/identity/c2pa_cert.pem`**, regenerated on key rotation.
3. **Tier 1 ARM fallback:** Tier 1 produces FieldWitness records; Tier 2 generates C2PA export
on their behalf.
4. **Pin `c2pa-python>=0.6.0`**, add shim layer for API stability.
5. **Hard binding computed by `c2pa-python` Builder** automatically.
---
## FieldWitness's Unique C2PA Value
- **Cross-org chain of custody** via gossip federation (delivery ack records as ingredients)
- **Perceptual hash matching** embedded in C2PA (survives JPEG recompression via
WhatsApp/Telegram)
- **Merkle log inclusion proofs** in manifest (proves attestation committed to append-only log)
- **Entropy witnesses** as soft timestamp attestation (makes backdating harder without
RFC 3161)
- **Privacy-preserving by design** (org certs, GPS downsampling, zero-identity mode)
- **Fully offline end-to-end verification** (bundled cert + `c2pa-python`, no network needed)

View File

@ -0,0 +1,214 @@
# Go-to-Market Feasibility Plan
**Audience:** Internal planning (solo developer)
**Status:** Active planning document
**Last updated:** 2026-04-01
## Overview
Phased plan for building credibility and visibility for FieldWitness in the press freedom and
digital security space. Constraints: solo developer, ~10-15 hrs/week, portfolio/learning
project that should also produce real-world value.
---
## Current Strengths
- Federation layer is genuinely novel: gossip-based attestation sync across orgs with
offline-first design and append-only hash chains
- Three-tier deployment model maps to how press freedom orgs actually work
- C2PA export is well-timed as CAI gains momentum
- Working codebase with tests, deployment configs, documentation
## Core Challenges
- **Trust deficit:** "Some guy built a tool" is a warning sign in this space, not a
selling point
- **Chicken-and-egg:** Need audit for credibility, need credibility/money for audit,
need adoption for money
- **Limited bandwidth:** 10-15 hrs/week makes sequencing critical
- **Stego perception risk:** Steganography angle can be a credibility liability if
positioned as headline feature (perceived as "hacker toy")
---
## Phase 1: Foundation (Months 1-6)
**Goal:** Make the project legible to the ecosystem.
### Technical credibility (60% of time)
- Ship C2PA export as v0.3.0 headline feature (target: 8 weeks)
- Write formal threat model document at `docs/security/threat-model.md`
- Model after Signal protocol docs or Tor design doc
- De-emphasize steganography in public surfaces -- lead with "offline-first provenance
attestation with gossip federation"
- Set up reproducible builds with pinned dependencies
- Get CI/CD visibly working with test/lint/type-check/coverage badges
### Positioning and documentation (20% of time)
- Write "Why FieldWitness Exists" document (~1500 words): the problem, why existing tools
don't solve it, what FieldWitness does differently, who it's for, what it needs
- Create 2-minute demo video: field attestation -> sneakernet sync -> federation ->
verification
### Community engagement (20% of time)
- Lurk on `liberationtech@lists.stanford.edu` -- do NOT announce tool cold; wait for
relevant threads
- GitHub engagement with adjacent projects (real contributions, not performative):
- `guardian/proofmode-android`
- `contentauth/c2pa-python`
- `freedomofpress/securedrop`
- Post Show HN when C2PA export ships
---
## Phase 2: Credibility Escalation (Months 7-12)
**Goal:** Get external validation from at least one recognized entity.
### OTF (Open Technology Fund) -- https://www.opentech.fund/
**Internet Freedom Fund:** $50K-$900K over 12-36 months. Solo developers eligible.
Rolling applications.
**Red Team Lab:** FREE security audits commissioned through partner firms (Cure53, Trail
of Bits, Radically Open Security). This is the single highest-leverage action.
**Usability Lab:** Free UX review.
**Application timeline:** 2-4 months from submission to decision.
**Strategy:** Apply to Red Team Lab for audit FIRST (lower commitment for OTF, validates
you as "OTF-vetted").
### Compelling application elements
1. Lead with problem: "Provenance attestation tools assume persistent internet. For
journalists in [specific scenario], this fails."
2. Lead with differentiator: "Gossip federation for cross-org attestation sync,
offline-first, bridges to C2PA."
3. Be honest about status: "Working prototype at v0.3.0, needs audit and field testing."
4. Budget: stipend, audit (if Red Team Lab unavailable), 1-2 conferences, federation
relay hosting.
### Backup audit and funding paths
| Organization | URL | Notes |
|---|---|---|
| OSTIF | https://ostif.org/ | Funds audits for open-source projects; may be too early-stage |
| Radically Open Security | https://www.radicallyopensecurity.com/ | Nonprofit, reduced rates for internet freedom projects; focused audit ~$15-30K |
| NLnet Foundation | https://nlnet.nl/ | EUR 5-50K grants, lightweight process, solo devs welcome, includes audit funding |
| Filecoin Foundation for Decentralized Web | https://fil.org/grants | Relevant to federation/provenance angle |
### Community building
- Submit talk to **IFF 2027** (Internet Freedom Festival, Valencia, ~March)
- Open sessions and tool showcases have low barriers
- Talk title: "Federated Evidence Chains: Offline Provenance for Journalists in
Hostile Environments"
- Cold outreach to 3-5 specific people:
- Access Now Digital Security Helpline trainers
- Harlo Holmes (FPF Director of Digital Security)
- Guardian Project developers (ProofMode team)
- Position as complementary, not competitive
- Lead with "I want honest feedback"
- Conferences:
- **RightsCon** -- https://www.rightscon.org/
- **IFF** -- https://internetfreedomfestival.org/
- **USENIX Security / PETS** -- academic venues, for federation protocol paper
---
## Phase 3: Traction or Pivot (Months 13-24)
### Green lights (keep going)
- OTF Red Team Lab acceptance or any grant funding
- A digital security trainer says "I could see using this"
- A journalist or NGO runs it in any scenario
- Another developer contributes a meaningful PR
- Conference talk accepted
### Red lights (pivot positioning)
- Zero response from outreach after 6+ months
- Funders say problem is already solved
- Security reviewers find fundamental design flaws
### If green (months 13-24)
- Execute audit, publish results publicly (radical transparency)
- Build pilot deployment guide
- Apply for Internet Freedom Fund
- Present at RightsCon 2027/2028
### If red (months 13-24)
- Reposition as reference implementation / research project
- Write federation protocol as academic paper
- Lean into portfolio angle
---
## Professional Portfolio Positioning
### Framing
"I designed and implemented a gossip-based federation protocol for offline-first
provenance attestation, targeting field deployment in resource-constrained environments.
The system uses Ed25519 signing, Merkle trees with consistency proofs, append-only hash
chains with CBOR serialization, and bridges to the C2PA industry standard."
### Skills demonstrated
- Cryptographic protocol design
- Distributed systems (gossip, consistency proofs)
- Security engineering (threat modeling, audit prep, key management)
- Systems architecture (three-tier, offline-first)
- Domain expertise (press freedom, evidence integrity)
- Grant writing (if pursued)
### Target roles
- Security engineer (FPF, EFF, Access Now, Signal, Cloudflare)
- Protocol engineer (decentralized systems)
- Developer advocate (security companies)
- Infrastructure engineer
### Key portfolio artifacts
- Threat model document (shows security thinking)
- Audit report, even with findings (shows maturity)
- C2PA bridge (shows standards interop, not just NIH)
---
## Timeline (10-15 hrs/week)
| Month | Focus | Deliverable | Time split |
|-------|-------|-------------|------------|
| 1-2 | C2PA export + threat model | v0.3.0, `threat-model.md` | 12 code, 3 docs |
| 3-4 | Demo video + "Why FieldWitness" + CI | Video, doc, badges | 8 code, 4 docs, 3 outreach |
| 5-6 | OTF Red Team Lab app + community | Application submitted, Show HN | 5 code, 5 grants, 5 outreach |
| 7-9 | Community + backup grants | Outreach emails, NLnet/FFDW apps | 8 code, 3 grants, 4 outreach |
| 10-12 | IFF submission + traction check | Talk submitted, go/no-go decision | 8 code, 2 grants, 5 outreach |
| 13-18 | (If green) Audit + pilot guide | Published audit, pilot doc | 10 code, 5 docs |
| 19-24 | (If green) Conference + IFF app | Talk, major grant application | 5 code, 5 grant, 5 outreach |
---
## What NOT to Bother With
- Paid marketing, ads, PR
- Product Hunt, startup directories, "launch" campaigns
- Project website beyond clean README
- Corporate partnerships
- Whitepapers before audit
- Mobile apps
- Discord/Slack community (dead community is worse than none)
- Press coverage (too early)
- Competing with SecureDrop on source protection
- General tech conference talks (domain-specific venues only)

View File

@ -0,0 +1,480 @@
# FieldWitness Threat Model
**Status:** Living document -- updated as the design evolves and as external review
identifies gaps. Version numbers track significant revisions.
**Document version:** 0.1 (2026-04-01)
**Corresponds to:** FieldWitness v0.2.0
This document follows the style of the Signal Protocol specification and the Tor design
document: it makes precise claims, distinguishes what is guaranteed from what is not, and
does not use marketing language. Unresolved questions and known gaps are stated plainly.
**This document has not been externally audited.** Claims here reflect the designer's
intent and analysis. An independent security review is planned as part of Phase 2 (see
`docs/planning/gtm-feasibility.md`). Until that review is complete, treat this document
as a design statement, not a security certification.
---
## Table of Contents
1. [Intended Users](#1-intended-users)
2. [Adversary Model](#2-adversary-model)
3. [Assets Protected](#3-assets-protected)
4. [Trust Boundaries](#4-trust-boundaries)
5. [Security Guarantees](#5-security-guarantees)
6. [Non-Guarantees](#6-non-guarantees)
7. [Cryptographic Primitives](#7-cryptographic-primitives)
8. [Key Management Model](#8-key-management-model)
9. [Federation Trust Model](#9-federation-trust-model)
10. [Known Limitations](#10-known-limitations)
---
## 1. Intended Users
FieldWitness is designed for three overlapping user populations:
**Field reporters and documenters.** Journalists, human rights monitors, and election
observers working in environments where physical device seizure is a plausible risk.
Operating assumption: the user may be detained, the device may be confiscated, and the
operator at Tier 2 (the org server) may not be reachable in real time. The user needs to
attest evidence locally on a Tier 1 (field device) and sync later -- or never, if the USB
is destroyed.
**Organizational administrators.** IT staff and security-aware operators at newsrooms or
NGOs running Tier 2 deployments. They manage keys, configure threat levels, operate the
source drop box, and maintain federation peering with partner organizations. They are
expected to understand basic operational security concepts but are not expected to be
cryptographers.
**Partner organizations.** Organizations that receive attested evidence bundles from the
primary organization and need to verify chain-of-custody without installing FieldWitness. They
interact with standalone `verify.py` scripts included in evidence packages.
FieldWitness is **not** designed as a general-purpose secure communications tool, a replacement
for SecureDrop's source protection model, or a consumer privacy application.
---
## 2. Adversary Model
### 2.1 Passive Network Observer
**Capability:** Can observe all network traffic between nodes, including Tier 2 to Tier 3
communication and gossip federation traffic. Cannot break TLS or Ed25519.
**Goal:** Determine which organizations are communicating, the timing and volume of
attestation syncs, and potentially correlate sync events with news events.
**FieldWitness's position:** Transport-level metadata (IP addresses, timing, volume) is not
hidden. TLS (self-signed, port 8000) protects payload content. A passive observer can
determine that two Tier 2 servers are federating; they cannot read the attestation records
being exchanged without the relevant Ed25519 public keys.
**Gap:** No traffic padding, no onion routing, no anonymization of federation topology.
Organizations with strong network-level adversaries should route federation traffic through
Tor or a VPN. This is not built in.
### 2.2 Active Network Adversary
**Capability:** Can intercept, modify, replay, and drop traffic. Can present forged
TLS certificates if the operator hasn't pinned the peer's certificate.
**Goal:** Inject forged attestation records into the federation, suppress legitimate
records, or cause evidence to appear tampered.
**FieldWitness's position:** All attestation records are Ed25519-signed. A network adversary
cannot forge a valid signature without the private key. The append-only hash chain makes
retroactive injection detectable: inserting a record at position N requires recomputing all
subsequent hashes. Consistency proofs during gossip sync detect log divergence.
**Gap:** Certificate pinning for federation peers is not implemented as of v0.2.0. The
Tier 3 relay uses a self-signed certificate; operators should verify its fingerprint
out-of-band. Gossip peers authenticate by Ed25519 fingerprint, not certificate, which
provides a secondary check.
### 2.3 Physical Access
**Capability:** Has physical access to the field device (Tier 1 USB) or the org server
(Tier 2). May have forensic tools.
**Goal:** Extract private keys, recover attested evidence, identify the operator, or
determine what evidence was collected.
**FieldWitness's position:**
- Tier 1 is designed for amnesia: Debian Live USB with LUKS-encrypted persistent
partition. Pulling the USB from the host leaves no trace on the host machine. If the USB
itself is seized, LUKS protects the persistent partition.
- The killswitch (`fieldwitness fieldkit purge`) destroys all key material and data under
`~/.fwmetadata/` in sensitivity order. The deep forensic scrub removes Python bytecache, pip
metadata, download cache, and shell history entries. The final step is `pip uninstall
-y fieldwitness`.
- The dead man's switch fires the killswitch automatically if check-in is missed.
- Private keys are stored as PEM files with `0600` permissions. Key material is not
additionally encrypted at rest beyond the filesystem (LUKS on Tier 1; operator-managed
on Tier 2).
**Gap:** If the device is seized before the killswitch fires and LUKS has been unlocked
(i.e., the device is running), private keys are accessible. Cold boot attacks against
unlocked LUKS volumes are not mitigated. Key material is not stored in a hardware security
module or OS keychain.
### 2.4 Legal Compulsion
**Capability:** Can compel the operator (or their legal jurisdiction) to produce data,
keys, or records. May use court orders, search warrants, or jurisdiction-specific
administrative processes.
**Goal:** Obtain attestation records or private keys under legal authority.
**FieldWitness's position:** FieldWitness provides tools (selective disclosure, evidence packages)
for producing specific records under court order without revealing the full chain.
The Federation Relay (Tier 3) stores only hashes and signatures -- never private keys or
plaintext. Placing Tier 3 in a jurisdiction with strong press protections limits one
compulsion surface.
**Gap:** If private keys are seized, all past and future attestations signed by those keys
are attributable to the key holder. Key rotation limits forward exposure after a
compromise, but prior records signed by the old key remain attributable. FieldWitness does not
implement deniable authentication.
### 2.5 Insider Threat
**Capability:** Has legitimate access to the FieldWitness instance (e.g., a trusted
administrator or a compromised org server). Can read key material, attestation records,
and logs.
**Goal:** Selectively alter or delete records, export keys, or suppress evidence.
**FieldWitness's position:** The append-only hash chain makes deletion or modification of prior
records detectable: the chain head hash changes and any external anchor (RFC 3161 TSA,
blockchain transaction) will no longer match. Key rotation is logged in the chain as a
`fieldwitness/key-rotation-v1` record signed by the old key, creating an auditable trail.
**Gap:** An insider with direct filesystem access can overwrite `chain.bin` entirely,
including the chain head, before an external anchor is taken. The chain provides integrity
guarantees only to the extent that external anchors are taken regularly and independently
(by another party or a public TSA). Frequency of anchoring is an operational decision, not
enforced by the software.
---
## 3. Assets Protected
The following assets are in scope for FieldWitness's security model:
| Asset | Description | Primary Protection |
|---|---|---|
| Attestation records | Ed25519-signed records linking a file hash to a time, identity, and optional metadata | Append-only chain, Ed25519 signatures |
| Identity private key | Ed25519 private key used to sign attestations | `0600` filesystem permissions, LUKS on Tier 1, killswitch |
| Channel key | AES-256-GCM key used for steganographic encoding | `0600` filesystem permissions, separate from identity key |
| Source submissions | Anonymous uploads through the drop box | EXIF stripping, no-branding upload page, HMAC receipt codes |
| Evidentiary metadata | GPS, timestamp, device model extracted from EXIF | Stored in attestation record, dangerous fields stripped |
| Federation topology | Which organizations are peering | Not protected at network level (see 2.1) |
The following are **out of scope** (not protected by FieldWitness):
- Source identity beyond what is stripped from EXIF
- Operator identity (FieldWitness does not provide anonymity)
- Content of files beyond what is hashed and signed (files are not encrypted at rest
unless encrypted before attestation)
- The Tier 3 relay's knowledge of federation topology
---
## 4. Trust Boundaries
```
[Field reporter / Tier 1] --- LUKS + killswitch --- [Seized device adversary]
|
[USB sneakernet or LAN]
|
[Org Server / Tier 2] ------- TLS (self-signed) ---- [Network adversary]
| [Active attacker: no forged sigs]
[Gossip federation]
|
[Federation Relay / Tier 3] - Stores hashes+sigs only, no keys
|
[Partner Org / Tier 2] ------ Ed25519 trust store -- [Untrusted peers rejected]
|
[Verifying party] ----------- standalone verify.py, cryptography package only
```
**Tier 1 trusts:** Its own key material (generated locally), the Tier 2 server it was
configured to sync with.
**Tier 2 trusts:** Its own key material, the Ed25519 public keys in its trust store
(imported explicitly by the administrator), the Tier 3 relay for transport only (not
content validation).
**Tier 3 trusts:** Nothing. It is a content-unaware relay. It cannot validate the
semantic content of what it stores because it has no access to private keys.
**Verifying party trusts:** The signer's Ed25519 public key (received out-of-band, e.g.,
in the evidence package), the `cryptography` Python package, and the chain linkage logic
in `verify.py`.
---
## 5. Security Guarantees
The following are properties FieldWitness is designed to provide. Each is conditional on the
named preconditions.
**G1: Attestation integrity.** Given an attestation record and the signer's Ed25519 public
key, a verifier can determine whether the record has been modified since signing.
_Precondition:_ The verifier has the correct public key and the `cryptography` package.
_Mechanism:_ Ed25519 signature over deterministic JSON serialization of the record.
**G2: Chain append-only property.** If a record is in the chain at position N, it cannot
be removed or modified without invalidating every subsequent record's hash linkage.
_Precondition:_ The verifier has observed the chain head at some prior point or has an
external anchor.
_Mechanism:_ Each record includes `prev_hash = SHA-256(canonical_bytes(record[N-1]))`.
**G3: Timestamp lower bound.** If the chain head has been submitted to an RFC 3161 TSA
and the token is preserved, all records prior to the anchored head provably existed before
the TSA's signing time.
_Precondition:_ The TSA's clock and signing key are trusted.
_Mechanism:_ RFC 3161 timestamp tokens stored in `chain/anchors/`.
**G4: Selective disclosure soundness.** A selective disclosure bundle proves that the
disclosed records are part of an unbroken chain without revealing the contents of
non-disclosed records.
_Precondition:_ The verifier has the chain head hash from an external source.
_Mechanism:_ Non-selected records appear as hashes only; chain linkage is preserved.
**G5: Federation record authenticity.** Records received via federation are accepted only
if signed by a key in the local trust store.
_Precondition:_ The trust store contains only keys the operator has explicitly imported.
_Mechanism:_ Ed25519 verification against trust store before appending federated records.
**G6: Source drop box anonymity (limited).** A source submitting via the drop box does
not need an account, FieldWitness is not mentioned on the upload page, and dangerous EXIF fields
are stripped before the file is stored.
_Precondition:_ The source accesses the drop box URL over HTTPS without revealing their
identity through other means (IP, browser fingerprint, etc.).
_Limitation:_ FieldWitness does not route drop box traffic through Tor or any anonymization
layer. Network-level anonymity is the source's responsibility.
---
## 6. Non-Guarantees
The following properties are explicitly **not** provided by FieldWitness. Including them here
prevents users from assuming protection that does not exist.
**NG1: Operator anonymity.** FieldWitness does not hide the identity of the organization
running the instance. The Tier 2 server has an IP address. The federation relay knows
which Tier 2 servers are peering.
**NG2: Deniable authentication.** Attestation records are non-repudiably signed by
Ed25519 keys. There is no plausible deniability about which key produced a signature.
**NG3: Forward secrecy for attestation keys.** Ed25519 identity keys are long-lived.
If a private key is compromised, all attestations signed by that key are attributable to
the key holder. Key rotation limits future exposure but does not retroactively remove
attributability.
**NG4: Protection against a compromised Tier 2 before anchoring.** An insider with full
Tier 2 access can rewrite the chain before any external anchor is taken. External anchors
are the primary protection against insider tampering; their value is proportional to how
frequently and independently they are taken.
**NG5: Content confidentiality.** FieldWitness does not encrypt attested files at rest. Files
are hashed and signed, not encrypted. Encryption before attestation is the operator's
responsibility.
**NG6: Source protection beyond EXIF stripping.** The drop box strips dangerous EXIF
fields and does not log source IP addresses in attestation records. It does not provide
the same source protection model as SecureDrop. Organizations with strong source
protection requirements should use SecureDrop for intake and FieldWitness for evidence chain
management.
**NG7: Auditability of the Tier 3 relay.** The relay stores only hashes and signatures,
but FieldWitness does not currently provide a mechanism for operators to audit what the relay
has and has not forwarded. The relay is trusted for availability, not integrity.
---
## 7. Cryptographic Primitives
All cryptographic choices are documented here to support independent review and long-term
archival verifiability.
### Signing
| Primitive | Algorithm | Parameters | Use |
|---|---|---|---|
| Identity signing | Ed25519 | RFC 8032 | Sign attestation records, key rotation records, delivery acks |
| Key storage | PEM | PKCS8 (private), SubjectPublicKeyInfo (public) | Disk format for identity keypair |
Ed25519 was chosen for: short key and signature sizes (32-byte public key, 64-byte
signature), deterministic signing (no random oracle required per operation), strong
security margins, and wide library support.
### Encryption (Stego channel key domain)
| Primitive | Algorithm | Parameters | Use |
|---|---|---|---|
| Symmetric encryption | AES-256-GCM | 256-bit key, 96-bit IV, 128-bit tag | Payload encryption in stego encode |
| Key derivation | Argon2id | time=3, memory=65536, parallelism=4, saltlen=16 | Derive AES key from passphrase + PIN + reference photo fingerprint |
**Note:** The AES-256-GCM channel key domain (Stego) and the Ed25519 identity key
domain (Attest) are kept strictly separate. They serve different security purposes and
share no key material.
### Hashing
| Primitive | Algorithm | Use |
|---|---|---|
| Cryptographic hash | SHA-256 | Chain record linkage (`prev_hash`), content fingerprinting |
| Content fingerprinting | SHA-256 | `ImageHashes.sha256` for all file types |
| Perceptual hash | pHash (DCT-based) | Image tamper detection, survives compression |
| Perceptual hash | dHash (difference hash) | Image tamper detection |
| Perceptual hash | aHash (average hash) | Fuzzy matching, high tolerance |
| Chain serialization | CBOR (RFC 7049) | Canonical encoding for chain records |
| HMAC | HMAC-SHA256 | Drop box receipt code derivation |
### External Timestamping
| Mechanism | Standard | Use |
|---|---|---|
| RFC 3161 TSA | RFC 3161 | Automated, signed timestamp tokens |
| Manual anchor | Any external witness | Chain head hash submitted to blockchain, email, etc. |
---
## 8. Key Management Model
### Key types and locations
| Key | Type | Location | Purpose |
|---|---|---|---|
| Identity private key | Ed25519 | `~/.fwmetadata/identity/private.pem` | Sign all attestation records |
| Identity public key | Ed25519 | `~/.fwmetadata/identity/public.pem` | Shared with verifiers; included in evidence packages |
| Channel key | AES-256-GCM | `~/.fwmetadata/stego/channel.key` | Stego encoding/decoding shared secret |
| Trust store keys | Ed25519 (public only) | `~/.fwmetadata/trusted_keys/<fingerprint>/` | Verify federated records from partners |
### Key rotation
Identity rotation creates a `fieldwitness/key-rotation-v1` chain record signed by the **old**
key, containing the new public key. This establishes a cryptographic chain of trust from
the original key through all rotations. Verifiers following the rotation chain can confirm
that new attestations come from the same organizational identity as old ones.
Channel (AES) key rotation creates a new key and archives the old one. Old channel keys
are required to decode stego payloads encoded with them; archived keys are preserved under
`~/.fwmetadata/stego/archived/`.
### Identity recovery
After device loss, a `fieldwitness/key-recovery-v1` chain record is signed by the **new** key,
carrying the old key's fingerprint and optional cosigner fingerprints. This is an
auditable assertion, not a cryptographic proof that the old key authorized the recovery
(the old key is lost). The recovery record's legitimacy depends on out-of-band
confirmation (e.g., cosigner verification, organizational attestation).
### Backup
The keystore manager (`fieldwitness/keystore/manager.py`) tracks backup state. Encrypted key
bundles can be exported to the SOOBNDL format for cold storage. The backup reminder
interval is configurable; the default is 7 days.
---
## 9. Federation Trust Model
### Peer authentication
Federation peers are identified by their Ed25519 public key fingerprint (first 16 bytes of
SHA-256 of the public key, hex-encoded). Peering is established by explicit administrator
action: the peer's public key fingerprint is configured locally. There is no automatic
peer discovery or trust-on-first-use.
### Record acceptance
A record received via federation is accepted only if:
1. It is signed by an Ed25519 key in the local trust store.
2. The signature is valid over the record's canonical serialization.
3. The record does not duplicate a record already in the local log (by record ID).
Records signed by unknown keys are silently dropped. There is no mechanism to accept
records from temporarily trusted but unregistered peers.
### The Tier 3 relay
The Tier 3 relay is a content-unaware intermediary. It forwards attestation bundles between
Tier 2 nodes but has no access to private keys and cannot validate the semantic content of
records. It is trusted for availability (it should forward what it receives) but not for
integrity (it cannot be used as an authority for whether records are authentic).
### Consistency proofs
During gossip sync, nodes exchange their current Merkle log root and size. If roots
differ, the node with fewer records requests a consistency proof from the node with more
records. The consistency proof proves that the smaller log is a prefix of the larger log,
preventing log divergence. Records are fetched incrementally after the proof verifies.
The consistency proof implementation is in `src/fieldwitness/attest/merkle.py`.
---
## 10. Known Limitations
This section is a candid accounting of current gaps. Items here are candidates for future
work, not dismissals.
**L1: No hardware key storage.** Private keys are stored as PEM files protected only by
filesystem permissions and LUKS (on Tier 1). A hardware security module (HSM), TPM, or OS
keychain would provide stronger protection against physical extraction from a running
system. This is a significant gap for high-threat deployments.
**L2: No certificate pinning for federation.** Tier 2 to Tier 3 connections use TLS with
self-signed certificates. The peer's certificate fingerprint is not currently pinned in
configuration. An active network adversary with the ability to present a forged certificate
could intercept federation traffic. The Ed25519 peer fingerprint provides a secondary check
but is not a substitute.
**L3: Killswitch reliability.** The killswitch's effectiveness depends on the operating
system's file deletion semantics. On HDDs without secure erase, file overwriting may not
prevent forensic recovery. On SSDs with wear leveling, even overwriting does not guarantee
physical deletion. The deep forensic scrub does multiple passes, but this is not
equivalent to verified physical destruction. For critical-threat deployments, physical
destruction of storage media is more reliable than software scrub.
**L4: Anchor frequency is an operational decision.** The chain's tamper-evidence
properties against insider threats depend on how frequently external anchors are taken.
FieldWitness does not enforce or automate anchor frequency. An organization that anchors
infrequently has a larger window during which insider tampering is undetectable.
**L5: Gossip topology is not hidden.** The list of peers a node gossips with is visible
to a network observer. For organizations where federation topology is itself sensitive
information, all federation should be routed through Tor or equivalent.
**L6: No audit of Tier 3 relay behavior.** FieldWitness does not currently provide a way for
operators to verify that the Tier 3 relay has faithfully forwarded all bundles it received.
A malicious or compromised relay could suppress specific records. The design mitigation is
to use the relay only for transport, never as an authoritative source -- but no
verification mechanism is implemented.
**L7: Drop box source anonymity is limited.** The drop box does not log source IP addresses
in attestation records or require accounts, but it does not anonymize the source's network
connection. A source's IP is visible to the Tier 2 server operator in web server access
logs. Organizations providing source protection should use Tor for source access and may
wish to configure the web server to not log IP addresses.
**L8: Steganalysis resistance is not guaranteed.** The steganography backend includes a
steganalysis module (`stego/steganalysis.py`) for estimating detection resistance, but
stego channels are not guaranteed to be undetectable by modern ML-based steganalysis tools
under all conditions. Stego should be treated as a covert channel with meaningful
detection risk, not a guaranteed-invisible channel.
**L9: No formal security proof.** The security of FieldWitness's federation protocol, chain
construction, and selective disclosure has not been formally analyzed. The design draws on
established primitives (Ed25519, SHA-256, RFC 3161, Merkle trees) and patterns (gossip,
append-only logs, Certificate Transparency-inspired consistency proofs), but informal
design analysis is not a substitute for a formal proof or an independent security audit.

View File

@ -1,15 +1,15 @@
# Source Drop Box Setup Guide
**Audience**: Administrators setting up SooSeF's anonymous source intake feature.
**Audience**: Administrators setting up FieldWitness's anonymous source intake feature.
**Prerequisites**: A running SooSeF instance with web UI enabled (`soosef[web]` extra),
**Prerequisites**: A running FieldWitness instance with web UI enabled (`fieldwitness[web]` extra),
an admin account, and HTTPS configured (self-signed is acceptable).
---
## Overview
The source drop box is a SecureDrop-style anonymous file intake built into the SooSeF web
The source drop box is a SecureDrop-style anonymous file intake built into the FieldWitness web
UI. Admins create time-limited upload tokens, sources open the token URL in a browser and
submit files without creating an account. Files are processed through the extract-then-strip
EXIF pipeline and automatically attested on receipt. Sources receive HMAC-derived receipt
@ -24,7 +24,7 @@ codes that prove delivery.
## How It Works
```
Admin Source SooSeF Server
Admin Source FieldWitness Server
| | |
|-- Create token ------------->| |
| (label, expiry, max_files) | |
@ -58,11 +58,11 @@ The drop box should always be served over HTTPS. Sources must be able to trust t
connection is not being intercepted.
```bash
$ soosef serve --host 0.0.0.0
$ fieldwitness serve --host 0.0.0.0
```
SooSeF auto-generates a self-signed certificate on first HTTPS start. For production use,
place a reverse proxy with a proper TLS certificate in front of SooSeF.
FieldWitness auto-generates a self-signed certificate on first HTTPS start. For production use,
place a reverse proxy with a proper TLS certificate in front of FieldWitness.
### Step 2: Create an upload token
@ -95,7 +95,7 @@ Share the upload URL over an already-secure channel:
### Step 4: Source uploads files
The source opens the URL in their browser. The upload page is minimal -- no SooSeF branding,
The source opens the URL in their browser. The upload page is minimal -- no FieldWitness branding,
no identifying marks, generic styling. The page works over Tor Browser with JavaScript
enabled (no external resources, no CDN, no fonts, no analytics).
@ -118,7 +118,7 @@ The admin panel at `/dropbox/admin` shows:
## The Extract-Then-Strip Pipeline
Every file uploaded through the drop box is processed through SooSeF's EXIF pipeline:
Every file uploaded through the drop box is processed through FieldWitness's EXIF pipeline:
1. **Extract**: all EXIF metadata is read from the original image bytes
2. **Classify**: fields are split into evidentiary (GPS coordinates, capture timestamp --
@ -173,10 +173,10 @@ comparing their locally computed hash with the server's receipt.
| What | Where |
|---|---|
| Uploaded files (stripped) | `~/.soosef/temp/dropbox/` (mode 0700) |
| Token metadata | `~/.soosef/auth/dropbox.db` (SQLite) |
| Receipt codes | `~/.soosef/auth/dropbox.db` (SQLite) |
| Attestation records | `~/.soosef/attestations/` (standard attestation log) |
| Uploaded files (stripped) | `~/.fwmetadata/temp/dropbox/` (mode 0700) |
| Token metadata | `~/.fwmetadata/auth/dropbox.db` (SQLite) |
| Receipt codes | `~/.fwmetadata/auth/dropbox.db` (SQLite) |
| Attestation records | `~/.fwmetadata/attestations/` (standard attestation log) |
Expired tokens are cleaned up automatically on every admin page load.
@ -186,9 +186,9 @@ Expired tokens are cleaned up automatically on every admin page load.
### Source safety
- **No SooSeF branding** on the upload page. Generic "Secure File Upload" title.
- **No FieldWitness branding** on the upload page. Generic "Secure File Upload" title.
- **No authentication required** -- sources never create accounts or reveal identity.
- **No IP logging** -- SooSeF does not log source IP addresses. Ensure your reverse proxy
- **No IP logging** -- FieldWitness does not log source IP addresses. Ensure your reverse proxy
(if any) also does not log access requests to `/dropbox/upload/` paths.
- **Self-contained page** -- inline CSS and JavaScript only. No external resources, CDN
calls, web fonts, or analytics. Works with Tor Browser.
@ -203,12 +203,12 @@ Expired tokens are cleaned up automatically on every admin page load.
Once reached, the link stops accepting uploads.
- **Revoke immediately** -- if a token is compromised or no longer needed, revoke it from
the admin panel. This deletes the token and all associated receipt records from SQLite.
- **Audit trail** -- token creation events are logged to `~/.soosef/audit.jsonl` with the
- **Audit trail** -- token creation events are logged to `~/.fwmetadata/audit.jsonl` with the
action `dropbox.token_created`.
### Running as a Tor hidden service
For maximum source protection, run SooSeF as a Tor hidden service:
For maximum source protection, run FieldWitness as a Tor hidden service:
1. Install Tor on the server
2. Configure a hidden service in `torrc` pointing to `127.0.0.1:5000`

View File

@ -1,7 +1,7 @@
# SooSeF Admin Operations Guide
# FieldWitness Admin Operations Guide
**Audience**: IT administrators, system operators, and technically competent journalists
responsible for deploying, configuring, and maintaining SooSeF instances for their
responsible for deploying, configuring, and maintaining FieldWitness instances for their
organization.
**Prerequisites**: Familiarity with Linux command line, Docker basics, and SSH. For Tier 1
@ -15,9 +15,9 @@ This guide covers the operational tasks an admin performs after initial deployme
installation and deployment, see [deployment.md](../deployment.md). For architecture
details, see [docs/architecture/](../architecture/).
Your responsibilities as a SooSeF admin:
Your responsibilities as a FieldWitness admin:
1. Deploy and maintain SooSeF instances (Tier 1 USB, Tier 2 server, Tier 3 relay)
1. Deploy and maintain FieldWitness instances (Tier 1 USB, Tier 2 server, Tier 3 relay)
2. Manage user accounts and access
3. Configure threat level presets for your environment
4. Manage the source drop box
@ -42,7 +42,7 @@ Each user has:
From the admin panel, issue a temporary password for a locked-out user. The user should
change it on next login. All password resets are recorded in the audit log
(`~/.soosef/audit.jsonl`).
(`~/.fwmetadata/audit.jsonl`).
### Account Lockout
@ -54,7 +54,7 @@ For persistent lockout (e.g., a compromised account), delete the user from the a
### Audit Trail
All admin actions are logged to `~/.soosef/audit.jsonl` in JSON-lines format:
All admin actions are logged to `~/.fwmetadata/audit.jsonl` in JSON-lines format:
```json
{"timestamp": "2026-04-01T12:00:00+00:00", "actor": "admin", "action": "user.create", "target": "user:reporter1", "outcome": "success", "source": "web"}
@ -70,13 +70,13 @@ Actions logged: `user.create`, `user.delete`, `user.password_reset`,
## 2. Threat Level Configuration
SooSeF ships four presets at `deploy/config-presets/`. Select based on your operational
FieldWitness ships four presets at `deploy/config-presets/`. Select based on your operational
environment.
### Applying a Preset
```bash
$ cp deploy/config-presets/high-threat.json ~/.soosef/config.json
$ cp deploy/config-presets/high-threat.json ~/.fwmetadata/config.json
```
Restart the server to apply.
@ -92,7 +92,7 @@ Restart the server to apply.
### Custom Configuration
Edit `~/.soosef/config.json` directly. All fields have defaults. Key fields for security:
Edit `~/.fwmetadata/config.json` directly. All fields have defaults. Key fields for security:
| Field | What It Controls |
|---|---|
@ -104,7 +104,7 @@ Edit `~/.soosef/config.json` directly. All fields have defaults. Key fields for
| `deadman_grace_hours` | Grace period after missed check-in before auto-purge |
| `deadman_warning_webhook` | URL to POST a JSON warning during grace period |
| `cover_name` | CN for the self-signed TLS certificate (cover/duress mode) |
| `backup_reminder_days` | Days before `soosef status` warns about overdue backups |
| `backup_reminder_days` | Days before `fieldwitness status` warns about overdue backups |
> **Warning**: Setting `auth_enabled: false` disables all login requirements. Never
> do this on a network-accessible instance.
@ -136,14 +136,14 @@ Share the URL over an already-secure channel only:
### What Happens When a Source Uploads
1. The source opens the URL in any browser (no account needed, no SooSeF branding)
1. The source opens the URL in any browser (no account needed, no FieldWitness branding)
2. Their browser computes SHA-256 hashes client-side before upload (SubtleCrypto)
3. Files are uploaded and processed:
- EXIF metadata is extracted (evidentiary fields: GPS, timestamp)
- All metadata is stripped from the stored copy (protects source device info)
- The original bytes are attested (signed) before stripping
4. The source receives a receipt code (HMAC of file hash + token)
5. Files are stored in `~/.soosef/temp/dropbox/` with mode 0700
5. Files are stored in `~/.fwmetadata/temp/dropbox/` with mode 0700
### Revoking Tokens
@ -157,16 +157,16 @@ their receipt code. This returns the filename, SHA-256, and reception timestamp.
### Operational Security
- The upload page has no SooSeF branding -- it is a minimal HTML form
- The upload page has no FieldWitness branding -- it is a minimal HTML form
- No external resources are loaded (no CDN, fonts, analytics) -- Tor Browser compatible
- SooSeF does not log source IP addresses
- FieldWitness does not log source IP addresses
- If using a reverse proxy (nginx, Caddy), disable access logging for `/dropbox/upload/`
- Tokens auto-expire and are cleaned up on every admin page load
- For maximum source protection, run SooSeF as a Tor hidden service
- For maximum source protection, run FieldWitness as a Tor hidden service
### Storage Management
Uploaded files accumulate in `~/.soosef/temp/dropbox/`. Periodically review and process
Uploaded files accumulate in `~/.fwmetadata/temp/dropbox/`. Periodically review and process
submissions, then remove them from the temp directory. The files are not automatically
cleaned up (they persist until you act on them or the killswitch fires).
@ -176,34 +176,34 @@ cleaned up (they persist until you act on them or the killswitch fires).
### Two Key Domains
SooSeF manages two independent key types:
FieldWitness manages two independent key types:
| Key | Algorithm | Location | Purpose |
|---|---|---|---|
| **Identity key** | Ed25519 | `~/.soosef/identity/` | Sign attestations, chain records |
| **Channel key** | AES-256-GCM (Argon2id-derived) | `~/.soosef/stegasoo/channel.key` | Steganographic encoding |
| **Identity key** | Ed25519 | `~/.fwmetadata/identity/` | Sign attestations, chain records |
| **Channel key** | AES-256-GCM (Argon2id-derived) | `~/.fwmetadata/stego/channel.key` | Steganographic encoding |
These are never merged. Rotating one does not affect the other.
### Key Rotation
**Identity rotation** archives the old keypair and generates a new one. If the chain is
enabled, a `soosef/key-rotation-v1` record is signed by the OLD key, creating a
enabled, a `fieldwitness/key-rotation-v1` record is signed by the OLD key, creating a
verifiable trust chain.
```bash
$ soosef keys rotate-identity
$ fieldwitness keys rotate-identity
```
After rotating, immediately:
1. Take a fresh backup (`soosef keys export`)
1. Take a fresh backup (`fieldwitness keys export`)
2. Notify all collaborators of the new fingerprint
3. Update trusted-key lists at partner organizations
**Channel rotation** archives the old key and generates a new one:
```bash
$ soosef keys rotate-channel
$ fieldwitness keys rotate-channel
```
After rotating, share the new channel key with all stego correspondents.
@ -214,7 +214,7 @@ Import collaborator public keys so you can verify their attestations and accept
federation bundles:
```bash
$ soosef keys trust --import /media/usb/partner-pubkey.pem
$ fieldwitness keys trust --import /media/usb/partner-pubkey.pem
```
Always verify fingerprints out-of-band (in person or over a known-secure voice channel).
@ -222,25 +222,25 @@ Always verify fingerprints out-of-band (in person or over a known-secure voice c
List trusted keys:
```bash
$ soosef keys show
$ fieldwitness keys show
```
Remove a trusted key:
```bash
$ soosef keys untrust <fingerprint>
$ fieldwitness keys untrust <fingerprint>
```
### Backup Schedule
SooSeF warns when backups are overdue (configurable via `backup_reminder_days`).
FieldWitness warns when backups are overdue (configurable via `backup_reminder_days`).
```bash
# Create encrypted backup
$ soosef keys export -o /media/usb/backup.enc
$ fieldwitness keys export -o /media/usb/backup.enc
# Check backup status
$ soosef status
$ fieldwitness status
```
Store backups on separate physical media, in a different location from the device.
@ -249,7 +249,7 @@ Store backups on separate physical media, in a different location from the devic
## 5. Federation Setup
Federation allows multiple SooSeF instances to exchange attestation records.
Federation allows multiple FieldWitness instances to exchange attestation records.
### Adding Federation Peers
@ -266,19 +266,19 @@ and Ed25519 fingerprint.
Before two organizations can federate, exchange public keys:
1. Export your public key: `cp ~/.soosef/identity/public.pem /media/usb/our-pubkey.pem`
1. Export your public key: `cp ~/.fwmetadata/identity/public.pem /media/usb/our-pubkey.pem`
2. Give it to the partner organization (physical handoff or secure channel)
3. Import their key: `soosef keys trust --import /media/usb/their-pubkey.pem`
3. Import their key: `fieldwitness keys trust --import /media/usb/their-pubkey.pem`
4. Verify fingerprints out-of-band
### Exporting Attestation Bundles
```bash
# Export all records
$ soosef chain export --output /media/usb/bundle.zip
$ fieldwitness chain export --output /media/usb/bundle.zip
# Export a specific range
$ soosef chain export --start 100 --end 200 --output /media/usb/bundle.zip
$ fieldwitness chain export --start 100 --end 200 --output /media/usb/bundle.zip
# Export filtered by investigation
# (investigation tag is set during attestation)
@ -296,7 +296,7 @@ On the receiving instance, imported records are:
If the Tier 2 server and Tier 3 relay have network connectivity, gossip sync runs
automatically at the configured interval (default: 60 seconds, set via
`VERISOO_GOSSIP_INTERVAL` environment variable).
`FIELDWITNESS_GOSSIP_INTERVAL` environment variable).
Gossip flow:
1. Nodes exchange Merkle roots
@ -326,7 +326,7 @@ No network connectivity is required at any point.
Verify the full chain periodically:
```bash
$ soosef chain verify
$ fieldwitness chain verify
```
This checks all hash linkage and Ed25519 signatures. It also verifies key rotation
@ -338,10 +338,10 @@ Anchor the chain head to prove it existed before a given time:
```bash
# Automated (requires network)
$ soosef chain anchor --tsa https://freetsa.org/tsr
$ fieldwitness chain anchor --tsa https://freetsa.org/tsr
# Manual (prints hash for external submission)
$ soosef chain anchor
$ fieldwitness chain anchor
```
A single anchor implicitly timestamps every prior record (the chain is append-only).
@ -358,7 +358,7 @@ For legal discovery or court orders, produce a proof showing specific records wh
keeping others redacted:
```bash
$ soosef chain disclose -i 42,43,44 -o disclosure.json
$ fieldwitness chain disclose -i 42,43,44 -o disclosure.json
```
The output includes full records for selected indices and hash-only entries for everything
@ -370,7 +370,7 @@ else. A third party can verify the selected records are part of an unbroken chai
### Evidence Packages
For handing evidence to lawyers, courts, or organizations without SooSeF:
For handing evidence to lawyers, courts, or organizations without FieldWitness:
Self-contained ZIP containing original images, attestation records, chain data, your
public key, a standalone `verify.py`, and a README. The recipient verifies with:
@ -413,7 +413,7 @@ Returns capabilities (stego-lsb, stego-dct, attest, fieldkit, chain).
### System Status
```bash
$ soosef status --json
$ fieldwitness status --json
```
Checks: identity key, channel key, chain integrity, dead man's switch state, backup
@ -440,7 +440,7 @@ The Docker images include `HEALTHCHECK` directives that poll `/health` every 30
### Device Seizure (Imminent)
1. Trigger killswitch: `soosef fieldkit purge --confirm CONFIRM-PURGE`
1. Trigger killswitch: `fieldwitness fieldkit purge --confirm CONFIRM-PURGE`
2. For Tier 1 USB: pull the USB stick and destroy it physically if possible
3. Verify with a separate device that federation copies are intact
@ -465,8 +465,8 @@ The Docker images include `HEALTHCHECK` directives that poll `/health` every 30
Data is gone. Restore from the most recent backup:
```bash
$ soosef init
$ soosef keys import -b /media/usb/backup.enc
$ fieldwitness init
$ fieldwitness keys import -b /media/usb/backup.enc
```
Federation copies of attestation data are unaffected. Local attestations created since
@ -480,22 +480,22 @@ the last federation sync or backup are lost.
| Task | Frequency | Command |
|---|---|---|
| Check system status | Daily | `soosef status` |
| Check in (if deadman armed) | Per interval | `soosef fieldkit checkin` |
| Backup keys | Per `backup_reminder_days` | `soosef keys export` |
| Verify chain integrity | Weekly | `soosef chain verify` |
| Anchor chain | Weekly | `soosef chain anchor` |
| Check system status | Daily | `fieldwitness status` |
| Check in (if deadman armed) | Per interval | `fieldwitness fieldkit checkin` |
| Backup keys | Per `backup_reminder_days` | `fieldwitness keys export` |
| Verify chain integrity | Weekly | `fieldwitness chain verify` |
| Anchor chain | Weekly | `fieldwitness chain anchor` |
| Review drop box submissions | As needed | `/dropbox/admin` |
| Clean temp files | Monthly | Remove processed files from `~/.soosef/temp/` |
| Clean temp files | Monthly | Remove processed files from `~/.fwmetadata/temp/` |
| Create cold archive | Monthly | Export via CLI or web |
| Update SooSeF | As releases are available | `pip install --upgrade soosef` |
| Update FieldWitness | As releases are available | `pip install --upgrade fieldwitness` |
### Docker Volume Backup
```bash
$ docker compose stop server
$ docker run --rm -v server-data:/data -v /backup:/backup \
busybox tar czf /backup/soosef-$(date +%Y%m%d).tar.gz -C /data .
busybox tar czf /backup/fieldwitness-$(date +%Y%m%d).tar.gz -C /data .
$ docker compose start server
```
@ -505,8 +505,8 @@ $ docker compose start server
periodically. The audit log is append-only; truncate by copying the tail:
```bash
$ tail -n 10000 ~/.soosef/audit.jsonl > ~/.soosef/audit.jsonl.tmp
$ mv ~/.soosef/audit.jsonl.tmp ~/.soosef/audit.jsonl
$ tail -n 10000 ~/.fwmetadata/audit.jsonl > ~/.fwmetadata/audit.jsonl.tmp
$ mv ~/.fwmetadata/audit.jsonl.tmp ~/.fwmetadata/audit.jsonl
```
> **Warning**: Truncating the audit log removes historical records. Archive the full

View File

@ -1,7 +1,7 @@
# Administrator Quick Reference
**Audience**: IT staff and technical leads responsible for deploying and maintaining
SooSeF instances.
FieldWitness instances.
---
@ -47,23 +47,23 @@ Exposes port 8001 (federation API only).
### Kubernetes
```bash
$ docker build -t soosef-server --target server -f deploy/docker/Dockerfile .
$ docker build -t soosef-relay --target relay -f deploy/docker/Dockerfile .
$ docker build -t fieldwitness-server --target server -f deploy/docker/Dockerfile .
$ docker build -t fieldwitness-relay --target relay -f deploy/docker/Dockerfile .
$ kubectl apply -f deploy/kubernetes/namespace.yaml
$ kubectl apply -f deploy/kubernetes/server-deployment.yaml
$ kubectl apply -f deploy/kubernetes/relay-deployment.yaml
```
Single-replica only. SooSeF uses SQLite -- do not scale horizontally.
Single-replica only. FieldWitness uses SQLite -- do not scale horizontally.
---
## Threat Level Presets
Copy the appropriate preset to configure SooSeF for the operational environment:
Copy the appropriate preset to configure FieldWitness for the operational environment:
```bash
$ cp deploy/config-presets/<level>-threat.json ~/.soosef/config.json
$ cp deploy/config-presets/<level>-threat.json ~/.fwmetadata/config.json
```
| Level | Session | Killswitch | Dead Man | Cover Name |
@ -81,45 +81,45 @@ $ cp deploy/config-presets/<level>-threat.json ~/.soosef/config.json
| Command | Description |
|---|---|
| `soosef init` | Create directory structure, generate keys, write default config |
| `soosef serve --host 0.0.0.0` | Start web UI (LAN-accessible) |
| `soosef status` | Pre-flight check: keys, chain, deadman, backup, geofence |
| `soosef status --json` | Machine-readable status output |
| `fieldwitness init` | Create directory structure, generate keys, write default config |
| `fieldwitness serve --host 0.0.0.0` | Start web UI (LAN-accessible) |
| `fieldwitness status` | Pre-flight check: keys, chain, deadman, backup, geofence |
| `fieldwitness status --json` | Machine-readable status output |
### Keys
| Command | Description |
|---|---|
| `soosef keys show` | Display current key info and fingerprints |
| `soosef keys export -o backup.enc` | Export encrypted key bundle |
| `soosef keys import -b backup.enc` | Import key bundle from backup |
| `soosef keys rotate-identity` | Rotate Ed25519 identity (records in chain) |
| `soosef keys rotate-channel` | Rotate AES-256-GCM channel key |
| `soosef keys trust --import pubkey.pem` | Trust a collaborator's public key |
| `fieldwitness keys show` | Display current key info and fingerprints |
| `fieldwitness keys export -o backup.enc` | Export encrypted key bundle |
| `fieldwitness keys import -b backup.enc` | Import key bundle from backup |
| `fieldwitness keys rotate-identity` | Rotate Ed25519 identity (records in chain) |
| `fieldwitness keys rotate-channel` | Rotate AES-256-GCM channel key |
| `fieldwitness keys trust --import pubkey.pem` | Trust a collaborator's public key |
### Fieldkit
| Command | Description |
|---|---|
| `soosef fieldkit status` | Show fieldkit state (deadman, geofence, USB, tamper) |
| `soosef fieldkit checkin` | Reset dead man's switch timer |
| `soosef fieldkit check-deadman` | Check if deadman timer expired (for cron) |
| `soosef fieldkit purge --confirm CONFIRM-PURGE` | Activate killswitch |
| `soosef fieldkit geofence set --lat X --lon Y --radius M` | Set GPS boundary |
| `soosef fieldkit usb snapshot` | Record USB whitelist baseline |
| `soosef fieldkit tamper baseline` | Record file integrity baseline |
| `fieldwitness fieldkit status` | Show fieldkit state (deadman, geofence, USB, tamper) |
| `fieldwitness fieldkit checkin` | Reset dead man's switch timer |
| `fieldwitness fieldkit check-deadman` | Check if deadman timer expired (for cron) |
| `fieldwitness fieldkit purge --confirm CONFIRM-PURGE` | Activate killswitch |
| `fieldwitness fieldkit geofence set --lat X --lon Y --radius M` | Set GPS boundary |
| `fieldwitness fieldkit usb snapshot` | Record USB whitelist baseline |
| `fieldwitness fieldkit tamper baseline` | Record file integrity baseline |
### Chain and Evidence
| Command | Description |
|---|---|
| `soosef chain status` | Show chain head, length, integrity |
| `soosef chain verify` | Verify full chain (hashes + signatures) |
| `soosef chain log --count 20` | Show recent chain entries |
| `soosef chain export -o bundle.zip` | Export attestation bundle |
| `soosef chain disclose -i 5,12,47 -o disclosure.json` | Selective disclosure |
| `soosef chain anchor` | Manual anchor (prints hash for external witness) |
| `soosef chain anchor --tsa https://freetsa.org/tsr` | RFC 3161 automated anchor |
| `fieldwitness chain status` | Show chain head, length, integrity |
| `fieldwitness chain verify` | Verify full chain (hashes + signatures) |
| `fieldwitness chain log --count 20` | Show recent chain entries |
| `fieldwitness chain export -o bundle.zip` | Export attestation bundle |
| `fieldwitness chain disclose -i 5,12,47 -o disclosure.json` | Selective disclosure |
| `fieldwitness chain anchor` | Manual anchor (prints hash for external witness) |
| `fieldwitness chain anchor --tsa https://freetsa.org/tsr` | RFC 3161 automated anchor |
---
@ -132,7 +132,7 @@ The web UI admin panel at `/admin` provides:
- Reset passwords (temporary password issued)
- View active sessions
User credentials are stored in SQLite at `~/.soosef/auth/soosef.db`.
User credentials are stored in SQLite at `~/.fwmetadata/auth/fieldwitness.db`.
---
@ -140,9 +140,9 @@ User credentials are stored in SQLite at `~/.soosef/auth/soosef.db`.
| What | How often | Command |
|---|---|---|
| Key bundle | After every rotation, weekly minimum | `soosef keys export -o backup.enc` |
| Cold archive | Weekly or before travel | `soosef archive export --include-keys -o archive.zip` |
| Docker volume | Before updates | `docker compose stop server && docker run --rm -v server-data:/data -v /backup:/backup busybox tar czf /backup/soosef-$(date +%Y%m%d).tar.gz -C /data .` |
| Key bundle | After every rotation, weekly minimum | `fieldwitness keys export -o backup.enc` |
| Cold archive | Weekly or before travel | `fieldwitness archive export --include-keys -o archive.zip` |
| Docker volume | Before updates | `docker compose stop server && docker run --rm -v server-data:/data -v /backup:/backup busybox tar czf /backup/fieldwitness-$(date +%Y%m%d).tar.gz -C /data .` |
Store backups on separate physical media. Keep one copy offsite.
@ -151,11 +151,11 @@ Store backups on separate physical media. Keep one copy offsite.
## Federation Setup
1. Exchange public keys between organizations (verify fingerprints out-of-band)
2. Import collaborator keys: `soosef keys trust --import /path/to/pubkey.pem`
2. Import collaborator keys: `fieldwitness keys trust --import /path/to/pubkey.pem`
3. Register peers via web UI at `/federation` or via CLI
4. Gossip starts automatically; monitor at `/federation`
For airgapped federation: `soosef chain export` to USB, carry to partner, import there.
For airgapped federation: `fieldwitness chain export` to USB, carry to partner, import there.
---
@ -177,7 +177,7 @@ For airgapped federation: `soosef chain export` to USB, carry to partner, import
- [ ] Disable unnecessary services (bluetooth, avahi-daemon)
- [ ] Apply a threat level preset appropriate for the environment
- [ ] Set `cover_name` in config if operating under cover
- [ ] Set `SOOSEF_DATA_DIR` to an inconspicuous path if needed
- [ ] Set `FIELDWITNESS_DATA_DIR` to an inconspicuous path if needed
- [ ] Enable HTTPS (default) or place behind a reverse proxy with TLS
- [ ] Create systemd service for bare metal (see `docs/deployment.md` Section 7)
- [ ] Set up regular backups (key bundle + cold archive)
@ -194,11 +194,11 @@ For airgapped federation: `soosef chain export` to USB, carry to partner, import
| Web UI unreachable from LAN | `host` must be `0.0.0.0`, not `127.0.0.1`. Check firewall. |
| Docker container exits | `docker compose logs server` -- check for port conflict or volume permissions |
| Dead man fires unexpectedly | Service crashed and exceeded interval+grace. Ensure `Restart=on-failure`. |
| Permission errors on `~/.soosef/` | Run SooSeF as the same user who ran `soosef init` |
| Permission errors on `~/.fwmetadata/` | Run FieldWitness as the same user who ran `fieldwitness init` |
| Drop box tokens expire immediately | System clock wrong. Run `date -u` and fix if needed. |
| Chain anchor TSA fails | Requires network. Use manual anchor on airgapped devices. |
| Account locked out | Wait for lockout to expire, or restart the server. |
| SSL cert shows wrong name | Delete `~/.soosef/certs/cert.pem`, set `cover_name`, restart. |
| SSL cert shows wrong name | Delete `~/.fwmetadata/certs/cert.pem`, set `cover_name`, restart. |
---
@ -215,5 +215,5 @@ $ curl http://localhost:8000/health
$ curl http://localhost:8001/health
# Full system status
$ soosef status --json
$ fieldwitness status --json
```

View File

@ -1,6 +1,6 @@
# Emergency Reference Card
**Audience**: All SooSeF users. Print, laminate, and carry in your wallet.
**Audience**: All FieldWitness users. Print, laminate, and carry in your wallet.
---
@ -17,7 +17,7 @@ In the browser: **Fieldkit** > **Emergency Purge** > type `CONFIRM-PURGE` > clic
From a terminal:
```
soosef fieldkit purge --confirm CONFIRM-PURGE
fieldwitness fieldkit purge --confirm CONFIRM-PURGE
```
### Option 3: Hardware button (Raspberry Pi only)
@ -51,7 +51,7 @@ If enabled, you must check in before the deadline or all data will be destroyed.
**Check in**: Browser > **Fieldkit** > **Check In**
Or: `soosef fieldkit checkin`
Or: `fieldwitness fieldkit checkin`
If you cannot check in, contact your editor. They may be able to disarm it remotely.

View File

@ -1,22 +1,22 @@
# SooSeF Reporter Field Guide
# FieldWitness Reporter Field Guide
**Audience**: Reporters, field researchers, and documentarians using SooSeF to protect
**Audience**: Reporters, field researchers, and documentarians using FieldWitness to protect
and verify their work. No technical background required.
**Prerequisites**: A working SooSeF instance (Tier 1 USB or web UI access to a Tier 2
**Prerequisites**: A working FieldWitness instance (Tier 1 USB or web UI access to a Tier 2
server). Your IT admin should have set this up for you.
---
## What SooSeF Does For You
## What FieldWitness Does For You
SooSeF helps you do three things:
FieldWitness helps you do three things:
1. **Prove your photos and files are authentic** -- every photo you attest gets a
cryptographic signature that proves you took it, when, and that it has not been
tampered with since.
2. **Hide messages in images** -- send encrypted messages that look like ordinary photos.
3. **Destroy everything if compromised** -- if your device is about to be seized, SooSeF
3. **Destroy everything if compromised** -- if your device is about to be seized, FieldWitness
can erase all evidence of itself and your data in seconds.
---
@ -37,7 +37,7 @@ permanent, tamper-evident record.
5. Add a location if relevant (optional)
6. Click **Attest**
SooSeF will:
FieldWitness will:
- Extract GPS coordinates and timestamp from the photo's EXIF data (for the provenance record)
- Strip device-identifying information (serial numbers, firmware version) from the stored copy
- Sign the photo with your Ed25519 identity key
@ -46,7 +46,7 @@ SooSeF will:
**Through the CLI (if available):**
```bash
$ soosef attest IMAGE photo.jpg --caption "Market protest, central square"
$ fieldwitness attest IMAGE photo.jpg --caption "Market protest, central square"
```
> **Warning**: Attest the original, unedited photo. If you crop, filter, or resize
@ -57,12 +57,12 @@ $ soosef attest IMAGE photo.jpg --caption "Market protest, central square"
If you have a folder of photos from a field visit:
```bash
$ soosef attest batch ./field-photos/ --caption "Site visit 2026-04-01"
$ fieldwitness attest batch ./field-photos/ --caption "Site visit 2026-04-01"
```
### Checking Your Status
Run `soosef status` or visit the web UI home page to see:
Run `fieldwitness status` or visit the web UI home page to see:
- Whether your identity key is set up
- How many attestations you have
- Whether your dead man's switch needs a check-in
@ -92,7 +92,7 @@ message, passphrase, and PIN.
**CLI:**
```bash
$ soosef stego encode vacation.jpg -r shared_photo.jpg -m "Meeting moved to Thursday"
$ fieldwitness stego encode vacation.jpg -r shared_photo.jpg -m "Meeting moved to Thursday"
# Passphrase: (enter your passphrase, hidden)
# PIN: (enter your PIN, hidden)
```
@ -101,22 +101,22 @@ The output is a normal-looking image file that contains your hidden message.
### Transport-Aware Encoding
If you are sending the image through a messaging app, tell SooSeF which platform. The
app will recompress images, so SooSeF needs to use a survival-resistant encoding:
If you are sending the image through a messaging app, tell FieldWitness which platform. The
app will recompress images, so FieldWitness needs to use a survival-resistant encoding:
```bash
$ soosef stego encode photo.jpg -r shared.jpg -m "Safe house confirmed" --transport whatsapp
$ soosef stego encode photo.jpg -r shared.jpg -m "Safe house confirmed" --transport signal
$ soosef stego encode photo.jpg -r shared.jpg -m "Safe house confirmed" --transport telegram
$ fieldwitness stego encode photo.jpg -r shared.jpg -m "Safe house confirmed" --transport whatsapp
$ fieldwitness stego encode photo.jpg -r shared.jpg -m "Safe house confirmed" --transport signal
$ fieldwitness stego encode photo.jpg -r shared.jpg -m "Safe house confirmed" --transport telegram
```
> **Warning**: Never reuse the same carrier image twice. SooSeF will warn you if you
> **Warning**: Never reuse the same carrier image twice. FieldWitness will warn you if you
> do. Comparing two versions of the same image trivially reveals steganographic changes.
### Decoding a Message
```bash
$ soosef stego decode received_image.jpg -r shared_photo.jpg
$ fieldwitness stego decode received_image.jpg -r shared_photo.jpg
# Passphrase: (same passphrase)
# PIN: (same PIN)
```
@ -126,7 +126,7 @@ $ soosef stego decode received_image.jpg -r shared_photo.jpg
## Check-In (Dead Man's Switch)
If your admin has enabled the dead man's switch, you must check in regularly. If you miss
your check-in window, SooSeF assumes something has gone wrong and will eventually destroy
your check-in window, FieldWitness assumes something has gone wrong and will eventually destroy
all data to protect you.
**Check in through the web UI:** Visit the **Fieldkit** page and click **Check In**.
@ -134,7 +134,7 @@ all data to protect you.
**Check in through the CLI:**
```bash
$ soosef fieldkit checkin
$ fieldwitness fieldkit checkin
```
> **Warning**: If you will be unable to check in (traveling without the device, planned
@ -150,7 +150,7 @@ If your device is about to be seized or compromised:
**CLI:**
```bash
$ soosef fieldkit purge --confirm CONFIRM-PURGE
$ fieldwitness fieldkit purge --confirm CONFIRM-PURGE
```
**Web UI:** Visit the **Fieldkit** page and use the emergency purge button.
@ -169,9 +169,9 @@ access.
4. All attestation records and chain data
5. Temporary files and audit logs
6. Configuration
7. System log entries mentioning SooSeF
8. Python bytecache and pip metadata (to hide that SooSeF was installed)
9. The SooSeF package itself
7. System log entries mentioning FieldWitness
8. Python bytecache and pip metadata (to hide that FieldWitness was installed)
9. The FieldWitness package itself
> **Warning**: This is irreversible. Make sure you have recent backups stored
> separately before relying on the killswitch. See "Backups" below.
@ -180,25 +180,25 @@ access.
## Backups
Back up your keys regularly. SooSeF will remind you if your backup is overdue.
Back up your keys regularly. FieldWitness will remind you if your backup is overdue.
### Creating a Backup
```bash
$ soosef keys export -o /media/usb/soosef-backup.enc
$ fieldwitness keys export -o /media/usb/fieldwitness-backup.enc
```
You will be prompted for a passphrase. This creates an encrypted bundle containing your
identity key and channel key. Store the USB drive **in a different physical location**
from your SooSeF device.
from your FieldWitness device.
### Restoring From Backup
On a fresh SooSeF instance:
On a fresh FieldWitness instance:
```bash
$ soosef init
$ soosef keys import -b /media/usb/soosef-backup.enc
$ fieldwitness init
$ fieldwitness keys import -b /media/usb/fieldwitness-backup.enc
```
---
@ -206,11 +206,11 @@ $ soosef keys import -b /media/usb/soosef-backup.enc
## Evidence Packages
When you need to hand evidence to a lawyer, a court, or a partner organization that does
not use SooSeF:
not use FieldWitness:
1. Go to the web UI or use the CLI to create an evidence package
2. Select the photos to include
3. SooSeF creates a ZIP file containing:
3. FieldWitness creates a ZIP file containing:
- Your original photos
- Attestation records with signatures
- The chain segment proving order and integrity
@ -218,7 +218,7 @@ not use SooSeF:
- A standalone verification script
- A README with instructions
The recipient can verify the evidence using only Python -- they do not need SooSeF.
The recipient can verify the evidence using only Python -- they do not need FieldWitness.
---
@ -238,7 +238,7 @@ encrypted with keys derived from the passphrase, PIN, and reference photo. If yo
any of the three, the message cannot be recovered.
**You need to share evidence with a court**: Use selective disclosure
(`soosef chain disclose`) to produce a proof that includes only the specific records
(`fieldwitness chain disclose`) to produce a proof that includes only the specific records
requested. The court can verify these records are part of an authentic, unbroken chain
without seeing your other work.
@ -259,5 +259,5 @@ the killswitch fires automatically.
- **Back up regularly** and store backups in a separate physical location.
- **Lock the browser** or close it when you walk away. Session timeouts help, but do not
rely on them.
- **Do not discuss SooSeF by name** in environments where your communications may be
- **Do not discuss FieldWitness by name** in environments where your communications may be
monitored. If `cover_name` is configured, the tool presents itself under that name.

View File

@ -1,6 +1,6 @@
# Reporter Quick-Start Card
**Audience**: Field reporters using a SooSeF Tier 1 bootable USB device.
**Audience**: Field reporters using a FieldWitness Tier 1 bootable USB device.
No technical background assumed.
**Print this page on a single sheet, laminate it, and keep it with the USB stick.**
@ -14,7 +14,7 @@ No technical background assumed.
3. **Enter your passphrase** when the blue screen appears (this unlocks your data)
4. **Wait for the browser** to open automatically
You are now running SooSeF. The laptop's own hard drive is never touched.
You are now running FieldWitness. The laptop's own hard drive is never touched.
---
@ -54,7 +54,7 @@ If your admin has enabled the dead man's switch, you must check in regularly.
Or from a terminal:
```
soosef fieldkit checkin
fieldwitness fieldkit checkin
```
If you miss your check-in window, the system will destroy all data after the grace period.
@ -80,7 +80,7 @@ Everything is gone. Keys, photos, attestations, messages -- all destroyed.
1. **Close the browser**
2. **Pull the USB stick**
The laptop returns to its normal state. No trace of SooSeF remains.
The laptop returns to its normal state. No trace of FieldWitness remains.
---

View File

@ -1,27 +1,27 @@
"""
SooSeF Web Frontend
FieldWitness Web Frontend
Flask application factory that unifies Stegasoo (steganography) and Verisoo
Flask application factory that unifies Stego (steganography) and Attest
(provenance attestation) into a single web UI with fieldkit security features.
ARCHITECTURE
============
The stegasoo web UI (3,600+ lines, 60 routes) is mounted wholesale via
_register_stegasoo_routes() rather than being rewritten into a blueprint.
The stego web UI (3,600+ lines, 60 routes) is mounted wholesale via
_register_stego_routes() rather than being rewritten into a blueprint.
This preserves the battle-tested subprocess isolation, async job management,
and all existing route logic without modification.
SooSeF-native features (attest, fieldkit, keys) are clean blueprints.
FieldWitness-native features (attest, fieldkit, keys) are clean blueprints.
Stegasoo routes (mounted at root):
Stego routes (mounted at root):
/encode, /decode, /generate, /tools, /api/*
SooSeF blueprints:
FieldWitness blueprints:
/attest, /verify attest blueprint
/fieldkit/* fieldkit blueprint
/keys/* keys blueprint
/admin/* admin blueprint (extends stegasoo's)
/admin/* admin blueprint (extends stego's)
"""
import io
@ -42,18 +42,18 @@ from flask import (
url_for,
)
import soosef
from soosef.config import SoosefConfig
from soosef.paths import INSTANCE_DIR, SECRET_KEY_FILE, TEMP_DIR, ensure_dirs
import fieldwitness
from fieldwitness.config import FieldWitnessConfig
from fieldwitness.paths import INSTANCE_DIR, SECRET_KEY_FILE, TEMP_DIR, ensure_dirs
# Suppress numpy/scipy warnings in subprocesses
os.environ["NUMPY_MADVISE_HUGEPAGE"] = "0"
os.environ["OMP_NUM_THREADS"] = "1"
def create_app(config: SoosefConfig | None = None) -> Flask:
def create_app(config: FieldWitnessConfig | None = None) -> Flask:
"""Application factory."""
config = config or SoosefConfig.load()
config = config or FieldWitnessConfig.load()
ensure_dirs()
web_dir = Path(__file__).parent
@ -68,7 +68,7 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
app.config["MAX_CONTENT_LENGTH"] = config.max_upload_mb * 1024 * 1024
app.config["AUTH_ENABLED"] = config.auth_enabled
app.config["HTTPS_ENABLED"] = config.https_enabled
app.config["SOOSEF_CONFIG"] = config
app.config["FIELDWITNESS_CONFIG"] = config
# Session security: timeout + secure cookie flags
from datetime import timedelta
@ -84,7 +84,7 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
csrf = CSRFProtect(app)
# Point temp_storage at ~/.soosef/temp/ before any routes run, so all
# Point temp_storage at ~/.fieldwitness/temp/ before any routes run, so all
# uploaded files land where the killswitch's destroy_temp_files step
# expects them. Must happen after ensure_dirs() so the directory exists.
import temp_storage as _ts
@ -103,10 +103,10 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
init_auth(app)
# ── Register stegasoo routes ──────────────────────────────────
_register_stegasoo_routes(app)
# ── Register stego routes ──────────────────────────────────
_register_stego_routes(app)
# ── Register SooSeF-native blueprints ─────────────────────────
# ── Register FieldWitness-native blueprints ─────────────────────────
from frontends.web.blueprints.attest import bp as attest_bp
from frontends.web.blueprints.fieldkit import bp as fieldkit_bp
from frontends.web.blueprints.keys import bp as keys_bp
@ -131,7 +131,7 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
@app.context_processor
def inject_globals():
from soosef.keystore import KeystoreManager
from fieldwitness.keystore import KeystoreManager
ks = KeystoreManager()
ks_status = ks.status()
@ -139,7 +139,7 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
# Fieldkit alert level
fieldkit_status = "ok"
if config.deadman_enabled:
from soosef.fieldkit.deadman import DeadmanSwitch
from fieldwitness.fieldkit.deadman import DeadmanSwitch
dm = DeadmanSwitch()
if dm.should_fire():
@ -147,10 +147,10 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
elif dm.is_overdue():
fieldkit_status = "warn"
# Stegasoo capabilities
# Stego capabilities
try:
from soosef.stegasoo import HAS_AUDIO_SUPPORT, get_channel_status, has_dct_support
from soosef.stegasoo.constants import (
from fieldwitness.stego import HAS_AUDIO_SUPPORT, get_channel_status, has_dct_support
from fieldwitness.stego.constants import (
DEFAULT_PASSPHRASE_WORDS,
MAX_FILE_PAYLOAD_SIZE,
MAX_MESSAGE_CHARS,
@ -166,7 +166,7 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
has_audio = HAS_AUDIO_SUPPORT
channel_status = get_channel_status()
# Stegasoo-specific template vars (needed by stego templates)
# Stego-specific template vars (needed by stego templates)
stego_vars = {
"has_dct": has_dct,
"has_audio": has_audio,
@ -188,13 +188,13 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
has_audio = False
stego_vars = {}
# Verisoo availability
# Attest availability
try:
import soosef.verisoo # noqa: F401
import fieldwitness.attest # noqa: F401
has_verisoo = True
has_attest = True
except ImportError:
has_verisoo = False
has_attest = False
# Saved channel keys for authenticated users
saved_channel_keys = []
@ -209,8 +209,8 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
pass
base_vars = {
"version": soosef.__version__,
"has_verisoo": has_verisoo,
"version": fieldwitness.__version__,
"has_attest": has_attest,
"has_fieldkit": config.killswitch_enabled or config.deadman_enabled,
"fieldkit_status": fieldkit_status,
"channel_configured": ks_status.has_channel_key,
@ -247,22 +247,22 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
# (deadman status, key presence, memory, etc. are operational intel)
if not auth_is_authenticated():
from flask import jsonify
return jsonify({"status": "ok", "version": __import__("soosef").__version__})
return jsonify({"status": "ok", "version": __import__("fieldwitness").__version__})
import platform
import sys
from flask import jsonify
from soosef.keystore.manager import KeystoreManager
from fieldwitness.keystore.manager import KeystoreManager
ks = KeystoreManager()
# Core modules
modules = {}
for name, import_path in [
("stegasoo", "soosef.stegasoo"),
("verisoo", "soosef.verisoo"),
("stego", "fieldwitness.stego"),
("attest", "fieldwitness.attest"),
]:
try:
mod = __import__(import_path, fromlist=["__version__"])
@ -275,27 +275,27 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
# DCT steganography
try:
from soosef.stegasoo import has_dct_support
from fieldwitness.stego import has_dct_support
capabilities["stego_dct"] = {
"status": "ok" if has_dct_support() else "unavailable",
"hint": None if has_dct_support() else "Install soosef[stego-dct] (scipy, jpeglib, reedsolo)",
"hint": None if has_dct_support() else "Install fieldwitness[stego-dct] (scipy, jpeglib, reedsolo)",
}
except ImportError:
capabilities["stego_dct"] = {"status": "missing", "hint": "Install soosef[stego-dct]"}
capabilities["stego_dct"] = {"status": "missing", "hint": "Install fieldwitness[stego-dct]"}
# Audio steganography
try:
from soosef.stegasoo import HAS_AUDIO_SUPPORT
from fieldwitness.stego import HAS_AUDIO_SUPPORT
capabilities["stego_audio"] = {
"status": "ok" if HAS_AUDIO_SUPPORT else "unavailable",
"hint": None if HAS_AUDIO_SUPPORT else "Install soosef[stego-audio] (soundfile, numpy)",
"hint": None if HAS_AUDIO_SUPPORT else "Install fieldwitness[stego-audio] (soundfile, numpy)",
}
except ImportError:
capabilities["stego_audio"] = {"status": "missing", "hint": "Install soosef[stego-audio]"}
capabilities["stego_audio"] = {"status": "missing", "hint": "Install fieldwitness[stego-audio]"}
# Video steganography
try:
from soosef.stegasoo.constants import VIDEO_ENABLED
from fieldwitness.stego.constants import VIDEO_ENABLED
capabilities["stego_video"] = {
"status": "ok" if VIDEO_ENABLED else "unavailable",
"hint": None if VIDEO_ENABLED else "Requires ffmpeg in PATH",
@ -303,33 +303,33 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
except (ImportError, AttributeError):
capabilities["stego_video"] = {"status": "missing", "hint": "Requires ffmpeg"}
# LMDB (verisoo storage)
# LMDB (attest storage)
try:
import lmdb # noqa: F401
capabilities["lmdb"] = {"status": "ok"}
except ImportError:
capabilities["lmdb"] = {"status": "missing", "hint": "Install soosef[attest]"}
capabilities["lmdb"] = {"status": "missing", "hint": "Install fieldwitness[attest]"}
# Perceptual hashing
try:
import imagehash # noqa: F401
capabilities["imagehash"] = {"status": "ok"}
except ImportError:
capabilities["imagehash"] = {"status": "missing", "hint": "Install soosef[attest]"}
capabilities["imagehash"] = {"status": "missing", "hint": "Install fieldwitness[attest]"}
# USB monitoring
try:
import pyudev # noqa: F401
capabilities["usb_monitor"] = {"status": "ok"}
except ImportError:
capabilities["usb_monitor"] = {"status": "unavailable", "hint": "Install soosef[fieldkit] (Linux only)"}
capabilities["usb_monitor"] = {"status": "unavailable", "hint": "Install fieldwitness[fieldkit] (Linux only)"}
# GPIO (RPi killswitch)
try:
import gpiozero # noqa: F401
capabilities["gpio"] = {"status": "ok"}
except ImportError:
capabilities["gpio"] = {"status": "unavailable", "hint": "Install soosef[rpi] (Raspberry Pi only)"}
capabilities["gpio"] = {"status": "unavailable", "hint": "Install fieldwitness[rpi] (Raspberry Pi only)"}
# Key status (existence only, no material)
keys = {
@ -350,7 +350,7 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
"chain_enabled": config.chain_enabled,
}
if config.deadman_enabled:
from soosef.fieldkit.deadman import DeadmanSwitch
from fieldwitness.fieldkit.deadman import DeadmanSwitch
dm = DeadmanSwitch()
dm_status = dm.status()
fieldkit["deadman_armed"] = dm_status["armed"]
@ -380,7 +380,7 @@ def create_app(config: SoosefConfig | None = None) -> Flask:
return jsonify({
"status": "ok" if all_ok else "degraded",
"version": __import__("soosef").__version__,
"version": __import__("fieldwitness").__version__,
"modules": modules,
"capabilities": capabilities,
"keys": keys,
@ -408,26 +408,26 @@ except ImportError:
_HAS_QRCODE_READ = False
# ── Stegasoo route mounting ──────────────────────────────────────────
# ── Stego route mounting ──────────────────────────────────────────
def _register_stegasoo_routes(app: Flask) -> None:
def _register_stego_routes(app: Flask) -> None:
"""
Mount all stegasoo web routes into the Flask app.
Mount all stego web routes into the Flask app.
Rather than rewriting 3,600 lines of battle-tested route logic,
we import stegasoo's app.py and re-register its routes.
The stegasoo templates are in templates/stego/ and extend our base.html.
we import fieldwitness.stego's app.py and re-register its routes.
The stego templates are in templates/stego/ and extend our base.html.
"""
import temp_storage
from auth import admin_required, login_required
from soosef.stegasoo import (
from fieldwitness.stego import (
export_rsa_key_pem,
generate_credentials,
get_channel_status,
load_rsa_key,
)
from soosef.stegasoo.constants import (
from fieldwitness.stego.constants import (
DEFAULT_PASSPHRASE_WORDS,
MAX_PIN_LENGTH,
MIN_PASSPHRASE_WORDS,
@ -435,7 +435,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
TEMP_FILE_EXPIRY,
VALID_RSA_SIZES,
)
from soosef.stegasoo.qr_utils import (
from fieldwitness.stego.qr_utils import (
can_fit_in_qr,
generate_qr_code,
)
@ -443,7 +443,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
SubprocessStego,
)
from soosef.audit import log_action
from fieldwitness.audit import log_action
# Initialize subprocess wrapper
subprocess_stego = SubprocessStego(timeout=180)
@ -511,7 +511,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
_login_attempts[username] = attempts
if len(attempts) >= max_attempts:
from soosef.audit import log_action
from fieldwitness.audit import log_action
log_action(
actor=username,
@ -818,7 +818,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
io.BytesIO(qr_png),
mimetype="image/png",
as_attachment=True,
download_name="soosef_rsa_key_qr.png",
download_name="fieldwitness_rsa_key_qr.png",
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@ -838,7 +838,7 @@ def _register_stegasoo_routes(app: Flask) -> None:
private_key = load_rsa_key(key_pem.encode("utf-8"))
encrypted_pem = export_rsa_key_pem(private_key, password=password)
key_id = secrets.token_hex(4)
filename = f"soosef_key_{private_key.key_size}_{key_id}.pem"
filename = f"fieldwitness_key_{private_key.key_size}_{key_id}.pem"
return send_file(
io.BytesIO(encrypted_pem),
mimetype="application/x-pem-file",

View File

@ -1,5 +1,5 @@
"""
Stegasoo Authentication Module (v4.1.0)
Stego Authentication Module (v4.1.0)
Multi-user authentication with role-based access control.
- Admin user created at first-run setup
@ -20,7 +20,7 @@ from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from flask import current_app, flash, g, redirect, session, url_for
# Argon2 password hasher (lighter than stegasoo's 256MB for faster login)
# Argon2 password hasher (lighter than stego's 256MB for faster login)
ph = PasswordHasher(
time_cost=3,
memory_cost=65536, # 64MB
@ -51,8 +51,8 @@ class User:
def get_db_path() -> Path:
"""Get database path — uses soosef auth directory."""
from soosef.paths import AUTH_DB
"""Get database path — uses fieldwitness auth directory."""
from fieldwitness.paths import AUTH_DB
AUTH_DB.parent.mkdir(parents=True, exist_ok=True)
return AUTH_DB
@ -273,7 +273,7 @@ def verify_and_reset_admin_password(recovery_key: str, new_password: str) -> tup
Returns:
(success, message) tuple
"""
from soosef.stegasoo.recovery import verify_recovery_key
from fieldwitness.stego.recovery import verify_recovery_key
stored_hash = get_recovery_key_hash()
if not stored_hash:

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.
"""

View File

@ -83,7 +83,7 @@ def generate_self_signed_cert(
# Create certificate
subject = issuer = x509.Name(
[
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Stegasoo"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "FieldWitness"),
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
]
)

View File

@ -1,9 +1,9 @@
/**
* Stegasoo Authentication Pages JavaScript
* FieldWitness Authentication Pages JavaScript
* Handles login, setup, account, and admin user management pages
*/
const StegasooAuth = {
const StegoAuth = {
// ========================================================================
// PASSWORD VISIBILITY TOGGLE
@ -128,15 +128,15 @@ const StegasooAuth = {
// Make togglePassword available globally for onclick handlers
function togglePassword(inputId, btn) {
StegasooAuth.togglePassword(inputId, btn);
StegoAuth.togglePassword(inputId, btn);
}
// Make copyField available globally for onclick handlers
function copyField(fieldId) {
StegasooAuth.copyField(fieldId);
StegoAuth.copyField(fieldId);
}
// Make regeneratePassword available globally for onclick handlers
function regeneratePassword() {
StegasooAuth.regeneratePassword();
StegoAuth.regeneratePassword();
}

View File

@ -1,9 +1,9 @@
/**
* Stegasoo Frontend JavaScript
* FieldWitness Frontend JavaScript
* Shared functionality across encode, decode, and generate pages
*/
const Stegasoo = {
const Stego = {
// ========================================================================
// PASSWORD/PIN VISIBILITY TOGGLES
@ -97,10 +97,10 @@ const Stegasoo = {
if (this.files && this.files[0]) {
const file = this.files[0];
if (file.type.startsWith('image/') && preview) {
Stegasoo.showImagePreview(file, preview, label, zone);
Stego.showImagePreview(file, preview, label, zone);
} else if (file.type.startsWith('audio/') || !file.type.startsWith('image/')) {
// Audio or non-image files: show file info instead of image preview
Stegasoo.showAudioFileInfo(file, zone);
Stego.showAudioFileInfo(file, zone);
if (label) {
label.classList.add('d-none');
}
@ -155,9 +155,9 @@ const Stegasoo = {
// Trigger appropriate animation
if (isScanContainer) {
Stegasoo.triggerScanAnimation(zone, file);
Stego.triggerScanAnimation(zone, file);
} else if (isPixelContainer) {
Stegasoo.triggerPixelReveal(zone, file);
Stego.triggerPixelReveal(zone, file);
}
};
reader.readAsDataURL(file);
@ -264,7 +264,7 @@ const Stegasoo = {
if (hashEl) {
// Generate a deterministic fake hash preview from filename + size
const fakeHash = Stegasoo.generateFakeHash(file.name + file.size);
const fakeHash = Stego.generateFakeHash(file.name + file.size);
hashEl.textContent = `SHA256: ${fakeHash.substring(0, 8)}····${fakeHash.substring(56)}`;
}
}
@ -328,7 +328,7 @@ const Stegasoo = {
tracesContainer.style.left = imgLeft + 'px';
// Generate Tron-style circuit traces covering the image
Stegasoo.generateEmbedTraces(tracesContainer, imgWidth, imgHeight);
Stego.generateEmbedTraces(tracesContainer, imgWidth, imgHeight);
};
// Wait for image to be ready
@ -349,7 +349,7 @@ const Stegasoo = {
if (grid) grid.remove();
// Populate data panel
Stegasoo.populatePixelDataPanel(container, file, preview);
Stego.populatePixelDataPanel(container, file, preview);
}, duration);
},
@ -453,7 +453,7 @@ const Stegasoo = {
input.addEventListener('change', function() {
if (this.files && this.files[0]) {
Stegasoo.showImagePreview(this.files[0], preview, label, container);
Stego.showImagePreview(this.files[0], preview, label, container);
}
});
});
@ -1721,10 +1721,10 @@ const Stegasoo = {
document.addEventListener('DOMContentLoaded', () => {
// Detect page and initialize
if (document.getElementById('encodeForm')) {
Stegasoo.initEncodePage();
Stego.initEncodePage();
} else if (document.getElementById('decodeForm')) {
Stegasoo.initDecodePage();
Stego.initDecodePage();
} else if (document.querySelector('[data-page="generate"]')) {
Stegasoo.initGeneratePage();
Stego.initGeneratePage();
}
});

View File

@ -1,9 +1,9 @@
/**
* Stegasoo Generate Page JavaScript
* FieldWitness Stego Generate Page JavaScript
* Handles credential generation form and display
*/
const StegasooGenerate = {
const StegoGenerate = {
// ========================================================================
// FORM CONTROLS
@ -260,20 +260,20 @@ const StegasooGenerate = {
// Global function wrappers for onclick handlers
function togglePinVisibility() {
StegasooGenerate.togglePinVisibility();
StegoGenerate.togglePinVisibility();
}
function togglePassphraseVisibility() {
StegasooGenerate.togglePassphraseVisibility();
StegoGenerate.togglePassphraseVisibility();
}
function printQrCode() {
StegasooGenerate.printQrCode();
StegoGenerate.printQrCode();
}
// Auto-init form controls
document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('[data-page="generate"]')) {
StegasooGenerate.initForm();
StegoGenerate.initForm();
}
});

View File

@ -1,6 +1,6 @@
/* ============================================================================
SooSeF - Main Stylesheet
Adapted from Stegasoo's style.css same dark theme, same patterns.
FieldWitness - Main Stylesheet
Dark theme stylesheet for the FieldWitness web UI.
============================================================================ */
:root {
@ -26,7 +26,7 @@
letter-spacing: 0.05em;
}
/* Nav icon + label pattern from stegasoo */
/* Nav icon + label pattern */
.nav-icons .nav-link {
display: flex;
align-items: center;

View File

@ -1,7 +1,7 @@
"""
Stegasoo encode/decode/tools routes.
Stego encode/decode/tools routes.
Ported from stegasoo's frontends/web/app.py. These routes handle:
Ported from fieldwitness.stego's frontends/web/app.py. These routes handle:
- Image encode with async progress tracking
- Audio encode (v4.3.0)
- Image/audio decode
@ -33,7 +33,7 @@ from PIL import Image
def register_stego_routes(app, **deps):
"""Register all stegasoo encode/decode routes on the Flask app."""
"""Register all stego encode/decode routes on the Flask app."""
# Unpack dependencies passed from app.py
login_required = deps["login_required"]
@ -41,7 +41,7 @@ def register_stego_routes(app, **deps):
temp_storage = deps["temp_storage"]
_has_qrcode_read = deps.get("has_qrcode_read", False)
from soosef.stegasoo import (
from fieldwitness.stego import (
HAS_AUDIO_SUPPORT,
CapacityError,
DecryptionError,
@ -49,7 +49,7 @@ def register_stego_routes(app, **deps):
InvalidHeaderError,
InvalidMagicBytesError,
ReedSolomonError,
StegasooError,
StegoError,
generate_filename,
has_dct_support,
validate_file_payload,
@ -60,13 +60,13 @@ def register_stego_routes(app, **deps):
validate_rsa_key,
validate_security_factors,
)
from soosef.stegasoo.channel import resolve_channel_key
from soosef.stegasoo.constants import (
from fieldwitness.stego.channel import resolve_channel_key
from fieldwitness.stego.constants import (
TEMP_FILE_EXPIRY,
THUMBNAIL_QUALITY,
THUMBNAIL_SIZE,
)
from soosef.stegasoo.qr_utils import (
from fieldwitness.stego.qr_utils import (
decompress_data,
extract_key_from_qr,
is_compressed,
@ -152,7 +152,7 @@ def register_stego_routes(app, **deps):
else:
return f"{n/(1024*1024):.1f} MB"
# ── Routes below are extracted from stegasoo app.py ──
# ── Routes below are extracted from fieldwitness.stego app.py ──
def _run_encode_job(job_id: str, encode_params: dict) -> None:
"""Background thread function for async encode."""
@ -686,7 +686,7 @@ def register_stego_routes(app, **deps):
return _error_response(result.error_message)
# Pre-check payload capacity BEFORE encode (fail fast)
from soosef.stegasoo.steganography import will_fit_by_mode
from fieldwitness.stego.steganography import will_fit_by_mode
payload_size = (
len(payload.data) if hasattr(payload, "data") else len(payload.encode("utf-8"))
@ -770,7 +770,7 @@ def register_stego_routes(app, **deps):
error_msg = encode_result.error or "Encoding failed"
if "capacity" in error_msg.lower():
raise CapacityError(error_msg)
raise StegasooError(error_msg)
raise StegoError(error_msg)
# Determine actual output format for filename and storage
if embed_mode == "dct" and dct_output_format == "jpeg":
@ -813,7 +813,7 @@ def register_stego_routes(app, **deps):
except CapacityError as e:
return _error_response(str(e))
except StegasooError as e:
except StegoError as e:
return _error_response(str(e))
except Exception as e:
return _error_response(f"Error: {e}")
@ -1443,7 +1443,7 @@ def register_stego_routes(app, **deps):
or decode_result.error_type == "DecryptionError"
):
raise DecryptionError(error_msg)
raise StegasooError(error_msg)
raise StegoError(error_msg)
if decode_result.is_file:
# File content - store temporarily for download
@ -1479,7 +1479,7 @@ def register_stego_routes(app, **deps):
except InvalidMagicBytesError:
flash(
"This doesn't appear to be a Stegasoo image. Try a different mode (LSB/DCT).",
"This doesn't appear to be a Stego image. Try a different mode (LSB/DCT).",
"warning",
)
return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
@ -1501,7 +1501,7 @@ def register_stego_routes(app, **deps):
"warning",
)
return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
except StegasooError as e:
except StegoError as e:
flash(str(e), "error")
return render_template("stego/decode.html", has_qrcode_read=_has_qrcode_read)
except Exception as e:
@ -1613,8 +1613,8 @@ def register_stego_routes(app, **deps):
@app.route("/about")
def about():
from auth import get_current_user
from soosef.stegasoo import has_argon2
from soosef.stegasoo.channel import get_channel_status
from fieldwitness.stego import has_argon2
from fieldwitness.stego.channel import get_channel_status
channel_status = get_channel_status()
current_user = get_current_user()
@ -1644,7 +1644,7 @@ def register_stego_routes(app, **deps):
@login_required
def api_tools_capacity():
"""Calculate image capacity for steganography."""
from soosef.stegasoo.dct_steganography import estimate_capacity_comparison
from fieldwitness.stego.dct_steganography import estimate_capacity_comparison
carrier = request.files.get("image")
if not carrier:
@ -1666,7 +1666,7 @@ def register_stego_routes(app, **deps):
"""Strip EXIF/metadata from image."""
import io
from soosef.stegasoo.utils import strip_image_metadata
from fieldwitness.stego.utils import strip_image_metadata
image_file = request.files.get("image")
if not image_file:
@ -1689,7 +1689,7 @@ def register_stego_routes(app, **deps):
@login_required
def api_tools_exif():
"""Read EXIF metadata from image."""
from soosef.stegasoo.utils import read_image_exif
from fieldwitness.stego.utils import read_image_exif
image_file = request.files.get("image")
if not image_file:
@ -1718,7 +1718,7 @@ def register_stego_routes(app, **deps):
@login_required
def api_tools_exif_update():
"""Update EXIF fields in image."""
from soosef.stegasoo.utils import write_image_exif
from fieldwitness.stego.utils import write_image_exif
image_file = request.files.get("image")
if not image_file:
@ -1757,7 +1757,7 @@ def register_stego_routes(app, **deps):
@login_required
def api_tools_exif_clear():
"""Remove all EXIF metadata from image."""
from soosef.stegasoo.utils import strip_image_metadata
from fieldwitness.stego.utils import strip_image_metadata
image_file = request.files.get("image")
if not image_file:
@ -2062,7 +2062,7 @@ def register_stego_routes(app, **deps):
@app.route("/test-capacity", methods=["POST"])
def test_capacity():
"""Minimal capacity test - no stegasoo code, just PIL."""
"""Minimal capacity test - no stego code, just PIL."""
carrier = request.files.get("carrier")
if not carrier:
return jsonify({"error": "No carrier image provided"}), 400
@ -2095,7 +2095,7 @@ def register_stego_routes(app, **deps):
@app.route("/test-capacity-nopil", methods=["POST"])
def test_capacity_nopil():
"""Ultra-minimal test - no PIL, no stegasoo."""
"""Ultra-minimal test - no PIL, no stego."""
carrier = request.files.get("carrier")
if not carrier:
return jsonify({"error": "No carrier image provided"}), 400

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Stegasoo Subprocess Worker (v4.0.0)
Stego Subprocess Worker (v4.0.0)
This script runs in a subprocess and handles encode/decode operations.
If it crashes due to jpeglib/scipy issues, the parent Flask process survives.
@ -25,12 +25,12 @@ import sys
import traceback
from pathlib import Path
# Ensure stegasoo is importable
# Ensure stego is importable
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
sys.path.insert(0, str(Path(__file__).parent))
# Configure logging for worker subprocess
_log_level = os.environ.get("STEGASOO_LOG_LEVEL", "").strip().upper()
_log_level = os.environ.get("FIELDWITNESS_LOG_LEVEL", "").strip().upper()
if _log_level and hasattr(logging, _log_level):
logging.basicConfig(
level=getattr(logging, _log_level),
@ -38,19 +38,19 @@ if _log_level and hasattr(logging, _log_level):
datefmt="%H:%M:%S",
stream=sys.stderr,
)
elif os.environ.get("STEGASOO_DEBUG", "").strip() in ("1", "true", "yes"):
elif os.environ.get("FIELDWITNESS_DEBUG", "").strip() in ("1", "true", "yes"):
logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)s.%(msecs)03d] [%(levelname)s] [%(name)s] %(message)s",
datefmt="%H:%M:%S",
stream=sys.stderr,
)
logger = logging.getLogger("stegasoo.worker")
logger = logging.getLogger("stego.worker")
def _resolve_channel_key(channel_key_param):
"""
Resolve channel_key parameter to value for stegasoo.
Resolve channel_key parameter to value for stego.
Args:
channel_key_param: 'auto', 'none', explicit key, or None
@ -73,7 +73,7 @@ def _get_channel_info(resolved_key):
Returns:
(mode, fingerprint) tuple
"""
from soosef.stegasoo import get_channel_status, has_channel_key
from fieldwitness.stego import get_channel_status, has_channel_key
if resolved_key == "":
return "public", None
@ -94,7 +94,7 @@ def _get_channel_info(resolved_key):
def encode_operation(params: dict) -> dict:
"""Handle encode operation."""
logger.debug("encode_operation: mode=%s", params.get("embed_mode", "lsb"))
from soosef.stegasoo import FilePayload, encode
from fieldwitness.stego import FilePayload, encode
# Decode base64 inputs
carrier_data = base64.b64decode(params["carrier_b64"])
@ -173,7 +173,7 @@ def _write_decode_progress(progress_file: str | None, percent: int, phase: str)
def decode_operation(params: dict) -> dict:
"""Handle decode operation."""
logger.debug("decode_operation: mode=%s", params.get("embed_mode", "auto"))
from soosef.stegasoo import decode
from fieldwitness.stego import decode
progress_file = params.get("progress_file")
@ -227,7 +227,7 @@ def decode_operation(params: dict) -> dict:
def compare_operation(params: dict) -> dict:
"""Handle compare_modes operation."""
from soosef.stegasoo import compare_modes
from fieldwitness.stego import compare_modes
carrier_data = base64.b64decode(params["carrier_b64"])
result = compare_modes(carrier_data)
@ -240,7 +240,7 @@ def compare_operation(params: dict) -> dict:
def capacity_check_operation(params: dict) -> dict:
"""Handle will_fit_by_mode operation."""
from soosef.stegasoo import will_fit_by_mode
from fieldwitness.stego import will_fit_by_mode
carrier_data = base64.b64decode(params["carrier_b64"])
@ -259,7 +259,7 @@ def capacity_check_operation(params: dict) -> dict:
def encode_audio_operation(params: dict) -> dict:
"""Handle audio encode operation (v4.3.0)."""
logger.debug("encode_audio_operation: mode=%s", params.get("embed_mode", "audio_lsb"))
from soosef.stegasoo import FilePayload, encode_audio
from fieldwitness.stego import FilePayload, encode_audio
carrier_data = base64.b64decode(params["carrier_b64"])
reference_data = base64.b64decode(params["reference_b64"])
@ -324,7 +324,7 @@ def encode_audio_operation(params: dict) -> dict:
def decode_audio_operation(params: dict) -> dict:
"""Handle audio decode operation (v4.3.0)."""
logger.debug("decode_audio_operation: mode=%s", params.get("embed_mode", "audio_auto"))
from soosef.stegasoo import decode_audio
from fieldwitness.stego import decode_audio
progress_file = params.get("progress_file")
_write_decode_progress(progress_file, 5, "reading")
@ -370,9 +370,9 @@ def decode_audio_operation(params: dict) -> dict:
def audio_info_operation(params: dict) -> dict:
"""Handle audio info operation (v4.3.0)."""
from soosef.stegasoo import get_audio_info
from soosef.stegasoo.audio_steganography import calculate_audio_lsb_capacity
from soosef.stegasoo.spread_steganography import calculate_audio_spread_capacity
from fieldwitness.stego import get_audio_info
from fieldwitness.stego.audio_steganography import calculate_audio_lsb_capacity
from fieldwitness.stego.spread_steganography import calculate_audio_spread_capacity
audio_data = base64.b64decode(params["audio_b64"])
@ -397,7 +397,7 @@ def audio_info_operation(params: dict) -> dict:
def channel_status_operation(params: dict) -> dict:
"""Handle channel status check (v4.0.0)."""
from soosef.stegasoo import get_channel_status
from fieldwitness.stego import get_channel_status
status = get_channel_status()
reveal = params.get("reveal", False)

View File

@ -1,7 +1,7 @@
"""
Subprocess Steganography Wrapper (v4.0.0)
Runs stegasoo operations in isolated subprocesses to prevent crashes
Runs stego operations in isolated subprocesses to prevent crashes
from taking down the Flask server.
CHANGES in v4.0.0:
@ -743,7 +743,7 @@ def generate_job_id() -> str:
def get_progress_file_path(job_id: str) -> str:
"""Get the progress file path for a job ID."""
return str(Path(tempfile.gettempdir()) / f"stegasoo_progress_{job_id}.json")
return str(Path(tempfile.gettempdir()) / f"stego_progress_{job_id}.json")
def read_progress(job_id: str) -> dict | None:

View File

@ -12,7 +12,7 @@ Files are stored in a temp directory with:
IMPORTANT: This module ONLY manages files in the temp directory.
It does NOT touch instance/ (auth database) or any other directories.
All temp files are written to ~/.soosef/temp/ (soosef.paths.TEMP_DIR) so
All temp files are written to ~/.fieldwitness/temp/ (fieldwitness.paths.TEMP_DIR) so
that the killswitch's destroy_temp_files step covers them.
"""
@ -24,9 +24,9 @@ import time
from pathlib import Path
from threading import Lock
import soosef.paths as paths
import fieldwitness.paths as paths
# Default temp directory — always under ~/.soosef/temp/ so the killswitch
# Default temp directory — always under ~/.fieldwitness/temp/ so the killswitch
# (which purges paths.TEMP_DIR) can reach every file written here.
DEFAULT_TEMP_DIR: Path = paths.TEMP_DIR

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Account - Stegasoo{% endblock %}
{% block title %}Account - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
@ -269,16 +269,16 @@
{% block scripts %}
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script src="{{ url_for('static', filename='js/soosef.js') }}"></script>
<script src="{{ url_for('static', filename='js/fieldwitness.js') }}"></script>
{% if is_admin %}
<script src="{{ url_for('static', filename='js/qrcode.min.js') }}"></script>
{% endif %}
<script>
StegasooAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
StegoAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
// Webcam QR scanning for channel key input (v4.1.5)
document.getElementById('scanChannelKeyBtn')?.addEventListener('click', function() {
Stegasoo.showQrScanner((text) => {
Stego.showQrScanner((text) => {
const input = document.getElementById('channelKeyInput');
if (input) {
// Clean and format the key
@ -294,7 +294,7 @@ document.getElementById('scanChannelKeyBtn')?.addEventListener('click', function
// Format channel key input as user types
document.getElementById('channelKeyInput')?.addEventListener('input', function() {
Stegasoo.formatChannelKeyInput(this);
Stego.formatChannelKeyInput(this);
});
function renameKey(keyId, currentName) {
@ -336,7 +336,7 @@ document.getElementById('qrDownload')?.addEventListener('click', function() {
const keyName = document.getElementById('qrKeyName').textContent;
if (canvas) {
const link = document.createElement('a');
link.download = 'stegasoo-channel-key-' + keyName.toLowerCase().replace(/\s+/g, '-') + '.png';
link.download = 'stego-channel-key-' + keyName.toLowerCase().replace(/\s+/g, '-') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Password Reset - Stegasoo{% endblock %}
{% block title %}Password Reset - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,9 +1,9 @@
{% extends "base.html" %}
{% block title %}Settings — SooSeF Admin{% endblock %}
{% block title %}Settings — FieldWitness Admin{% endblock %}
{% block content %}
<h2><i class="bi bi-sliders me-2"></i>System Settings</h2>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
System settings will be migrated from stegasoo's admin panel.
System settings will be migrated from fieldwitness.stego's admin panel.
</div>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}User Created - Stegasoo{% endblock %}
{% block title %}User Created - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Add User - Stegasoo{% endblock %}
{% block title %}Add User - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Manage Users - Stegasoo{% endblock %}
{% block title %}Manage Users - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Attest Image — SooSeF{% endblock %}
{% block title %}Attest Image — FieldWitness{% endblock %}
{% block content %}
<div class="row justify-content-center">
@ -18,7 +18,7 @@
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>No identity configured.</strong> Generate one from the
<a href="/keys" class="alert-link">Keys page</a> or run <code>soosef init</code>.
<a href="/keys" class="alert-link">Keys page</a> or run <code>fieldwitness init</code>.
</div>
{% endif %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Attestation Log — SooSeF{% endblock %}
{% block title %}Attestation Log — FieldWitness{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Attestation Record — SooSeF{% endblock %}
{% block title %}Attestation Record — FieldWitness{% endblock %}
{% block content %}
<h2><i class="bi bi-file-earmark-check me-2"></i>Attestation Record</h2>
<p class="text-muted">Record ID: <code>{{ record_id }}</code></p>

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Attestation Created — SooSeF{% endblock %}
{% block title %}Attestation Created — FieldWitness{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Verify Image — SooSeF{% endblock %}
{% block title %}Verify Image — FieldWitness{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Verification Result — SooSeF{% endblock %}
{% block title %}Verification Result — FieldWitness{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}SooSeF{% endblock %}</title>
<title>{% block title %}FieldWitness{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link href="{{ url_for('static', filename='vendor/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/css/bootstrap-icons.min.css') }}" rel="stylesheet">
@ -13,7 +13,7 @@
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/" style="padding-left: 6px; margin-right: 8px;">
<strong>SooSeF</strong>
<strong>FieldWitness</strong>
</a>
{# Channel + Identity indicators #}
@ -40,7 +40,7 @@
</li>
{% if not auth_enabled or is_authenticated %}
{# ── Stegasoo ── #}
{# ── Stego ── #}
<li class="nav-item">
<a class="nav-link nav-expand" href="/encode"><i class="bi bi-lock"></i><span>Encode</span></a>
</li>
@ -51,8 +51,8 @@
<a class="nav-link nav-expand" href="/generate"><i class="bi bi-key"></i><span>Generate</span></a>
</li>
{# ── Verisoo ── #}
{% if has_verisoo %}
{# ── Attest ── #}
{% if has_attest %}
<li class="nav-item">
<a class="nav-link nav-expand" href="/attest"><i class="bi bi-patch-check"></i><span>Attest</span></a>
</li>
@ -140,9 +140,9 @@
<footer class="py-4 mt-5">
<div class="container text-center text-muted">
<small>
SooSeF v{{ version }} — Soo Security Fieldkit
FieldWitness v{{ version }} — FieldWitness
<span class="mx-2">|</span>
<span class="text-muted">Stegasoo + Verisoo</span>
<span class="text-muted">Stego + Attest</span>
</small>
</div>
</footer>

View File

@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}Source Drop Box — SooSeF{% endblock %}
{% block title %}Source Drop Box — FieldWitness{% endblock %}
{% block content %}
<h2><i class="bi bi-inbox me-2"></i>Source Drop Box</h2>
<p class="text-muted">Create time-limited upload links for sources who cannot install SooSeF.</p>
<p class="text-muted">Create time-limited upload links for sources who cannot install FieldWitness.</p>
<div class="card bg-dark mb-4">
<div class="card-body">

View File

@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}Federation — SooSeF{% endblock %}
{% block title %}Federation — FieldWitness{% endblock %}
{% block content %}
<h2><i class="bi bi-diagram-3 me-2"></i>Federation</h2>
<p class="text-muted">Gossip-based attestation sync between SooSeF instances.</p>
<p class="text-muted">Gossip-based attestation sync between FieldWitness instances.</p>
<div class="row mb-4">
<div class="col-md-4">

View File

@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}Keys — SooSeF{% endblock %}
{% block title %}Keys — FieldWitness{% endblock %}
{% block content %}
<h2><i class="bi bi-key me-2"></i>Key Management</h2>
<p class="text-muted">Manage Stegasoo channel keys and Verisoo Ed25519 identity.</p>
<p class="text-muted">Manage Stego channel keys and Attest Ed25519 identity.</p>
<div class="row g-4">
{# Channel Key #}
@ -13,7 +13,7 @@
{% if keystore.has_channel_key %}
<p class="text-muted small">
Fingerprint: <code>{{ keystore.channel_fingerprint }}</code><br>
Used for Stegasoo deployment isolation.
Used for Stego deployment isolation.
</p>
{% else %}
<p class="text-muted small">No channel key configured.</p>
@ -36,7 +36,7 @@
{% if keystore.has_identity %}
<p class="text-muted small">
Fingerprint: <code>{{ keystore.identity_fingerprint }}</code><br>
Used for Verisoo attestation signing.
Used for Attest attestation signing.
</p>
{% else %}
<p class="text-muted small">No identity configured.</p>

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Killswitch — SooSeF{% endblock %}
{% block title %}Killswitch — FieldWitness{% endblock %}
{% block content %}
<h2 class="text-danger"><i class="bi bi-exclamation-octagon me-2"></i>Emergency Killswitch</h2>
<p class="text-muted">Destroy all key material and sensitive data. This action is irreversible.</p>
@ -9,7 +9,7 @@
<h5 class="card-title text-danger">Destruction Order</h5>
<ol class="text-muted small">
<li>Ed25519 identity keys (signing identity)</li>
<li>Stegasoo channel key (deployment binding)</li>
<li>Stego channel key (deployment binding)</li>
<li>Flask session secret (invalidates all sessions)</li>
<li>Auth database (user accounts)</li>
<li>Attestation log + index (provenance records)</li>

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Fieldkit Status — SooSeF{% endblock %}
{% block title %}Fieldkit Status — FieldWitness{% endblock %}
{% block content %}
<h2><i class="bi bi-speedometer2 me-2"></i>Fieldkit Status</h2>
<p class="text-muted">Security monitors and system health.</p>

View File

@ -1,19 +1,19 @@
{% extends "base.html" %}
{% block title %}SooSeF — Soo Security Fieldkit{% endblock %}
{% block title %}FieldWitness — FieldWitness{% endblock %}
{% block content %}
<div class="text-center mb-5">
<h1 class="display-5 fw-bold">Soo Security Fieldkit</h1>
<h1 class="display-5 fw-bold">FieldWitness</h1>
<p class="lead text-muted">Offline-first security toolkit for field operations</p>
</div>
<div class="row g-4">
{# ── Stegasoo Card ── #}
{# ── Stego Card ── #}
<div class="col-md-6 col-lg-4">
<div class="card h-100 bg-dark border-secondary">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-lock me-2 text-primary"></i>Encode</h5>
<p class="card-text text-muted">Hide encrypted messages in images or audio using Stegasoo's hybrid authentication.</p>
<p class="card-text text-muted">Hide encrypted messages in images or audio using Stego's hybrid authentication.</p>
<a href="/encode" class="btn btn-outline-primary btn-sm">Encode Message</a>
</div>
</div>
@ -37,8 +37,8 @@
</div>
</div>
{# ── Verisoo Cards ── #}
{% if has_verisoo %}
{# ── Attest Cards ── #}
{% if has_attest %}
<div class="col-md-6 col-lg-4">
<div class="card h-100 bg-dark border-secondary">
<div class="card-body">
@ -102,10 +102,10 @@
<i class="bi bi-image me-1"></i>DCT: {{ 'Available' if has_dct else 'Unavailable' }}
</span>
</div>
{% if has_verisoo %}
{% if has_attest %}
<div class="col-auto">
<span class="badge bg-success">
<i class="bi bi-patch-check me-1"></i>Verisoo: Active
<i class="bi bi-patch-check me-1"></i>Attest: Active
</span>
</div>
{% endif %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Login - Stegasoo{% endblock %}
{% block title %}Login - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Password Recovery - Stegasoo{% endblock %}
{% block title %}Password Recovery - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
@ -116,7 +116,7 @@
<div class="alert alert-warning mt-4 small">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Note:</strong> This will reset the admin password. If you don't have a valid recovery key,
you'll need to delete the database and reconfigure Stegasoo.
you'll need to delete the database and reconfigure Stego.
</div>
</div>
</div>
@ -125,6 +125,6 @@
{% block scripts %}
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script>
StegasooAuth.initPasswordConfirmation('recoverForm', 'passwordInput', 'passwordConfirmInput');
StegoAuth.initPasswordConfirmation('recoverForm', 'passwordInput', 'passwordConfirmInput');
</script>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Regenerate Recovery Key - Stegasoo{% endblock %}
{% block title %}Regenerate Recovery Key - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
@ -142,7 +142,7 @@ function copyToClipboard() {
// Download as text file
function downloadTextFile() {
const key = document.getElementById('recoveryKey').value;
const content = `Stegasoo Recovery Key
const content = `Stego Recovery Key
=====================
${key}
@ -158,7 +158,7 @@ Generated: ${new Date().toISOString()}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'stegasoo-recovery-key.txt';
a.download = 'stego-recovery-key.txt';
a.click();
URL.revokeObjectURL(url);
}
@ -170,7 +170,7 @@ function downloadQRImage() {
const a = document.createElement('a');
a.href = img.src;
a.download = 'stegasoo-recovery-qr.png';
a.download = 'stego-recovery-qr.png';
a.click();
}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Setup - Stegasoo{% endblock %}
{% block title %}Setup - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
@ -12,7 +12,7 @@
</div>
<div class="card-body">
<p class="text-muted text-center mb-4">
Welcome to Stegasoo! Create your admin account to get started.
Welcome to Stego! Create your admin account to get started.
</p>
<form method="POST" action="{{ url_for('setup') }}" id="setupForm">
@ -72,6 +72,6 @@
{% block scripts %}
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script>
StegasooAuth.initPasswordConfirmation('setupForm', 'passwordInput', 'passwordConfirmInput');
StegoAuth.initPasswordConfirmation('setupForm', 'passwordInput', 'passwordConfirmInput');
</script>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Recovery Key Setup - Stegasoo{% endblock %}
{% block title %}Recovery Key Setup - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
@ -135,7 +135,7 @@ function copyToClipboard() {
// Download as text file
function downloadTextFile() {
const key = document.getElementById('recoveryKey').value;
const content = `Stegasoo Recovery Key
const content = `Stego Recovery Key
=====================
${key}
@ -151,7 +151,7 @@ Generated: ${new Date().toISOString()}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'stegasoo-recovery-key.txt';
a.download = 'stego-recovery-key.txt';
a.click();
URL.revokeObjectURL(url);
}
@ -163,7 +163,7 @@ function downloadQRImage() {
const a = document.createElement('a');
a.href = img.src;
a.download = 'stegasoo-recovery-qr.png';
a.download = 'stego-recovery-qr.png';
a.click();
}

View File

@ -1,17 +1,17 @@
{% extends "base.html" %}
{% block title %}About - Stegasoo{% endblock %}
{% block title %}About - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>About Stegasoo</h5>
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>About Stego</h5>
</div>
<div class="card-body">
<p class="lead">
Stegasoo hides encrypted messages and files inside images using multi-factor authentication.
Stego hides encrypted messages and files inside images using multi-factor authentication.
</p>
<h6 class="text-primary mt-4 mb-3">Features</h6>

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Decode Message - Stegasoo{% endblock %}
{% block title %}Decode Message - Stego{% endblock %}
{% block content %}
<style>
@ -487,7 +487,7 @@
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/soosef.js') }}"></script>
<script src="{{ url_for('static', filename='js/fieldwitness.js') }}"></script>
<script>
// ============================================================================
// MODE HINT - Dynamic text based on selected extraction mode
@ -677,6 +677,6 @@ if (document.getElementById('modeDct')?.disabled) {
// LOADING STATE
// ============================================================================
Stegasoo.initFormLoading('decodeForm', 'decodeBtn', 'Decoding...');
Stego.initFormLoading('decodeForm', 'decodeBtn', 'Decoding...');
</script>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Encode Message - Stegasoo{% endblock %}
{% block title %}Encode Message - Stego{% endblock %}
{% block content %}
<style>
@ -507,7 +507,7 @@
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/soosef.js') }}"></script>
<script src="{{ url_for('static', filename='js/fieldwitness.js') }}"></script>
<script>
// ============================================================================
// MODE HINT - Dynamic text based on selected embedding mode

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Encode Success - Stegasoo{% endblock %}
{% block title %}Encode Success - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
@ -218,7 +218,7 @@ if (navigator.share && navigator.canShare) {
try {
await navigator.share({
files: [file],
title: 'Stegasoo Image',
title: 'Stego Image',
});
} catch (err) {
if (err.name !== 'AbortError') {

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Generate Credentials - Stegasoo{% endblock %}
{% block title %}Generate Credentials - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center" data-page="generate">
@ -500,7 +500,7 @@
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/soosef.js') }}"></script>
<script src="{{ url_for('static', filename='js/fieldwitness.js') }}"></script>
<script src="{{ url_for('static', filename='js/generate.js') }}"></script>
{% if generated %}
<script>
@ -508,7 +508,7 @@
const passphraseWords = '{{ passphrase|default("", true) }}'.split(' ').filter(w => w.length > 0);
function copyPin() {
Stegasoo.copyToClipboard(
Stego.copyToClipboard(
'{{ pin|default("", true) }}',
document.getElementById('pinCopyIcon'),
document.getElementById('pinCopyText')
@ -516,7 +516,7 @@ function copyPin() {
}
function copyPassphrase() {
Stegasoo.copyToClipboard(
Stego.copyToClipboard(
'{{ passphrase|default("", true) }}',
document.getElementById('passphraseCopyIcon'),
document.getElementById('passphraseCopyText')
@ -524,11 +524,11 @@ function copyPassphrase() {
}
function toggleMemoryAid() {
StegasooGenerate.toggleMemoryAid(passphraseWords);
StegoGenerate.toggleMemoryAid(passphraseWords);
}
function regenerateStory() {
StegasooGenerate.regenerateStory(passphraseWords);
StegoGenerate.regenerateStory(passphraseWords);
}
</script>
{% endif %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Tools - Stegasoo{% endblock %}
{% block title %}Tools - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@ -3,11 +3,11 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "soosef"
name = "fieldwitness"
version = "0.2.0"
description = "Soo Security Fieldkit — offline-first security toolkit for journalists, NGOs, and at-risk organizations"
description = "FieldWitness — offline-first security toolkit for journalists, NGOs, and at-risk organizations"
readme = "README.md"
license = "MIT"
license = "GPL-3.0-only"
requires-python = ">=3.11"
authors = [
{ name = "Aaron D. Lee" }
@ -28,7 +28,7 @@ classifiers = [
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
@ -84,13 +84,13 @@ web = [
"qrcode>=7.3.0",
"pyzbar>=0.1.9",
"piexif>=1.1.0",
"soosef[attest,stego-dct]",
"fieldwitness[attest,stego-dct]",
]
api = [
"fastapi>=0.109.0",
"uvicorn[standard]>=0.27.0",
"python-multipart>=0.0.6",
"soosef[stego-dct]",
"fieldwitness[stego-dct]",
]
fieldkit = [
"watchdog>=4.0.0",
@ -99,15 +99,19 @@ fieldkit = [
federation = [
"aiohttp>=3.9.0",
]
c2pa = [
"c2pa-python>=0.6.0",
"fieldwitness[attest]",
]
rpi = [
"soosef[web,cli,fieldkit]",
"fieldwitness[web,cli,fieldkit]",
"gpiozero>=2.0",
]
all = [
"soosef[stego-dct,stego-audio,stego-compression,attest,cli,web,api,fieldkit,federation]",
"fieldwitness[stego-dct,stego-audio,stego-compression,attest,cli,web,api,fieldkit,federation,c2pa]",
]
dev = [
"soosef[all]",
"fieldwitness[all]",
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
@ -116,11 +120,11 @@ dev = [
]
[project.scripts]
soosef = "soosef.cli:main"
fieldwitness = "fieldwitness.cli:main"
[project.urls]
Homepage = "https://github.com/alee/soosef"
Repository = "https://github.com/alee/soosef"
Homepage = "https://github.com/alee/fieldwitness"
Repository = "https://github.com/alee/fieldwitness"
[tool.hatch.build.targets.sdist]
include = [
@ -129,18 +133,18 @@ include = [
]
[tool.hatch.build.targets.wheel]
packages = ["src/soosef", "frontends"]
packages = ["src/fieldwitness", "frontends"]
[tool.hatch.build.targets.wheel.sources]
"src" = ""
[tool.hatch.build.targets.wheel.force-include]
"src/soosef/stegasoo/data/bip39-words.txt" = "soosef/stegasoo/data/bip39-words.txt"
"src/fieldwitness/stego/data/bip39-words.txt" = "fieldwitness/stego/data/bip39-words.txt"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --cov=soosef --cov-report=term-missing"
addopts = "-v --cov=fieldwitness --cov-report=term-missing"
[tool.black]
line-length = 100
@ -155,11 +159,11 @@ ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
# YCbCr colorspace variables (R, G, B, Y, Cb, Cr) are standard names
"src/soosef/stegasoo/dct_steganography.py" = ["N803", "N806"]
"src/fieldwitness/stego/dct_steganography.py" = ["N803", "N806"]
# MDCT transform variables (N, X) are standard mathematical names
"src/soosef/stegasoo/spread_steganography.py" = ["N803", "N806"]
"src/fieldwitness/stego/spread_steganography.py" = ["N803", "N806"]
# Package __init__.py has imports after try/except and aliases - intentional structure
"src/soosef/stegasoo/__init__.py" = ["E402"]
"src/fieldwitness/stego/__init__.py" = ["E402"]
[tool.mypy]
python_version = "3.11"

View File

@ -0,0 +1,14 @@
"""
FieldWitness FieldWitness
Offline-first security toolkit for journalists, NGOs, and at-risk organizations.
Combines Stego (steganography) and Attest (provenance attestation) with
field-hardened security features.
Part of the Soo Suite:
- Stego: hide encrypted messages in media
- Attest: prove image provenance and authenticity
- FieldWitness: unified fieldkit with killswitch, dead man's switch, and key management
"""
__version__ = "0.2.0"

View File

@ -0,0 +1,31 @@
"""Runtime availability checks for optional fieldwitness subpackages."""
def has_stego() -> bool:
"""Check if fieldwitness.stego is importable (core deps are always present)."""
try:
import fieldwitness.stego # noqa: F401
return True
except ImportError:
return False
def has_attest() -> bool:
"""Check if fieldwitness.attest is importable (requires [attest] extra)."""
try:
import fieldwitness.attest # noqa: F401
return True
except ImportError:
return False
def has_c2pa() -> bool:
"""Check if c2pa-python is importable (requires [c2pa] extra)."""
try:
import c2pa # noqa: F401
return True
except ImportError:
return False

36
src/fieldwitness/api.py Normal file
View File

@ -0,0 +1,36 @@
"""Optional unified FastAPI app combining stego and attest APIs.
Usage::
uvicorn fieldwitness.api:app --host 0.0.0.0 --port 8000
Requires the [api] extra: pip install fieldwitness[api]
"""
from fastapi import FastAPI
app = FastAPI(
title="FieldWitness API",
version="0.1.0",
description="Unified steganography and attestation API",
)
try:
from fieldwitness.stego.api import app as stego_api
app.mount("/stego", stego_api)
except ImportError:
pass
try:
from fieldwitness.attest.api import app as attest_api
app.mount("/attest", attest_api)
except ImportError:
pass
@app.get("/health")
async def health():
"""Health check endpoint."""
return {"status": "ok"}

View File

@ -2,8 +2,8 @@
Cold archive export for long-term evidence preservation.
Produces a self-describing archive containing everything needed to
reconstitute a SooSeF evidence store on a fresh instance or verify
evidence decades later without any SooSeF installation.
reconstitute a FieldWitness evidence store on a fresh instance or verify
evidence decades later without any FieldWitness installation.
Designed for OAIS (ISO 14721) alignment: the archive is self-describing,
includes its own verification code, and documents the cryptographic
@ -24,13 +24,13 @@ def export_cold_archive(
include_keys: bool = True,
key_password: bytes | None = None,
) -> dict:
"""Export a full cold archive of the SooSeF evidence store.
"""Export a full cold archive of the FieldWitness evidence store.
Contents:
- chain/chain.bin raw append-only hash chain
- chain/state.cbor chain state checkpoint
- chain/anchors/ external timestamp anchors
- attestations/log.bin verisoo attestation log
- attestations/log.bin attest attestation log
- attestations/index/ LMDB index (if present)
- keys/public.pem signer's public key
- keys/bundle.enc encrypted key bundle (if include_keys + password)
@ -50,7 +50,7 @@ def export_cold_archive(
"""
import shutil
from soosef.paths import (
from fieldwitness.paths import (
ATTESTATIONS_DIR,
CHAIN_DIR,
IDENTITY_DIR,
@ -109,8 +109,8 @@ def export_cold_archive(
# Encrypted key bundle (optional)
if include_keys and key_password:
from soosef.keystore.export import export_bundle
from soosef.paths import CHANNEL_KEY_FILE
from fieldwitness.keystore.export import export_bundle
from fieldwitness.paths import CHANNEL_KEY_FILE
import tempfile
@ -126,7 +126,7 @@ def export_cold_archive(
tmp_path.unlink(missing_ok=True)
# Algorithm documentation
algorithms = """SOOSEF CRYPTOGRAPHIC ALGORITHMS
algorithms = """FIELDWITNESS CRYPTOGRAPHIC ALGORITHMS
================================
This archive uses the following algorithms:
@ -151,10 +151,10 @@ CHAIN FORMAT
- Each record signed by Ed25519, linked by prev_hash (SHA-256)
ATTESTATION LOG
- Verisoo binary log: [magic "VERISOO\\x00"] [uint32 version] [records]
- Attest binary log: [magic "VERISOO\\x00"] [uint32 version] [records]
- LMDB index: SHA-256, pHash, attestor fingerprint lookups
To verify this archive without SooSeF:
To verify this archive without FieldWitness:
1. pip install cryptography cbor2
2. python verify.py
"""
@ -165,7 +165,7 @@ To verify this archive without SooSeF:
manifest = {
"archive_version": "1",
"created_at": ts.isoformat(),
"soosef_version": "0.2.0",
"fieldwitness_version": "0.2.0",
"contents": contents,
"file_count": len(contents),
"content_hashes": {},
@ -183,21 +183,21 @@ To verify this archive without SooSeF:
contents.append("manifest.json")
# README
readme = f"""SOOSEF COLD ARCHIVE
readme = f"""FIELDWITNESS COLD ARCHIVE
===================
Created: {ts.isoformat()}
Files: {len(contents)}
This archive contains a complete snapshot of a SooSeF evidence store.
This archive contains a complete snapshot of a FieldWitness evidence store.
It is self-describing and includes everything needed to verify the
evidence it contains, even if SooSeF no longer exists.
evidence it contains, even if FieldWitness no longer exists.
See ALGORITHMS.txt for cryptographic algorithm documentation.
Run verify.py to check archive integrity.
To restore on a fresh SooSeF instance:
soosef archive import <this-file.zip>
To restore on a fresh FieldWitness instance:
fieldwitness archive import <this-file.zip>
"""
zf.writestr("README.txt", readme)

View File

@ -1,16 +1,16 @@
"""
Verisoo - Decentralized image provenance and attestation.
Attest - Decentralized image provenance and attestation.
Part of the Soo Suite:
- Stegasoo: covert communication, hiding encrypted messages in images
- Verisoo: overt attestation, proving provenance and building decentralized reputation
- Stego: covert communication, hiding encrypted messages in images
- Attest: overt attestation, proving provenance and building decentralized reputation
"""
__version__ = "0.1.0"
try:
from .models import Attestation, AttestationRecord, Identity
from .exceptions import VerisooError, AttestationError, VerificationError
from .exceptions import AttestError, AttestationError, VerificationError
_AVAILABLE = True
except ImportError:
@ -22,7 +22,7 @@ __all__ = [
"Attestation",
"AttestationRecord",
"Identity",
"VerisooError",
"AttestError",
"AttestationError",
"VerificationError",
]

View File

@ -1,5 +1,5 @@
"""
FastAPI verification service for Verisoo.
FastAPI verification service for Attest.
Lightweight REST API for:
- Verifying images against attestation records
@ -8,7 +8,7 @@ Lightweight REST API for:
Designed for media orgs and fact-checkers to integrate easily.
Run with: uvicorn verisoo.api:app --host 0.0.0.0 --port 8000
Run with: uvicorn attest.api:app --host 0.0.0.0 --port 8000
"""
from __future__ import annotations
@ -24,7 +24,7 @@ try:
from fastapi.responses import JSONResponse
from pydantic import BaseModel
except ImportError:
raise ImportError("API requires fastapi: pip install verisoo[api]")
raise ImportError("API requires fastapi: pip install attest[api]")
from .hashing import compute_all_distances, hash_image, is_same_image
from .models import AttestationRecord, ImageHashes, ProofLink
@ -32,11 +32,11 @@ from .storage import LocalStorage
from .crypto import verify_signature, load_public_key_from_bytes
# Configuration via environment
DATA_DIR = Path(os.environ.get("SOOSEF_DATA_DIR", Path.home() / ".soosef"))
BASE_URL = os.environ.get("VERISOO_BASE_URL", "https://verisoo.io")
DATA_DIR = Path(os.environ.get("FIELDWITNESS_DATA_DIR", Path.home() / ".fieldwitness"))
BASE_URL = os.environ.get("VERISOO_BASE_URL", "https://attest.io")
app = FastAPI(
title="Verisoo",
title="Attest",
description="Decentralized image provenance and attestation API",
version="0.1.0",
docs_url="/docs",
@ -179,7 +179,7 @@ def record_to_attestation_response(
async def root():
"""API root - basic info."""
return {
"service": "Verisoo",
"service": "Attest",
"description": "Decentralized image provenance and attestation",
"docs": "/docs",
"verify": "POST /verify with image file",
@ -274,7 +274,7 @@ async def get_proof_short(short_id: str):
Get attestation proof by short ID.
This is the endpoint for shareable proof links:
verisoo.io/v/a8f3c2d1e9b7
attest.io/v/a8f3c2d1e9b7
"""
return await _get_proof(short_id)
@ -486,7 +486,7 @@ async def attest_from_mobile(
from pathlib import Path
import tempfile
# Check if we can embed (JPEG with stegasoo available)
# Check if we can embed (JPEG with stego available)
# Save image temporarily to check format
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
f.write(image_data)
@ -570,7 +570,7 @@ async def health():
# --- Federation endpoints ---
# These 4 endpoints implement the PeerTransport protocol server side,
# enabling gossip-based attestation sync between SooSeF instances.
# enabling gossip-based attestation sync between FieldWitness instances.
_storage_cache: LocalStorage | None = None
@ -659,7 +659,7 @@ async def federation_push_records(body: dict):
# Load trusted fingerprints
trusted_fps = set()
try:
from soosef.keystore.manager import KeystoreManager
from fieldwitness.keystore.manager import KeystoreManager
ks = KeystoreManager()
for key in ks.get_trusted_keys():

View File

@ -1,7 +1,7 @@
"""
Attestation Creation Module for Verisoo.
Attestation Creation Module for Attest.
This module is the core of Verisoo's provenance system. An attestation is a
This module is the core of Attest's provenance system. An attestation is a
cryptographic proof that binds together:
1. AN IMAGE - identified by multiple hashes (SHA-256 + perceptual)
@ -59,7 +59,7 @@ Usage Example:
from .crypto import load_private_key
# Load attestor's private key
private_key = load_private_key("~/.verisoo/private.pem")
private_key = load_private_key("~/.attest/private.pem")
# Create attestation with auto EXIF extraction
attestation = create_attestation(
@ -417,7 +417,7 @@ def create_attestation(
- The signature covers ALL fields (hashes, fingerprint, timestamp, metadata)
- Changing any field invalidates the signature
- Timestamp is attestation time, not necessarily capture time
- Verify attestations using verisoo.verification module
- Verify attestations using attest.verification module
"""
# -------------------------------------------------------------------------
# STEP 1: Establish attestation timestamp

View File

@ -27,7 +27,7 @@ from dataclasses import dataclass
from pathlib import Path
from typing import BinaryIO, Iterator
from .exceptions import VerisooError
from .exceptions import AttestError
MAGIC = b"VERISOO\x00"
@ -36,7 +36,7 @@ HEADER_SIZE = len(MAGIC) + 4 # magic + version
RECORD_HEADER_SIZE = 8 # length + crc32
class LogCorruptionError(VerisooError):
class LogCorruptionError(AttestError):
"""Log file is corrupted."""
def __init__(self, message: str, offset: int) -> None:

View File

@ -1,14 +1,14 @@
"""
Command-Line Interface (CLI) for Verisoo.
Command-Line Interface (CLI) for Attest.
This module provides the `verisoo` command-line tool for interacting with
This module provides the `attest` command-line tool for interacting with
the image provenance system. It wraps the core library functionality in
a user-friendly interface.
Command Structure:
-----------------
verisoo
attest
identity # Manage attestor identity (Ed25519 keypair)
generate # Create new identity
show # Display current identity fingerprint
@ -36,12 +36,12 @@ Command Structure:
Global Options:
--------------
--data-dir PATH Override default data directory (~/.verisoo)
--data-dir PATH Override default data directory (~/.attest)
--json Output in JSON format (for scripting)
Data Directory Structure:
------------------------
~/.verisoo/
~/.attest/
private.pem # Ed25519 private key (PEM format)
public.pem # Ed25519 public key (PEM format)
identity.json # Identity metadata (name, created_at)
@ -53,19 +53,19 @@ Data Directory Structure:
Usage Examples:
--------------
# First-time setup
$ verisoo identity generate --name "Photographer Name"
$ attest identity generate --name "Photographer Name"
# Attest a photo with location
$ verisoo attest photo.jpg -l "50.45,30.52,10,Kyiv" -c "Morning scene"
$ attest attest photo.jpg -l "50.45,30.52,10,Kyiv" -c "Morning scene"
# Verify an image (even after social media compression)
$ verisoo verify downloaded_photo.jpg
$ attest verify downloaded_photo.jpg
# Start API server for remote verification
$ verisoo serve --port 8000
$ attest serve --port 8000
# Check log status
$ verisoo log status
$ attest log status
Exit Codes:
----------
@ -89,7 +89,7 @@ from typing import Any
try:
import click
except ImportError:
print("CLI requires click: pip install verisoo[cli]", file=sys.stderr)
print("CLI requires click: pip install attest[cli]", file=sys.stderr)
sys.exit(1)
@ -99,7 +99,7 @@ except ImportError:
@click.pass_context
def main(ctx: click.Context, data_dir: Path | None, json_output: bool) -> None:
"""
Verisoo - Decentralized image provenance and attestation.
Attest - Decentralized image provenance and attestation.
Part of the Soo Suite. Prove when images were created and by whom.
"""
@ -165,7 +165,7 @@ def identity_show(ctx: click.Context) -> None:
storage = LocalStorage(ctx.obj.get("data_dir"))
if not storage.has_node_identity():
raise click.ClickException("No identity configured. Run: verisoo identity generate")
raise click.ClickException("No identity configured. Run: attest identity generate")
public_key = load_public_key(storage.public_key_path)
fingerprint = fingerprint_from_pubkey(public_key.public_key_bytes())
@ -289,7 +289,7 @@ def _parse_location(location_str: str) -> dict[str, Any]:
@click.option("--caption", "-c", help="Photographer's notes")
@click.option("--no-exif", "no_exif", is_flag=True, help="Disable auto EXIF extraction")
@click.option("--embed", "-e", is_flag=True, help="Embed proof link in image (JPEG: DCT, other: XMP sidecar)")
@click.option("--base-url", default="https://verisoo.io", help="Base URL for proof links")
@click.option("--base-url", default="https://attest.io", help="Base URL for proof links")
@click.pass_context
def attest(
ctx: click.Context,
@ -322,19 +322,19 @@ def attest(
\b
EXAMPLES:
# Basic attestation (auto-extracts EXIF)
verisoo attest photo.jpg
attest attest photo.jpg
# With proof link embedded in image
verisoo attest photo.jpg --embed
attest attest photo.jpg --embed
# With manual location (overrides EXIF GPS)
verisoo attest photo.jpg -l "50.45,30.52,10,Kyiv"
attest attest photo.jpg -l "50.45,30.52,10,Kyiv"
# With caption and tags
verisoo attest photo.jpg -c "Morning scene" -t news -t ukraine
attest attest photo.jpg -c "Morning scene" -t news -t ukraine
# Skip EXIF extraction
verisoo attest photo.jpg --no-exif
attest attest photo.jpg --no-exif
\b
OUTPUT:
@ -354,7 +354,7 @@ def attest(
storage = LocalStorage(ctx.obj.get("data_dir"))
if not storage.has_node_identity():
raise click.ClickException("No identity configured. Run: verisoo identity generate")
raise click.ClickException("No identity configured. Run: attest identity generate")
# -------------------------------------------------------------------------
# Load the attestor's private key
@ -653,7 +653,7 @@ def peer_list(ctx: click.Context) -> None:
@click.option("--port", default=8000, type=int, help="Port to listen on")
def serve(host: str, port: int) -> None:
"""
Run the Verisoo verification API server.
Run the Attest verification API server.
Starts a FastAPI server that exposes verification endpoints. This allows
remote clients to verify images against your local attestation log.
@ -668,13 +668,13 @@ def serve(host: str, port: int) -> None:
\b
EXAMPLES:
# Start on default port
verisoo serve
attest serve
# Custom port
verisoo serve --port 9000
attest serve --port 9000
# Bind to localhost only (no external access)
verisoo serve --host 127.0.0.1
attest serve --host 127.0.0.1
\b
CLIENT USAGE:
@ -687,7 +687,7 @@ def serve(host: str, port: int) -> None:
\b
ENVIRONMENT VARIABLES:
VERISOO_DATA_DIR Override data directory
VERISOO_BASE_URL Base URL for proof links (default: https://verisoo.io)
VERISOO_BASE_URL Base URL for proof links (default: https://attest.io)
\b
SECURITY NOTES:
@ -697,18 +697,18 @@ def serve(host: str, port: int) -> None:
"""
# -------------------------------------------------------------------------
# Import the API module (requires fastapi, uvicorn)
# These are optional dependencies: pip install verisoo[api]
# These are optional dependencies: pip install attest[api]
# -------------------------------------------------------------------------
try:
from .api import serve as run_server
except ImportError:
raise click.ClickException("API server requires fastapi: pip install verisoo[api]")
raise click.ClickException("API server requires fastapi: pip install attest[api]")
# -------------------------------------------------------------------------
# Start the server
# Uses uvicorn as the ASGI server
# -------------------------------------------------------------------------
click.echo(f"Starting Verisoo API server on {host}:{port}")
click.echo(f"Starting Attest API server on {host}:{port}")
click.echo("Press Ctrl+C to stop")
run_server(host=host, port=port)

View File

@ -1,5 +1,5 @@
"""
Cryptographic primitives for Verisoo.
Cryptographic primitives for Attest.
Ed25519 for signatures (fast, small keys, deterministic).
SHA-256 for content hashing.

View File

@ -1,10 +1,10 @@
"""
Proof Link Embedding Module for Verisoo.
Proof Link Embedding Module for Attest.
This module handles embedding proof links into images after attestation.
Two strategies are used depending on the image format:
1. JPEG: DCT steganography via stegasoo
1. JPEG: DCT steganography via stego
- Embeds in frequency domain (survives recompression)
- Uses center region for robustness against cropping
- Invisible to human eye
@ -14,7 +14,7 @@ Two strategies are used depending on the image format:
- Travels with the image file
- Not steganographic (plaintext XML)
The proof link format: https://verisoo.io/v/{short_id}
The proof link format: https://attest.io/v/{short_id}
"""
from __future__ import annotations
@ -27,10 +27,10 @@ from typing import Any
from PIL import Image
# Stegasoo integration — imported as a pip dependency (no path hacks needed).
# Install stegasoo[dct] to enable DCT steganography for JPEG proof embedding.
# Stego integration — imported as a pip dependency (no path hacks needed).
# Install stego[dct] to enable DCT steganography for JPEG proof embedding.
try:
from soosef.stegasoo.dct_steganography import (
from fieldwitness.stego.dct_steganography import (
embed_in_dct,
extract_from_dct,
has_dct_support,
@ -48,15 +48,15 @@ except ImportError:
# CONSTANTS
# =============================================================================
# Fixed public seed for Verisoo proof links
# Fixed public seed for Attest proof links
# This is intentionally public - anyone should be able to extract the proof link
VERISOO_SEED = b"verisoo"
VERISOO_SEED = b"attest"
# Base URL for proof links
DEFAULT_BASE_URL = "https://verisoo.io"
DEFAULT_BASE_URL = "https://attest.io"
# XMP namespace for Verisoo
XMP_NAMESPACE = "https://verisoo.io/ns/1.0/"
# XMP namespace for Attest
XMP_NAMESPACE = "https://attest.io/ns/1.0/"
# Supported formats for DCT embedding
DCT_FORMATS = {".jpg", ".jpeg"}
@ -101,13 +101,13 @@ def generate_xmp_sidecar(
caption: str | None = None,
) -> str:
"""
Generate XMP sidecar XML content for a Verisoo attestation.
Generate XMP sidecar XML content for a Attest attestation.
This creates a standard XMP file that can be read by Lightroom,
Darktable, and other photo management software.
Args:
proof_link: Full proof URL (e.g., "https://verisoo.io/v/abc123")
proof_link: Full proof URL (e.g., "https://attest.io/v/abc123")
fingerprint: Attestor's fingerprint
attested_at: Attestation timestamp
image_sha256: SHA-256 hash of the image
@ -123,17 +123,17 @@ def generate_xmp_sidecar(
.replace(">", "&gt;")
.replace('"', "&quot;"))
caption_attr = f'\n verisoo:Caption="{escape(caption)}"' if caption else ""
caption_attr = f'\n attest:Caption="{escape(caption)}"' if caption else ""
return f'''<?xml version="1.0" encoding="UTF-8"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Verisoo">
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Attest">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:verisoo="{XMP_NAMESPACE}"
verisoo:ProofLink="{escape(proof_link)}"
verisoo:Fingerprint="{escape(fingerprint)}"
verisoo:AttestedAt="{attested_at.isoformat()}"
verisoo:ImageSHA256="{escape(image_sha256)}"{caption_attr}/>
xmlns:attest="{XMP_NAMESPACE}"
attest:ProofLink="{escape(proof_link)}"
attest:Fingerprint="{escape(fingerprint)}"
attest:AttestedAt="{attested_at.isoformat()}"
attest:ImageSHA256="{escape(image_sha256)}"{caption_attr}/>
</rdf:Description>
</x:xmpmeta>
'''
@ -181,7 +181,7 @@ def write_xmp_sidecar(
def read_xmp_sidecar(image_path: Path) -> dict[str, str] | None:
"""
Read Verisoo metadata from an XMP sidecar file.
Read Attest metadata from an XMP sidecar file.
Args:
image_path: Path to the image (sidecar path is derived)
@ -205,11 +205,11 @@ def read_xmp_sidecar(image_path: Path) -> dict[str, str] | None:
result = {}
patterns = {
"proof_link": r'verisoo:ProofLink="([^"]*)"',
"fingerprint": r'verisoo:Fingerprint="([^"]*)"',
"attested_at": r'verisoo:AttestedAt="([^"]*)"',
"image_sha256": r'verisoo:ImageSHA256="([^"]*)"',
"caption": r'verisoo:Caption="([^"]*)"',
"proof_link": r'attest:ProofLink="([^"]*)"',
"fingerprint": r'attest:Fingerprint="([^"]*)"',
"attested_at": r'attest:AttestedAt="([^"]*)"',
"image_sha256": r'attest:ImageSHA256="([^"]*)"',
"caption": r'attest:Caption="([^"]*)"',
}
for key, pattern in patterns.items():
@ -234,26 +234,26 @@ def embed_proof_in_jpeg(
"""
Embed a proof link into a JPEG image using DCT steganography.
Uses stegasoo's DCT embedding with:
- Fixed public seed (b"verisoo") so anyone can extract
Uses stego's DCT embedding with:
- Fixed public seed (b"attest") so anyone can extract
- Center-biased embedding for crop resistance (TODO)
- Minimal quality impact (only ~25 blocks needed)
Args:
image_data: Original JPEG bytes
proof_link: Proof URL to embed (e.g., "https://verisoo.io/v/abc123")
proof_link: Proof URL to embed (e.g., "https://attest.io/v/abc123")
Returns:
Tuple of (embedded_image_bytes, stats_dict)
Raises:
ImportError: If stegasoo is not available
ImportError: If stego is not available
ValueError: If image is too small or embedding fails
"""
if not HAS_STEGASOO:
raise ImportError(
"DCT embedding requires stegasoo. "
"Ensure stegasoo is installed or available at ../stegasoo"
"DCT embedding requires stego. "
"Ensure stego is installed or available at ../stego"
)
if not has_jpegio_support():
@ -302,7 +302,7 @@ def extract_proof_from_jpeg(image_data: bytes) -> str | None:
# Validate it looks like a proof link
proof_link = payload.decode("utf-8")
if "verisoo" in proof_link.lower() or proof_link.startswith("http"):
if "attest" in proof_link.lower() or proof_link.startswith("http"):
return proof_link
return None
@ -330,7 +330,7 @@ def get_embed_method(image_path: Path) -> str:
if HAS_STEGASOO and has_jpegio_support():
return "dct"
else:
return "xmp" # Fallback to XMP if stegasoo unavailable
return "xmp" # Fallback to XMP if stego unavailable
if suffix in XMP_FORMATS or suffix in RAW_FORMATS:
return "xmp"

View File

@ -1,5 +1,5 @@
"""
Exception hierarchy for Verisoo.
Exception hierarchy for Attest.
Follows the pattern established in the Soo Suite for typed, informative errors.
"""
@ -7,19 +7,19 @@ Follows the pattern established in the Soo Suite for typed, informative errors.
from __future__ import annotations
class VerisooError(Exception):
"""Base exception for all Verisoo errors."""
class AttestError(Exception):
"""Base exception for all Attest errors."""
pass
class AttestationError(VerisooError):
class AttestationError(AttestError):
"""Errors during attestation creation or signing."""
pass
class VerificationError(VerisooError):
class VerificationError(AttestError):
"""Errors during attestation verification."""
def __init__(self, message: str, *, reason: str | None = None) -> None:
@ -46,19 +46,19 @@ class SignatureError(VerificationError):
super().__init__(message, reason="invalid_signature")
class IdentityError(VerisooError):
class IdentityError(AttestError):
"""Errors related to identity/key management."""
pass
class MerkleError(VerisooError):
class MerkleError(AttestError):
"""Errors in merkle tree operations."""
pass
class FederationError(VerisooError):
class FederationError(AttestError):
"""Errors in peer communication and sync."""
pass

View File

@ -1,5 +1,5 @@
"""
Federation and gossip protocol for Verisoo.
Federation and gossip protocol for Attest.
Nodes sync their merkle logs via gossip:
1. Periodically exchange merkle roots with peers
@ -83,7 +83,7 @@ class PeerTransport(Protocol):
class GossipNode:
"""
A node in the Verisoo federation network.
A node in the Attest federation network.
Manages:
- Local merkle log
@ -332,10 +332,10 @@ class GossipNode:
class HttpTransport:
"""HTTP-based peer transport using aiohttp.
Communicates with the federation endpoints exposed by verisoo's
Communicates with the federation endpoints exposed by attest's
FastAPI server (/federation/status, /federation/records, etc.).
Requires the [federation] extra: pip install soosef[federation]
Requires the [federation] extra: pip install fieldwitness[federation]
"""
def __init__(self, timeout: float = 30.0) -> None:
@ -348,7 +348,7 @@ class HttpTransport:
import aiohttp
except ImportError:
raise ImportError(
"Federation requires aiohttp. Install with: pip install soosef[federation]"
"Federation requires aiohttp. Install with: pip install fieldwitness[federation]"
)
self._session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=self.timeout)

View File

@ -1,5 +1,5 @@
"""
Multi-algorithm image hashing for Verisoo.
Multi-algorithm image hashing for Attest.
Designed to survive social media mangling:
- JPEG recompression (Instagram, Twitter, Facebook)

Some files were not shown because too many files have changed in this diff Show More