Overview
Proofmode defines a standard way to prove a Nostr video event was recorded on a real device and has not been altered.
It augments NIP-71 video events on kinds 21 and 22,
reuses NIP-92 imeta (including the per-variant SHA-256 x),
and follows NIP conventions from NIP-01, NIP-94, and NIP-98.
What you get: integrity (OpenPGP-signed manifest), provenance (device attestation on mobile), and optional frame-level hashing.
When to use
- Journalism, witness, or civic video where AI/synthetic media confusion is likely.
- Any video where the audience benefits from origin + integrity proof.
Event kinds & relationship to NIP-71
Proofmode attaches to kind 21 (normal) and kind 22 (short) video events defined by NIP-71.
Publishers keep using imeta for the media variants; Proofmode adds proof tags.
// NIP-71 excerpt: kinds 21/22 use imeta with url/m/dim/x...
// Proofmode tags live alongside:
["proof-version","1"],
["verification-level","verified_mobile" | "verified_web" | "basic_proof" | "unverified"],
["proof-manifest","<base64(JSON)>"],
["pgp-pubkey","<ASCII armored>"],
["pgp-fingerprint","<hex>"],
["device-attestation","<platform token>"],
["frame-hashes","<base64(JSON)>"],
["recording-segments","<int>"],
["human-interactions","<int>"]
Reference: NIP-71 kinds and use of imeta. The manifest’s media_hash must match the chosen x in the selected variant.
Verification levels
verified_mobile
PGP signature valid + device attestation valid.
verified_web
PGP signature valid; no device attestation.
basic_proof
PGP signature valid; integrity only.
unverified
No manifest or verification failed.
Verification algorithm (client/relay)
- Decode
proof-manifest; parse JSON. - Import
pgp-pubkey; verifypgp-fingerprint. - Verify OpenPGP
signatureover canonicalized manifest (excludingsignature). - Match
manifest.media_hashto the selectedimetavariant’sx(NIP-92). - If present, validate
device-attestation(issuer keys, freshness, package/app id, integrity flags, nonce binding). - Optionally sample re-hash frames and compare with
frame-hashes. - Check segment & interaction counts vs manifest; basic timing sanity.
- Assign level; downgrade if claim > derived.
Examples
Kind 21 with full Proofmode (verified_mobile)
{
"kind": 21,
"content": "Protest on Main St.",
"tags": [
["title","Protest Footage"],
["imeta","dim 1920x1080","url https://cdn.example/v/abc.mp4","x 5c...e2","m video/mp4","image https://cdn.example/v/abc.jpg"],
["published_at","1736112000"],
["duration","120"],
["proof-version","1"],
["verification-level","verified_mobile"],
["proof-manifest","<base64(JSON with media_hash==imeta.x, times, signature)>"],
["device-attestation","<JWT>"],
["pgp-pubkey","-----BEGIN PGP PUBLIC KEY BLOCK-----..."],
["pgp-fingerprint","094F9BAE18958C238F55F793560CC828C726365F"],
["frame-hashes","<base64(JSON of samples)>"],
["recording-segments","1"],
["human-interactions","2"]
]
}
Kind 22 with web capture (verified_web)
{
"kind": 22,
"content": "Quick update",
"tags": [
["title","Short clip"],
["imeta","dim 720x1280","url https://cdn.example/s/xyz.webm","x 7a...90","m video/webm"],
["proof-version","1"],
["verification-level","verified_web"],
["proof-manifest","<base64(JSON)>"],
["pgp-pubkey","-----BEGIN PGP PUBLIC KEY BLOCK-----..."],
["pgp-fingerprint","A1B2C3D4E5F6..."]
]
}
Verification (TypeScript, outline)
async function verifyProofMode(ev: NostrEvent) {
const t = indexTags(ev.tags);
if (!t["proof-manifest"]) return {level:"unverified", reason:"missing manifest"};
const manifest = JSON.parse(atob(t["proof-manifest"][0]));
const pub = await importPGP(t["pgp-pubkey"][0]);
if (pub.fingerprint !== t["pgp-fingerprint"][0]) return {level:"unverified", reason:"fp mismatch"};
const {signature, ...payload} = manifest;
const canonical = canonicalizeJSON(payload);
if (!(await verifyPGP(signature, canonical, pub))) return {level:"unverified", reason:"bad signature"};
const chosenImetaX = pickPrimaryImetaHash(ev.tags); // from NIP-92 imeta
if (manifest.media_hash !== chosenImetaX) return {level:"unverified", reason:"hash mismatch"};
let attestOK = false;
if (t["device-attestation"]) {
attestOK = await verifyAttestation(t["device-attestation"][0], pub);
}
// optional: sample frame rehash
const derived = attestOK ? "verified_mobile" : "basic_proof"; // or "verified_web" per context
return {level: derived, attestOK};
}
Security & Privacy
- Replay/proof reuse: bind nonces to PGP key or manifest hash; check freshness.
- Frame injection: file hash + signature prevents; frame samples add granularity.
- Attestation limits: depends on OS/device integrity; failures downgrade level.
- Crypto agility: future versions can switch hash/sig algorithms.
- Privacy: prefer per-event keys; omit location by default; note correlation risk if reusing keys.
Interoperability
- Backward-compatible: unknown tags ignored per NIP-01.
- Works with NIP-71 kinds 21/22; uses NIP-92 imeta and NIP-94 file hashes.
- Discovery via tag presence; future extensions via
proof-version.
References
License: CC0.