Files
nostr_quantum_preparation/nip_proposal.md
T

309 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NIP-QR: Post-Quantum Key-Link Attestations
`draft` `optional`
This NIP defines a way for a Nostr user to **link** their existing secp256k1 identity to one or more post-quantum (PQ) public keys, **without** changing how events are signed, **without** requiring the network to agree on a single PQ algorithm, and **without** users abandoning their identity or social graph.
It is intentionally additive. It does not modify NIP-01's event format, signature scheme, or relay behavior. Legacy clients continue to work unchanged; PQ-aware clients gain the ability to verify a pre-quantum, Bitcoin-anchored link between an identity and a set of PQ keys.
## Motivation
### The threat
Nostr's identity, authentication, and encryption (NIP-04, NIP-44) all rest on secp256k1. Shor's algorithm breaks secp256k1 in polynomial time on a sufficiently large fault-tolerant quantum computer. An attacker who has recorded any Nostr event can recover the private key, forge signatures, impersonate the user, and decrypt every NIP-04/NIP-44 message ever sent to them. NIP-44 itself states: "No post-quantum security."
This includes a **collect-now-decrypt-later** threat: encrypted messages on public relays can be stored today and decrypted once a quantum computer exists. The symmetric ciphers are quantum-safe; the secp256k1 ECDH key agreement is not.
### Why Bitcoin's hash-the-pubkey trick does not help Nostr
Bitcoin has partial quantum resistance because addresses are hashes of pubkeys and pubkeys are only revealed at spend time. Nostr cannot do this: the pubkey *is* the identity, it must be revealed on every event for Schnorr verification, and it is reused across thousands of events over years. Hashing the pubkey would give zero seconds of quantum resistance — the pubkey is revealed the moment the first event is published.
The fix is post-quantum signatures and post-quantum key agreement, not hashing the pubkey. This NIP describes how to *link* PQ keys to an existing identity so that the link is established **now**, while secp256k1 can still be trusted, and is later verifiable even after secp256k1 is broken.
### Why a link, not a swap
Replacing secp256k1 event signatures with PQ signatures (as proposed elsewhere) requires the whole network to agree on an algorithm, change relay validation, and re-onboard every user. This NIP takes a different approach: the user publishes a **one-time attestation** that links their secp256k1 identity to a set of PQ keys. The attestation is signed by the existing identity (valid now) and by each PQ signature scheme (valid forever). It is anchored to the Bitcoin blockchain via OpenTimestamps (NIP-03) so it cannot be backdated.
After a quantum break, an attacker who forges the secp256k1 key can publish a *fraudulent* link event, but they cannot produce an OpenTimestamps proof anchoring it to a Bitcoin block earlier than the real one. The real link is cryptographically distinguishable by its earlier Bitcoin anchor.
### Why multiple schemes
The PQ standardization landscape is still young. SIKE was a NIST finalist broken in 2022 by a classical algorithm that ran in an hour on a laptop. Picking a single PQ algorithm now is risky. This NIP allows a user to link to **multiple** PQ schemes simultaneously. If one is later broken, the others remain valid. Clients verify whichever subset they support. No community-wide algorithm consensus is required to proceed.
## Design summary
A user with an existing Nostr identity (the **attesting identity**) publishes two events:
1. A **kind 1 announcement** — a human-readable attestation whose `content` is signed by each PQ signature scheme, with the PQ public keys and signatures carried in `algorithm` tags.
2. A **kind 9999 proof carrier** — a non-replaceable event that embeds the full signed kind 1 event as JSON content and carries an OpenTimestamps proof anchoring the kind 1 event to a Bitcoin block. Upgrades (pending → confirmed) publish a new kind 9999 event with an `upgrade_of` tag referencing the previous one; both remain on relays permanently.
The PQ keys are derived deterministically from a BIP39 seed phrase via BIP32, at fixed child indices under the NIP-06 base path. This means a user who has backed up their seed phrase can always recover the same PQ keys, on any conforming implementation.
```mermaid
flowchart TD
SEED[BIP39 seed - recovery root] --> SECP[secp256k1 keypair - child 0]
SEED --> PQ[PQ keypairs - children 1..8]
SECP --> K1[Kind 1 announcement - attesting identity signs]
PQ --> K1
K1 --> OTS[OpenTimestamps anchor to Bitcoin block]
OTS --> K9999[Kind 9999 proof carrier - carries OTS proof]
K9999 --> RELAY[Published to relays]
```
## PQ key derivation from a BIP39 seed
All PQ keys are derived from a BIP39 seed via **BIP32 hierarchical deterministic derivation**, the same standard NIP-06 uses for secp256k1 keys. PQ keys live at fixed child indices under the NIP-06 base path.
**Base path:** `m/44'/1237'/0'/0/` (NIP-06 account 0, change 0)
| Child index/indices | Algorithm | Seed length needed | BIP32 path | Derivation |
|---|---|---|---|---|
| 0 | secp256k1 (NIP-06) | 32 bytes | `m/44'/1237'/0'/0/0` | Standard BIP32; private key used directly |
| 1 | ML-DSA-44 | 32 bytes | `m/44'/1237'/0'/0/1` | Single child; 32-byte private key is the PQ seed |
| 2 | ML-DSA-65 | 32 bytes | `m/44'/1237'/0'/0/2` | Single child; 32-byte private key is the PQ seed |
| 3 + 4 | SLH-DSA-128s | 48 bytes | `m/44'/1237'/0'/0/3` + `m/44'/1237'/0'/0/4` | Two children concatenated (64 bytes), first 48 used |
| 5 + 6 | Falcon-512 | 48 bytes | `m/44'/1237'/0'/0/5` + `m/44'/1237'/0'/0/6` | Two children concatenated (64 bytes), first 48 used |
| 7 + 8 | ML-KEM-768 | 64 bytes | `m/44'/1237'/0'/0/7` + `m/44'/1237'/0'/0/8` | Two children concatenated (64 bytes) |
### Concatenation and truncation rule (normative)
BIP32 child derivation produces exactly 32 bytes per child. Some PQ algorithms need more than 32 bytes for their `keygen()` seed:
- For a 48-byte seed, derive **two** children, concatenate them in **child-index order** (lower index first) to produce 64 bytes, and use the **first 48 bytes**.
- For a 64-byte seed, derive **two** children, concatenate them in **child-index order**, and use all 64 bytes.
Implementations MUST use the first N bytes after concatenation in ascending child-index order. A future implementer who takes the *last* 48 bytes, or concatenates in the opposite order, will produce different keys and break seed-phrase recoverability. This rule is pinned here precisely so that two independent implementations produce the same PQ keys from the same seed.
### Why BIP32 and not HKDF
BIP32 is the HD-wallet standard already used by NIP-06. Using BIP32 paths for PQ keys means:
- Consistency with NIP-06 — PQ keys are derived the same way as secp256k1 keys, at different child indices.
- Wallet compatibility — HD wallet infrastructure (hardware wallets, Amber-style signers, bunkers) can derive these same paths.
- Compartmentalization — hardened derivation at the account level means breaking one leaf key does not compromise siblings; the parent chain code is a secret symmetric value that a quantum computer cannot recover from a leaf public key.
### Seed phrase entropy
A 12-word BIP39 mnemonic carries 128 bits of entropy (~64 bits of post-quantum security under Grover's quadratic speedup). A 24-word mnemonic carries 256 bits (~128 bits PQ). Implementations SHOULD support 24-word mnemonics for users concerned about quantum attacks on the seed entropy itself. The dominant risk to seed phrases remains classical (theft, phishing), not quantum.
## The kind 1 announcement event
A regular Nostr text note (kind 1) that serves as a public, human-readable attestation. It is signed with the user's **existing** secp256k1 identity (the attesting identity), via NIP-07 (`window.nostr.signEvent`) or any conforming signer.
### Content
The `content` is a human-readable attestation statement. The exact wording is not normative, but it MUST include the attesting identity's npub and hex pubkey, the Bitcoin block height at signing time, and a list of the PQ algorithms whose keys appear in the tags. Each PQ signature scheme signs `TextEncoder.encode(content)` — the UTF-8 bytes of the content string.
Example content:
```
I am signaling that the post-quantum public keys listed in the tags of this event
were generated by me and I hold the private keys. I may use these keys in the future
as successors to my current Nostr identity.
My current identity:
npub: <npub>
hex: <hex pubkey>
This attestation is established pre-quantum at Bitcoin block height <height>.
Post-quantum public keys in tags:
ML-DSA-44 (Dilithium, FIPS 204, NIST Level 2)
ML-DSA-65 (Dilithium, FIPS 204, NIST Level 3)
SLH-DSA-128s (SPHINCS+, FIPS 205, NIST Level 1)
Falcon-512 (FIPS 206 draft, NIST Level 1)
ML-KEM-768 (Kyber, FIPS 203, NIST Level 3)
Each post-quantum key has cryptographically signed this attestation. This event is
pending timestamp on the Bitcoin blockchain via OpenTimestamps.
```
### Tags
- `["block_height", "<height>"]` — the Bitcoin block height at signing time, as a decimal string.
- `["algorithm", "<algorithm-id>", "<base64 pubkey>", "<base64 signature>"]` — one tag per PQ **signature** scheme. The signature is over `TextEncoder.encode(content)`.
- `["algorithm", "ml-kem-768", "<base64 pubkey>"]` — for ML-KEM-768. ML-KEM is a KEM, not a signature scheme, so it has no signature field. Its ownership is asserted by the attestation text and authorized by the attesting identity's secp256k1 signature over the kind 1 event (which covers the tags, including the ML-KEM pubkey).
### Algorithm identifiers
The following `algorithm-id` strings are defined by this NIP:
| `algorithm-id` | FIPS | Type | Public key size | Signature size |
|---|---|---|---|---|
| `ml-dsa-44` | 204 | Signature | 1312 bytes | 2420 bytes |
| `ml-dsa-65` | 204 | Signature | 1952 bytes | 3309 bytes |
| `slh-dsa-128s` | 205 | Signature | 32 bytes | 7856 bytes |
| `falcon-512` | 206 (draft) | Signature | 897 bytes | ~666 bytes |
| `ml-kem-768` | 203 | KEM | 1184 bytes | n/a (ciphertext 1088 bytes) |
Future NIPs may register additional `algorithm-id` values and additional BIP32 child indices. Implementations MUST ignore `algorithm` tags whose `algorithm-id` they do not recognize.
### Example
```json
{
"kind": 1,
"pubkey": "<attesting identity secp256k1 pubkey, hex>",
"content": "I am signaling that the post-quantum public keys ...",
"tags": [
["block_height", "840000"],
["algorithm", "ml-dsa-44", "<base64 pubkey>", "<base64 signature>"],
["algorithm", "ml-dsa-65", "<base64 pubkey>", "<base64 signature>"],
["algorithm", "slh-dsa-128s", "<base64 pubkey>", "<base64 signature>"],
["algorithm", "falcon-512", "<base64 pubkey>", "<base64 signature>"],
["algorithm", "ml-kem-768", "<base64 pubkey>"]
],
"sig": "<attesting identity Schnorr signature>"
}
```
## The kind 9999 proof carrier event
A **non-replaceable** event (kind 9999, in the 0–9999 range) that wraps the kind 1 announcement and carries the OpenTimestamps proof. It is signed with the attesting identity's secp256k1 key.
Kind 9999 is non-replaceable: each publication is permanent on relays. Upgrades (e.g. pending → confirmed OTS proof) publish a **new** kind 9999 event with an `upgrade_of` tag referencing the previous one. Both events remain on relays. Clients fetch all kind 9999 events for a pubkey and select the one with the earliest valid Bitcoin anchor.
The kind number is provisional and subject to maintainer allocation.
### Content
`JSON.stringify(kind1Event)` — the full signed kind 1 event embedded as a JSON string. This lets verifiers validate the kind 1 event, its PQ signatures, and its secp256k1 signature without fetching it from relays.
### Tags
- `["e", "<kind 1 event id>"]` — reference to the kind 1 announcement.
- `["sha256", "<hex SHA-256 of the full signed kind 1 event JSON>"]` — the digest that was submitted to OpenTimestamps.
- `["ots", "<base64 .ots proof>"]` — the OpenTimestamps proof (pending or confirmed).
### Example
```json
{
"kind": 9999,
"pubkey": "<attesting identity secp256k1 pubkey, hex>",
"content": "<JSON string of the full signed kind 1 event>",
"tags": [
["e", "<kind 1 event id>"],
["sha256", "<hex SHA-256 of full signed kind 1 event JSON>"],
["ots", "<base64 .ots proof>"]
],
"sig": "<attesting identity Schnorr signature>"
}
```
## OpenTimestamps anchoring
The kind 1 announcement MUST be timestamped via OpenTimestamps (NIP-03). The digest submitted to the calendar servers is the **SHA-256 of the full signed kind 1 event JSON** — i.e. `sha256(JSON.stringify(kind1Event))`, where the serialized event includes its `id` and `sig` fields. This digest is recorded in the wrapper's `sha256` tag, and the resulting `.ots` proof is embedded in the wrapper's `ots` tag.
The wrapper *carries* the proof; the kind 1 event *is* what is timestamped.
### Why the kind 1 event must be anchored
Without an OTS anchor on the key-link event, a future quantum attacker who breaks the secp256k1 key can publish a fraudulent key-link event linking the identity to the attacker's PQ keys. Both the real and fraudulent events would have valid secp256k1 signatures (the key is compromised), and `created_at` is forgeable, so clients could not distinguish them.
With an OTS anchor, the real event is committed to a specific Bitcoin block before quantum computers exist. A fraudulent event published later cannot produce an OTS proof for that block or any earlier block — doing so would require re-mining historical Bitcoin blocks (infeasible even with a quantum computer, due to cumulative proof-of-work) or finding a SHA-256 preimage (still infeasible under Grover's quadratic speedup on 256-bit hashes).
### Verification rule: earliest valid anchor wins
When a client finds multiple kind 9999 proof carriers for the same attesting identity, it verifies the OTS proof in each and selects the one whose Bitcoin attestation has the **earliest block height**. That proof carrier's embedded kind 1 event is the canonical key-link for that identity. Later proof carriers, even if validly signed, are treated as superseding only if they are signed by a PQ key already linked by the earliest proof carrier.
### OTS proof states
An OTS proof may be **pending** (not yet anchored in a Bitcoin block) or **confirmed** (anchored). A pending proof is evidence that the digest was submitted to calendar servers; it is **not** an independent timestamp and depends on calendar behavior. A confirmed proof asserts a Bitcoin block commitment. Clients SHOULD treat a confirmed proof as authoritative. A pending proof can be upgraded to a confirmed proof later by re-querying the calendars; when that happens, a **new** proof carrier event is published carrying the upgraded proof and referencing the original via an `upgrade_of` tag. The original event remains on relays permanently.
### Client-side verification
Clients SHOULD perform full client-side OTS verification: parse the proof, walk the op tree, validate the Merkle path, and check the attested Merkle root against the actual Bitcoin block header for the attested height. A client that cannot perform full verification MAY fall back to checking that a Bitcoin attestation is structurally present in the proof, but MUST label such a link as unverified.
## Client behavior
### PQ-aware clients
A PQ-aware client, when it encounters a kind 9999 proof carrier for a pubkey it cares about:
1. Parses the embedded kind 1 event from the proof carrier's `content`.
2. Verifies the attesting identity's secp256k1 signature on both the kind 1 and the kind 9999 events.
3. Verifies that the proof carrier's pubkey matches the embedded kind 1 event's pubkey (identity binding).
3. Verifies the `sha256` tag matches `sha256(JSON.stringify(kind1Event))`.
4. For each `algorithm` tag with a signature, verifies the PQ signature against the pubkey in the tag, over `TextEncoder.encode(kind1Event.content)`.
5. Verifies the OTS proof in the `ots` tag against the `sha256` digest.
6. Records the link: attesting identity → {set of PQ pubkeys}, anchored at the verified Bitcoin block height.
7. If multiple wrappers exist for the same identity, applies the earliest-valid-anchor rule.
After a quantum break of secp256k1, the client trusts the PQ keys from the earliest-anchored link for any future PQ-signed events or PQ-encrypted messages from that identity.
### Legacy clients
Legacy clients see the kind 1 announcement as an ordinary text note and the kind 9999 proof carrier as an unknown event. They ignore both. Nothing breaks.
## What this NIP proves and does not prove
**It proves:**
- Each PQ signature scheme listed in the tags signed the attestation text. Verified by checking each PQ signature against its tag's pubkey.
- The attesting identity authorized the link. Verified by the secp256k1 Schnorr signature on the kind 1 and kind 9999 events, valid pre-quantum.
- The link existed at a specific pre-quantum Bitcoin block. Verified by the OTS proof.
**It does not prove:**
- The PQ keys share a common seed origin. Common-origin is *asserted* in the attestation text and *authorized* by the attesting identity's signature, but it is not proven by a seed-derived signature. The security model relies on OTS precedence, not on a cryptographic proof of common origin.
- The attesting identity still controls the PQ private keys. The link only proves the keys existed and were authorized at anchor time.
This is a deliberate design choice. The threat model is backdating of fraudulent links, which OTS defeats. Forging a link *after* a quantum break produces a later anchor than the real link, so the real link is cryptographically distinguishable.
## Revocation and broken schemes
If a specific PQ scheme is later found to be weak (as SIKE was in 2022):
1. Clients stop trusting the broken scheme — no revocation event is required. Clients simply ignore `algorithm` tags whose `algorithm-id` is on a locally-maintained broken-scheme list.
2. The remaining links stay valid.
3. A user MAY publish a new key-link event omitting the broken scheme, signed by the still-valid PQ keys from the prior link.
## Algorithms: why this set
| Algorithm | Why included |
|---|---|
| ML-DSA-44 | Lattice-based signature, FIPS 204, NIST Category 2. Compatibility with proposals that pick ML-DSA-44 alone. |
| ML-DSA-65 | Lattice-based signature, FIPS 204, NIST Category 3. Primary PQ signature. |
| SLH-DSA-128s | Hash-based signature, FIPS 205, NIST Category 1. Very conservative assumptions; the fallback if all lattice schemes break. Large signatures (7856 bytes) are acceptable for a one-time event. |
| Falcon-512 | Lattice-based signature, FIPS 206 draft, NIST Category 1. Compact signatures (~666 bytes). |
| ML-KEM-768 | Lattice-based KEM, FIPS 203, NIST Category 3. For future PQ encryption (NIP-04/NIP-44 successors). Cannot sign; pubkey only. |
Multiple schemes are used simultaneously so that if one class is broken, the others remain. The community does not need to agree on a single algorithm; clients verify whichever subset they support.
## Out of scope
The following are explicitly **not** part of this NIP. They may be addressed by separate future NIPs:
- **PQ signatures on routine events.** This NIP links keys; it does not change how events are signed. PQ-signed events are a separate problem.
- **PQ encryption of messages.** ML-KEM-768 pubkeys are linked here so future encryption NIPs can use them, but this NIP does not define a NIP-04/NIP-44 replacement.
- **Migration of users with a raw nsec and no seed phrase.** That is a key-management problem orthogonal to the link format defined here.
- **Quantum-safe self-storage** (e.g. one-time pad or seed-derived symmetric encryption of kind 30078 application data). Orthogonal to identity linking.
- **HD wallet compartmentalization.** A property of BIP32/NIP-06, not something this NIP re-specifies.
## Prior art
This NIP builds on and is compatible with several existing community proposals:
- **PR #391** (eznix86, "NIP-101 Algorithm Transition") introduced the cross-signed transition event concept. This NIP extends it with OpenTimestamps anchoring, multi-scheme hedging, and seed-phrase-derived keys.
- **PR #2185** (trbouma, "Add NIP for PQ") proposes swapping event signatures to ML-DSA-44. This NIP is compatible with that work — a future PQ-signature NIP can use the PQ keys linked here.
- **Issue #1971** (paulmillr, "NIP-44: post-quantum security") is the main community discussion. The "whole architecture" problem identified there (breaking nsec breaks everything derived from it) is why this NIP uses a BIP39 seed phrase as the root of trust rather than deriving PQ keys from nsec.
- **PR #1647** (fiatjaf, "nip4e: Decoupling encryption from identity") separates encryption keys from identity keys. This NIP's ML-KEM-768 link is compatible with that pattern.
- **NIP-03** (OpenTimestamps Attestations for Events) provides the anchoring primitive this NIP relies on.
- **NIP-06** (Basic key derivation from seed) defines the BIP32 base path under which this NIP allocates PQ child indices.
## Reference implementation
> **⚠️ Research prototype.** The implementation is experimental and is not a complete post-quantum
> migration. It links PQ keys to an existing identity and anchors that link in Bitcoin; it does not
> yet define PQ authentication for routine events, key rotation/revocation, or PQ encryption.
A static, client-side-only web implementation exists at `https://laantungir.net/post-quantum/`.
Cryptographic operations (BIP39, BIP32, PQ keygen/sign/verify, OTS submission and Merkle-path
verification) happen in the browser. Private keys are handled in the browser or via a NIP-07 signer
extension, which may be a remote signer (NIP-46); in that case the signing key does not touch the
page origin. OTS proof *upgrading* uses a server-side helper, and Bitcoin block-header confirmation
relies on a public explorer API (mempool.space) — this is **API-assisted verification**, not a full
Bitcoin light-client verification. Source: [`www/js/pq-crypto.mjs`](www/js/pq-crypto.mjs:1) in this
repository.