From ab82231a959247e9ae95814f4706a6a748a8c548 Mon Sep 17 00:00:00 2001 From: PrarthanaPurohit Date: Tue, 7 Jul 2026 16:34:30 +0000 Subject: [PATCH] add: NIP-80 hardware-attested media provenance --- 80.md | 408 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 80.md diff --git a/80.md b/80.md new file mode 100644 index 0000000000..76a60091da --- /dev/null +++ b/80.md @@ -0,0 +1,408 @@ +NIP-80 +====== + +Hardware-Attested Media Provenance +---------------------------------- + +`draft` `optional` + +This NIP defines events that let a physical capture device (a "trusted camera") attest, at the moment of capture, that a specific piece of media was produced by it — and lets anyone later verify that attestation, locate it from the media alone, follow the media through permitted transformations, and evaluate whether the device was endorsed or revoked. Attestations are published to relays out-of-band from the media file itself, so verification survives metadata stripping, re-hosting, and platform takedowns. + +An attestation proves **capture provenance**, not scene truth. A trusted camera pointed at a screen will faithfully attest a photograph of that screen. What a verifier learns is precisely: *this media came from this device, at approximately this time, and is byte- or pixel-identical to what the device produced (or derives from it through declared transformations)*. Nothing in this NIP claims more than that, and clients MUST NOT present attestations as proof that depicted events occurred. + +## Conventions + +- All hashes are SHA-256. All hex strings are lowercase. Base64 is the standard RFC 4648 alphabet, with padding. +- `uint32_be` / `uint64_be` denote fixed-width big-endian unsigned integers. `||` denotes byte concatenation; string literals in messages are UTF-8 bytes. +- `c14n` and transform `op` identifiers MUST match `[a-z0-9/-]+`. In particular they never contain a `0x00` byte, which keeps the `M_cap` framing unambiguous. +- Where this NIP defines JSON `content`, the `content` field MUST be either the empty string or a JSON-encoded object. Unknown members MUST be ignored. Examples below show `content` pretty-printed for readability; on the wire it is a JSON-encoded string. +- Events defined by this NIP MUST NOT carry a [NIP-40](40.md) `expiration` tag: expiring an attestation or a revocation silently destroys the record it exists to preserve. +- Events violating a cardinality rule stated below ("exactly one", "at most one") are malformed and MUST be ignored by verifiers. + +## Event kinds + +| kind | type | description | +| ------- | ----------- | ------------------------------------ | +| `1080` | regular | Capture Attestation | +| `1081` | regular | Derivation Attestation | +| `1082` | regular | Revocation | +| `11080` | replaceable | Device Announcement | +| `31080` | addressable | Device Endorsement | +| `31081` | addressable | Device Set | + +## Roles + +- **Device key**: a Nostr keypair intended to be generated on and never leave the capture device. It authors `kind:11080` and `kind:1080` events; the device is identified by this pubkey. The protocol cannot verify key custody — endorsements exist to assert it. +- **Hardware key**: an optional second keypair inside a secure element that cannot produce BIP-340 signatures. It is bound to the device key (see *Kind 11080*) and co-signs captures (`hwsig`). +- **Operator**: the person using the device. Optional and intentionally decoupled from the device key. +- **Endorser**: any pubkey (an organization, a manufacturer, a newsroom, an individual) that vouches for a device via `kind:31080`. This NIP defines no authorities; each verifier chooses which endorsers to trust. Trust flows through endorsements, never bare device keys. +- **Verifier**: any client or service evaluating media against attestations. + +## Signature algorithms + +Signatures outside the normal Nostr event signature use one of the following algorithms, identified by an `alg` string: + +| `alg` | scheme | public key encoding | signature encoding | +| -------- | ----------------------- | -------------------------------- | ------------------------------------------- | +| `ES256` | ECDSA over NIST P-256 | 33-byte SEC1 compressed, base64 | 64 bytes `r \|\| s` big-endian, low-S, base64 | +| `ES256K` | ECDSA over secp256k1 | 33-byte SEC1 compressed, base64 | 64 bytes `r \|\| s` big-endian, low-S, base64 | +| `BIP340` | Schnorr per BIP-340 | 32-byte x-only, base64 | 64 bytes per BIP-340, base64 | + +For a message `M` (defined per context below), the digest is `d = SHA-256(M)`. `ES256`/`ES256K` signatures are ECDSA with `d` used directly as the hash input — matching secure elements that sign caller-supplied 32-byte digests. `BIP340` signatures use `d` as the 32-byte BIP-340 message. Low-S is REQUIRED for ECDSA; verifiers MUST reject high-S signatures. Signers whose hardware emits high-S values normalize by replacing `s` with `n − s` — valid for the same key and message, requiring no re-signing and no secure-element support. Future algorithms enter as new `alg` identifiers in revisions of this NIP; the meaning of an existing identifier is immutable. The device key's own algorithm is inherited from Nostr event signing and evolves only if Nostr does; this NIP's cryptographic agility lives entirely in `alg`, `c14n` identifiers, transform ops, proof systems, and anchoring mechanisms. + +Three domain-separated messages are defined: + +``` +M_bind = "hamp/binding/v1" || <32-byte device pubkey> +M_cap = "hamp/capture/v1" || || 0x00 || <32-byte canonical hash> || uint64_be(created_at) +M_anon = "hamp/anon/v1" || <32-byte canonical hash> || uint64_be(created_at) +``` + +The domain prefix derives from this NIP's *title* (hardware-attested media provenance), not its number: NIP numbers are provisional until merge, while these strings ship in device firmware where they can never change. + +## Kind 11080: Device Announcement + +A replaceable event, authored by the device key, describing the device. OPTIONAL, but REQUIRED for any capture carrying `hwsig` to fully verify. Discovered with `{"kinds":[11080],"authors":[""]}`. + +```jsonc +{ + "kind": 11080, + "pubkey": "2875b66ac7b4490a84607c4d2fb53a49c86257826887b53ce3941e8865180be0", // device key + "created_at": 1781514000, + "tags": [], + "content": { + "name": "field-unit-07", + "model": "rpi5-cam3-r2", + "firmware": { "version": "1.4.2", "hash": "" }, + "hardware_key": { + "alg": "ES256", + "pubkey": "<33-byte SEC1 compressed public key, base64>", + "binding_sig": "sA9bSZrGp27hi0wQBxIUtVju4OQlCR4u0DMh1T2ha7vKb+aVGa5zbBQHp4+PLkzXu3s/QMyV/IUgDpktB0jyTA==", + "certs": ["", ""] + } + }, + // ...id, sig +} +``` + +`content` is a JSON object: + +- `name` (optional): human label. Operators in hostile environments SHOULD leave this generic. +- `model` (optional): hardware model identifier. +- `firmware` (optional): `version` and `hash` (SHA-256 of the firmware image, hex), enabling reproducible-build verification. +- `hardware_key` (optional): + - `alg`: per the table above. + - `pubkey`: the hardware key, encoded per `alg`. + - `binding_sig`: signature by the hardware key over `M_bind`, binding the secure element to the device's Nostr identity. + - `certs` (optional): base64 DER certificate chain (e.g., a manufacturer attestation over the hardware key), ordered leaf-first. Verifying the chain against a manufacturer PKI is between endorsers/verifiers and that PKI; this NIP only transports it. + +A hardware key is immutable for the lifetime of a device identity: because announcements are replaceable, publishing a new `hardware_key` makes `hwsig` verification fail for every earlier capture. Re-provisioning a device with a new secure element (or key) therefore warrants a **new device key**, not an announcement update. + +Devices SHOULD publish a [NIP-65](65.md) relay list (`kind:10002`) so their events — including a possible self-revocation — are discoverable through the outbox model. Two consequences of replaceability: announcements carry **no revocation semantics** (revocation is exclusively `kind:1082`), and firmware history is **not preserved** on relays — endorsers pin firmware facts inside their endorsements, and verifiers needing history must archive announcements themselves. + +## Kind 1080: Capture Attestation + +A regular event created and signed **on the device** at capture time, one per media file. + +```jsonc +{ + "kind": 1080, + "pubkey": "2875b66ac7b4490a84607c4d2fb53a49c86257826887b53ce3941e8865180be0", // device key + "created_at": 1783000800, // capture time; MAY be rounded down for operational safety + "tags": [ + ["x", "b878a2da0f23a1ac2da0e8491bdc44129160b30fe4edabf4eed0754998ca7e2c", "canonical"], + ["x", "92e48d090362300821ccb4e757bef85578f2f3b22c30f252c6413774bed29555", "file"], + ["c14n", "px1"], + ["m", "image/jpeg"], + ["dim", "4056x3040"], + ["g", "tuvz2"], // optional truncated geohash + ["url", "https://blossom.example.org/92e48d090362300821ccb4e757bef85578f2f3b22c30f252c6413774bed29555.jpg"], + ["hwsig", "JbTU3gi/LmziyPvy0a8cTeuHW+ThqEv0ptMPz6KQYqCHGc20RceWVVwAtu0+FIGhFkB0q4OQiRSLG7/mKFx9Cw=="] + ], + "content": "{}", + // ...id, sig +} +``` + +Tags: + +| tag | count | description | +| ------------- | ----- | ------------------------------------------------------------------------------------------------------------------ | +| `x` …`canonical` | exactly 1 | SHA-256 of the media's canonical form (see *Media canonicalization*), hex. The primary lookup key. | +| `x` …`file` | 0–1 | SHA-256 of the exact encoded file bytes, hex, matching [NIP-94](94.md) `x` semantics. Enables cheap exact-file lookup. An `x` tag with **no** marker MUST be read as `file`. | +| `c14n` | exactly 1 | canonicalization algorithm identifier used for the canonical hash | +| `m` | exactly 1 | MIME type of the attested file, lowercase. MUST be within the scope of the `c14n` identifier. | +| `dim` | 0–1 | `x` in pixels | +| `g` | 0–1 | geohash of the capture location. See *Privacy considerations* before including. | +| `location` | 0–1 | free-text location | +| `url` | 0+ | locations where the file can be fetched, preferably SHA-256-addressed ([NIP-B7](B7.md)) | +| `hwsig` | 0–1 | `["hwsig", ]`: hardware-key signature over `M_cap`; the algorithm and public key come from the announcement's `hardware_key`. Required by some endorsement policies. | +| `p` | 0–1 | operator pubkey. See *Privacy considerations*; usually better omitted. | +| `a` | 0–1 | reference to a `kind:31081` device set — anonymous attestations only (see *Device sets*) | + +Wherever a window or `effective` timestamp is compared against a capture, the comparison uses `created_at`. Attestations whose `created_at` lies beyond reasonable clock skew in the verifier's future are invalid. + +`content` fields: `caption` (optional, human text) and `proof` (anonymous attestations only). + +A `kind:1080` carrying an `a` tag is an **anonymous attestation**: its author is a single-use throwaway key, not a device, and it is verified per *Device sets* — never by author identity. Without an `a` tag, the author *is* the device. + +Both `x` values are indexed by relays, so a verifier holding only the media can locate attestations with `{"kinds":[1080,1081],"#x":[""]}` after computing the hash locally — no metadata in the file is required, which is the point: verification survives platforms that strip embedded metadata. + +Because the event is fully formed and signed on the device, it can be carried offline and broadcast later, by anyone, from anywhere ("courier publishing"). Relays and clients MUST NOT infer anything about the capture from *who* delivered the event or *when* it arrived; `created_at` is the device's claim, bounded externally only by the mechanisms in *Timestamp anchoring* and by endorsement validity windows. Couriers SHOULD target relays that accept historical `created_at` values; some relays reject events dated far in the past. + +## Kind 31080: Device Endorsement + +An addressable event by which any pubkey vouches for a device. The `d` tag is the device pubkey as 64 lowercase hex characters, so all endorsements for a device are found with `{"kinds":[31080],"#d":[""]}`, and all devices endorsed by an organization with `{"kinds":[31080],"authors":[""]}`. + +```jsonc +{ + "kind": 31080, + "pubkey": "ea0b357c46a4a0b6ce3d21f20ee71f83ac4dc1caa702f405a54b815a77907899", // endorser + "created_at": 1781514000, + "tags": [ + ["d", "2875b66ac7b4490a84607c4d2fb53a49c86257826887b53ce3941e8865180be0"], + ["p", "2875b66ac7b4490a84607c4d2fb53a49c86257826887b53ce3941e8865180be0"] + ], + "content": { + "statement": "Provisioned during in-person enrollment; manufacturer certificate chain verified; firmware hash matches our reproducible build.", + "methods": ["in_person", "cert_chain", "firmware_reproducible"], + "requires_hwsig": true, + "hardware_key": { "alg": "ES256", "pubkey": "<33-byte SEC1 compressed public key, base64>" }, + "valid_from": 1781514000, + "valid_until": 1813050000 + }, + // ...id, sig +} +``` + +The `p` tag duplicates the device key so endorsements also surface through mention-based (`#p`) queries. + +`content` fields (all optional): `statement` (human-readable), `methods` (machine-readable strings describing how the device was verified), `requires_hwsig` (if `true`, this endorser only stands behind captures carrying a valid `hwsig`), `hardware_key` (a snapshot `{ "alg", "pubkey" }` of the device's hardware key as verified at enrollment — REQUIRED when `requires_hwsig` is true, since requiring a signature without preserving its key is self-defeating; a `requires_hwsig` endorsement lacking it is malformed), `valid_from` / `valid_until` (unix timestamps bounding which `created_at` values the endorsement covers; a missing bound is unbounded on that side, though verifiers MAY apply their own staleness policy). + +Endorsements are assertions of *procedure*, not truth: "we verified this device according to these methods." Verifiers decide which endorsers, and which methods, they consider sufficient. This NIP deliberately defines no root authorities and no conformance program; trust topology is the verifier's choice, in the same spirit as [NIP-85](85.md) assertions. + +Endorsers SHOULD monitor the announcements of devices they endorse and revoke on unexpected `hardware_key` changes. + +An endorser withdrawing an endorsement MUST publish a `kind:1082` revocation. It MAY additionally update the `kind:31080` event, but because addressable events are replaceable, the update alone carries no security weight. + +## Kind 1082: Revocation + +A regular — therefore permanent — event revoking trust in a device key or withdrawing an endorsement. + +```jsonc +{ + "kind": 1082, + "pubkey": "ea0b357c46a4a0b6ce3d21f20ee71f83ac4dc1caa702f405a54b815a77907899", // device key (self-revocation) or endorser + "created_at": 1783067400, + "tags": [ + ["p", "2875b66ac7b4490a84607c4d2fb53a49c86257826887b53ce3941e8865180be0"], // REQUIRED: the revoked device key + ["a", "31080:ea0b357c46a4a0b6ce3d21f20ee71f83ac4dc1caa702f405a54b815a77907899:2875b66ac7b4490a84607c4d2fb53a49c86257826887b53ce3941e8865180be0"], // optional pointer when withdrawing an endorsement + ["effective", "1783000000"] + ], + "content": "Device seized at checkpoint on 2026-07-02.", + // ...id, sig +} +``` + +The `p` tag (exactly one) is REQUIRED, so the single filter `{"kinds":[1082],"#p":[""]}` retrieves every revocation concerning a device. + +**A revocation's effect is determined solely by its author:** + +- authored by the device key named in `p` → revokes the device itself, unconditionally; +- authored by an endorser `E` → withdraws **every** `kind:31080` endorsement by `E` for that device. The `a` tag is an optional pointer; the effect is author-scoped regardless of its presence. +- authored by anyone else → **no effect; verifiers MUST ignore it.** Without this rule, any third party could grief a device with fake revocations. + +`effective` (0–1): captures with `created_at` **at or after** this timestamp are affected. When absent, the revocation covers all captures. This lets a seizure revoke future signatures without discarding the pre-seizure archive. + +**Revocation is monotonic.** Once a verifier has seen an effective `kind:1082`, no subsequent event — including a fresher device announcement or endorsement by the same endorser — un-revokes it. Clients MUST treat revocations as permanent for their stated scope; anything weaker lets an attacker who compromised the key "revoke the revocation." Verifiers MUST query for revocations on every verification (see *Verification*) — an attestation checked without a revocation lookup MUST NOT be reported as valid. Returning a once-revoked device to service means provisioning a **new** device key; re-endorsing a revoked key cannot resurrect it. + +Deletion requests ([NIP-09](09.md)) are an un-revocation vector: an attacker holding a compromised device key could ask relays to delete the device's own self-revocation. Relays SHOULD NOT honor `kind:5` requests targeting `kind:1082` events, and clients MUST NOT issue them. + +## Kind 1081: Derivation Attestation + +Links a derived piece of media (recompressed, resized, cropped, redacted) to an attested original. May be published by anyone — a newsroom resizing a photo, a platform, the operator. **Its validity comes entirely from its cryptographic content, never from its author**; verifiers MUST ignore the author identity when evaluating it. + +```jsonc +{ + "kind": 1081, + "pubkey": "", + "created_at": 1783067400, + "tags": [ + ["e", "5ea3f85d419869337e59ede2cbed3e911fabea3190d701f6ff995f6cce4fcc42", "", "root"], // the kind:1080 + ["x", "54968e8899addeb7e2f2046465cd9b6bfdb7539c5fdf36390b05053f30036593", "canonical"], // this event's derived media + ["x", "b878a2da0f23a1ac2da0e8491bdc44129160b30fe4edabf4eed0754998ca7e2c", "original"], // optional convenience copy + ["c14n", "px1"], + ["m", "image/jpeg"] + ], + "content": { + "mode": "zk", + "transforms": [ + { "op": "resize/1", "w": 1280, "h": 960, "filter": "bilinear" }, + { "op": "jpeg/1", "q": 85 } + ], + "proof": { + "system": "groth16", + "curve": "bn254", + "circuit": "img-resize-jpeg/1", + "circuit_hash": "0975331f9b074b7bf0326c821a144fa6c35025df382bf279c5606e680f6e875c", + "vk_hash": "", + "vk_url": "https://blossom.example.org/", + "public_inputs": ["", ""], // informative only — verifiers reconstruct (see below) + "proof": "", + "proof_url": "https://blossom.example.org/", + "proof_hash": "" + } + }, + // ...id, sig +} +``` + +The `x` tag marked `canonical` (exactly one) is always the canonical hash of **the media this event attests** — the derived file — keeping the `#x` lookup rule uniform across kinds 1080 and 1081: a verifier hashes whatever file it holds and queries. `c14n` (exactly one) names the algorithm for that hash and MUST equal the root attestation's `c14n` — cross-algorithm derivations are not defined in this version. An `x` marked `original` (0–1) is a convenience copy of the root's canonical hash; if present it MUST equal the root attestation's `canonical` value, and a mismatch makes the event malformed. + +Two modes: + +- `reveal`: no proof. The `transforms` list is a claim checked by anyone holding the *original*: apply the declared ops, in order, to the original's canonical raster using the ops' reference implementations, canonicalize the result, and compare with the `canonical` hash. Cheap, but verification requires the original. +- `zk`: a zero-knowledge proof of the statement: *there exists a raster `I` whose canonical hash equals the `original` hash, such that applying the declared ops in order to `I` yields a raster whose canonical hash equals the `canonical` hash* — without revealing `I`. Both canonical hashes and **every op parameter** MUST be bound as public inputs, laid out as defined by the circuit identified by `circuit_hash`. Verifiers reconstruct the public inputs from the event itself — the two `x` values and the `transforms` array — and MUST NOT take them from the envelope's `public_inputs` field, which is informative only: a proof verified against envelope-supplied inputs proves a true statement about *whatever the prover chose*, not about this event's media. Proof generation happens off-device; only verification needs to be cheap. + +### Transform registry + +| op | parameters | semantics | +| ------------- | -------------------------------------------- | ---------------------------------------------------- | +| `resize/1` | `w`, `h` (ints); `filter` ∈ `nearest`, `bilinear` | scale the full frame to `w`×`h` | +| `crop/1` | `x`, `y`, `w`, `h` (ints) | crop rectangle; origin top-left | +| `rotate/1` | `deg` ∈ 90, 180, 270 | clockwise rotation | +| `grayscale/1` | — | per-pixel Rec. 601 luma, replicated to R, G, and B | +| `redact/1` | `regions`: array of `[x, y, w, h]` | fill each region with RGB(0, 0, 0) | +| `jpeg/1` | `q` (int, 1–100) | re-encode with the op's pinned reference encoder (baseline, 4:2:0), then decode per the `c14n` identifier | + +Like `c14n` identifiers, **each op version is pinned to a reference implementation with normative test vectors**; pixel arithmetic — rounding, edge handling, chroma subsampling — is defined by the reference implementation, not by this prose. Lossy ops such as `jpeg/1` are only deterministic *because* the encoder is pinned. Ops apply strictly in declared order; each op's coordinates and dimensions refer to the frame produced by the op before it. An op is usable in `reveal` mode once its reference implementation and vectors are published, and in `zk` mode once a circuit for it is published; an op or `c14n` identifier without published vectors MUST NOT be used. + +Derivations MAY chain: a `kind:1081` MAY carry an additional `["e", , "", "parent"]` tag when derived from an already-derived image. Every event in a chain MUST reference the root `kind:1080` with the `root` marker (exactly one `root`; at most one `parent`). Verifiers MUST verify every link, MUST reject cycles, and MAY cap chain depth and the number of ops in a single derivation. + +Note the asymmetry with the base protocol: signatures and `reveal` derivations are plain, reviewable cryptography; `zk` derivations depend on the soundness of specific circuits and proof systems. Clients SHOULD distinguish the two in their UI, and implementations SHOULD treat `zk` support as an independent, optional module. + +## Kind 31081: Device Sets and anonymous attestations + +Signed attestations identify the device, and through endorsement records potentially the operator. When that linkage is dangerous, an endorser MAY publish a **device set**: a commitment to a group of endorsed devices, against which membership can be proven without revealing which member signed. + +```jsonc +{ + "kind": 31081, + "pubkey": "ea0b357c46a4a0b6ce3d21f20ee71f83ac4dc1caa702f405a54b815a77907899", + "created_at": 1781514000, + "tags": [ + ["d", "field-devices-2026"], + ["p", "2875b66ac7b4490a84607c4d2fb53a49c86257826887b53ce3941e8865180be0"] + // ...one p tag per member device + ], + "content": { + "root": "2b65312ebf1083deba73904a1e20460c8020f96202799462ec1c67fa7c7a4185", + "count": 512, + "status": "active" + }, + // ...id, sig +} +``` + +The Merkle tree is fully determined by the `p` tags: + +1. Take the set of member pubkeys from the `p` tags (32 raw bytes each), deduplicated, sorted ascending as byte strings. `count` MUST equal the number of distinct members and MUST be at least 1 — the Merkle root of an empty roster is undefined, and such a set is malformed. +2. `leaf_i = SHA-256( 0x00 || pubkey_i )`. +3. Build upward: `node = SHA-256( 0x01 || left || right )`; when a level has an odd number of nodes, the last node is promoted unchanged to the next level. +4. `root` is the final node, hex. + +Members MUST be listed publicly (`p` tags), and **verifiers MUST recompute the root from the `p` tags and reject the set on mismatch (likewise a `count` not matching the roster)**: a set whose membership is secret cannot be audited, and the endorser could pad it with keys it controls. Anonymity comes from not knowing *which* member signed, not from hiding the roster — and it is only as strong as `count` and as the roster's plausibility. Members SHOULD also carry individual `kind:31080` endorsements, so rosters can be audited against enrollment records rather than taken on faith. Endorsers rotate compromised members out by publishing a new set under a new `d` tag and setting the old set's `status` to `"retired"`. Editing a roster *in place* changes the recomputed root and thereby invalidates every prior anonymous attestation referencing that `d`; this is intentional — an emergency kill-switch for a wholly compromised set — and routine rotation always uses a new `d`. + +An **anonymous capture attestation** is a `kind:1080` published under a single-use throwaway key, with: + +- an `["a", "31081::"]` tag, +- no `hwsig`, no `p`, and no or heavily truncated `g` (each would narrow the anonymity set), +- `content.proof`: a proof (same envelope as kind 1081) of the statement: *the prover knows a private key whose x-only public key `P` hashes to a leaf of the Merkle tree with root `R`, and a valid BIP-340 signature by `P` exists over `SHA-256(M_anon)`* — with `R`, the canonical hash, and `created_at` as public inputs. Binding `created_at` prevents replay of the proof under a different timestamp. + +Verifiers evaluate anonymous attestations as: "some device in set *S*, endorsed by *E*, captured this." Revocation of an individual member cannot be applied to anonymous attestations retroactively; verifiers SHOULD prefer attestations against an endorser's `active` sets. Anonymous attestations bind to the device's *Nostr* key: on devices whose only extraction-resistant key is an ECDSA hardware key, they carry software-key assurance only (verifying a hardware-key signature inside the circuit is possible but out of scope for this version). This mechanism is EXPERIMENTAL and gated on circuit availability; everything else in this NIP stands without it. + +## Media canonicalization + +The canonical hash exists so that verification keys off *pixel content*, not container bytes: stripping EXIF, rewriting metadata, or re-muxing the container does not change it. Re-encoding pixels **does** change it — that case is handled by `kind:1081`, not by fuzzy matching. Canonical hashes are exact; this NIP uses no perceptual hashing. + +Canonicalization algorithms are versioned by identifier. This NIP defines: + +- **`px1`** — still images; scope: JPEG (baseline and progressive, 8-bit) and PNG (all standard color types; palette images are expanded by the reference decoder, 16-bit samples reduced as below). + 1. Decode with the algorithm's pinned reference decoder. ICC profiles and other color-management metadata are ignored: samples are taken as decoded, with no color conversion. EXIF orientation is likewise ignored — the raster is as decoded, unrotated, and `width`/`height` are the coded dimensions. + 2. 16-bit samples are reduced to 8 bits by taking the high byte (`v >> 8`). Grayscale is expanded to RGB by replication. The alpha channel, if present, is dropped without compositing. CMYK and other color models are out of scope for `px1`. + 3. Serialize as `"PX1" || uint32_be(width) || uint32_be(height) || 0x03 || `; the canonical hash is SHA-256 of that serialization. + +Because image decoders differ in rounding (notably JPEG IDCT), **the reference decoder and published test vectors are normative** for each identifier: an implementation conforms to `px1` if and only if it reproduces the test vectors. New identifiers (RAW formats, video, audio, CMYK) MUST be introduced as new `c14n` values with their own reference implementations and vectors, never by changing the meaning of an existing identifier. A video or audio identifier MAY define its canonical form as a Merkle root over canonical segments, so excerpts verify as derivations against the root without the full recording. During a migration between identifiers, devices MAY publish parallel attestations for the same capture under both the old and new identifier; the shared `file` hash ties them together. + +Signers compute the canonical hash the same way verifiers will: by decoding their **own encoded output** with the reference decoder. Hashing the pre-encode sensor raster produces a value nothing else can ever reproduce, since encoding is lossy. + +A verifier that does not implement an event's `c14n` identifier MUST NOT treat the canonical hash as verified; it MAY still perform exact-file verification via the `file` hash. Signers SHOULD always include the `file` hash alongside the `canonical` hash, so exact-file verification never depends on a decoder at all. + +## Verification + +Given a media file, a verifier (the steps are requirements, not a strict sequence): + +1. Computes the file's SHA-256 and, per supported `c14n` algorithms, its canonical hash(es). +2. Queries relays: `{"kinds":[1080,1081],"#x":[]}`. Every match is evaluated — a file may legitimately carry several attestations, and all are reported; clients SHOULD display every valid claim rather than electing one as canonical. Relay filters ignore markers, so each hit MUST then be matched *by marker*: the file at hand is attested only when its hash equals a `canonical` value (or a `file` value, for exact bytes); a hit on an `original` value means the file is an ancestor of that event's media, not its subject. +3. For a matching `kind:1080` **without** an `a` tag: checks cardinality rules; verifies the event signature (the author *is* the device). If `hwsig` is present, it is verified over `M_cap` per endorsement, against each applicable endorsement's `hardware_key` snapshot; on this path `binding_sig` is not re-checked — the endorser attested the key–device pairing at enrollment, which is what a snapshot asserts — so a replaced or deleted announcement is harmless. Only outside any snapshot (unendorsed evaluation, or an endorsement without one) does the verifier fall back to the live announcement, fetched via `{"kinds":[11080],"authors":[]}`, where it MUST verify `binding_sig` over `M_bind` before trusting `hwsig`. +4. For a matching `kind:1081`: verifies the derivation (recomputation for `reveal`, proof verification for `zk`), fetches each referenced event by id, walks `parent` links to the `root` `kind:1080`, verifies every link, then verifies the root as in step 3. +5. Fetches endorsements `{"kinds":[31080],"#d":[]}`, keeps those from endorsers the verifier trusts, and checks each one's `valid_from`/`valid_until` against the capture's `created_at` and its `requires_hwsig` policy against the capture. An endorsement carrying a `hardware_key` snapshot is self-contained: `hwsig` for captures evaluated under it is verified against the snapshot key, so replacing or deleting the announcement can neither forge endorsed captures nor orphan the verifiable archive. `hwsig` validity is evaluated per endorsement, against that endorsement's snapshot. A snapshot disagreeing with the live announcement — or two endorsers' snapshots disagreeing with each other — is itself a red flag verifiers SHOULD surface. +6. **Fetches revocations — mandatory**: `{"kinds":[1082],"#p":[]}` across the verifier's relay set plus the NIP-65 relay lists of the device and of each considered endorser. Applies the author-scoping rule from *Kind 1082*, ignores all other revocations, and applies effective ones monotonically, honoring `effective`. +7. For a `kind:1080` **with** an `a` tag (anonymous): fetches the referenced `kind:31081`, recomputes the Merkle root from its `p` tags, and verifies `content.proof` with public inputs reconstructed as (recomputed root, the event's `canonical` hash, the event's `created_at`) — never as read from the envelope. Root mismatch or proof failure invalidates the attestation. +8. Optionally checks timestamp anchoring (below). +9. Reports: attested by device *D* (or: by an unknown member of set *S*) at time *T*; endorsed by [*E₁*, *E₂*] under stated methods; derivation path; revocation status; anchoring status. Clients MUST surface this as capture provenance and MUST NOT collapse it into a bare "authentic" badge. + +The absence of an attestation proves nothing. Most of the world's media is unsigned; clients MUST NOT present unsigned media as suspect merely for being unsigned. + +For ecosystem consistency, a RECOMMENDED (non-normative) result vocabulary reports along independent dimensions rather than one flattened state — integrity: `exact` | `derived-reveal` | `derived-zk` | `unverified` (with a reason, e.g. unsupported `c14n` or proof system); device binding: `hwsig-valid` | `hwsig-absent` | `hwsig-unverifiable`; attribution: `device` | `anonymous-set`; endorsement, per endorser: `applies` | `window-mismatch` | `policy-mismatch` | `withdrawn`; revocation: `none-found` | `revoked (effective T)`; anchoring: `anchored-by T` | `none`. The dimensions combine — a capture can be `derived-zk`, `hwsig-valid`, and `revoked` at once — which is exactly why they MUST NOT be flattened into a single badge. + +## Timestamp anchoring + +A device's `created_at` is self-asserted. To bound it from above, anyone MAY publish a [NIP-03](03.md) `kind:1040` OpenTimestamps attestation over the `kind:1080` event id (with a `["k", "1080"]` tag), establishing that the attestation existed no later than a Bitcoin block time. Note NIP-03's current `unrecommended` status and its caveats: anchoring gives an upper bound only ("existed by"), never a lower bound ("not before"), and inherits any weaknesses of the anchoring mechanism. Endorsement validity windows provide an independent, coarser bound. This version specifies OpenTimestamps; future revisions MAY admit equivalent anchoring mechanisms under the same upper-bound-only semantics. + +Anchoring is also what lets attestations outlive their own cryptography. Hash functions age far more gracefully than signature schemes: if the device key's curve is ever broken, an attacker can forge backdated signatures at will, and the only thing separating a genuine old attestation from a fresh forgery is proof that the event existed before the break. Archives SHOULD therefore anchor attestations they intend to preserve, and SHOULD re-anchor under newer mechanisms before older ones weaken — a chain of timestamps carries a signature's validity across cryptographic generations. Attestation events are small, so indefinite proof archival is cheap relative to the media itself. + +## Embedding in other events + +Media posted through existing NIPs can point at its attestation. An `imeta` tag ([NIP-92](92.md)) MAY include an `attestation` field whose value is the `kind:1080`/`kind:1081` event id (64 hex chars) or a [NIP-19](19.md) `nevent`: + +``` +["imeta", "url https://blossom.example.org/92e48d09….jpg", "m image/jpeg", "x 92e48d09…", "attestation 5ea3f85d419869337e59ede2cbed3e911fabea3190d701f6ff995f6cce4fcc42"] +``` + +The same field applies wherever `imeta`/NIP-94 fields are accepted, including `kind:20` ([NIP-68](68.md)), video events ([NIP-71](71.md)), and `kind:1063` ([NIP-94](94.md)). The pointer is a convenience only; verifiers MUST verify the attestation itself *against the accompanying media's own hashes* — a pointed-to attestation that does not match the file's `canonical` or `file` value MUST be treated as absent, or careless clients become a lamination service for stolen provenance. Verifiers MUST also handle media with no pointer at all via the `#x` lookup. Media files SHOULD be stored SHA-256-addressed ([NIP-B7](B7.md)) so the `file` hash doubles as the retrieval key. + +## What an attestation does not prove + +- **Scene authenticity.** Rebroadcast ("photograph of a screen") defeats any capture-attestation design. Capture-side heuristics (multi-frame capture, sensor-noise consistency, depth/parallax checks) can raise the cost and MAY be referenced in endorsement `methods`, but they are heuristics; the protocol makes no claim here. +- **Location.** A `g` tag reflects the device's GPS input, and GPS is spoofable. Treat location as device-asserted metadata. +- **Time (lower bound).** A device can be back-dated; only anchoring and endorsement windows constrain the claim, and only as an upper bound. +- **Exclusive authorship.** Hashes are public: any device can attest bytes it possesses but did not capture. An attestation asserts one device's claim of capture — not that it was the first or only claimant. Verifiers surface all attestations for a hash; timestamp anchoring and endorsement context are the tiebreakers, and endorsers SHOULD treat attesting others' content as grounds for revocation. +- **Endorser honesty or competence.** An endorsement means an endorser said something, under stated methods. Choosing endorsers is the verifier's judgment. + +## Security considerations + +- **Key extraction is total compromise.** A device whose signing key leaves the device can attest arbitrary bytes. Deployments serious about this NIP keep keys in hardware that will not export them, and endorsers state so in `methods`. The `hwsig` mechanism exists precisely because commodity boards have no native secure keystore for BIP-340. Note that `hwsig` alone cannot backstop an extracted device key — announcements are author-replaceable, so the attacker simply announces a hardware key they control. The `hardware_key` snapshot in endorsements is what turns the hardware key into an independent trust root, and because it is self-contained, announcement replacement or deletion cannot orphan verification of the pre-compromise archive. +- **The device key must never sign caller-supplied digests.** Firmware constructs `M_bind`/`M_cap`/`M_anon` internally from their components. A device exposing a raw sign-this-digest interface for its Nostr key would hand callers something indistinguishable from a signature over a Nostr event id — i.e., the ability to forge arbitrary events from the device. +- **Skipping revocation checks voids everything.** A verifier that validates signatures but not revocations will happily accept media signed with a seized device — a failure mode observed in deployed provenance ecosystems. Hence step 6 is mandatory and revocation is monotonic. +- **Revocation authorship matters.** The author-scoping rule in *Kind 1082* is what makes the mandatory revocation lookup safe: without it, the lookup itself becomes a denial-of-service surface where any key can "revoke" any device. +- **Endorser key compromise is recoverable because revocation is author-scoped.** A compromised endorser key can permanently withdraw only its *own* endorsements and can issue no device-level revocations; affected devices are re-endorsed under the endorser's next key. +- **Replaceable events carry security state only when their author is the trust anchor.** `kind:11080` and `kind:31081` are informational or auditable-by-recomputation, and anything that must not be silently rewritten lives in regular events (`1080`, `1081`, `1082`). The one exception is deliberate: the endorsement's `hardware_key` snapshot, rewritable only by the endorser — whose honesty the verifier is already assuming. +- **Trust flows through endorsements, never bare device keys.** A seized device cannot self-revoke; it is neutralized by its endorsers' revocations. A verifier accepting unendorsed device keys accepts keys with no revocation path, and SHOULD treat such attestations as unverifiable claims. +- **Relay censorship and eclipse.** Attestations and especially revocations MUST be published to multiple independent relays; verifiers SHOULD query a relay set they choose, plus the device's and endorsers' NIP-65 relay lists, and treat "no revocation found" as only as strong as their relay coverage. +- **Large payloads.** Events SHOULD stay well under common relay size limits (~64 KB). Certificates beyond a small chain, proofs, and verification keys go external as SHA-256-addressed blobs (`*_url` + `*_hash`); a verifier MUST check the hash of anything fetched by URL, and SHOULD enforce a size limit on such fetches. Device sets with very large rosters can also exceed relay tag-count limits; endorsers SHOULD split such sets. +- **ZK soundness is per-circuit.** A `zk` derivation is only as sound as its circuit and proof system. Circuit identifiers, hashes, and verification-key hashes are part of the verifiable statement; clients SHOULD ship pinned, audited circuit sets rather than fetching unknown circuits dynamically. A proof whose system or circuit the verifier does not recognize leaves the derivation unverified — never partially trusted. + +## Privacy considerations + +Provenance data is a forensic trail. In hostile environments the trail can identify, locate, and convict the operator — the person this NIP is supposed to serve. Accordingly: + +- Signing clients SHOULD round `created_at` (to the hour or day) by default; timestamp precision is a deliberate per-capture choice, never a default. +- `g` tags SHOULD be truncated (5 geohash characters ≈ a few kilometers) or omitted; `location` free text is safer still. Note that `g` is relay-indexed: anyone can hold a standing `#g` subscription for an area of interest, turning precise tags into a surveillance feed. +- The operator `p` tag SHOULD be omitted unless public authorship is the goal. +- Device announcements SHOULD NOT contain names, serial numbers, or purchase-traceable identifiers beyond what endorsement requires. +- Endorsement rosters are public by design; operators for whom *owning an endorsed device* is itself incriminating need anonymous attestations (device sets) or should not be enrolled under identifying endorsers. +- Courier publishing separates capture from network origin: the device never needs connectivity, and the publisher's IP reveals the courier, not the photographer. Publishing clients SHOULD support routing through privacy networks. +- Attestations are permanent by design — revocation withdraws trust but unpublishes nothing — so publishing one is an irreversible disclosure decision, made per capture. +- Fetching `url` / `vk_url` / `proof_url` targets reveals the verifier's interest to those hosts. Privacy-conscious verifiers fetch through proxies or prefer content-addressed retrieval from multiple hosts. +- Nothing here hides *that* a device set or endorser is associated with sensitive documentation. Threat modeling that association is out of protocol scope and must happen at the deployment level. \ No newline at end of file