Provenance

Video attestation

Video can't be hashed as one blob during live capture, and MP4/WebM containers have no clean XMP-equivalent for embedding. So video attestation makes two fixed deviations from the image flow.

Chunk hash chain

The recording is segmented into fixed-duration chunks (target 2s). Each chunk hashes into a chain:

chunk_hash_i = SHA256( prev_chunk_hash || SHA256(chunk_bytes) )

The chain seed (prev_chunk_hash for chunk 0) is:

This makes the chain tamper-evident: altering, dropping, or reordering any chunk breaks every subsequent hash (frame_hash_mismatch, +50). sensor_hash for video is the final chain hash, so all downstream logic works unchanged.

Sidecar manifest

The bundle is stored and transmitted alongside the video, keyed by attestation_id and the final chain hash:

"video_manifest": {
  "chunk_duration_ms": 2000,
  "duration_seconds": 14.2,
  "codec": "video/webm;codecs=vp9",
  "chunks": [ { "index": 0, "byte_length": 183211, "chunk_hash": "…", "captured_at": "…" } ]
}

Container-embedded manifests (MP4 boxes / C2PA video) are on the roadmap; today manifests travel as a sidecar only.

Two flows, one mechanism

Replay resistance

For live sessions, the session_nonce is the primary anti-replay defense: a pre-recorded, pre-hashed video can't produce a valid chain because its seed can't contain a nonce the server only just generated. A mismatch scores session_nonce_mismatch (+70).

What liveness_check_failed does and does NOT mean

liveness_check_failed (+40) is a secondary heuristic layered on the nonce check. It is rule-based timing/continuity only: inter-chunk arrival gaps far outside the expected cadence, non-monotonic client timestamps, wall-clock vs claimed-duration drift, or chunks arriving after finalize.

It is not biometric liveness. Real liveness detection (challenge-response, texture analysis) is ML and not part of the current release. Don't read this factor as "we confirmed a live human" — read it as "the chunk timing was inconsistent."