2026-07-16 20:36:25 -04:00
2026-07-16 20:36:25 -04:00
2026-07-16 20:36:25 -04:00
2026-07-16 20:36:25 -04:00

Post-Quantum Nostr

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

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:

  1. Extract the public key from the event.
  2. Run Shor's algorithm to recover the private key.
  3. 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:

  1. 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.
  2. 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.
  3. 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

  1. No social graph loss — existing follows and identity carry forward
  2. No user re-onboarding — the seed phrase becomes the new root of trust
  3. Backward compatible — legacy clients continue working; PQ is additive
  4. No algorithm consensus required — link to all viable PQ schemes now, converge later
  5. Pre-quantum timestamping — establish links now, before quantum computers exist
  6. 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, quantum-safe]
    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:

  1. Consistency with NIP-06 — PQ keys are derived the same way as secp256k1 keys, just at different child indices
  2. Compartmentalization — hardened derivation at the account level means breaking one key doesn't compromise siblings
  3. Wallet compatibility — HD wallet infrastructure (Amber, hardware wallets, etc.) can derive these same paths
  4. Tree structure — the derivation path documents exactly which key is where
  5. 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 Borderline
24 words 256 bits ~128 bits Adequate

For users concerned about quantum attacks on the seed entropy itself, 24-word mnemonics are recommended. The bigger risk to seed phrases is classical (theft, phishing), not quantum. (Note: the current implementation defaults to 12-word mnemonics; 24-word support is recommended for production use.)


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 11112]

    KIND1 --> OTS[OpenTimestamp the kind 1 event hash]
    OTS --> KIND11112[Kind 11112 Wrapper Event - carries OTS proof]
    KIND11112 --> 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 KIND11112 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 11112 Wrapper (machine-verifiable, carries the OTS proof)

A replaceable event (kind 11112, in the 1000019999 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).

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 11112 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):

  1. Clients simply stop trusting the broken scheme — no revocation event needed; clients just ignore links to algorithms on a known-broken list
  2. The remaining links are still valid
  3. 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 11112 wrapper'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 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_at is 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:

  1. Submitted to OpenTimestamps calendar servers to obtain a pending .ots proof.
  2. Recorded in the kind 11112 wrapper's sha256 tag.
  3. The .ots proof is embedded in the kind 11112 wrapper's ots tag.

The kind 11112 wrapper 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

The current implementation detects Bitcoin confirmation by searching the .ots proof bytes for the Bitcoin block-header attestation magic bytes, and delegates proof upgrading to a server-side helper. Full client-side OTS verification (parsing the proof, validating the Merkle path, checking the block header against the Bitcoin chain) is planned but not yet implemented. The vendored javascript-opentimestamps library in resources/ provides the necessary primitives and will be integrated in a future release.

What Should Be OpenTimestamped

Not every event needs OTS. Priority list:

  1. Key-link events — absolutely critical, foundation of PQ migration
  2. Identity-defining events — NIP-05 verification, profile events (kind 0)
  3. Important content — contracts, announcements, evidence
  4. 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:

  1. Generate a standard 12-word BIP39 phrase (128 bits entropy)
  2. Derive a 32-byte encryption key from the BIP39 seed: enc_key = HKDF-Extract(salt="nostr-legacy-key", ikm=bip39_seed)
  3. Encrypt the old nsec: encrypted_nsec = ChaCha20(enc_key, nonce=0, old_nsec) → 32 bytes
  4. Encode the 32-byte encrypted nsec as 24 BIP39 words (with its own checksum)
  5. 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.

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:

  1. Generate a 12-word BIP39 phrase
  2. Derive from seed: new secp256k1 key (NIP-06 path), PQ keys (BIP32), symmetric encryption key (HKDF)
  3. Encrypt old nsec with the symmetric key: encrypted_nsec = ChaCha20-Poly1305(enc_key, old_nsec)
  4. Publish as kind 30078 event, signed with the new secp256k1 key
  5. Publish key-link event (cross-signed, OpenTimestamped via NIP-03)
  6. 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 18) 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:

  1. Compute IL using the chain code + child public key + index
  2. Recover the parent private key
  3. Derive ALL children at that level
  4. 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

  1. Use separate accounts for separate Nostr identities — hardened derivation contains the damage
  2. Never publish xpubs — chain codes are the keys to the kingdom
  3. Use 24-word mnemonics for adequate PQ security on the seed entropy
  4. Don't reuse keys across services — each identity gets its own account index
  5. 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 --> KIND11112[Build kind 11112 wrapper with OTS proof]
        KIND11112 --> SECP2[Account 1 signs kind 11112 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[Republish kind 11112 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

  1. User signs in with their existing Nostr identity (NIP-07 signer or nostr-login-lite)
  2. Client generates a 12-word phrase, shows it to the user to write down
  3. Client derives 5 PQ keypairs from the seed via BIP32 paths
  4. Each PQ signature scheme signs the human-readable attestation text
  5. The user's signer signs the kind 1 announcement and the kind 11112 wrapper
  6. The SHA-256 of the full signed kind 1 event is submitted to OpenTimestamps
  7. Both events are published to relays
  8. The client polls for Bitcoin confirmation and republishes the kind 11112 wrapper with the confirmed OTS proof
  9. 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 11112 wrapper 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 client-side OTS verification Parse the .ots proof, validate the Merkle path, check the block header against the Bitcoin chain (the vendored javascript-opentimestamps library in resources/ provides the primitives)
OTS target-digest binding Verify the OTS proof's target digest equals the event's sha256 tag
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
Test suite Known-answer tests for derivation, PQ sign/verify round-trips, event verification
24-word mnemonic default Currently defaults to 12 words; 24 recommended for production

Dependencies


References


License

This document is released into the public domain. The migration strategy described herein is intended as a community resource for the Nostr ecosystem.

S
Description
No description provided
Readme
6.8 MiB
Languages
JavaScript 87.5%
CSS 6.1%
HTML 5.7%
Shell 0.7%