mirror of
https://github.com/nostr-protocol/nips.git
synced 2025-12-08 16:18:50 +00:00
Compare commits
2 Commits
2a33cceff6
...
proofmode-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
317e51b961 | ||
|
|
d44476eee8 |
5
11.md
5
11.md
@@ -17,7 +17,6 @@ When a relay receives an HTTP(s) request with an `Accept` header of `application
|
||||
"banner": <a link to an image (e.g. in .jpg, or .png format)>,
|
||||
"icon": <a link to an icon (e.g. in .jpg, or .png format>,
|
||||
"pubkey": <administrative contact pubkey>,
|
||||
"self": <relay's own pubkey>,
|
||||
"contact": <administrative alternate contact>,
|
||||
"supported_nips": <a list of NIP numbers supported by the relay>,
|
||||
"software": <string identifying relay software URL>,
|
||||
@@ -61,10 +60,6 @@ An administrative contact may be listed with a `pubkey`, in the same format as N
|
||||
|
||||
Relay operators have no obligation to respond to direct messages.
|
||||
|
||||
### Self
|
||||
|
||||
A relay MAY maintain an identity independent from its administrator using the `self` field, which MUST be a 32-byte hex public key. This allows relays to respond to requests with events published either in advance or on demand by their own key.
|
||||
|
||||
### Contact
|
||||
|
||||
An alternative contact may be listed under the `contact` field as well, with the same purpose as `pubkey`. Use of a Nostr public key and direct message SHOULD be preferred over this. Contents of this field SHOULD be a URI, using schemes such as `mailto` or `https` to provide users with a means of contact.
|
||||
|
||||
2
51.md
2
51.md
@@ -52,7 +52,7 @@ Aside from their main identifier, the `"d"` tag, sets can optionally have a `"ti
|
||||
| --- | --- | --- | --- |
|
||||
| Follow sets | 30000 | categorized groups of users a client may choose to check out in different circumstances | `"p"` (pubkeys) |
|
||||
| Relay sets | 30002 | user-defined relay groups the user can easily pick and choose from during various operations | `"relay"` (relay URLs) |
|
||||
| Bookmark sets | 30003 | user-defined bookmarks categories , for when bookmarks must be in labeled separate groups | `"e"` (kind:1 notes), `"a"` (kind:30023 articles) |
|
||||
| Bookmark sets | 30003 | user-defined bookmarks categories , for when bookmarks must be in labeled separate groups | `"e"` (kind:1 notes), `"a"` (kind:30023 articles), `"t"` (hashtags), `"r"` (URLs) |
|
||||
| Curation sets | 30004 | groups of articles picked by users as interesting and/or belonging to the same category | `"a"` (kind:30023 articles), `"e"` (kind:1 notes) |
|
||||
| Curation sets | 30005 | groups of videos picked by users as interesting and/or belonging to the same category | `"e"` (kind:21 videos) |
|
||||
| Kind mute sets | 30007 | mute pubkeys by kinds<br>`"d"` tag MUST be the kind string | `"p"` (pubkeys) |
|
||||
|
||||
2
59.md
2
59.md
@@ -99,8 +99,6 @@ AUTH, and refuse to serve wrapped events to non-recipients.
|
||||
|
||||
When adding expiration tags to both `seal` and `gift wrap` layers, implementations SHOULD use independent random timestamps for each layer. Using different `created_at` values increases timing variance and helps protect against metadata correlation attacks.
|
||||
|
||||
Since signing keys are random, relays SHOULD delete `kind 1059` events whose p-tag matches the signer of
|
||||
[NIP-09](09.md) deletions or [NIP-62](62.md) vanish requests.
|
||||
|
||||
## An Example
|
||||
|
||||
|
||||
8
69.md
8
69.md
@@ -41,8 +41,7 @@ Events are [addressable events](01.md#kinds) and use `38383` as event kind, a p2
|
||||
["name", "Nakamoto"],
|
||||
["g", "<geohash>"],
|
||||
["bond", "0"],
|
||||
["expires_at", "1719391096"],
|
||||
["expiration", "1719995896"],
|
||||
["expiration", "1719391096"],
|
||||
["y", "lnp2pbot"],
|
||||
["z", "order"]
|
||||
],
|
||||
@@ -56,7 +55,7 @@ Events are [addressable events](01.md#kinds) and use `38383` as event kind, a p2
|
||||
- `d` < Order ID >: A unique identifier for the order.
|
||||
- `k` < Order type >: `sell` or `buy`.
|
||||
- `f` < Currency >: The asset being traded, using the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) standard.
|
||||
- `s` < Status >: `pending`, `canceled`, `in-progress`, `success`, `expired`.
|
||||
- `s` < Status >: `pending`, `canceled`, `in-progress`, `success`.
|
||||
- `amt` < Amount >: The amount of Bitcoin to be traded, the amount is defined in satoshis, if `0` means that the amount of satoshis will be obtained from a public API after the taker accepts the order.
|
||||
- `fa` < Fiat amount >: The fiat amount being traded, for range orders two values are expected, the minimum and maximum amount.
|
||||
- `pm` < Payment method >: The payment method used for the trade, if the order has multiple payment methods, they should be separated by a comma.
|
||||
@@ -68,8 +67,7 @@ Events are [addressable events](01.md#kinds) and use `38383` as event kind, a p2
|
||||
- `name` [Name]: The name of the maker.
|
||||
- `g` [Geohash]: The geohash of the operation, it can be useful in a face to face trade.
|
||||
- `bond` [Bond]: The bond amount, the bond is a security deposit that both parties must pay.
|
||||
- `expires_at` < Expires At\>: The expiration date of the event being published in `pending` status, after this time the event status SHOULD be changed to `expired`.
|
||||
- `expiration` < Expiration\>: The expiration date of the event, after this time the relay SHOULD delete it ([NIP-40](40.md)).
|
||||
- `expiration` < Expiration\>: The expiration date of the order ([NIP-40](40.md)).
|
||||
- `y` < Platform >: The platform that created the order.
|
||||
- `z` < Document >: `order`.
|
||||
|
||||
|
||||
137
BE.md
137
BE.md
@@ -1,137 +0,0 @@
|
||||
NIP-BE
|
||||
======
|
||||
|
||||
Nostr BLE Communications Protocol
|
||||
---------------------------------
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
This NIP specifies how Nostr apps can use BLE to communicate and synchronize with each other. The BLE protocol follows a client-server pattern, so this NIP emulates the WS structure in a similar way, but with some adaptations to its limitations.
|
||||
|
||||
## Device advertisement
|
||||
A device advertises itself with:
|
||||
- Service UUID: `0000180f-0000-1000-8000-00805f9b34fb`
|
||||
- Data: Device UUID in ByteArray format
|
||||
|
||||
## GATT service
|
||||
The device exposes a Nordic UART Service with the following characteristics:
|
||||
|
||||
1. Write Characteristic
|
||||
- UUID: `87654321-0000-1000-8000-00805f9b34fb`
|
||||
- Properties: Write
|
||||
|
||||
2. Read Characteristic
|
||||
- UUID: `12345678-0000-1000-8000-00805f9b34fb`
|
||||
- Properties: Notify, Read
|
||||
|
||||
## Role assignment
|
||||
|
||||
When one device initially finds another advertising the service, it will read the service's data to get the device UUID and compare it with its own advertised device UUID. For this communication, the device with the highest ID will take the role of GATT Server (Relay), the other will be considered the GATT Client (Client) and will proceed to establish the connection.
|
||||
|
||||
For devices whose purpose will require a single role, its device UUID will always be:
|
||||
|
||||
- GATT Server: `FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF`
|
||||
- GATT Client: `00000000-0000-0000-0000-000000000000`
|
||||
|
||||
## Messages
|
||||
|
||||
All messages will follow [NIP-01](/01.md) message structure. For a given message, a compression stream (DEFLATE) is applied to the message to generate a byte array. Depending on the BLE version, the byte array can be too large for a single message (20-23 bytes in BLE 4.2, 256 bytes in BLE > 4.2). In that case, this byte array is split into any number of batches following the structure:
|
||||
|
||||
```
|
||||
[batch index (first 2 bytes)][batch n][is last batch (last byte)]
|
||||
```
|
||||
After reception of all batches, the other device can then join them and decompress. To ensure reliability, only 1 message will be read/written at a time. MTU can be negotiated in advance. The maximum size for a message is 64KB; bigger messages will be rejected.
|
||||
|
||||
## Examples
|
||||
|
||||
This example implements a function to split and compress a byte array into chunks, as well as another function to join and decompress them in order to obtain the initial result:
|
||||
|
||||
```kotlin
|
||||
fun splitInChunks(message: ByteArray): Array<ByteArray> {
|
||||
val chunkSize = 500 // define the chunk size
|
||||
var byteArray = compressByteArray(message)
|
||||
val numChunks = (byteArray.size + chunkSize - 1) / chunkSize // calculate the number of chunks
|
||||
var chunkIndex = 0
|
||||
val chunks = Array(numChunks) { ByteArray(0) }
|
||||
|
||||
for (i in 0 until numChunks) {
|
||||
val start = i * chunkSize
|
||||
val end = minOf((i + 1) * chunkSize, byteArray.size)
|
||||
val chunk = byteArray.copyOfRange(start, end)
|
||||
|
||||
// add chunk index to the first 2 bytes and last chunk flag to the last byte
|
||||
val chunkWithIndex = ByteArray(chunk.size + 2)
|
||||
chunkWithIndex[0] = chunkIndex.toByte() // chunk index
|
||||
chunk.copyInto(chunkWithIndex, 1)
|
||||
chunkWithIndex[chunkWithIndex.size - 1] = numChunks.toByte()
|
||||
|
||||
// store the chunk in the array
|
||||
chunks[i] = chunkWithIndex
|
||||
|
||||
chunkIndex++
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
fun joinChunks(chunks: Array<ByteArray>): ByteArray {
|
||||
val sortedChunks = chunks.sortedBy { it[0] }
|
||||
var reassembledByteArray = ByteArray(0)
|
||||
for (chunk in sortedChunks) {
|
||||
val chunkData = chunk.copyOfRange(1, chunk.size - 1)
|
||||
reassembledByteArray = reassembledByteArray.copyOf(reassembledByteArray.size + chunkData.size)
|
||||
chunkData.copyInto(reassembledByteArray, reassembledByteArray.size - chunkData.size)
|
||||
}
|
||||
|
||||
return decompressByteArray(reassembledByteArray)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Workflows
|
||||
|
||||
### Client to relay
|
||||
|
||||
- Any message the client wants to send to a relay will be a write message.
|
||||
- Any message the client receives from a relay will be a read message.
|
||||
|
||||
### Relay to client
|
||||
|
||||
The relay should notify the client about any new event matching subscription's filters by using the Notify action of the Read Characteristic. After that, the client can proceed to read messages from the relay.
|
||||
|
||||
### Device synchronization
|
||||
|
||||
Given the nature of BLE, it is expected that the direct connection between two devices might be extremely intermittent, with gaps of hours or even days. That's why it's crucial to define a synchronization process by following [NIP-77](./77.md) but with an adaptation to the limitations of the technology.
|
||||
|
||||
After two devices have successfully connected and established the Client-Server roles, the devices will use half-duplex communication to intermittently send and receive messages.
|
||||
|
||||
#### Half-duplex synchronization
|
||||
|
||||
Right after the 2 devices connect, the Client starts the workflow by sending the first message.
|
||||
|
||||
1. Client - Writes ["NEG-OPEN"](/77.md#initial-message-client-to-relay) message.
|
||||
2. Server - Sends `write-success`.
|
||||
3. Client - Sends `read-message`.
|
||||
4. Server - Responds with ["NEG-MSG"](./77.md#subsequent-messages-bidirectional) message.
|
||||
5. Client -
|
||||
1. If the Client has messages missing on the Server, it writes one `EVENT`.
|
||||
2. If the Client doesn't have any messages missing on the Server, it writes `EOSE`. In this case, subsequent messages to the Server will be empty while the Server claims to have more notes for the Client.
|
||||
6. Server - Sends `write-success`.
|
||||
7. Client - Sends `read-message`.
|
||||
8. Server -
|
||||
1. If the Server has messages missing on the Client, it responds with one `EVENT`.
|
||||
2. If the Client doesn't have any messages missing on the Server, it responds with `EOSE`. In this case, subsequent responses to the Client will be empty.
|
||||
9. If the Client detects that the devices are not synchronized yet, jump to step 5.
|
||||
10. After the two devices detect that there are no more missing events on both ends, the workflow will pause at this point.
|
||||
|
||||
#### Half-duplex event spread
|
||||
|
||||
While two devices are connected and synchronized, it might happen that one of them receives a new message from another connected peer. Devices MUST keep track of which notes have been sent to its peers while they are connected. If the newly received event is detected as missing in one of the connected and synchronized peers:
|
||||
|
||||
1. If the peer is a Server:
|
||||
1. Client - It writes the `EVENT`.
|
||||
2. Server - Sends `write-success`.
|
||||
2. If the peer is a Client:
|
||||
1. Server - It will send an empty notification to the Client.
|
||||
2. Client - Sends `read-message`.
|
||||
3. Server - Responds with the `EVENT`.
|
||||
4
C0.md
4
C0.md
@@ -23,9 +23,9 @@ The `.content` field contains the actual code snippet text.
|
||||
- `extension` - File extension (without the dot). Examples: "js", "py", "rs"
|
||||
- `description` - Brief description of what the code does
|
||||
- `runtime` - Runtime or environment specification (e.g., "node v18.15.0", "python 3.11")
|
||||
- `license` - License under which the code (along with any related data contained within the event, when available, such as the description) is shared. This MUST be a standard [SPDX](https://spdx.org/licenses/) short identifier (e.g., "MIT", "GPL-3.0-or-later", "Apache-2.0") when available. An additional parameter containing a reference to the actual text of the license MAY be provided. This tag can be repeated, to indicate multi-licensing, allowing recipients to use the code under any license of choosing among the referenced ones
|
||||
- `license` - License under which the code is shared (e.g., "MIT", "GPL-3.0", "Apache-2.0")
|
||||
- `dep` - Dependency required for the code to run (can be repeated)
|
||||
- `repo` - Reference to a repository where this code originates. This MUST be a either standard URL or, alternatively, the address of a [NIP-34](34.md) Git repository annoucement event in the form `"30617:<32-bytes hex a pubkey>:<d tag value>"`. If a repository announcement is referenced, a recommended relay URL where to find the event should be provided as an additional parameter
|
||||
- `repo` - Reference to a repository where this code originates
|
||||
|
||||
## Format
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
|
||||
- [NIP-39: External Identities in Profiles](39.md)
|
||||
- [NIP-40: Expiration Timestamp](40.md)
|
||||
- [NIP-42: Authentication of clients to relays](42.md)
|
||||
- [NIP-43: Relay Access Metadata and Requests](43.md)
|
||||
- [NIP-44: Encrypted Payloads (Versioned)](44.md)
|
||||
- [NIP-45: Counting results](45.md)
|
||||
- [NIP-46: Nostr Remote Signing](46.md)
|
||||
@@ -105,7 +104,6 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
|
||||
- [NIP-A0: Voice Messages](A0.md)
|
||||
- [NIP-B0: Web Bookmarks](B0.md)
|
||||
- [NIP-B7: Blossom](B7.md)
|
||||
- [NIP-BE: Nostr BLE Communications Protocol](BE.md)
|
||||
- [NIP-C0: Code Snippets](C0.md)
|
||||
- [NIP-C7: Chats](C7.md)
|
||||
- [NIP-EE: E2EE Messaging using MLS Protocol](EE.md)
|
||||
@@ -184,8 +182,6 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
|
||||
| `7376` | Cashu Wallet History | [60](60.md) |
|
||||
| `7516` | Geocache log | [geocaching][geocaching] |
|
||||
| `7517` | Geocache proof of find | [geocaching][geocaching] |
|
||||
| `8000` | Add User | [43](43.md) |
|
||||
| `8001` | Remove User | [43](43.md) |
|
||||
| `9000`-`9030` | Group Control Events | [29](29.md) |
|
||||
| `9041` | Zap Goal | [75](75.md) |
|
||||
| `9321` | Nutzap | [61](61.md) |
|
||||
@@ -217,7 +213,6 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
|
||||
| `10377` | Proxy Announcement | [Nostr Epoxy][nostr-epoxy] |
|
||||
| `11111` | Transport Method Announcement | [Nostr Epoxy][nostr-epoxy] |
|
||||
| `13194` | Wallet Info | [47](47.md) |
|
||||
| `13534` | Membership Lists | [43](43.md) |
|
||||
| `17375` | Cashu Wallet Event | [60](60.md) |
|
||||
| `21000` | Lightning Pub RPC | [Lightning.Pub][lnpub] |
|
||||
| `22242` | Client Authentication | [42](42.md) |
|
||||
@@ -226,9 +221,6 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos
|
||||
| `24133` | Nostr Connect | [46](46.md) |
|
||||
| `24242` | Blobs stored on mediaservers | [Blossom][blossom] |
|
||||
| `27235` | HTTP Auth | [98](98.md) |
|
||||
| `28934` | Join Request | [43](43.md) |
|
||||
| `28935` | Invite Request | [43](43.md) |
|
||||
| `28936` | Leave Request | [43](43.md) |
|
||||
| `30000` | Follow sets | [51](51.md) |
|
||||
| `30001` | Generic lists | 51 (deprecated) |
|
||||
| `30002` | Relay sets | [51](51.md) |
|
||||
|
||||
358
XX.md
Normal file
358
XX.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# NIP-XX: ProofMode - Cryptographic Video Verification
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
## Abstract
|
||||
|
||||
This NIP defines a standard for attaching cryptographic proof manifests to video events (NIP-71) to enable verification of video authenticity, recording continuity, and device integrity. ProofMode allows viewers to verify that a video was recorded on a specific device at a specific time without editing or tampering.
|
||||
|
||||
## Motivation
|
||||
|
||||
Social media platforms are increasingly vulnerable to deepfakes, edited videos, and synthetic media. While blockchain timestamping exists, it doesn't prove video continuity or prevent frame-level manipulation. ProofMode solves this by:
|
||||
|
||||
1. **Frame-level verification** - SHA256 hashes of captured frames prove recording continuity
|
||||
2. **Hardware attestation** - iOS App Attest and Android Play Integrity verify the recording device
|
||||
3. **Cryptographic signing** - PGP signatures ensure manifest authenticity
|
||||
4. **Tamper detection** - Any edit to the video invalidates the proof chain
|
||||
5. **Segment tracking** - Recording pauses are documented with sensor data
|
||||
|
||||
## Specification
|
||||
|
||||
### Event Tags
|
||||
|
||||
ProofMode data is attached to video events (typically Kind 34236) using the following tags:
|
||||
|
||||
#### Required Tags
|
||||
|
||||
- `["verification", "<level>"]` - Verification level (see Verification Levels below)
|
||||
- `["proofmode", "<manifest_json>"]` - Complete ProofManifest as compact JSON
|
||||
|
||||
#### Optional Tags
|
||||
|
||||
- `["device_attestation", "<token>"]` - Hardware attestation token from iOS App Attest or Android Play Integrity
|
||||
- `["pgp_fingerprint", "<fingerprint>"]` - PGP public key fingerprint used to sign the manifest
|
||||
|
||||
### Verification Levels
|
||||
|
||||
The `verification` tag indicates the strength of cryptographic proof:
|
||||
|
||||
- `verified_mobile` - Highest level: has device attestation + PGP signature + complete manifest
|
||||
- `verified_web` - Medium level: has PGP signature + complete manifest (no hardware attestation)
|
||||
- `basic_proof` - Low level: has proof data but no cryptographic signature
|
||||
- `unverified` - No meaningful proof data
|
||||
|
||||
### ProofManifest Structure
|
||||
|
||||
The `proofmode` tag contains a JSON object with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "<unique_session_id>",
|
||||
"challengeNonce": "<16_char_nonce>",
|
||||
"vineSessionStart": "<ISO8601_timestamp>",
|
||||
"vineSessionEnd": "<ISO8601_timestamp>",
|
||||
"totalDuration": 6500,
|
||||
"recordingDuration": 6000,
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "<segment_id>",
|
||||
"startTime": "<ISO8601_timestamp>",
|
||||
"endTime": "<ISO8601_timestamp>",
|
||||
"duration": 3000,
|
||||
"frameHashes": [
|
||||
"<sha256_hash_1>",
|
||||
"<sha256_hash_2>",
|
||||
"..."
|
||||
],
|
||||
"frameTimestamps": [
|
||||
"<ISO8601_timestamp_1>",
|
||||
"<ISO8601_timestamp_2>",
|
||||
"..."
|
||||
],
|
||||
"sensorData": {
|
||||
"accelerometer": {"x": 0.1, "y": 0.2, "z": 9.8},
|
||||
"gyroscope": {"x": 0.01, "y": 0.02, "z": 0.01}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pauseProofs": [
|
||||
{
|
||||
"startTime": "<ISO8601_timestamp>",
|
||||
"endTime": "<ISO8601_timestamp>",
|
||||
"duration": 500,
|
||||
"sensorData": {
|
||||
"timestamp": "<ISO8601_timestamp>",
|
||||
"accelerometer": {"x": 0.1, "y": 0.2, "z": 9.8},
|
||||
"gyroscope": {"x": 0.01, "y": 0.02, "z": 0.01},
|
||||
"magnetometer": {"x": 45.0, "y": 12.0, "z": -30.0},
|
||||
"light": 150.0
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"timestamp": "<ISO8601_timestamp>",
|
||||
"interactionType": "touch",
|
||||
"coordinates": {"x": 180, "y": 640},
|
||||
"pressure": 0.5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"interactions": [
|
||||
{
|
||||
"timestamp": "<ISO8601_timestamp>",
|
||||
"interactionType": "start|stop|touch",
|
||||
"coordinates": {"x": 180, "y": 640},
|
||||
"pressure": 0.5,
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"finalVideoHash": "<sha256_hash_of_complete_video>",
|
||||
"deviceAttestation": {
|
||||
"token": "<platform_specific_attestation_token>",
|
||||
"platform": "iOS|Android|Web",
|
||||
"deviceId": "<device_identifier>",
|
||||
"isHardwareBacked": true,
|
||||
"createdAt": "<ISO8601_timestamp>",
|
||||
"challenge": "<challenge_nonce>",
|
||||
"metadata": {
|
||||
"attestationType": "app_attest|play_integrity|fallback",
|
||||
"deviceInfo": {
|
||||
"platform": "iOS",
|
||||
"model": "iPhone 15 Pro",
|
||||
"version": "17.0",
|
||||
"manufacturer": "Apple"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pgpSignature": {
|
||||
"signature": "-----BEGIN PGP SIGNATURE-----\n...\n-----END PGP SIGNATURE-----",
|
||||
"publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"publicKeyFingerprint": "1A2B3C4D5E6F7890..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
#### Core Fields
|
||||
- `sessionId` - Unique identifier for the recording session
|
||||
- `challengeNonce` - Random nonce generated at session start, used in device attestation to prevent replay attacks
|
||||
- `vineSessionStart` / `vineSessionEnd` - Recording session boundaries
|
||||
- `totalDuration` - Total elapsed time in milliseconds (including pauses)
|
||||
- `recordingDuration` - Actual recording time in milliseconds (excluding pauses)
|
||||
|
||||
#### Segments
|
||||
Recording can be paused and resumed, creating multiple segments. Each segment contains:
|
||||
- `segmentId` - Unique segment identifier
|
||||
- `startTime` / `endTime` - Segment boundaries
|
||||
- `frameHashes` - Array of SHA256 hashes of captured video frames
|
||||
- `frameTimestamps` - Timestamps when each frame was captured (optional)
|
||||
- `sensorData` - Device sensor readings during recording (optional)
|
||||
|
||||
#### Pause Proofs
|
||||
When recording is paused, sensor data is collected to prove device continuity:
|
||||
- `startTime` / `endTime` - Pause boundaries
|
||||
- `sensorData` - Sensor readings during pause (accelerometer, gyroscope, magnetometer, light)
|
||||
- `interactions` - User touch/tap events during pause
|
||||
|
||||
#### Interactions
|
||||
User interactions recorded throughout the session:
|
||||
- `timestamp` - When interaction occurred
|
||||
- `interactionType` - Type of interaction (start, stop, touch)
|
||||
- `coordinates` - Screen coordinates of interaction
|
||||
- `pressure` - Touch pressure (optional)
|
||||
|
||||
#### Final Video Hash
|
||||
- `finalVideoHash` - SHA256 hash of the complete rendered video file
|
||||
|
||||
#### Device Attestation
|
||||
Platform-specific hardware attestation proving the device is genuine:
|
||||
- **iOS**: Uses App Attest API (iOS 14+)
|
||||
- **Android**: Uses Play Integrity API
|
||||
- **Web/Other**: Fallback software attestation
|
||||
|
||||
Fields:
|
||||
- `token` - Platform-specific attestation token
|
||||
- `platform` - Operating system (iOS, Android, Web)
|
||||
- `deviceId` - Device identifier
|
||||
- `isHardwareBacked` - Whether attestation uses hardware security module
|
||||
- `challenge` - Challenge nonce used in attestation (matches `challengeNonce`)
|
||||
- `metadata` - Platform-specific attestation details
|
||||
|
||||
#### PGP Signature
|
||||
Cryptographic signature of the entire manifest:
|
||||
- `signature` - PGP signature in ASCII-armored format
|
||||
- `publicKey` - PGP public key in ASCII-armored format
|
||||
- `publicKeyFingerprint` - Key fingerprint for quick lookup
|
||||
|
||||
## Implementation
|
||||
|
||||
### Recording Phase
|
||||
|
||||
1. **Start Session**
|
||||
- Generate unique `sessionId` and `challengeNonce`
|
||||
- Request hardware device attestation with challenge nonce
|
||||
- Initialize ProofMode session
|
||||
|
||||
2. **Capture Frames**
|
||||
- During recording, periodically capture video frames
|
||||
- Generate SHA256 hash of each frame
|
||||
- Store frame hashes with timestamps
|
||||
- Optionally collect sensor data (accelerometer, gyroscope, etc.)
|
||||
|
||||
3. **Handle Pauses**
|
||||
- When recording pauses, stop current segment
|
||||
- Begin collecting pause proof data (sensor readings, interactions)
|
||||
- When resuming, start new segment
|
||||
|
||||
4. **Finalize Session**
|
||||
- Stop recording and close final segment
|
||||
- Hash complete video file
|
||||
- Compile ProofManifest with all segments, pauses, and interactions
|
||||
- Sign manifest with PGP private key
|
||||
- Attach ProofManifest to video event as tags
|
||||
|
||||
### Verification Phase
|
||||
|
||||
To verify a ProofMode video, clients should:
|
||||
|
||||
1. **Extract ProofManifest**
|
||||
- Parse `proofmode` tag from video event
|
||||
- Extract `deviceAttestation` and `pgpSignature` from separate tags
|
||||
|
||||
2. **Verify PGP Signature**
|
||||
- Extract PGP public key from manifest
|
||||
- Verify signature of manifest JSON
|
||||
- Check public key fingerprint matches `pgp_fingerprint` tag
|
||||
|
||||
3. **Verify Device Attestation** (if present)
|
||||
- Validate attestation token against platform-specific APIs
|
||||
- Verify challenge nonce matches manifest `challengeNonce`
|
||||
- Check attestation timestamp is recent (within 24 hours of recording)
|
||||
|
||||
4. **Verify Frame Hashes** (advanced)
|
||||
- Re-encode video to extract individual frames
|
||||
- Generate SHA256 hashes of extracted frames
|
||||
- Compare against hashes in manifest segments
|
||||
- Verify frame count and timestamps match recording duration
|
||||
|
||||
5. **Verify Recording Continuity**
|
||||
- Check that segment timestamps are contiguous
|
||||
- Verify pause durations match gaps between segments
|
||||
- Validate total recording duration matches video length
|
||||
|
||||
6. **Display Verification Badge**
|
||||
- `verified_mobile` - Show "Verified" badge with hardware attestation icon
|
||||
- `verified_web` - Show "Signed" badge
|
||||
- `basic_proof` - Show "Basic Proof" indicator
|
||||
- `unverified` - No badge or "Unverified" indicator
|
||||
|
||||
## Example Event
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 34236,
|
||||
"pubkey": "...",
|
||||
"created_at": 1730326800,
|
||||
"tags": [
|
||||
["d", "unique-video-identifier"],
|
||||
["title", "My Verified Video"],
|
||||
["url", "https://media.example.com/video.mp4", "720x1280"],
|
||||
["thumb", "https://media.example.com/thumb.jpg", "720x1280"],
|
||||
["duration", "6"],
|
||||
["verification", "verified_mobile"],
|
||||
["proofmode", "{\"sessionId\":\"session_1730326800000_1234\",\"challengeNonce\":\"a1b2c3d4e5f6789\",\"vineSessionStart\":\"2025-10-30T10:00:00.000Z\",\"vineSessionEnd\":\"2025-10-30T10:00:06.500Z\",\"totalDuration\":6500,\"recordingDuration\":6000,\"segments\":[{\"segmentId\":\"segment_1\",\"startTime\":\"2025-10-30T10:00:00.000Z\",\"endTime\":\"2025-10-30T10:00:06.000Z\",\"frameHashes\":[\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"]}],\"pauseProofs\":[],\"interactions\":[{\"timestamp\":\"2025-10-30T10:00:00.000Z\",\"interactionType\":\"start\",\"coordinates\":{\"x\":180,\"y\":640}}],\"finalVideoHash\":\"d4e5f6a7b8c9...\"}"],
|
||||
["device_attestation", "AAABBBCCC..."],
|
||||
["pgp_fingerprint", "1A2B3C4D5E6F7890..."]
|
||||
],
|
||||
"content": "Check out this verified video!",
|
||||
"sig": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
|
||||
ProofMode protects against:
|
||||
- ✅ **Post-recording video editing** - Frame hashes detect any modifications
|
||||
- ✅ **Deepfakes and synthetic videos** - Hardware attestation proves real device
|
||||
- ✅ **Timestamp manipulation** - Device attestation includes trusted timestamps
|
||||
- ✅ **Replay attacks** - Challenge nonce prevents reuse of attestations
|
||||
|
||||
ProofMode does NOT protect against:
|
||||
- ❌ **Screen recording** - A user can screen-record another video
|
||||
- ❌ **Camera lens manipulation** - Physical objects placed in front of camera
|
||||
- ❌ **Compromised devices** - Rooted/jailbroken devices may forge attestations
|
||||
- ❌ **Social engineering** - User can intentionally create misleading content
|
||||
|
||||
### Privacy Considerations
|
||||
|
||||
- **Device Identifiers**: The `deviceId` field may be sensitive. Clients should:
|
||||
- Hash or truncate device IDs before publishing
|
||||
- Allow users to opt-out of device attestation
|
||||
- Clearly indicate when ProofMode is active
|
||||
|
||||
- **Sensor Data**: Accelerometer and gyroscope data may reveal user location or behavior. Clients should:
|
||||
- Allow disabling sensor data collection
|
||||
- Sanitize or omit sensitive sensor readings
|
||||
- Aggregate sensor data to reduce precision
|
||||
|
||||
- **PGP Keys**: Users should be able to:
|
||||
- Rotate PGP keys periodically
|
||||
- Revoke compromised keys
|
||||
- Use separate keys for different purposes
|
||||
|
||||
### Verification Best Practices
|
||||
|
||||
Verifying clients should:
|
||||
|
||||
1. **Always check PGP signature** - This is the minimum verification
|
||||
2. **Validate device attestation** when present - But gracefully handle missing/invalid attestations
|
||||
3. **Display verification level prominently** - Users should understand confidence level
|
||||
4. **Cache verification results** - Re-verification is expensive
|
||||
5. **Handle expired attestations** - Attestations may expire after 24-48 hours
|
||||
6. **Warn on missing proofs** - But don't assume malice if ProofMode is absent
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
OpenVine provides a complete reference implementation:
|
||||
- **Recording**: `ProofModeSessionService` in OpenVine mobile app
|
||||
- **Publishing**: `VideoEventPublisher` adds ProofMode tags to Nostr events
|
||||
- **Verification**: `ProofModeHelpers` and verification UI components
|
||||
|
||||
Source: https://github.com/openvine/openvine
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
This NIP is fully backwards compatible:
|
||||
- Events without ProofMode tags are treated as unverified
|
||||
- Older clients ignore ProofMode tags
|
||||
- ProofMode is opt-in - videos without it still work normally
|
||||
|
||||
## Future Extensions
|
||||
|
||||
Possible future enhancements:
|
||||
|
||||
1. **Witness Signatures** - Multiple devices co-sign the same recording
|
||||
2. **Location Proofs** - GPS coordinates with cryptographic verification
|
||||
3. **Biometric Proof** - Prove human presence during recording
|
||||
4. **Chain of Custody** - Track video transfer and handling
|
||||
5. **Selective Disclosure** - Zero-knowledge proofs for privacy-preserving verification
|
||||
|
||||
## References
|
||||
|
||||
- [NIP-01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [NIP-71: Video Events](https://github.com/nostr-protocol/nips/blob/master/71.md)
|
||||
- [iOS App Attest](https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity)
|
||||
- [Android Play Integrity](https://developer.android.com/google/play/integrity)
|
||||
- [ProofMode Original Project](https://proofmode.org)
|
||||
|
||||
## Authors
|
||||
|
||||
- Evan Henshaw-Plath (Rabble)
|
||||
|
||||
## License
|
||||
|
||||
This NIP is released into the public domain.
|
||||
|
||||
Reference in New Issue
Block a user