Proofmode for Nostr Video (Kinds 21 & 22)

Cryptographic authenticity proofs for NIP-71 video events using device attestation, OpenPGP signatures, and content hashes.

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

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.

Tag specifications

proof-version
Required. Version string (e.g. "1").
verification-level
Required. verified_mobile | verified_web | basic_proof | unverified.
proof-manifest
Required. Base64 of compact JSON manifest including media_hash, times, segments, optional interactions/frame hashes, and an OpenPGP detached signature over the canonicalized manifest.
pgp-pubkey
Required. ASCII-armored OpenPGP public key (or base64 binary).
pgp-fingerprint
Required. Hex fingerprint for quick match & cache.
device-attestation
Required for verified_mobile. App Attest (iOS) or Play Integrity (Android) token (JWT/CBOR), nonce bound to key/manifest.
frame-hashes
Optional. Base64 of JSON (sampled hashes or Merkle root) to enable spot verification.
recording-segments
Optional. Number of pause/resume segments.
human-interactions
Optional. Count of on-record user interactions.

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)

  1. Decode proof-manifest; parse JSON.
  2. Import pgp-pubkey; verify pgp-fingerprint.
  3. Verify OpenPGP signature over canonicalized manifest (excluding signature).
  4. Match manifest.media_hash to the selected imeta variant’s x (NIP-92).
  5. If present, validate device-attestation (issuer keys, freshness, package/app id, integrity flags, nonce binding).
  6. Optionally sample re-hash frames and compare with frame-hashes.
  7. Check segment & interaction counts vs manifest; basic timing sanity.
  8. 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

Interoperability

References

License: CC0.