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:
parent
6325e86873
commit
490f9d4a1d
@ -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
|
||||
|
||||
99
CLAUDE.md
99
CLAUDE.md
@ -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
674
LICENSE
Normal 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
519
README.md
@ -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 -->
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
@ -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)
|
||||
```
|
||||
|
||||
@ -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')"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: soosef
|
||||
name: fieldwitness
|
||||
labels:
|
||||
app.kubernetes.io/name: soosef
|
||||
app.kubernetes.io/name: fieldwitness
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 ==="
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 &
|
||||
|
||||
@ -12,7 +12,7 @@ libssl-dev
|
||||
gfortran
|
||||
libopenblas-dev
|
||||
|
||||
## SooSeF runtime dependencies
|
||||
## FieldWitness runtime dependencies
|
||||
gpsd
|
||||
gpsd-clients
|
||||
cryptsetup
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 - \
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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")` |
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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. |
|
||||
|
||||
240
docs/planning/c2pa-integration.md
Normal file
240
docs/planning/c2pa-integration.md
Normal 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)
|
||||
214
docs/planning/gtm-feasibility.md
Normal file
214
docs/planning/gtm-feasibility.md
Normal 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)
|
||||
480
docs/security/threat-model.md
Normal file
480
docs/security/threat-model.md
Normal 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.
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""
|
||||
Admin routes are registered directly in app.py via _register_stegasoo_routes()
|
||||
Admin routes are registered directly in app.py via _register_stego_routes()
|
||||
alongside the auth routes (setup, login, logout, account, admin/users).
|
||||
"""
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Attestation blueprint — attest and verify images via Verisoo.
|
||||
Attestation blueprint — attest and verify images via Attest.
|
||||
|
||||
Wraps verisoo's attestation and verification libraries to provide:
|
||||
Wraps attest's attestation and verification libraries to provide:
|
||||
- Image attestation: upload → hash → sign → store in append-only log
|
||||
- Image verification: upload → hash → search log → display matches
|
||||
- Verification receipt: same as verify but returns a downloadable JSON file
|
||||
@ -21,27 +21,27 @@ bp = Blueprint("attest", __name__)
|
||||
|
||||
|
||||
def _get_storage():
|
||||
"""Get verisoo LocalStorage pointed at soosef's attestation directory."""
|
||||
from soosef.verisoo.storage import LocalStorage
|
||||
"""Get attest LocalStorage pointed at fieldwitness's attestation directory."""
|
||||
from fieldwitness.attest.storage import LocalStorage
|
||||
|
||||
from soosef.paths import ATTESTATIONS_DIR
|
||||
from fieldwitness.paths import ATTESTATIONS_DIR
|
||||
|
||||
return LocalStorage(base_path=ATTESTATIONS_DIR)
|
||||
|
||||
|
||||
def _get_private_key():
|
||||
"""Load the Ed25519 private key from soosef identity directory."""
|
||||
from soosef.verisoo.crypto import load_private_key
|
||||
"""Load the Ed25519 private key from fieldwitness identity directory."""
|
||||
from fieldwitness.attest.crypto import load_private_key
|
||||
|
||||
from soosef.paths import IDENTITY_PRIVATE_KEY
|
||||
from fieldwitness.paths import IDENTITY_PRIVATE_KEY
|
||||
|
||||
if not IDENTITY_PRIVATE_KEY.exists():
|
||||
return None
|
||||
return load_private_key(IDENTITY_PRIVATE_KEY)
|
||||
|
||||
|
||||
def _wrap_in_chain(verisoo_record, private_key, metadata: dict | None = None):
|
||||
"""Wrap a Verisoo attestation record in the hash chain.
|
||||
def _wrap_in_chain(attest_record, private_key, metadata: dict | None = None):
|
||||
"""Wrap a Attest attestation record in the hash chain.
|
||||
|
||||
Returns the chain record, or None if chain is disabled.
|
||||
"""
|
||||
@ -49,23 +49,23 @@ def _wrap_in_chain(verisoo_record, private_key, metadata: dict | None = None):
|
||||
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
|
||||
from soosef.config import SoosefConfig
|
||||
from soosef.federation.chain import ChainStore
|
||||
from soosef.paths import CHAIN_DIR, IDENTITY_PRIVATE_KEY
|
||||
from fieldwitness.config import FieldWitnessConfig
|
||||
from fieldwitness.federation.chain import ChainStore
|
||||
from fieldwitness.paths import CHAIN_DIR, IDENTITY_PRIVATE_KEY
|
||||
|
||||
config = SoosefConfig.load()
|
||||
config = FieldWitnessConfig.load()
|
||||
if not config.chain_enabled or not config.chain_auto_wrap:
|
||||
return None
|
||||
|
||||
# Hash the verisoo record bytes as chain content
|
||||
# Hash the attest record bytes as chain content
|
||||
record_bytes = (
|
||||
verisoo_record.to_bytes()
|
||||
if hasattr(verisoo_record, "to_bytes")
|
||||
else str(verisoo_record).encode()
|
||||
attest_record.to_bytes()
|
||||
if hasattr(attest_record, "to_bytes")
|
||||
else str(attest_record).encode()
|
||||
)
|
||||
content_hash = hashlib.sha256(record_bytes).digest()
|
||||
|
||||
# Load Ed25519 key for chain signing (need the cryptography key, not verisoo's)
|
||||
# Load Ed25519 key for chain signing (need the cryptography key, not attest's)
|
||||
priv_pem = IDENTITY_PRIVATE_KEY.read_bytes()
|
||||
chain_private_key = load_pem_private_key(priv_pem, password=None)
|
||||
|
||||
@ -79,7 +79,7 @@ def _wrap_in_chain(verisoo_record, private_key, metadata: dict | None = None):
|
||||
store = ChainStore(CHAIN_DIR)
|
||||
return store.append(
|
||||
content_hash=content_hash,
|
||||
content_type="verisoo/attestation-v1",
|
||||
content_type="attest/attestation-v1",
|
||||
private_key=chain_private_key,
|
||||
metadata=chain_metadata,
|
||||
)
|
||||
@ -111,7 +111,7 @@ def attest():
|
||||
if request.method == "POST":
|
||||
if not has_identity:
|
||||
flash(
|
||||
"No identity configured. Run 'soosef init' or generate one from the Keys page.",
|
||||
"No identity configured. Run 'fieldwitness init' or generate one from the Keys page.",
|
||||
"error",
|
||||
)
|
||||
return redirect(url_for("attest.attest"))
|
||||
@ -151,7 +151,7 @@ def attest():
|
||||
# Extract-then-classify: get evidentiary metadata before attestation
|
||||
# so user can control what's included
|
||||
if auto_exif and strip_device:
|
||||
from soosef.metadata import extract_and_classify
|
||||
from fieldwitness.metadata import extract_and_classify
|
||||
|
||||
extraction = extract_and_classify(image_data)
|
||||
# Merge evidentiary fields (GPS, timestamp) but exclude
|
||||
@ -166,7 +166,7 @@ def attest():
|
||||
metadata[f"exif_{key}"] = str(value)
|
||||
|
||||
# Create the attestation
|
||||
from soosef.verisoo.attestation import create_attestation
|
||||
from fieldwitness.attest.attestation import create_attestation
|
||||
|
||||
attestation = create_attestation(
|
||||
image_data=image_data,
|
||||
@ -194,14 +194,14 @@ def attest():
|
||||
|
||||
# Save our own identity so we can look it up during verification
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from soosef.verisoo.models import Identity
|
||||
from fieldwitness.attest.models import Identity
|
||||
|
||||
pub_key = private_key.public_key()
|
||||
pub_bytes = pub_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
identity = Identity(
|
||||
public_key=pub_bytes,
|
||||
fingerprint=attestation.record.attestor_fingerprint,
|
||||
metadata={"name": "SooSeF Local Identity"},
|
||||
metadata={"name": "FieldWitness Local Identity"},
|
||||
)
|
||||
try:
|
||||
storage.save_identity(identity)
|
||||
@ -246,11 +246,11 @@ def attest_batch():
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
from soosef.verisoo.hashing import hash_image
|
||||
from fieldwitness.attest.hashing import hash_image
|
||||
|
||||
private_key = _get_private_key()
|
||||
if private_key is None:
|
||||
return {"error": "No identity key. Run soosef init first."}, 400
|
||||
return {"error": "No identity key. Run fieldwitness init first."}, 400
|
||||
|
||||
files = request.files.getlist("images")
|
||||
if not files:
|
||||
@ -271,14 +271,14 @@ def attest_batch():
|
||||
results.append({"file": filename, "status": "skipped", "reason": "already attested"})
|
||||
continue
|
||||
|
||||
from soosef.verisoo.attestation import create_attestation
|
||||
from fieldwitness.attest.attestation import create_attestation
|
||||
|
||||
attestation = create_attestation(image_data, private_key)
|
||||
index = storage.append_record(attestation.record)
|
||||
|
||||
# Wrap in chain if enabled
|
||||
chain_index = None
|
||||
config = request.app.config.get("SOOSEF_CONFIG") if hasattr(request, "app") else None
|
||||
config = request.app.config.get("FIELDWITNESS_CONFIG") if hasattr(request, "app") else None
|
||||
if config and getattr(config, "chain_enabled", False) and getattr(config, "chain_auto_wrap", False):
|
||||
try:
|
||||
chain_record = _wrap_in_chain(attestation.record, private_key, {})
|
||||
@ -365,11 +365,11 @@ def _verify_image(image_data: bytes) -> dict:
|
||||
"""Run the full verification pipeline against the attestation log.
|
||||
|
||||
Returns a dict with keys:
|
||||
query_hashes — ImageHashes object from verisoo
|
||||
query_hashes — ImageHashes object from fieldwitness.attest
|
||||
matches — list of match dicts (record, match_type, distances, attestor_name)
|
||||
record_count — total records searched
|
||||
"""
|
||||
from soosef.verisoo.hashing import compute_all_distances, hash_image, is_same_image
|
||||
from fieldwitness.attest.hashing import compute_all_distances, hash_image, is_same_image
|
||||
|
||||
query_hashes = hash_image(image_data)
|
||||
storage = _get_storage()
|
||||
@ -541,12 +541,12 @@ def verify_receipt():
|
||||
|
||||
# Chain position proof — look up this attestation in the hash chain
|
||||
try:
|
||||
from soosef.config import SoosefConfig
|
||||
from soosef.federation.chain import ChainStore
|
||||
from soosef.federation.serialization import compute_record_hash
|
||||
from soosef.paths import CHAIN_DIR
|
||||
from fieldwitness.config import FieldWitnessConfig
|
||||
from fieldwitness.federation.chain import ChainStore
|
||||
from fieldwitness.federation.serialization import compute_record_hash
|
||||
from fieldwitness.paths import CHAIN_DIR
|
||||
|
||||
chain_config = SoosefConfig.load()
|
||||
chain_config = FieldWitnessConfig.load()
|
||||
if chain_config.chain_enabled:
|
||||
chain_store = ChainStore(CHAIN_DIR)
|
||||
# Search chain for a record whose content_hash matches this attestation
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Source drop box blueprint — anonymous, token-gated file submission.
|
||||
|
||||
Provides a SecureDrop-like intake that lives inside SooSeF:
|
||||
Provides a SecureDrop-like intake that lives inside FieldWitness:
|
||||
- Admin creates a time-limited upload token
|
||||
- Source opens the token URL in a browser (no account needed)
|
||||
- Files are uploaded, EXIF-stripped, and auto-attested on receipt
|
||||
@ -21,8 +21,8 @@ from pathlib import Path
|
||||
from auth import admin_required, login_required
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
|
||||
|
||||
from soosef.audit import log_action
|
||||
from soosef.paths import AUTH_DIR, TEMP_DIR
|
||||
from fieldwitness.audit import log_action
|
||||
from fieldwitness.paths import AUTH_DIR, TEMP_DIR
|
||||
|
||||
bp = Blueprint("dropbox", __name__, url_prefix="/dropbox")
|
||||
|
||||
@ -188,7 +188,7 @@ def upload(token):
|
||||
# 1. Extract EXIF into attestation metadata (evidentiary fields)
|
||||
# 2. Attest the ORIGINAL bytes (hash matches what source submitted)
|
||||
# 3. Strip metadata from the stored copy (protect source device info)
|
||||
from soosef.metadata import extract_strip_pipeline
|
||||
from fieldwitness.metadata import extract_strip_pipeline
|
||||
|
||||
extraction, stripped_data = extract_strip_pipeline(raw_data)
|
||||
|
||||
@ -204,7 +204,7 @@ def upload(token):
|
||||
# is preserved in the attestation metadata; dangerous fields
|
||||
# (device serial) are excluded.
|
||||
try:
|
||||
from soosef.verisoo.attestation import create_attestation
|
||||
from fieldwitness.attest.attestation import create_attestation
|
||||
|
||||
from blueprints.attest import _get_private_key, _get_storage
|
||||
|
||||
@ -240,7 +240,7 @@ def upload(token):
|
||||
# insufficient), making valid receipts unforgeable.
|
||||
import hmac
|
||||
|
||||
from soosef.paths import SECRET_KEY_FILE
|
||||
from fieldwitness.paths import SECRET_KEY_FILE
|
||||
|
||||
server_secret = SECRET_KEY_FILE.read_bytes() if SECRET_KEY_FILE.exists() else token.encode()
|
||||
receipt_code = hmac.new(
|
||||
@ -279,7 +279,7 @@ def upload(token):
|
||||
return Response(receipt_text, content_type="text/plain")
|
||||
|
||||
# GET — show upload form with client-side SHA-256 hashing
|
||||
# Minimal page, no SooSeF branding (source safety)
|
||||
# Minimal page, no FieldWitness branding (source safety)
|
||||
remaining = token_data["max_files"] - token_data["used"]
|
||||
return f"""<!DOCTYPE html>
|
||||
<html><head><title>Secure Upload</title>
|
||||
|
||||
@ -12,7 +12,7 @@ bp = Blueprint("federation", __name__, url_prefix="/federation")
|
||||
@login_required
|
||||
def status():
|
||||
"""Federation status dashboard."""
|
||||
from soosef.verisoo.peer_store import PeerStore
|
||||
from fieldwitness.attest.peer_store import PeerStore
|
||||
|
||||
store = PeerStore()
|
||||
peers = store.list_peers()
|
||||
@ -21,9 +21,9 @@ def status():
|
||||
# Get local node info
|
||||
node_info = {"root": None, "size": 0}
|
||||
try:
|
||||
from soosef.verisoo.storage import LocalStorage
|
||||
from fieldwitness.attest.storage import LocalStorage
|
||||
|
||||
import soosef.paths as _paths
|
||||
import fieldwitness.paths as _paths
|
||||
|
||||
storage = LocalStorage(_paths.ATTESTATIONS_DIR)
|
||||
stats = storage.get_stats()
|
||||
@ -48,7 +48,7 @@ def status():
|
||||
@admin_required
|
||||
def peer_add():
|
||||
"""Add a federation peer."""
|
||||
from soosef.verisoo.peer_store import PeerStore
|
||||
from fieldwitness.attest.peer_store import PeerStore
|
||||
|
||||
url = request.form.get("url", "").strip()
|
||||
fingerprint = request.form.get("fingerprint", "").strip()
|
||||
@ -67,7 +67,7 @@ def peer_add():
|
||||
@admin_required
|
||||
def peer_remove():
|
||||
"""Remove a federation peer."""
|
||||
from soosef.verisoo.peer_store import PeerStore
|
||||
from fieldwitness.attest.peer_store import PeerStore
|
||||
|
||||
url = request.form.get("url", "").strip()
|
||||
store = PeerStore()
|
||||
|
||||
@ -5,7 +5,7 @@ Fieldkit blueprint — killswitch, dead man's switch, status dashboard.
|
||||
from auth import admin_required, get_username, login_required
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
|
||||
from soosef.audit import log_action
|
||||
from fieldwitness.audit import log_action
|
||||
|
||||
bp = Blueprint("fieldkit", __name__, url_prefix="/fieldkit")
|
||||
|
||||
@ -14,7 +14,7 @@ bp = Blueprint("fieldkit", __name__, url_prefix="/fieldkit")
|
||||
@login_required
|
||||
def status():
|
||||
"""Fieldkit status dashboard — all monitors and system health."""
|
||||
from soosef.fieldkit.deadman import DeadmanSwitch
|
||||
from fieldwitness.fieldkit.deadman import DeadmanSwitch
|
||||
|
||||
deadman = DeadmanSwitch()
|
||||
return render_template(
|
||||
@ -39,7 +39,7 @@ def killswitch():
|
||||
flash("Killswitch requires password confirmation.", "danger")
|
||||
return render_template("fieldkit/killswitch.html")
|
||||
|
||||
from soosef.fieldkit.killswitch import PurgeScope, execute_purge
|
||||
from fieldwitness.fieldkit.killswitch import PurgeScope, execute_purge
|
||||
|
||||
actor = username
|
||||
result = execute_purge(PurgeScope.ALL, reason="web_ui")
|
||||
@ -71,7 +71,7 @@ def killswitch():
|
||||
@login_required
|
||||
def deadman_checkin():
|
||||
"""Record a dead man's switch check-in."""
|
||||
from soosef.fieldkit.deadman import DeadmanSwitch
|
||||
from fieldwitness.fieldkit.deadman import DeadmanSwitch
|
||||
|
||||
deadman = DeadmanSwitch()
|
||||
deadman.checkin()
|
||||
|
||||
@ -5,7 +5,7 @@ Key management blueprint — unified view of all key material.
|
||||
from auth import get_username, login_required
|
||||
from flask import Blueprint, flash, redirect, render_template, url_for
|
||||
|
||||
from soosef.audit import log_action
|
||||
from fieldwitness.audit import log_action
|
||||
|
||||
bp = Blueprint("keys", __name__, url_prefix="/keys")
|
||||
|
||||
@ -14,7 +14,7 @@ bp = Blueprint("keys", __name__, url_prefix="/keys")
|
||||
@login_required
|
||||
def index():
|
||||
"""Key management dashboard."""
|
||||
from soosef.keystore import KeystoreManager
|
||||
from fieldwitness.keystore import KeystoreManager
|
||||
|
||||
ks = KeystoreManager()
|
||||
return render_template("fieldkit/keys.html", keystore=ks.status())
|
||||
@ -24,7 +24,7 @@ def index():
|
||||
@login_required
|
||||
def generate_channel():
|
||||
"""Generate a new channel key."""
|
||||
from soosef.keystore import KeystoreManager
|
||||
from fieldwitness.keystore import KeystoreManager
|
||||
|
||||
ks = KeystoreManager()
|
||||
try:
|
||||
@ -54,7 +54,7 @@ def generate_channel():
|
||||
@login_required
|
||||
def generate_identity():
|
||||
"""Generate a new Ed25519 identity."""
|
||||
from soosef.keystore import KeystoreManager
|
||||
from fieldwitness.keystore import KeystoreManager
|
||||
|
||||
ks = KeystoreManager()
|
||||
try:
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
"""
|
||||
Steganography routes are registered directly in app.py via _register_stegasoo_routes()
|
||||
rather than as a blueprint, because the stegasoo route logic (3,600+ lines) uses
|
||||
Steganography routes are registered directly in app.py via _register_stego_routes()
|
||||
rather than as a blueprint, because the stego route logic (3,600+ lines) uses
|
||||
module-level state (ThreadPoolExecutor, jobs dict, subprocess_stego instance)
|
||||
that doesn't translate cleanly to a blueprint.
|
||||
|
||||
The stego templates are in templates/stego/ and extend the soosef base.html.
|
||||
The stego templates are in templates/stego/ and extend the fieldwitness base.html.
|
||||
"""
|
||||
|
||||
@ -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),
|
||||
]
|
||||
)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
|
||||
14
src/fieldwitness/__init__.py
Normal file
14
src/fieldwitness/__init__.py
Normal 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"
|
||||
31
src/fieldwitness/_availability.py
Normal file
31
src/fieldwitness/_availability.py
Normal 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
36
src/fieldwitness/api.py
Normal 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"}
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
]
|
||||
@ -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():
|
||||
@ -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
|
||||
@ -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:
|
||||
@ -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)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Cryptographic primitives for Verisoo.
|
||||
Cryptographic primitives for Attest.
|
||||
|
||||
Ed25519 for signatures (fast, small keys, deterministic).
|
||||
SHA-256 for content hashing.
|
||||
@ -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(">", ">")
|
||||
.replace('"', """))
|
||||
|
||||
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"
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
Loading…
Reference in New Issue
Block a user