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>
124 lines
4.5 KiB
Python
124 lines
4.5 KiB
Python
"""Tests for CBOR serialization of chain records."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
|
|
from fieldwitness.federation.models import AttestationChainRecord, ChainState, EntropyWitnesses
|
|
from fieldwitness.federation.serialization import (
|
|
canonical_bytes,
|
|
compute_record_hash,
|
|
deserialize_record,
|
|
serialize_record,
|
|
)
|
|
|
|
|
|
def _make_record(**overrides) -> AttestationChainRecord:
|
|
"""Create a minimal test record with sensible defaults."""
|
|
defaults = {
|
|
"version": 1,
|
|
"record_id": b"\x01" * 16,
|
|
"chain_index": 0,
|
|
"prev_hash": ChainState.GENESIS_PREV_HASH,
|
|
"content_hash": hashlib.sha256(b"test content").digest(),
|
|
"content_type": "test/plain",
|
|
"metadata": {},
|
|
"claimed_ts": 1_700_000_000_000_000,
|
|
"entropy_witnesses": EntropyWitnesses(
|
|
sys_uptime=12345.678,
|
|
fs_snapshot=b"\xab" * 16,
|
|
proc_entropy=256,
|
|
boot_id="test-boot-id",
|
|
),
|
|
"signer_pubkey": b"\x02" * 32,
|
|
"signature": b"\x03" * 64,
|
|
}
|
|
defaults.update(overrides)
|
|
return AttestationChainRecord(**defaults)
|
|
|
|
|
|
def test_canonical_bytes_deterministic():
|
|
"""Same record always produces the same canonical bytes."""
|
|
record = _make_record()
|
|
b1 = canonical_bytes(record)
|
|
b2 = canonical_bytes(record)
|
|
assert b1 == b2
|
|
|
|
|
|
def test_canonical_bytes_excludes_signature():
|
|
"""Canonical bytes must not include the signature field."""
|
|
record_a = _make_record(signature=b"\x03" * 64)
|
|
record_b = _make_record(signature=b"\x04" * 64)
|
|
assert canonical_bytes(record_a) == canonical_bytes(record_b)
|
|
|
|
|
|
def test_canonical_bytes_sensitive_to_content():
|
|
"""Different content_hash must produce different canonical bytes."""
|
|
record_a = _make_record(content_hash=hashlib.sha256(b"a").digest())
|
|
record_b = _make_record(content_hash=hashlib.sha256(b"b").digest())
|
|
assert canonical_bytes(record_a) != canonical_bytes(record_b)
|
|
|
|
|
|
def test_serialize_deserialize_round_trip():
|
|
"""A record survives serialization and deserialization intact."""
|
|
original = _make_record()
|
|
data = serialize_record(original)
|
|
restored = deserialize_record(data)
|
|
|
|
assert restored.version == original.version
|
|
assert restored.record_id == original.record_id
|
|
assert restored.chain_index == original.chain_index
|
|
assert restored.prev_hash == original.prev_hash
|
|
assert restored.content_hash == original.content_hash
|
|
assert restored.content_type == original.content_type
|
|
assert restored.metadata == original.metadata
|
|
assert restored.claimed_ts == original.claimed_ts
|
|
assert restored.signer_pubkey == original.signer_pubkey
|
|
assert restored.signature == original.signature
|
|
|
|
# Entropy witnesses
|
|
assert restored.entropy_witnesses is not None
|
|
assert restored.entropy_witnesses.sys_uptime == original.entropy_witnesses.sys_uptime
|
|
assert restored.entropy_witnesses.fs_snapshot == original.entropy_witnesses.fs_snapshot
|
|
assert restored.entropy_witnesses.proc_entropy == original.entropy_witnesses.proc_entropy
|
|
assert restored.entropy_witnesses.boot_id == original.entropy_witnesses.boot_id
|
|
|
|
|
|
def test_serialize_includes_signature():
|
|
"""Full serialization must include the signature."""
|
|
record = _make_record(signature=b"\xaa" * 64)
|
|
data = serialize_record(record)
|
|
restored = deserialize_record(data)
|
|
assert restored.signature == b"\xaa" * 64
|
|
|
|
|
|
def test_compute_record_hash():
|
|
"""Record hash is SHA-256 of canonical bytes."""
|
|
record = _make_record()
|
|
expected = hashlib.sha256(canonical_bytes(record)).digest()
|
|
assert compute_record_hash(record) == expected
|
|
|
|
|
|
def test_record_hash_changes_with_content():
|
|
"""Different records produce different hashes."""
|
|
a = _make_record(content_hash=hashlib.sha256(b"a").digest())
|
|
b = _make_record(content_hash=hashlib.sha256(b"b").digest())
|
|
assert compute_record_hash(a) != compute_record_hash(b)
|
|
|
|
|
|
def test_metadata_preserved():
|
|
"""Arbitrary metadata survives round-trip."""
|
|
meta = {"backfilled": True, "caption": "test photo", "tags": ["evidence", "urgent"]}
|
|
record = _make_record(metadata=meta)
|
|
data = serialize_record(record)
|
|
restored = deserialize_record(data)
|
|
assert restored.metadata == meta
|
|
|
|
|
|
def test_empty_entropy_witnesses():
|
|
"""Record with no entropy witnesses round-trips correctly."""
|
|
record = _make_record(entropy_witnesses=None)
|
|
data = serialize_record(record)
|
|
restored = deserialize_record(data)
|
|
assert restored.entropy_witnesses is None
|