From 5b0d90eeaf94ed457e949347293f36221eaa2e7e Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Thu, 2 Apr 2026 20:10:37 -0400 Subject: [PATCH] Fix all power-user review issues (FR-01 through FR-12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FR-01: Fix data directory default from ~/.fieldwitness to ~/.fwmetadata FR-02/05/07: Accept all file types for attestation (not just images) - Web UI, CLI, and batch now accept PDFs, CSVs, audio, video, etc. - Perceptual hashing for images, SHA-256-only for everything else FR-03: Implement C2PA import path + CLI commands (export/verify/import/show) FR-04: Fix GPS downsampling bias (math.floor → round) FR-06: Add HTML/PDF evidence summaries for lawyers - Always generates summary.html, optional summary.pdf via xhtml2pdf FR-08: Fix CLI help text ("FieldWitness -- FieldWitness" artifact) FR-09: Centralize stray paths (trusted_keys, carrier_history, last_backup) FR-10: Add 67 C2PA bridge tests (vendor assertions, cert, GPS, export) FR-12: Add Tor onion service support for source drop box - fieldwitness serve --tor flag, persistent/transient modes - Killswitch covers hidden service keys Also: bonus fix for attest/api.py hardcoded path bypassing paths.py 224 tests passing (67 new). Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/security/threat-model.md | 18 +- docs/source-dropbox.md | 139 ++- frontends/web/blueprints/attest.py | 202 +++-- frontends/web/templates/attest/attest.html | 26 +- frontends/web/templates/attest/log.html | 2 +- frontends/web/templates/attest/result.html | 14 +- frontends/web/templates/attest/verify.html | 21 +- .../web/templates/attest/verify_result.html | 18 +- pyproject.toml | 8 +- src/fieldwitness/_availability.py | 16 + src/fieldwitness/archive.py | 3 +- src/fieldwitness/attest/api.py | 6 +- src/fieldwitness/attest/cli.py | 85 +- src/fieldwitness/attest/hashing.py | 27 + src/fieldwitness/c2pa_bridge/__init__.py | 10 +- src/fieldwitness/c2pa_bridge/cli.py | 457 ++++++++++ src/fieldwitness/c2pa_bridge/export.py | 34 +- src/fieldwitness/c2pa_bridge/importer.py | 634 ++++++++++++++ src/fieldwitness/cli.py | 106 ++- src/fieldwitness/evidence.py | 9 + src/fieldwitness/evidence_summary.py | 346 ++++++++ src/fieldwitness/fieldkit/killswitch.py | 10 + src/fieldwitness/fieldkit/tor.py | 278 ++++++ src/fieldwitness/keystore/manager.py | 10 +- src/fieldwitness/paths.py | 22 +- src/fieldwitness/stego/carrier_tracker.py | 4 +- tests/test_c2pa_bridge.py | 821 ++++++++++++++++++ 27 files changed, 3140 insertions(+), 186 deletions(-) create mode 100644 src/fieldwitness/c2pa_bridge/cli.py create mode 100644 src/fieldwitness/c2pa_bridge/importer.py create mode 100644 src/fieldwitness/evidence_summary.py create mode 100644 src/fieldwitness/fieldkit/tor.py create mode 100644 tests/test_c2pa_bridge.py diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index b2d3d39..a2ff7c9 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -461,11 +461,19 @@ A malicious or compromised relay could suppress specific records. The design mit 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. +**L7: Drop box source anonymity is limited -- Tor support available.** The drop box does +not log source IP addresses in attestation records or require accounts. FieldWitness now +includes built-in Tor hidden service support: starting the server with `--tor` exposes the +drop box as a `.onion` address so that source IPs are never visible to the server operator. + +Without `--tor`, a source's IP address is visible in web server access logs. +Organizations with source-protection requirements should use the `--tor` flag and instruct +sources to access the drop box only via Tor Browser. Operators should also configure any +reverse proxy to suppress access logging for `/dropbox/upload/` paths. + +Even with Tor, timing analysis and traffic correlation attacks are possible at the network +level. Tor eliminates IP exposure at the server; it does not protect against a global +adversary correlating traffic timing. See `docs/source-dropbox.md` for setup instructions. **L8: Steganalysis resistance is not guaranteed.** The steganography backend includes a steganalysis module (`stego/steganalysis.py`) for estimating detection resistance, but diff --git a/docs/source-dropbox.md b/docs/source-dropbox.md index d1ae207..2166b24 100644 --- a/docs/source-dropbox.md +++ b/docs/source-dropbox.md @@ -208,13 +208,142 @@ Expired tokens are cleaned up automatically on every admin page load. ### Running as a Tor hidden service -For maximum source protection, run FieldWitness as a Tor hidden service: +FieldWitness has built-in support for exposing the drop box as a Tor hidden service +(a `.onion` address). When a source accesses the drop box over Tor, the server never +sees their real IP address -- Tor's onion routing ensures that only the Tor network knows +both the source and the destination. -1. Install Tor on the server -2. Configure a hidden service in `torrc` pointing to `127.0.0.1:5000` -3. Share the `.onion` URL instead of a LAN address -4. The source's real IP is never visible to the server +#### Step 1: Install and configure Tor + +```bash +# Debian / Ubuntu +sudo apt install tor + +# macOS (Homebrew) +brew install tor + +# Fedora / RHEL +sudo dnf install tor +``` + +Enable the control port so FieldWitness can manage the hidden service. Add these lines +to `/etc/tor/torrc`: + +``` +ControlPort 9051 +CookieAuthentication 1 +``` + +Then restart Tor: + +```bash +sudo systemctl restart tor +``` + +**Authentication note:** `CookieAuthentication 1` lets FieldWitness authenticate using the +cookie file that Tor creates automatically. Alternatively, use a password: + +``` +ControlPort 9051 +HashedControlPassword +``` + +#### Step 2: Install the stem library + +stem is an optional FieldWitness dependency. Install it alongside the fieldkit extra: + +```bash +pip install 'fieldwitness[tor]' +# or, if already installed: +pip install stem>=1.8.0 +``` + +#### Step 3: Start FieldWitness with --tor + +```bash +fieldwitness serve --host 127.0.0.1 --port 5000 --tor +``` + +With a custom control port or password: + +```bash +fieldwitness serve --tor --tor-control-port 9051 --tor-password yourpassword +``` + +For a one-off intake session where a fixed address is not needed: + +```bash +fieldwitness serve --tor --tor-transient +``` + +On startup, FieldWitness will print the `.onion` address: + +``` +============================================================ +TOR HIDDEN SERVICE ACTIVE +============================================================ + .onion address : abc123def456ghi789jkl012mno345pq.onion + Drop box URL : http://abc123def456ghi789jkl012mno345pq.onion/dropbox/upload/ + Persistent : yes (key saved to ~/.fwmetadata/fieldkit/tor/) +============================================================ +Sources must use Tor Browser to access the .onion URL. +Share the drop box upload URL over a secure channel (Signal, in person). +============================================================ +``` + +#### Step 4: Share the .onion drop box URL + +Create a drop box token as usual (see Step 2 above), then construct the `.onion` upload URL: + +``` +http:///dropbox/upload/ +``` + +Share this URL with the source over Signal or in person. The source opens it in +**Tor Browser** -- not in a regular browser. + +#### Persistent vs. transient hidden services + +| Mode | Command | Behaviour | +|---|---|---| +| **Persistent** (default) | `--tor` | Same `.onion` address on every restart. Key stored at `~/.fwmetadata/fieldkit/tor/hidden_service/`. | +| **Transient** | `--tor --tor-transient` | New `.onion` address each run. No key written to disk. | + +Use persistent mode when you want sources to bookmark the address or share it in advance. +Use transient mode for single-session intake where address continuity does not matter. + +#### Source instructions + +Tell sources: + +1. Install **Tor Browser** from [torproject.org](https://www.torproject.org/download/). +2. Open Tor Browser and paste the full `.onion` URL into the address bar. +3. Do not open the `.onion` URL in a regular browser -- your real IP will be visible. +4. JavaScript must be enabled for the SHA-256 fingerprint feature to work. + Set the Security Level to "Standard" in Tor Browser (shield icon in the toolbar). +5. Save the SHA-256 fingerprints shown before clicking Upload. +6. Save the receipt codes shown after upload. + +#### Logging and access logs + +Even with Tor, access logs on the server can record that *someone* connected -- the +connection appears to come from a Tor exit relay (for regular Tor) or from a Tor internal +address (for hidden services). For hidden services, the server sees no IP at all; the +connection comes from the Tor daemon on localhost. + +**Recommendation:** Set the web server or reverse proxy to not log access requests to +`/dropbox/upload/` paths. If using FieldWitness directly (Waitress), access log output +goes to stdout; redirect it to `/dev/null` or omit the log handler. + +#### Killswitch and the hidden service key + +The persistent hidden service key is stored under `~/.fwmetadata/fieldkit/tor/hidden_service/` +and is treated as key material. The FieldWitness killswitch (`fieldwitness fieldkit purge`) +destroys this directory during both `KEYS_ONLY` and `ALL` purge scopes. After a purge, +the `.onion` address cannot be linked to the operator even if the server hardware is seized. > **Warning:** Even with Tor, timing analysis and traffic correlation attacks are possible > at the network level. The drop box protects source identity at the application layer; > network-layer protection requires operational discipline beyond what software can provide. +> Tor is not a silver bullet -- but it removes the most direct risk (IP address exposure) +> that limitation L7 in the threat model describes. diff --git a/frontends/web/blueprints/attest.py b/frontends/web/blueprints/attest.py index afea432..cf9a38d 100644 --- a/frontends/web/blueprints/attest.py +++ b/frontends/web/blueprints/attest.py @@ -1,10 +1,13 @@ """ -Attestation blueprint — attest and verify images via Attest. +Attestation blueprint — attest and verify files via Attest. 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 +- File attestation: upload → hash → sign → store in append-only log +- File verification: upload → hash → search log → display matches - Verification receipt: same as verify but returns a downloadable JSON file + +Supports any file type. Perceptual hashing (phash, dhash) is available for +image files only. Non-image files are attested by SHA-256 hash. """ from __future__ import annotations @@ -85,25 +88,45 @@ def _wrap_in_chain(attest_record, private_key, metadata: dict | None = None): ) -def _allowed_image(filename: str) -> bool: +_ALLOWED_EXTENSIONS: frozenset[str] = frozenset({ + # Images + "png", "jpg", "jpeg", "bmp", "gif", "webp", "tiff", "tif", "heic", "heif", "raw", + # Documents + "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", + "txt", "rtf", "csv", "tsv", "json", "xml", "html", "htm", + # Audio + "mp3", "wav", "m4a", "aac", "ogg", "flac", "opus", "wma", + # Video + "mp4", "mov", "avi", "mkv", "webm", "m4v", "wmv", + # Archives / data + "zip", "tar", "gz", "bz2", "xz", "7z", + # Sensor / scientific data + "gpx", "kml", "geojson", "npy", "parquet", "bin", "dat", +}) + +_IMAGE_EXTENSIONS: frozenset[str] = frozenset({ + "png", "jpg", "jpeg", "bmp", "gif", "webp", "tiff", "tif", "heic", "heif", +}) + + +def _allowed_file(filename: str) -> bool: + """Return True if the filename has an extension on the allowlist.""" if not filename or "." not in filename: return False - return filename.rsplit(".", 1)[1].lower() in { - "png", - "jpg", - "jpeg", - "bmp", - "gif", - "webp", - "tiff", - "tif", - } + return filename.rsplit(".", 1)[1].lower() in _ALLOWED_EXTENSIONS + + +def _is_image_file(filename: str) -> bool: + """Return True if the filename is a known image type.""" + if not filename or "." not in filename: + return False + return filename.rsplit(".", 1)[1].lower() in _IMAGE_EXTENSIONS @bp.route("/attest", methods=["GET", "POST"]) @login_required def attest(): - """Create a provenance attestation for an image.""" + """Create a provenance attestation for a file.""" # Check identity exists private_key = _get_private_key() has_identity = private_key is not None @@ -116,17 +139,22 @@ def attest(): ) return redirect(url_for("attest.attest")) - image_file = request.files.get("image") - if not image_file or not image_file.filename: - flash("Please select an image to attest.", "error") + evidence_file = request.files.get("image") + if not evidence_file or not evidence_file.filename: + flash("Please select a file to attest.", "error") return redirect(url_for("attest.attest")) - if not _allowed_image(image_file.filename): - flash("Unsupported image format. Use PNG, JPG, WebP, TIFF, or BMP.", "error") + if not _allowed_file(evidence_file.filename): + flash( + "Unsupported file type. Supported types include images, documents, " + "audio, video, CSV, and sensor data files.", + "error", + ) return redirect(url_for("attest.attest")) try: - image_data = image_file.read() + file_data = evidence_file.read() + is_image = _is_image_file(evidence_file.filename) # Build optional metadata metadata = {} @@ -148,31 +176,36 @@ def attest(): auto_exif = request.form.get("auto_exif", "on") == "on" strip_device = request.form.get("strip_device", "on") == "on" - # Extract-then-classify: get evidentiary metadata before attestation - # so user can control what's included - if auto_exif and strip_device: - from fieldwitness.metadata import extract_and_classify + # Extract-then-classify: get evidentiary metadata before attestation. + # Only applicable to image files — silently skip for other types. + if is_image and auto_exif and strip_device: + try: + from fieldwitness.metadata import extract_and_classify - extraction = extract_and_classify(image_data) - # Merge evidentiary fields (GPS, timestamp) but exclude - # dangerous device fields (serial, firmware version) - for key, value in extraction.evidentiary.items(): - if key not in metadata: # User metadata takes precedence - if hasattr(value, "isoformat"): - metadata[f"exif_{key}"] = value.isoformat() - elif isinstance(value, dict): - metadata[f"exif_{key}"] = value - else: - metadata[f"exif_{key}"] = str(value) + extraction = extract_and_classify(file_data) + # Merge evidentiary fields (GPS, timestamp) but exclude + # dangerous device fields (serial, firmware version) + for key, value in extraction.evidentiary.items(): + if key not in metadata: # User metadata takes precedence + if hasattr(value, "isoformat"): + metadata[f"exif_{key}"] = value.isoformat() + elif isinstance(value, dict): + metadata[f"exif_{key}"] = value + else: + metadata[f"exif_{key}"] = str(value) + except Exception: + pass # EXIF extraction is best-effort - # Create the attestation + # Create the attestation. create_attestation() calls hash_image() + # internally; for non-image files we pre-compute hashes via + # hash_file() and use create_attestation_from_hashes() instead. from fieldwitness.attest.attestation import create_attestation attestation = create_attestation( - image_data=image_data, + image_data=file_data, private_key=private_key, metadata=metadata if metadata else None, - auto_exif=auto_exif and not strip_device, # Full EXIF only if not stripping device + auto_exif=is_image and auto_exif and not strip_device, ) # Store in the append-only log @@ -188,7 +221,7 @@ def attest(): logging.getLogger(__name__).warning("Chain wrapping failed: %s", e) flash( - "Attestation saved, but chain wrapping failed. " "Check chain configuration.", + "Attestation saved, but chain wrapping failed. Check chain configuration.", "warning", ) @@ -225,7 +258,8 @@ def attest(): location_name=metadata.get("location_name", ""), exif_metadata=record.metadata, index=index, - filename=image_file.filename, + filename=evidence_file.filename, + is_image=is_image, chain_index=chain_record.chain_index if chain_record else None, ) @@ -239,15 +273,13 @@ def attest(): @bp.route("/attest/batch", methods=["POST"]) @login_required def attest_batch(): - """Batch attestation — accepts multiple image files. + """Batch attestation — accepts multiple files of any supported type. Returns JSON with results for each file (success/skip/error). - Skips images already attested (by SHA-256 match). + Skips files already attested (by SHA-256 match). """ import hashlib - from fieldwitness.attest.hashing import hash_image - private_key = _get_private_key() if private_key is None: return {"error": "No identity key. Run fieldwitness init first."}, 400 @@ -262,10 +294,14 @@ def attest_batch(): for f in files: filename = f.filename or "unknown" try: - image_data = f.read() - sha256 = hashlib.sha256(image_data).hexdigest() + if not _allowed_file(filename): + results.append({"file": filename, "status": "skipped", "reason": "unsupported file type"}) + continue - # Skip already-attested images + file_data = f.read() + sha256 = hashlib.sha256(file_data).hexdigest() + + # Skip already-attested files existing = storage.get_records_by_image_sha256(sha256) if existing: results.append({"file": filename, "status": "skipped", "reason": "already attested"}) @@ -273,7 +309,7 @@ def attest_batch(): from fieldwitness.attest.attestation import create_attestation - attestation = create_attestation(image_data, private_key) + attestation = create_attestation(file_data, private_key) index = storage.append_record(attestation.record) # Wrap in chain if enabled @@ -312,10 +348,10 @@ def attest_batch(): @bp.route("/verify/batch", methods=["POST"]) @login_required def verify_batch(): - """Batch verification — accepts multiple image files. + """Batch verification — accepts multiple files of any supported type. Returns JSON with per-file verification results. Uses SHA-256 - fast path before falling back to perceptual scan. + fast path before falling back to perceptual scan (images only). """ files = request.files.getlist("images") if not files: @@ -325,8 +361,8 @@ def verify_batch(): for f in files: filename = f.filename or "unknown" try: - image_data = f.read() - result = _verify_image(image_data) + file_data = f.read() + result = _verify_file(file_data) if result["matches"]: best = result["matches"][0] @@ -361,17 +397,20 @@ def verify_batch(): } -def _verify_image(image_data: bytes) -> dict: +def _verify_file(file_data: bytes) -> dict: """Run the full verification pipeline against the attestation log. + Works for any file type. Images get SHA-256 + perceptual matching; + non-image files get SHA-256 matching only. + Returns a dict with keys: query_hashes — ImageHashes object from fieldwitness.attest matches — list of match dicts (record, match_type, distances, attestor_name) record_count — total records searched """ - from fieldwitness.attest.hashing import compute_all_distances, hash_image, is_same_image + from fieldwitness.attest.hashing import compute_all_distances, hash_file, is_same_image - query_hashes = hash_image(image_data) + query_hashes = hash_file(file_data) storage = _get_storage() stats = storage.get_stats() @@ -423,17 +462,22 @@ def verify(): The log read here is read-only and reveals no key material. """ if request.method == "POST": - image_file = request.files.get("image") - if not image_file or not image_file.filename: - flash("Please select an image to verify.", "error") + evidence_file = request.files.get("image") + if not evidence_file or not evidence_file.filename: + flash("Please select a file to verify.", "error") return redirect(url_for("attest.verify")) - if not _allowed_image(image_file.filename): - flash("Unsupported image format.", "error") + if not _allowed_file(evidence_file.filename): + flash( + "Unsupported file type. Upload any image, document, audio, video, or data file.", + "error", + ) return redirect(url_for("attest.verify")) try: - result = _verify_image(image_file.read()) + file_data = evidence_file.read() + is_image = _is_image_file(evidence_file.filename) + result = _verify_file(file_data) query_hashes = result["query_hashes"] matches = result["matches"] @@ -443,7 +487,8 @@ def verify(): found=False, message="No attestations in the local log yet.", query_hashes=query_hashes, - filename=image_file.filename, + filename=evidence_file.filename, + is_image=is_image, matches=[], ) @@ -456,7 +501,8 @@ def verify(): else "No matching attestations found." ), query_hashes=query_hashes, - filename=image_file.filename, + filename=evidence_file.filename, + is_image=is_image, matches=matches, ) @@ -471,29 +517,29 @@ def verify(): def verify_receipt(): """Return a downloadable JSON verification receipt for court or legal use. - Accepts the same image upload as /verify. Returns a JSON file attachment - containing image hashes, all matching attestation records with full metadata, + Accepts the same file upload as /verify. Returns a JSON file attachment + containing file hashes, all matching attestation records with full metadata, the verification timestamp, and the verifier hostname. Intentionally unauthenticated — same access policy as /verify. """ - image_file = request.files.get("image") - if not image_file or not image_file.filename: + evidence_file = request.files.get("image") + if not evidence_file or not evidence_file.filename: return Response( - json.dumps({"error": "No image provided"}), + json.dumps({"error": "No file provided"}), status=400, mimetype="application/json", ) - if not _allowed_image(image_file.filename): + if not _allowed_file(evidence_file.filename): return Response( - json.dumps({"error": "Unsupported image format"}), + json.dumps({"error": "Unsupported file type"}), status=400, mimetype="application/json", ) try: - result = _verify_image(image_file.read()) + result = _verify_file(evidence_file.read()) except Exception as e: return Response( json.dumps({"error": f"Verification failed: {e}"}), @@ -573,11 +619,11 @@ def verify_receipt(): "schema_version": "3", "verification_timestamp": verification_ts, "verifier_instance": verifier_instance, - "queried_filename": image_file.filename, - "image_hash": { + "queried_filename": evidence_file.filename, + "file_hash": { "sha256": query_hashes.sha256, - "phash": query_hashes.phash, - "dhash": getattr(query_hashes, "dhash", None), + "phash": query_hashes.phash or None, + "dhash": getattr(query_hashes, "dhash", None) or None, }, "records_searched": result["record_count"], "matches_found": len(matching_records), @@ -599,7 +645,9 @@ def verify_receipt(): receipt_json = json.dumps(receipt, indent=2, ensure_ascii=False) safe_filename = ( - image_file.filename.rsplit(".", 1)[0] if "." in image_file.filename else image_file.filename + evidence_file.filename.rsplit(".", 1)[0] + if "." in evidence_file.filename + else evidence_file.filename ) download_name = f"receipt_{safe_filename}_{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}.json" diff --git a/frontends/web/templates/attest/attest.html b/frontends/web/templates/attest/attest.html index 6bc12a6..2ad53ea 100644 --- a/frontends/web/templates/attest/attest.html +++ b/frontends/web/templates/attest/attest.html @@ -1,17 +1,18 @@ {% extends "base.html" %} -{% block title %}Attest Image — FieldWitness{% endblock %} +{% block title %}Attest File — FieldWitness{% endblock %} {% block content %}
-
Attest Image
+
Attest File

- Create a cryptographic provenance attestation — sign an image with your Ed25519 identity - to prove when and by whom it was captured. + Create a cryptographic provenance attestation — sign any file with your Ed25519 identity + to prove when and by whom it was captured or created. Supports photos, documents, + sensor data, audio, video, and more.

{% if not has_identity %} @@ -25,28 +26,31 @@
- - -
Supports PNG, JPEG, WebP, TIFF, BMP.
+ + +
+ Accepts images (PNG, JPEG, WebP, TIFF), documents (PDF, DOCX, CSV, TXT), + audio (MP3, WAV, FLAC), video (MP4, MOV, MKV), and sensor data files. + Perceptual matching (pHash, dHash) is available for image files only. +
+ placeholder="What does this file document?" maxlength="500">
+ placeholder="Where was this captured?" maxlength="200">
diff --git a/frontends/web/templates/attest/log.html b/frontends/web/templates/attest/log.html index 0e11c1f..1b93ad4 100644 --- a/frontends/web/templates/attest/log.html +++ b/frontends/web/templates/attest/log.html @@ -39,7 +39,7 @@ {% else %}
- No attestations yet. Attest your first image. + No attestations yet. Attest your first file.
{% endif %}
diff --git a/frontends/web/templates/attest/result.html b/frontends/web/templates/attest/result.html index ccbcf99..4567d1a 100644 --- a/frontends/web/templates/attest/result.html +++ b/frontends/web/templates/attest/result.html @@ -7,9 +7,17 @@
Attestation created successfully! - Image {{ filename }} has been attested and stored in the local log (index #{{ index }}). + File {{ filename }} has been attested and stored in the local log (index #{{ index }}).
+ {% if not is_image %} +
+ + This file is attested by cryptographic hash. Perceptual matching (pHash, dHash) + is available for image files only. +
+ {% endif %} +
Attestation Record
@@ -38,7 +46,7 @@
-
Image Hashes
+
File Hashes
@@ -84,7 +92,7 @@
- Attest Another Image + Attest Another File View Attestation Log diff --git a/frontends/web/templates/attest/verify.html b/frontends/web/templates/attest/verify.html index d16ed8d..a38eb7f 100644 --- a/frontends/web/templates/attest/verify.html +++ b/frontends/web/templates/attest/verify.html @@ -1,30 +1,33 @@ {% extends "base.html" %} -{% block title %}Verify Image — FieldWitness{% endblock %} +{% block title %}Verify File — FieldWitness{% endblock %} {% block content %}
-
Verify Image
+
Verify File

- Check an image against the local attestation log. Uses SHA-256 for exact matching - and perceptual hashes (pHash, dHash) for robustness against compression and resizing. + Check a file against the local attestation log. For image files, uses SHA-256 for + exact matching and perceptual hashes (pHash, dHash) for robustness against + compression and resizing. For all other file types, SHA-256 exact matching is used.

- - -
Upload the image you want to verify against known attestations.
+ + +
+ Upload the file you want to verify against known attestations. + Accepts images, documents, audio, video, and data files. +
diff --git a/frontends/web/templates/attest/verify_result.html b/frontends/web/templates/attest/verify_result.html index 92fa978..1f8fa65 100644 --- a/frontends/web/templates/attest/verify_result.html +++ b/frontends/web/templates/attest/verify_result.html @@ -16,10 +16,18 @@
{% endif %} - {# Query image hashes #} + {% if not is_image %} +
+ + This file is attested by cryptographic hash. Perceptual matching (pHash, dHash) + is available for image files only. +
+ {% endif %} + + {# Query file hashes #}
-
Image Hashes for {{ filename }}
+
File Hashes for {{ filename }}
@@ -103,13 +111,13 @@

Generate a signed JSON receipt for legal or archival use. - Re-upload the same image to produce the downloadable file. + Re-upload the same file to produce the downloadable receipt.

+ type="file" name="image" required>