Post-Quantum Nostr
⚠️ Research prototype. This project is an experimental pre-quantum key-commitment and identity-link system. It does not, by itself, make a Nostr identity post-quantum secure: it links PQ keys to an existing identity and anchors that link in Bitcoin. Complete migration requires companion protocols (PQ event authentication, rotation, encryption, client adoption) that are not yet specified or implemented. Do not enter a valuable existing mnemonic into the web app. See the implementation status section for what exists today.
A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.
Table of Contents
- The Problem
- Why Bitcoin's Hash-the-Pubkey Trick Doesn't Work for Nostr
- The Strategy: Overview
- Component 1: Seed Phrase as Algorithm-Agnostic Root of Trust
- Component 2: Multi-Scheme Cross-Signed Key-Link Events
- Component 3: OpenTimestamps for Pre-Quantum Anchoring
- Component 4: Migrating Existing Users with Raw nsec (Design Only)
- Component 5: Quantum-Safe Self-Storage (Design Only)
- Component 6: Deterministic Wallet Compartmentalization
- The Complete Migration Flow
- What Remains Unsolved
- Implementation Status
The Problem
Nostr's cryptography is built entirely on secp256k1:
- Identity: Your pubkey IS your identity — it's in every event, it's your npub, it's what filters use, it's what NIP-05 verifies, it's what the social graph is built on.
- Authentication: Every event is signed with a Schnorr signature over secp256k1. Signature verification requires the public key, which is published in plaintext on every event.
- Encryption (NIP-04): Uses secp256k1 ECDH to derive a shared secret, then AES-256-CBC. The shared X coordinate is used directly as the AES key. The IV is published in plaintext.
- Encryption (NIP-44): Uses secp256k1 ECDH to derive a shared secret, then HKDF → ChaCha20 + HMAC-SHA256. The nonce is published in plaintext inside the payload.
Shor's algorithm breaks the elliptic curve discrete logarithm problem on secp256k1 in polynomial time. With a sufficiently large fault-tolerant quantum computer, an attacker who has recorded any Nostr event can:
- Extract the public key from the event.
- Run Shor's algorithm to recover the private key.
- Forge signatures, decrypt all NIP-04/NIP-44 messages, and impersonate the user.
NIP-04 vs NIP-44: No Quantum Advantage
NIP-44 is a substantial classical-security upgrade over NIP-04 (authenticated encryption, key separation, per-message keys, better cipher, length-hiding padding), but it offers zero post-quantum security advantage. Both derive their shared secret from secp256k1 ECDH. Everything NIP-44 adds (HKDF, per-message nonces, ChaCha20, HMAC-SHA256, custom padding) sits downstream of the broken ECDH step.
The NIP-44 spec itself acknowledges this:
No post-quantum security: a powerful quantum computer would be able to decrypt the messages
The symmetric primitives in both NIPs (AES-256, ChaCha20, HMAC-SHA256, HKDF-SHA256) are adequately post-quantum (~128-bit PQ security via Grover's quadratic speedup). The weakness is exclusively the secp256k1 ECDH key agreement, which has zero post-quantum security in both NIPs.
Why Bitcoin's Hash-the-Pubkey Trick Doesn't Work for Nostr
Bitcoin has limited quantum resistance because addresses are hashes of pubkeys (RIPEMD160(SHA256(pubkey))), and pubkeys are only revealed at spend time. This works because of three properties that Nostr does not have:
| Property | Bitcoin | Nostr |
|---|---|---|
| Identifier | Address (hash of pubkey) | Pubkey directly |
| Pubkey revealed | At spend time only | On every event |
| After reveal | UTXO consumed, key discarded | Identity persists, key reused |
In Nostr, the pubkey is load-bearing in ways it isn't in Bitcoin:
- The pubkey IS the identity. It's in every event, it's your npub, it's what filters use, it's what NIP-05 verifies, it's what the social graph is built on.
- The pubkey must be revealed for every event. Schnorr signature verification requires the public key. You can't verify a signature with a hash of the pubkey.
- Identity is never consumed. You publish thousands of events over years with the same pubkey. There's no "consume and rotate" moment.
If Nostr published hash(pubkey) instead, you'd get quantum resistance for exactly zero seconds — the time between key generation and your first event publication, at which point the pubkey must be revealed for signature verification.
The only ways to avoid revealing the pubkey (ZK-SNARKs, giving up public verifiability, one-time keys per event) are impractical or fundamentally change what Nostr is.
The real fix is post-quantum signatures and post-quantum key agreement, not hashing the pubkey. This document describes how to get there without breaking Nostr.
The Strategy: Overview
flowchart TD
subgraph SECURITY["Security Layers - All Quantum-Resistant"]
L1[Layer 1: Seed phrase - PQ-safe root of trust]
L2[Layer 2: Key-link event + OTS - PQ-safe identity migration]
L3[Layer 3: PQ signatures - PQ-safe future authentication]
L4[Layer 4: OTS on important events - PQ-safe historical provenance]
L5[Layer 5: OTP or PQ encryption - PQ-safe confidentiality]
end
L1 --> L2 --> L3
L1 --> L5
L2 --> L4
The strategy uses existing Nostr infrastructure (NIP-06, NIP-03) plus one new event type (a key-link NIP). No new cryptographic primitives are needed for the migration itself — PQ algorithms are used additively, and the community doesn't need to agree on which one to use.
Design Principles
- No social graph loss — existing follows and identity carry forward
- No user re-onboarding — the seed phrase becomes the new root of trust
- Backward compatible — legacy clients continue working; PQ is additive
- No algorithm consensus required — link to all viable PQ schemes now, converge later
- Pre-quantum timestamping — establish links now, before quantum computers exist
- Transparent to users — the client handles everything at seed-phrase login
Component 1: Seed Phrase as Algorithm-Agnostic Root of Trust
The BIP39 seed is a 64-byte entropy string derived from a mnemonic via PBKDF2-HMAC-SHA512 (2048 iterations). This is a symmetric KDF — no public-key cryptography involved. A quantum computer provides no advantage beyond Grover's quadratic speedup.
The seed is algorithm-agnostic. It's just entropy. You can derive any key type from it:
flowchart TD
SEED[BIP39 Seed - 64 bytes, recovery root]
SEED --> BIP32[BIP32 derivation - secp256k1 keypair]
SEED --> PQKEM[BIP32 derivation - ML-KEM keypair]
SEED --> PQDSA[BIP32 derivation - ML-DSA keypair]
SEED --> SLHDSA[BIP32 derivation - SLH-DSA keypair]
SEED --> FALCON[BIP32 derivation - Falcon keypair]
style SEED fill:#cfc
style BIP32 fill:#fcc
style PQKEM fill:#cfc
style PQDSA fill:#cfc
style SLHDSA fill:#cfc
style FALCON fill:#cfc
PQ Key Derivation from BIP39 Seeds
All keys — both secp256k1 and PQ — are derived from the BIP39 seed via BIP32 hierarchical deterministic derivation, the same standard used by NIP-06 for secp256k1 keys. PQ keys are derived at different child indices under the NIP-06 base path, using the BIP32 child private key (32 bytes) as the deterministic seed for each PQ algorithm's keygen() function.
Base path: m/44'/1237'/0'/0/ (NIP-06 account 0, change 0)
| Child index | Algorithm | Seed length | BIP32 path | How derived |
|---|---|---|---|---|
| 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) |
Why some algorithms need two children
BIP32 child derivation produces exactly 32 bytes (the private key) per child. Some PQ algorithms need more than 32 bytes for their keygen() seed:
- ML-DSA-44 and ML-DSA-65: need 32 bytes — one child is enough
- SLH-DSA-128s: needs 48 bytes — derive two children (64 bytes), use the first 48
- Falcon-512: needs 48 bytes — derive two children (64 bytes), use the first 48
- ML-KEM-768: needs 64 bytes — derive two children and concatenate (64 bytes)
Truncation rule (must be pinned for compatibility): when a PQ seed requires more than 32 bytes, two BIP32 children are derived and concatenated (64 bytes total). For 48-byte seeds, the first 48 bytes of the concatenation are used. For 64-byte seeds, all 64 bytes are used. A future implementer who takes the last 48 bytes would produce different keys and break compatibility — implementations must use the first N bytes.
Why BIP32 and not HKDF?
BIP32 is the standard for HD wallets. Using BIP32 paths gives us:
- Consistency with NIP-06 — PQ keys are derived the same way as secp256k1 keys, just at different child indices
- Compartmentalization — hardened derivation at the account level means breaking one key doesn't compromise siblings
- Wallet compatibility — HD wallet infrastructure (Amber, hardware wallets, etc.) can derive these same paths
- Tree structure — the derivation path documents exactly which key is where
- Security — BIP32 uses HMAC-SHA512 (the same HMAC used everywhere in HD wallets), not a custom construction
What is HMAC?
HMAC (Hash-based Message Authentication Code) is the cryptographic primitive that BIP32 is built on. Every BIP32 child key derivation is:
HMAC-SHA512(parent_chain_code, data) → 32-byte private key + 32-byte chain code
This is not a separate or weaker construction — it IS BIP32. Using BIP32 paths means we're using the same HMAC-based derivation that all HD wallets use.
Seed Phrase Entropy Considerations
| Mnemonic length | Entropy | Post-quantum security (Grover's) | Assessment |
|---|---|---|---|
| 12 words | 128 bits | ~64 bits | Testing only — weaker under a quantum brute-force model |
| 24 words | 256 bits | ~128 bits | Recommended for a PQ recovery root |
The implementation defaults to 24-word (256-bit) mnemonics, with a 12-word option available for testing. Users concerned about quantum attacks on the seed entropy itself should use 24 words. The bigger risk to seed phrases is classical (theft, phishing), not quantum. Note that BIP39's PBKDF2 iteration count is low; the seed phrase should be treated as a high-value secret and stored offline.
Component 2: Multi-Scheme Cross-Signed Key-Link Events
This is the core innovation. Instead of choosing one PQ algorithm, cross-sign with all viable PQ schemes now. Let consensus emerge later.
The Standardization Uncertainty Problem
The PQ standardization landscape is still evolving:
| Scheme | NIST Status | Concern |
|---|---|---|
| ML-DSA (Dilithium) | FIPS 204, finalized 2024 | Lattice-based — could have undiscovered structural weaknesses |
| SLH-DSA (SPHINCS+) | FIPS 205, finalized 2024 | Hash-based, very conservative, but huge signatures (8-50 KB) |
| FN-DSA (Falcon) | FIPS 206, still draft | Compact but complex FFT, implementation concerns |
| ML-KEM (Kyber) | FIPS 203, finalized 2024 | Lattice-based — same class as Dilithium |
History gives reason to be cautious: SIKE was a NIST PQ finalist that was broken in 2022 using a classical algorithm that ran in an hour on a laptop. If the Nostr community had picked SIKE early, we'd be back to square one.
The Multi-Scheme Solution
The implementation includes five PQ schemes — four signature schemes and one KEM:
flowchart TD
SEED[BIP39 Seed] --> SECP[secp256k1 keypair - child 0]
SEED --> MLDSA44[ML-DSA-44 keypair - child 1]
SEED --> MLDSA65[ML-DSA-65 keypair - child 2]
SEED --> SLHDSA[SLH-DSA-128s keypair - children 3+4]
SEED --> FALCON[Falcon-512 keypair - children 5+6]
SEED --> MLKEM[ML-KEM-768 keypair - children 7+8]
MLDSA44 --> SIGN1[ML-DSA-44 sign attestation]
MLDSA65 --> SIGN2[ML-DSA-65 sign attestation]
SLHDSA --> SIGN3[SLH-DSA-128s sign attestation]
FALCON --> SIGN4[Falcon-512 sign attestation]
SIGN1 --> KIND1[Kind 1 Announcement Event]
SIGN2 --> KIND1
SIGN3 --> KIND1
SIGN4 --> KIND1
MLKEM --> KIND1
SECP --> ACCT1[Account 1 - existing identity signs kind 1 and kind 9999]
KIND1 --> OTS[OpenTimestamp the kind 1 event hash]
OTS --> KIND9999[Kind 9999 Proof Carrier - carries OTS proof]
KIND9999 --> RELAY[Published to relays NOW]
RELAY --> CLIENT1[PQ-aware client - picks ML-DSA]
RELAY --> CLIENT2[PQ-aware client - picks SLH-DSA]
RELAY --> CLIENT3[Legacy client - ignores all PQ keys]
style KIND1 fill:#ffa
style KIND9999 fill:#ffa
style OTS fill:#cfc
The Two-Event Structure
The migration uses two Nostr events:
1. Kind 1 Announcement (human-readable, visible in Nostr feeds)
A regular Nostr text note (kind 1) that serves as a public announcement of the post-quantum key migration. The content is human-readable text with newlines. The PQ public keys and signatures go in the tags.
Content: A human-readable attestation statement, e.g.:
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['algorithm', '<algorithm>', '<base64 pubkey>', '<base64 signature>']— one per PQ signature scheme['algorithm', 'ml-kem-768', '<base64 pubkey>']— for ML-KEM (KEM, no signature)
Each PQ signature scheme signs TextEncoder.encode(content) — the human-readable attestation text. The event is signed with the user's existing Nostr identity (Account #1) via window.nostr.signEvent (NIP-07).
2. Kind 9999 Proof Carrier (machine-verifiable, carries the OTS proof)
A non-replaceable event (kind 9999, in the 0–9999 range) that wraps the kind 1 announcement and carries the OpenTimestamps proof. The content is the full kind 1 event as JSON (so verifiers don't need to fetch it from relays). Each publication is permanent — upgrades publish a new event with an upgrade_of tag referencing the previous one.
Content: JSON.stringify(kind1Event) — the full signed kind 1 event embedded as a JSON string.
Tags:
['e', '<kind1 event id>']— reference to the kind 1 announcement['sha256', '<hex SHA-256 of the full signed kind 1 event JSON>']— what was timestamped['ots', '<base64 .ots proof>']— the OpenTimestamps proof (pending or confirmed)
The wrapper is signed with the user's existing identity (Account #1) via window.nostr.signEvent.
What This Proves and What It Does Not
What the event proves:
- Each PQ signature scheme (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512) signed the attestation text — verified by checking each PQ signature against the pubkey in its tag.
- The user's existing Nostr identity (Account #1) authorized the migration — verified by the secp256k1 Schnorr signature on the kind 1 and kind 9999 events.
- The event is anchored to a pre-quantum Bitcoin block via OpenTimestamps — the event with the earliest valid OTS proof wins (see Component 3).
What the event does NOT prove:
- The PQ keys cannot cryptographically prove they share a common seed origin. They prove only that five PQ keypairs exist and each signed the attestation text. Common-origin is asserted in the attestation text and authorized by Account #1's signature, but it is not proven by a seed-derived signature.
- The seed-derived secp256k1 key (Account #2, child 0) is derived but not used to sign in the current implementation. There is no "successor signature" binding the PQ keys to the seed.
This is a deliberate design choice. The security model relies on OTS precedence (the real event is anchored pre-quantum; a forged event cannot be backdated) rather than a seed-derived signature. After a quantum break, an attacker who forges Account #1 can publish a fraudulent migration event, but it will have a later OTS timestamp than the real event — so the real event is cryptographically distinguishable by its earlier Bitcoin anchor.
ML-KEM Cannot Sign
ML-KEM (Kyber) is a KEM (Key Encapsulation Mechanism), not a signature scheme. It cannot sign the attestation. Instead, its public key is included in an algorithm tag (without a signature field), and its ownership is asserted by the attestation text and authorized by Account #1's signature over the kind 1 event (which covers the tags, including the ML-KEM pubkey).
This is weaker than a cross-signature but sufficient because:
- The secp256k1 signature is valid NOW (pre-quantum), establishing the link while we can trust secp256k1
- The ML-KEM key is used for encryption, not authentication — a false claim of ownership would just mean someone can't decrypt messages sent to you, which is self-correcting
Why This Is Better Than Picking One Scheme
| Concern | Pick-one approach | Multi-scheme approach |
|---|---|---|
| Scheme gets broken later | Migration fails, need new link | Other links still valid |
| Standardization changes | May need to re-link | Already covered |
| Consensus not reached | Can't proceed | Proceed immediately |
| Event size | Small | Larger (~20-30 KB), but one-time cost |
| Client complexity | Support one scheme | Support any subset |
| Race condition risk | Must publish before quantum | Published now, all schemes |
Event size is a non-issue because this is a one-time publication, not per-event overhead.
Revocation: If a Scheme Is Broken Later
If a specific PQ scheme is later found to be weak (like SIKE was):
- Clients simply stop trusting the broken scheme — no revocation event needed; clients just ignore links to algorithms on a known-broken list
- The remaining links are still valid
- Optionally, publish a new key-link event without the broken scheme, signed by the remaining valid PQ keys
Component 3: OpenTimestamps for Pre-Quantum Anchoring
NIP-03 (OpenTimestamps Attestations for Events) anchors Nostr event hashes to the Bitcoin blockchain via OpenTimestamps. The implementation submits the SHA-256 of the full signed kind 1 event JSON (including its id and sig) to the OpenTimestamps calendar servers, and embeds the resulting .ots proof in the kind 9999 proof carrier's ots tag.
Why Bitcoin Timestamping Is Quantum-Resistant
The Bitcoin proof-of-work chain is quantum-resistant — not because Bitcoin's signatures are PQ-safe (they're not for revealed pubkeys), but because re-mining historical blocks is infeasible even with a quantum computer. Grover's algorithm gives only a quadratic speedup on mining, which doesn't help retroactively alter past blocks.
An attacker who breaks your secp256k1 key via Shor's can forge new events but cannot backdate an event to before a specific Bitcoin block — they'd need to produce a valid OTS proof anchoring it to a past block, which requires either:
- Re-mining that Bitcoin block (impossible, even with a quantum computer, due to cumulative proof-of-work)
- Finding a SHA256 preimage for the event hash (still infeasible with Grover's quadratic speedup on 256-bit hashes)
The Critical Refinement: OpenTimestamp the Key-Link Event
The kind 1 announcement event MUST be timestamped via OpenTimestamps. This prevents a race condition attack:
Without OTS on the key-link event:
- Attacker breaks secp256k1 key via Shor's
- Attacker publishes a fraudulent key-link event linking your secp256k1 key to their PQ key
- Both real and fraudulent events have valid secp256k1 signatures (key is compromised)
- Clients can't distinguish them —
created_atis forgeable
With OTS on the key-link event:
- Your real key-link event is anchored to Bitcoin block N (before quantum computers)
- Attacker's fraudulent event is published later and cannot produce an OTS proof for block N or earlier
- Clients verify: "the key-link event with the earliest valid OTS proof wins"
- The real key-link is cryptographically distinguishable from the fraudulent one
What Is Timestamped
The implementation timestamps the SHA-256 of the full signed kind 1 event JSON (computed by hashing JSON.stringify(kind1Event), which includes the id and sig fields). This hash is:
- Submitted to OpenTimestamps calendar servers to obtain a pending
.otsproof. - Recorded in the kind 9999 proof carrier's
sha256tag. - The
.otsproof is embedded in the kind 9999 proof carrier'sotstag.
The kind 9999 proof carrier carries the OTS proof; the kind 1 event is what's timestamped. Verifiers re-compute the hash from the embedded kind 1 event and check it matches the sha256 tag.
Current OTS Verification Status
Research prototype — trust model. The current OTS verification is API-assisted, not a full Bitcoin light-client verification. Treat results accordingly.
The current implementation:
- Parses the
.otsproof and validates the Merkle path against a block header obtained from a public Bitcoin explorer API (mempool.space). - Detects Bitcoin confirmation by searching the proof bytes for the block-header attestation magic bytes and confirming the attested block height via the same API.
- Delegates proof upgrading (asking calendars for a confirmed proof) to a server-side helper.
What is NOT done:
- Full light-client verification — validating the block header's proof-of-work, difficulty, and
chain linkage independently, without trusting an explorer API. The vendored
javascript-opentimestampslibrary inresources/provides primitives for this and will be integrated in a future release. - Multi-explorer cross-checking — comparing independent APIs and rejecting disagreement.
Because the block header is trusted from a single API, a compromised or malicious explorer could falsify a confirmation. The UI labels this as "API-checked," not "cryptographically verified on Bitcoin."
What Should Be OpenTimestamped
Not every event needs OTS. Priority list:
- Key-link events — absolutely critical, foundation of PQ migration
- Identity-defining events — NIP-05 verification, profile events (kind 0)
- Important content — contracts, announcements, evidence
- High-value events — financial events (NIP-60 Cashu wallets, zaps)
Routine events (regular posts, reactions) probably don't need OTS — the impact of retroactive forgery is low.
Component 4: Migrating Existing Users with Raw nsec (Design Only)
Status: Design only — not yet implemented. The current implementation supports users who already have a Nostr identity (via NIP-07 signer or nostr-login-lite). The raw-nsec migration described below is future work.
Most Nostr users today have a raw nsec (random private key) with no seed phrase. Asking them to abandon their identity and social graph to adopt a seed phrase is a non-starter. This component addresses how to migrate them.
The Challenge
A secp256k1 private key is 32 bytes (256 bits). BIP39 encodes 11 bits per word. Encoding the old nsec requires 24 extra words. The old nsec was generated randomly — it cannot be "derived" from a seed (that's a preimage problem). It must be encoded (directly or encrypted).
Approach A: Extended Phrase (36 words)
flowchart LR
subgraph CREATION["One-time migration"]
OLD[Old nsec - 32 bytes]
NEW[Generate 12-word BIP39 phrase]
NEW --> SEED[BIP39 seed]
SEED --> ENCKEY[Derive encryption key via HKDF]
ENCKEY --> ENC[Encrypt old nsec: ChaCha20]
OLD --> ENC
ENC --> CIPHER[32 bytes encrypted nsec]
CIPHER --> WORDS[Encode as 24 BIP39 words + checksum]
NEW --> PHRASE[Full phrase: 12 + 24 = 36 words]
WORDS --> PHRASE
end
subgraph RECOVERY["Recovery with full phrase"]
FULL[36 words entered]
FULL --> FIRST[First 12 words]
FULL --> LAST[Last 24 words]
FIRST --> SEED2[BIP39 seed]
SEED2 --> ENCKEY2[Derive encryption key]
LAST --> CIPHER2[Decode to 32 bytes]
ENCKEY2 --> DEC[Decrypt]
CIPHER2 --> DEC
DEC --> OLDRECOV[Old nsec recovered]
SEED2 --> PQKEYS[PQ keys derived]
end
subgraph FUTURE["After migration - drop old key"]
FIRST12[First 12 words only]
FIRST12 --> SEED3[BIP39 seed]
SEED3 --> PQKEYS2[PQ keys only]
end
Design:
- Generate a standard 12-word BIP39 phrase (128 bits entropy)
- Derive a 32-byte encryption key from the BIP39 seed:
enc_key = HKDF-Extract(salt="nostr-legacy-key", ikm=bip39_seed) - Encrypt the old nsec:
encrypted_nsec = ChaCha20(enc_key, nonce=0, old_nsec)→ 32 bytes - Encode the 32-byte encrypted nsec as 24 BIP39 words (with its own checksum)
- Full phrase: 12 words (new seed) + 24 words (encrypted old nsec) = 36 words
Recovery with 36 words: First 12 → BIP39 seed → PQ keys + encryption key. Last 24 → decode → decrypt → old nsec. Both keys recovered.
Recovery with 12 words (after migration): First 12 → PQ keys only. Old nsec no longer needed.
Why encrypt the old nsec? The extra 24 words are meaningless without the first 12 (partial compromise protection). If someone steals only the last 24 words: useless. If someone steals only the first 12 words: they get PQ keys but not the old nsec.
Approach B: 12 Words + Relay Backup (Recommended)
flowchart TD
subgraph SETUP["One-time migration"]
OLD[Old nsec]
NEW[Generate 12-word BIP39 phrase]
NEW --> SEED[BIP39 seed]
SEED --> NEWSECP[New secp256k1 key via NIP-06]
SEED --> PQKEYS[PQ keys via BIP32]
SEED --> ENCKEY[Symmetric encryption key via HKDF]
ENCKEY --> ENC[Encrypt old nsec]
OLD --> ENC
ENC --> EVENT30078[kind 30078: encrypted old nsec]
NEWSECP --> SIGN1[Sign event with new secp256k1 key]
SIGN1 --> EVENT30078
EVENT30078 --> RELAY[Publish to relays]
OLD --> KEYLINK[Key-link event: cross-signed]
NEWSECP --> KEYLINK
PQKEYS --> KEYLINK
KEYLINK --> OTS[OpenTimestamp via NIP-03]
OTS --> BTC[Anchored in Bitcoin]
KEYLINK --> RELAY
end
subgraph RECOVER["Recovery from 12 words only"]
WORDS[12 words entered]
WORDS --> SEED2[BIP39 seed]
SEED2 --> NEWSECP2[New secp256k1 key]
SEED2 --> PQKEYS2[PQ keys]
SEED2 --> ENCKEY2[Encryption key]
NEWSECP2 --> FETCH[Fetch kind 30078 from relays]
FETCH --> CIPHER[Encrypted old nsec]
ENCKEY2 --> DEC[Decrypt]
CIPHER --> DEC
DEC --> OLDRECOV[Old nsec recovered]
end
Design:
- Generate a 12-word BIP39 phrase
- Derive from seed: new secp256k1 key (NIP-06 path), PQ keys (BIP32), symmetric encryption key (HKDF)
- Encrypt old nsec with the symmetric key:
encrypted_nsec = ChaCha20-Poly1305(enc_key, old_nsec) - Publish as kind 30078 event, signed with the new secp256k1 key
- Publish key-link event (cross-signed, OpenTimestamped via NIP-03)
- User backs up only 12 words
Recovery: 12 words → derive new key + encryption key → fetch kind 30078 from relays → decrypt → old nsec. Both keys recovered from 12 words + relay access.
No circular dependency: The kind 30078 event is found by the new pubkey (derived from the 12-word seed). The new secp256k1 key is the "storage identity." The old nsec is just data inside the event.
Approach C: Hybrid (Best of Both)
- Primary backup: 12-word phrase (PQ keys + relay recovery of old nsec)
- Optional fallback: also write down 24 extra words (encrypted old nsec) in case relays are unavailable
- If relays available: 12 words suffice
- If relays down: 12 + 24 = 36 words recover everything
Comparison
| Property | 36-word phrase | 12-word + relay backup | Hybrid |
|---|---|---|---|
| Backup size | 36 words | 12 words | 12 or 36 words |
| Relay dependency | None | Yes | Optional |
| Custom format | Yes | No | Optional |
| Self-contained | Yes | No | Yes (with 36) |
| Quantum-safe | Yes | Yes | Yes |
Component 5: Quantum-Safe Self-Storage (Design Only)
Status: Design only — not yet implemented. The current implementation does not include encrypted self-storage. The approaches below are design proposals for future work.
For self-storage use cases (e.g., kind 30078 application data), NIP-04 and NIP-44 are unnecessary and quantum-vulnerable. Two quantum-safe alternatives:
Option A: One-Time Pad (Information-Theoretic Security)
A one-time pad provides information-theoretic security — the strongest security guarantee in cryptography, stronger than any computational security (including post-quantum). It's a mathematical proof of perfect secrecy: no computer that could ever exist, in any universe governed by the laws of physics, can break it.
Requirements:
- Pad must be truly random (hardware RNG recommended, e.g., TrueRNG)
- Pad must be as long as the data
- Pad must never be reused
- Pad must be stored securely (e.g., USB drive)
For self-storage, OTP is a natural fit because you sidestep OTP's biggest limitation (key distribution between parties) — you're encrypting for yourself, so the pad lives on your device.
What a quantum attacker can and cannot do with OTP-encrypted kind 30078:
- ❌ Read your stored data — OTP is information-theoretically secure
- ✅ Forge your signature — Shor's breaks secp256k1 (identity compromised)
- ✅ Impersonate you, delete/replace your events
- ❌ Decrypt your stored data — even with your private key, the data is protected by the pad
This gives clean separation: quantum breakage of your secp256k1 key compromises your identity/authentication but NOT your stored data's confidentiality.
Critical warning: pad reuse is fatal. If any pad section is used twice, XOR of the two ciphertexts equals XOR of the two plaintexts — the encryption breaks. Multi-device sync requires an atomic reservation protocol for pad sections.
Option B: Symmetric Key from Seed (Computational PQ Security)
Derive a 256-bit symmetric key from the BIP39 seed (independent of secp256k1) and encrypt with ChaCha20-Poly1305 or AES-256-GCM:
storage_key = HKDF-Extract(salt="nostr-storage-v1", ikm=bip39_seed)
ciphertext = ChaCha20-Poly1305(storage_key, nonce, plaintext)
This is post-quantum because:
- HKDF-SHA256 is symmetric (~128-bit PQ security)
- ChaCha20 / AES-256 are symmetric (~128-bit PQ security)
- The secp256k1 key is never part of the encryption path
Tradeoff vs OTP: Simpler (no pad management, no USB drive), but computationally secure rather than information-theoretically secure. Adequate for most use cases.
Comparison
| Property | OTP | Symmetric key from seed |
|---|---|---|
| Security level | Information-theoretic (perfect secrecy) | Computational, ~128-bit PQ |
| Key material | Pad as long as all data | Single 256-bit key |
| Storage overhead | Pad on USB drive | Just the seed phrase |
| Quantum resistance | Unconditional | Conditional (Grover's is optimal) |
| Future-proofing | Permanent | Could theoretically fall to new quantum algorithms |
| Multi-device | Requires pad sync + offset coordination | Just share the seed |
| Practicality | More complex | Simpler |
Component 6: Deterministic Wallet Compartmentalization
BIP32/BIP39 deterministic wallets (NIP-06) provide meaningful quantum compartmentalization through their tree structure.
The Core Protection: Chain Codes Are Secret
When Shor's algorithm recovers a leaf private key from its published public key, the attacker gets only that key. To climb the tree (recover the parent private key), they need the parent chain code:
parent_private_key = child_private_key - IL mod n
where IL = HMAC-SHA512(parent_chain_code, parent_public_key || index)[:32]
The parent chain code is NOT published in normal Nostr usage. HMAC-SHA512 is a symmetric primitive — a quantum computer provides no advantage. Without chain codes, the attacker is stuck at the leaf level.
The Firewall Effect of Hardened Derivation
The NIP-06 path m/44'/1237'/account'/0/0 has hardened derivation at three levels. This means:
- Breaking all keys in Account 0 does NOT compromise Account 1 (hardened at
account'level) - The seed/master key remains secure even if all account keys are broken
- Hardened derivation requires the parent private key, not just the public key
Note on the PQ Derivation Paths
The PQ keys in this implementation are derived at non-hardened children (indices 1–8) under the hardened parent m/44'/1237'/0'/0'. Non-hardened derivation means anyone with the parent xpub (extended public key + chain code) at m/44'/1237'/0'/0' could derive the child public keys — but not the child private keys (which are what the PQ seeds are). So the PQ seeds remain secret unless the parent private key leaks. Never publish the xpub at m/44'/1237'/0'/0' — doing so would expose the child public keys (though not the PQ seeds themselves).
The Critical Vulnerability: Chain Code Leakage
If an attacker obtains a chain code at any level AND breaks one child key at that level via Shor's, they can:
- Compute
ILusing the chain code + child public key + index - Recover the parent private key
- Derive ALL children at that level
- Continue climbing if parent chain codes are also known
Where chain codes leak: published xpubs (extended public keys), wallet software that exports xpubs, backup exposure. In standard Nostr usage, chain codes are not published.
Summary
| Scenario | Compromised? |
|---|---|
| One leaf key broken via Shor's | Only that key |
| Multiple leaf keys from same account broken | Only those keys |
| Leaf key broken + chain code at that level known | Parent + all siblings |
| Account key broken + chain code at account level known | Parent + all accounts |
| Seed phrase stolen (classical) | Everything |
| Seed safe, quantum attacker, no chain codes leaked | Only published keys |
Recommendations
- Use separate accounts for separate Nostr identities — hardened derivation contains the damage
- Never publish xpubs — chain codes are the keys to the kingdom
- Use 24-word mnemonics for adequate PQ security on the seed entropy
- Don't reuse keys across services — each identity gets its own account index
- Assume published keys WILL be broken — plan for containment, not prevention
The Complete Migration Flow
flowchart TD
subgraph NOW["Pre-quantum - NOW"]
USER[User with existing Nostr identity]
USER --> SIGNIN[Sign in via NIP-07 signer]
SIGNIN --> SEED[Generate 12-word BIP39 phrase]
SEED --> DERIVE[Derive 5 PQ keypairs from seed via BIP32]
DERIVE --> SIGN[Each PQ key signs the attestation text]
SIGN --> KIND1[Build kind 1 announcement with PQ sigs in tags]
KIND1 --> SECP1[Account 1 signs kind 1 via signer]
SECP1 --> HASH[SHA-256 of full signed kind 1 event JSON]
HASH --> OTS[Submit hash to OpenTimestamps]
OTS --> KIND9999[Build kind 9999 proof carrier with OTS proof]
KIND9999 --> SECP2[Account 1 signs kind 9999 via signer]
SECP2 --> RELAY[Publish both events to relays]
SEED --> BACKUP[User writes down 12-word phrase]
end
subgraph CONFIRM["Bitcoin confirmation"]
POLL[Poll OTS upgrade service]
POLL --> UPGRADE[Upgrade .ots proof with Bitcoin attestation]
UPGRADE --> REPUBLISH[Publish new kind 9999 with confirmed proof]
end
subgraph FUTURE["Post-quantum - when quantum computers arrive"]
ATTACK[Attacker breaks secp256k1 via Shors]
ATTACK --> FORGE[Can forge secp256k1 signatures]
ATTACK --> CANNOT1[Cannot forge PQ signatures]
ATTACK --> CANNOT2[Cannot backdate events - OTS proofs anchor history]
VERIFY[Verify key-link event via OTS precedence]
VERIFY --> USEPQ[Switch to PQ signatures]
USEPQ --> REJECT[Reject forged secp256k1-only events]
end
NOW --> CONFIRM --> FUTURE
style NOW fill:#cfc
style CONFIRM fill:#ffa
style FUTURE fill:#fcc
User Experience
- User signs in with their existing Nostr identity (NIP-07 signer or nostr-login-lite)
- Client generates a 12-word phrase, shows it to the user to write down
- Client derives 5 PQ keypairs from the seed via BIP32 paths
- Each PQ signature scheme signs the human-readable attestation text
- The user's signer signs the kind 1 announcement and the kind 9999 proof carrier
- The SHA-256 of the full signed kind 1 event is submitted to OpenTimestamps
- Both events are published to relays
- The client polls for Bitcoin confirmation and publishes a new kind 9999 proof carrier with the confirmed OTS proof (append-only — the original remains on relays)
- After quantum migration: clients use PQ keys, old key retired
What Remains Unsolved
Historical Event Authenticity (Partially Solved)
Past secp256k1-signed events that were NOT OpenTimestamped remain forgeable after quantum break. An attacker can create fake old events that appear to be from you.
Mitigation: OpenTimestamp important events now (NIP-03). Events with valid OTS proofs are cryptographically anchored to a pre-quantum timestamp and cannot be forged retroactively. Events without OTS proofs become untrustworthy after quantum break.
Not fully solved: Routine events (regular posts, reactions) that aren't worth OpenTimestamping will become forgeable. The impact is low for most content, but high for events used as evidence, contracts, or historical records.
Relay Trust for Recovery
Approach B (12 words + relay backup, Component 4 — design only) depends on relays retaining the kind 30078 event containing the encrypted old nsec. If all relays delete it, the old nsec is lost.
Mitigations:
- Publish to multiple relays
- Use relays you control or trust
- Use the hybrid approach (Approach C) with 36-word fallback
- The event is small and signed, so it's easy to preserve
PQ Signature Size
PQ signatures are much larger than secp256k1 Schnorr signatures:
| Scheme | Signature size |
|---|---|
| secp256k1 Schnorr | 64 bytes |
| ML-DSA-44 | ~2.4 KB |
| ML-DSA-65 | ~3.3 KB |
| SLH-DSA-128s | ~8 KB |
| Falcon-512 | ~0.7 KB |
This increases the kind 1 announcement event size (~20-30 KB total for all PQ signatures). This is a one-time cost for the migration event, not per-event overhead.
PQ Algorithm Risk
Any PQ algorithm could theoretically be broken by a new classical or quantum algorithm (as SIKE was). The multi-scheme approach hedges against this — if one scheme falls, the others remain linked. But if ALL lattice-based schemes are broken simultaneously, only SLH-DSA (hash-based, very conservative) remains as a fallback.
Common-Origin Not Cryptographically Proven
The current implementation does not include a seed-derived secp256k1 signature binding the PQ keys to the seed (see Component 2, "What This Proves and What It Does Not"). The PQ keys self-attest by signing the attestation text, and the user's existing identity authorizes the migration. Common-origin is asserted, not proven. This is acceptable because OTS precedence (not a seed-derived signature) is the mechanism that distinguishes real from forged events after a quantum break.
Implementation Status
What Is Implemented
The current implementation is a static web app (www/) that performs the full migration flow for users who already have a Nostr identity:
| Component | Status | Location |
|---|---|---|
| BIP39 seed phrase generation (12-word, with optional user entropy) | Implemented | www/js/pq-crypto.mjs |
| BIP32 key derivation (secp256k1 + 5 PQ keypairs) | Implemented | www/js/pq-crypto.mjs |
| PQ signing (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512) | Implemented | www/js/pq-crypto.mjs |
| ML-KEM-768 keygen (KEM, no signing) | Implemented | www/js/pq-crypto.mjs |
| Kind 1 announcement event construction | Implemented | www/js/pq-crypto.mjs |
| Kind 9999 proof carrier event construction | Implemented | www/js/pq-crypto.mjs |
| NIP-01 event ID computation and Schnorr verification | Implemented | www/js/pq-crypto.mjs |
| OpenTimestamps submission (pending proof) | Implemented | www/js/pq-crypto.mjs |
| OTS upgrade polling (via server helper) | Implemented | www/js/pq-crypto.mjs |
| Relay publishing (WebSocket) | Implemented | www/index.html |
| Verification page (query relay or paste event JSON) | Implemented | www/verify.html |
| NIP-07 signer integration (nostr-login-lite) | Implemented | www/index.html |
What Is Not Yet Implemented
| Component | Description |
|---|---|
| Full light-client Bitcoin verification | Validate block headers, proof-of-work, difficulty, and chain linkage independently of an explorer API (the vendored javascript-opentimestamps library in resources/ provides primitives). Current path trusts a single explorer API for the header. |
| Multi-explorer cross-checking | Compare independent Bitcoin APIs and reject disagreement |
| Raw-nsec migration (Component 4) | Encrypt old nsec, publish kind 30078, 36-word phrase encoding |
| Quantum-safe self-storage (Component 5) | OTP or symmetric-key encryption for kind 30078 data |
| PQ event authentication / rotation / revocation | Companion protocols for signing future events with PQ keys, rotating/revoking keys, PQ encryption |
| Independent implementation / test vectors | A second implementation reproducing canonical encoding, derivation, and selection |
Dependencies
@noble/post-quantum— pure-JS PQ algorithms (ML-DSA, SLH-DSA, Falcon, ML-KEM)@noble/curves— secp256k1 Schnorr@noble/hashes— SHA-256@scure/bip39— BIP39 mnemonic generation/validation@scure/bip32— BIP32 HD wallet derivation@scure/base— bech32 (npub) encoding- OpenTimestamps — calendar servers for timestamping
javascript-opentimestamps— vendored OTS library (used as a fixture source; full light-client integration pending)- nostr-login-lite — NIP-07 / NIP-46 authentication
References
- NIP-03: OpenTimestamps Attestations for Events
- NIP-06: Basic key derivation from mnemonic seed phrase
- NIP-44: Encrypted Payloads (Versioned)
- BIP39: Mnemonic code for generating deterministic keys
- BIP32: Hierarchical Deterministic Wallets
- NIST FIPS 203: ML-KEM (Kyber)
- NIST FIPS 204: ML-DSA (Dilithium)
- NIST FIPS 205: SLH-DSA (SPHINCS+)
- OpenTimestamps
- Open Quantum Safe
License
This document is released into the public domain. The migration strategy described herein is intended as a community resource for the Nostr ecosystem.