Implement 7 field-scenario feature requests
1. Transport-aware stego encoding: --transport flag (whatsapp/signal/ telegram/discord/email/direct) auto-selects DCT mode, pre-resizes carrier to platform max dimension, prevents payload destruction by messaging app recompression. 2. Standalone verification bundle: chain export ZIP now includes verify_chain.py (zero-dep verification script) and README.txt with instructions for courts and fact-checkers. 3. Channel-key-only export/import: export_channel_key() and import_channel_key() with Argon2id encryption (64MB, lighter than full bundle). channel_key_to_qr_data() for in-person QR code exchange between collaborators. 4. Duress/cover mode: configurable SSL cert CN via cover_name config (defaults to "localhost" instead of "SooSeF Local"). SOOSEF_DATA_DIR already supports directory renaming. Killswitch PurgeScope.ALL now self-uninstalls the pip package. 5. Identity recovery from chain: find_signer_pubkey() searches chain by fingerprint prefix. append_key_recovery() creates a recovery record signed by new key with old fingerprint + cosigner list. verify_chain() accepts recovery records. 6. Batch verification: /verify/batch web endpoint accepts multiple files, returns per-file status (verified/unverified/error) with exact vs perceptual match breakdown. 7. Chain position proof in receipt: verification receipts (now schema v3) include chain_proof with chain_id, chain_index, prev_hash, and record_hash for court admissibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -283,6 +283,58 @@ def attest_batch():
|
||||
}
|
||||
|
||||
|
||||
@bp.route("/verify/batch", methods=["POST"])
|
||||
@login_required
|
||||
def verify_batch():
|
||||
"""Batch verification — accepts multiple image files.
|
||||
|
||||
Returns JSON with per-file verification results. Uses SHA-256
|
||||
fast path before falling back to perceptual scan.
|
||||
"""
|
||||
files = request.files.getlist("images")
|
||||
if not files:
|
||||
return {"error": "No files uploaded"}, 400
|
||||
|
||||
results = []
|
||||
for f in files:
|
||||
filename = f.filename or "unknown"
|
||||
try:
|
||||
image_data = f.read()
|
||||
result = _verify_image(image_data)
|
||||
|
||||
if result["matches"]:
|
||||
best = result["matches"][0]
|
||||
results.append({
|
||||
"file": filename,
|
||||
"status": "verified",
|
||||
"match_type": best["match_type"],
|
||||
"record_id": best["record"].short_id if hasattr(best["record"], "short_id") else "unknown",
|
||||
"matches": len(result["matches"]),
|
||||
})
|
||||
else:
|
||||
results.append({"file": filename, "status": "unverified", "matches": 0})
|
||||
except Exception as e:
|
||||
results.append({"file": filename, "status": "error", "error": str(e)})
|
||||
|
||||
verified = sum(1 for r in results if r["status"] == "verified")
|
||||
unverified = sum(1 for r in results if r["status"] == "unverified")
|
||||
errors = sum(1 for r in results if r["status"] == "error")
|
||||
|
||||
# Count by match type
|
||||
exact = sum(1 for r in results if r.get("match_type") == "exact")
|
||||
perceptual = verified - exact
|
||||
|
||||
return {
|
||||
"total": len(results),
|
||||
"verified": verified,
|
||||
"verified_exact": exact,
|
||||
"verified_perceptual": perceptual,
|
||||
"unverified": unverified,
|
||||
"errors": errors,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
def _verify_image(image_data: bytes) -> dict:
|
||||
"""Run the full verification pipeline against the attestation log.
|
||||
|
||||
@@ -460,10 +512,39 @@ def verify_receipt():
|
||||
}
|
||||
if safe_meta:
|
||||
rec_entry["metadata"] = safe_meta
|
||||
|
||||
# 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
|
||||
|
||||
chain_config = SoosefConfig.load()
|
||||
if chain_config.chain_enabled:
|
||||
chain_store = ChainStore(CHAIN_DIR)
|
||||
# Search chain for a record whose content_hash matches this attestation
|
||||
content_hash_hex = getattr(record, "image_hashes", None)
|
||||
if content_hash_hex and hasattr(content_hash_hex, "sha256"):
|
||||
target_sha = content_hash_hex.sha256
|
||||
for chain_rec in chain_store:
|
||||
if chain_rec.content_hash.hex() == target_sha or chain_rec.metadata.get("attestor") == record.attestor_fingerprint:
|
||||
rec_entry["chain_proof"] = {
|
||||
"chain_id": chain_store.state().chain_id.hex() if chain_store.state() else None,
|
||||
"chain_index": chain_rec.chain_index,
|
||||
"prev_hash": chain_rec.prev_hash.hex(),
|
||||
"record_hash": compute_record_hash(chain_rec).hex(),
|
||||
"content_type": chain_rec.content_type,
|
||||
"claimed_ts": chain_rec.claimed_ts,
|
||||
}
|
||||
break
|
||||
except Exception:
|
||||
pass # Chain proof is optional — don't fail the receipt
|
||||
|
||||
matching_records.append(rec_entry)
|
||||
|
||||
receipt = {
|
||||
"schema_version": "2",
|
||||
"schema_version": "3",
|
||||
"verification_timestamp": verification_ts,
|
||||
"verifier_instance": verifier_instance,
|
||||
"queried_filename": image_file.filename,
|
||||
|
||||
Reference in New Issue
Block a user