Files

12 KiB

NIP-QR: Two-Account Flow Architecture

⚠️ Design-only research document. This describes the proposed architecture; the current implementation is a research prototype and does not by itself make Nostr identities post-quantum secure.

The Flow (as proposed)

User has Account #1 (existing nsec, their current Nostr identity)
                          │
                          ▼
    ┌──────────────────────────────────────────┐
    │           PQ Web App (browser)           │
    │                                          │
    │  Step 1: User creates Account #2 in      │
    │          Amber (generates new seed)       │
    │                                          │
    │  Step 2: Web app retrieves seed phrase   │
    │          from Amber (Account #2)         │
    │                                          │
    │  Step 3: Web app derives PQ keys from    │
    │          seed (client-side WASM, liboqs) │
    │          - ML-DSA-65 keypair             │
    │          - SLH-DSA-128s keypair          │
    │          - ML-KEM-768 keypair            │
    │                                          │
    │  Step 4: Web app builds NIP-QR event     │
    │          and requests signatures:        │
    │          - Amber Account #1 signs with   │
    │            old nsec (secp256k1)          │
    │          - Amber Account #2 signs with   │
    │            seed-derived secp256k1        │
    │          - Web app PQ-signs with each    │
    │            PQ private key (client-side)  │
    │                                          │
    │  Step 5: Web app publishes NIP-QR event  │
    │          to relays + requests OTS        │
    └──────────────────────────────────────────┘

Why Two Accounts?

The two-account design solves a fundamental problem: how do you cryptographically link an existing nsec-based identity to PQ keys, when the existing nsec has no relationship to any seed phrase?

Account Key source Role in NIP-QR
Account #1 (existing) Raw nsec (random, no seed) The identity the social graph knows about. Signs to say "I am migrating to Account #2 and its PQ keys"
Account #2 (new) Seed phrase (BIP39 → NIP-06) The bridge. Its secp256k1 key is derived from the same seed as the PQ keys. Signs to say "These PQ keys come from the same seed as me"

The cryptographic chain of trust is:

Account #1 (old identity, social graph knows this npub)
    │
    │ signs: "I am the same entity as Account #2"
    │        (secp256k1 Schnorr signature, valid pre-quantum)
    ▼
Account #2 (seed-derived secp256k1 key)
    │
    │ same seed phrase ──────► PQ keys (ML-DSA, SLH-DSA, ML-KEM)
    │                              │
    │ signs: "These PQ keys        │ sign: "We are derived from
    │         come from my seed"   │       the same seed as
    │         (secp256k1 Schnorr)  │       Account #2"
    │                              │ (PQ signatures)
    ▼                              ▼
    NIP-QR event (published under Account #1's pubkey)

The NIP-QR Event Structure

{
  "kind": <NIP-QR kind, to be assigned>,
  "pubkey": "<Account #1 secp256k1 pubkey (hex)>",
  "created_at": <unix timestamp>,
  "tags": [
    ["successor", "<Account #2 secp256k1 pubkey (hex)>"],
    ["algorithm", "ml-dsa-65"],
    ["algorithm", "slh-dsa-128s"],
    ["algorithm", "ml-kem-768"]
  ],
  "content": "<JSON string, see below>",
  "sig": "<Account #1 Schnorr signature over the event id>"
}

Content field (JSON string):

{
  "statement": "Identity <npub1> is migrating to successor <npub2>. All PQ keys listed below are derived from the same BIP39 seed as <npub2>. This link is established pre-quantum.",
  "successor_pubkey": "<Account #2 secp256k1 pubkey (hex)>",
  "successor_signature": "<Account #2 Schnorr signature over the statement+successor_pubkey+all_pq_keys>",
  "pq_keys": [
    {
      "algorithm": "ml-dsa-65",
      "public_key": "<ML-DSA-65 public key, base64>",
      "signature": "<ML-DSA signature over the statement, base64>"
    },
    {
      "algorithm": "slh-dsa-128s",
      "public_key": "<SLH-DSA-128s public key, base64>",
      "signature": "<SLH-DSA signature over the statement, base64>"
    },
    {
      "algorithm": "ml-kem-768",
      "public_key": "<ML-KEM-768 public key, base64>",
      "note": "KEM key for encryption; ownership asserted by successor_signature covering this key"
    }
  ]
}

Signature layers:

  1. Account #1 Schnorr signature (the event sig field): Signs the event ID (standard NIP-01 signing). This is the old identity saying "I authorize this migration."

  2. Account #2 Schnorr signature (successor_signature in content): Signs the statement + all PQ public keys. This proves the seed-derived key endorses the PQ keys. Since both Account #2's secp256k1 key and the PQ keys come from the same seed, this transitively proves PQ key ownership.

  3. PQ signatures (in each pq_keys entry): Each PQ signature scheme signs the statement. These are the post-quantum signatures that remain valid after secp256k1 is broken.

  4. ML-KEM: Cannot sign (it's a KEM, not a signature scheme). Its ownership is proven by Account #2's signature covering its public key.

What the Web App Does vs. What Amber Does

Step Who What
Create Account #2 Amber Generates new BIP39 seed, derives secp256k1 key via NIP-06, stores both encrypted
Retrieve seed phrase Amber → Web app User exports seed from Amber's backup screen (or future NIP-46 method)
Derive PQ keys Web app (WASM) Uses liboqs-wasm to derive ML-DSA, SLH-DSA, ML-KEM keypairs from seed via HKDF
PQ sign statement Web app (WASM) Signs the link statement with each PQ private key
Sign with Account #1 Amber NIP-46 signEvent — old nsec signs the NIP-QR event
Sign with Account #2 Amber NIP-46 signEvent — seed-derived secp256k1 signs the statement
Assemble event Web app Combines all signatures into the NIP-QR event JSON
Publish to relays Web app Sends the event to Nostr relays
OpenTimestamps Web app Requests OTS attestation via NIP-03 for the event

PQ Key Derivation from Seed

The web app derives PQ keys from the BIP39 seed using a deterministic scheme:

seed = PBKDF2-HMAC-SHA512(mnemonic, passphrase, 2048 iterations)  // standard BIP39

// Derive each PQ key from the seed using HKDF with algorithm-specific labels
ml_dsa_privkey  = HKDF-SHA512(seed, "nostr-pq-ml-dsa-65",  length=32)  → expand to full key
slh_dsa_privkey = HKDF-SHA512(seed, "nostr-pq-slh-dsa-128s", length=32) → expand to full key
ml_kem_privkey  = HKDF-SHA512(seed, "nostr-pq-ml-kem-768",  length=32) → expand to full key

Note: The exact PQ key derivation from a seed needs careful specification. liboqs key generation is typically random, not deterministic from a seed. We need to use the seed as a deterministic RNG seed (e.g., DRBG(seed || algorithm_label)) to make PQ key generation reproducible. This is a detail to nail down in the NIP.

Seed Phrase Retrieval: Three Options

Option 1: Manual Export (No Amber changes)

  • User goes to Amber's Account #2 backup screen
  • Amber displays the seed words (SeedWordsPage.kt)
  • User copies/types seed words into the web app
  • Web app derives PQ keys client-side

Pros: No Amber code changes. Available today. Cons: User manually handles seed phrase (friction, potential for error).

Option 2: New NIP-46 Method (Amber changes)

  • Add get_seed_words method to Amber's NIP-46 handler
  • Web app requests seed words via NIP-46
  • Amber shows approval screen: "Web app X wants to read your seed phrase"
  • On approval, seed words are sent to the web app via NIP-46 (encrypted with NIP-44)

Pros: Smooth UX, no manual copy. Cons: Seed phrase transmitted over NIP-46 (even though encrypted). Requires Amber modification. Security concern — a malicious web app could steal the seed.

Option 3: Web App Generates Seed (No Amber changes for retrieval)

  • Web app generates a new BIP39 seed phrase client-side (WASM)
  • Web app derives both secp256k1 (Account #2) and PQ keys from the seed
  • Web app displays the seed phrase for the user to write down
  • User manually imports the seed phrase into Amber as Account #2 (via Amber's mnemonic login screen)
  • Web app already has the seed, so no retrieval needed

Pros: Web app has the seed from the start. No retrieval needed. User imports to Amber for future signing. Cons: User must manually import seed into Amber (but this is a one-time step). Seed exists in browser memory temporarily.

Recommendation: Start with Option 3 (web app generates seed, user imports to Amber). It requires zero Amber changes and gives the web app immediate access to the seed for PQ key derivation. Later, Option 2 can be added for a smoother UX.

Complete User Journey

First Visit (Migration)

  1. User opens the PQ web app in their browser
  2. Web app connects to Amber via NIP-46 (or NIP-07 browser extension)
  3. Web app requests Account #1's public key → gets existing npub
  4. Web app generates a new 12-word BIP39 seed phrase (client-side WASM)
  5. Web app displays the seed phrase: "Write this down. This is your quantum-safe backup."
  6. Web app derives Account #2's secp256k1 key from seed (NIP-06: m/44'/1237'/0'/0/0)
  7. Web app derives PQ keys from seed (liboqs-wasm)
  8. Web app constructs the NIP-QR event content
  9. Web app PQ-signs the statement with each PQ private key (client-side)
  10. Web app requests Amber to sign the statement with Account #2's secp256k1 key
    • But wait — Account #2 doesn't exist in Amber yet! The user needs to import the seed first.
    • Pause: Web app instructs user: "Open Amber, create new account with this seed phrase"
    • User imports seed into Amber as Account #2
    • Web app now requests Amber (Account #2) to sign the statement
  11. Web app requests Amber (Account #1) to sign the NIP-QR event
  12. Web app assembles the complete event with all signatures
  13. Web app publishes the event to relays
  14. Web app requests OpenTimestamps attestation (NIP-03)
  15. Web app shows success: "Your identity is now quantum-resistant. Save your seed phrase."

Future Login (After Migration)

  1. User opens any PQ-aware Nostr client
  2. Client finds the NIP-QR event for their npub (Account #1)
  3. Client sees: Account #1 → Account #2 → PQ keys
  4. User signs in with Amber (Account #2, seed-derived)
  5. Client can now verify PQ signatures and use PQ encryption

Post-Quantum Scenario (After secp256k1 is broken)

  1. Attacker breaks Account #1's secp256k1 key via Shor's algorithm
  2. Attacker can forge Account #1 signatures, but:
    • Cannot forge PQ signatures (ML-DSA, SLH-DSA still secure)
    • Cannot backdate a fraudulent NIP-QR event (OTS proof anchors the real one)
    • Cannot forge Account #2's signature (also secp256k1, also broken — BUT the OTS-anchored NIP-QR event already established the link pre-quantum)
  3. Clients verify: "The NIP-QR event with the earliest valid OTS proof wins"
  4. The real NIP-QR event (anchored pre-quantum) is trusted over any fraudulent event
  5. PQ signatures on the NIP-QR event remain valid and prove the PQ keys are the user's

What Needs to Be Built

Web App (new project)

  • Static HTML/JS/WASM site (no backend with secrets)
  • BIP39 seed generation (WASM or JS)
  • NIP-06 key derivation from seed (secp256k1)
  • liboqs-wasm integration for PQ key generation and signing
  • PQ key derivation from seed (deterministic via HKDF/DRBG)
  • NIP-46 client (connect to Amber)
  • NIP-07 client (fallback for browser extension signers)
  • NIP-QR event builder and assembler
  • Relay publisher (WebSocket)
  • NIP-03 OpenTimestamps client
  • UI: seed display, progress, verification

Amber (no changes required for Option 3)

  • Already supports: multiple accounts, seed phrase login, NIP-46 signing
  • Future enhancement (Option 2): get_seed_words NIP-46 method

NIP-QR Specification (new NIP document)

  • Event kind number
  • Content schema
  • Signature scheme (who signs what)
  • PQ key derivation from seed (exact HKDF labels, DRBG seeding)
  • OTS requirement
  • Client verification algorithm
  • Revocation/update mechanism