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:
-
Account #1 Schnorr signature (the event
sigfield): Signs the event ID (standard NIP-01 signing). This is the old identity saying "I authorize this migration." -
Account #2 Schnorr signature (
successor_signaturein 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. -
PQ signatures (in each
pq_keysentry): Each PQ signature scheme signs the statement. These are the post-quantum signatures that remain valid after secp256k1 is broken. -
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_wordsmethod 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)
- User opens the PQ web app in their browser
- Web app connects to Amber via NIP-46 (or NIP-07 browser extension)
- Web app requests Account #1's public key → gets existing npub
- Web app generates a new 12-word BIP39 seed phrase (client-side WASM)
- Web app displays the seed phrase: "Write this down. This is your quantum-safe backup."
- Web app derives Account #2's secp256k1 key from seed (NIP-06:
m/44'/1237'/0'/0/0) - Web app derives PQ keys from seed (liboqs-wasm)
- Web app constructs the NIP-QR event content
- Web app PQ-signs the statement with each PQ private key (client-side)
- 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
- Web app requests Amber (Account #1) to sign the NIP-QR event
- Web app assembles the complete event with all signatures
- Web app publishes the event to relays
- Web app requests OpenTimestamps attestation (NIP-03)
- Web app shows success: "Your identity is now quantum-resistant. Save your seed phrase."
Future Login (After Migration)
- User opens any PQ-aware Nostr client
- Client finds the NIP-QR event for their npub (Account #1)
- Client sees: Account #1 → Account #2 → PQ keys
- User signs in with Amber (Account #2, seed-derived)
- Client can now verify PQ signatures and use PQ encryption
Post-Quantum Scenario (After secp256k1 is broken)
- Attacker breaks Account #1's secp256k1 key via Shor's algorithm
- 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)
- Clients verify: "The NIP-QR event with the earliest valid OTS proof wins"
- The real NIP-QR event (anchored pre-quantum) is trusted over any fraudulent event
- 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_wordsNIP-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