fieldwitness/tests/test_serialization.py
Aaron D. Lee 51c9b0a99a Fix 14 bugs and add features from power-user security audit
Critical fixes:
- Fix admin_delete_user missing current_user_id argument (TypeError on every delete)
- Fix self-signed cert OOM: bytes(2130706433) → IPv4Address("127.0.0.1")
- Add @login_required to attestation routes (attest, log); verify stays public
- Add auth guards to fieldkit (@admin_required on killswitch) and keys blueprints
- Fix cleanup_temp_files NameError in generate() route

Security hardening:
- Unify temp storage to ~/.soosef/temp/ so killswitch purge covers web uploads
- Replace Path.unlink() with secure deletion (shred fallback) in temp_storage
- Add structured audit log (audit.jsonl) for admin, key, and killswitch actions

New features:
- Dead man's switch background enforcement thread in serve + check-deadman CLI
- Key rotation: soosef keys rotate-identity/rotate-channel with archiving
- Batch attestation: soosef attest batch <dir> with progress and error handling
- Geofence CLI: set/check/clear commands with config persistence
- USB CLI: snapshot/check commands against device whitelist
- Verification receipt download (/verify/receipt JSON endpoint + UI button)
- IdentityInfo.created_at populated from sidecar meta.json (mtime fallback)

Data layer:
- ChainStore.get() now O(1) via byte-offset index built during state rebuild
- Add federation module (chain, models, serialization, entropy)

Includes 45+ new tests across chain, deadman, key rotation, killswitch, and
serialization modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:06:33 -04:00

124 lines
4.5 KiB
Python

"""Tests for CBOR serialization of chain records."""
from __future__ import annotations
import hashlib
from soosef.federation.models import AttestationChainRecord, ChainState, EntropyWitnesses
from soosef.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