228 lines
8.1 KiB
Markdown
228 lines
8.1 KiB
Markdown
# Derivation Paths — A Simple Explanation
|
|
|
|
## The seed
|
|
|
|
When you create a mnemonic (seed phrase), it generates a single master key. Think of it as the root of a tree — one key that controls everything below it.
|
|
|
|
## The tree
|
|
|
|
From that master key, you can derive **child keys**. Each child key can have its own children, and so on. This creates a tree of keys, all derived from the same seed.
|
|
|
|
## The path
|
|
|
|
A **derivation path** is just a set of directions for walking down the tree. It tells you which branches to take, starting from the master key (`m`).
|
|
|
|
```
|
|
m / 44' / 1237' / 0' / 0 / 0
|
|
```
|
|
|
|
Read it left to right:
|
|
|
|
| Segment | Meaning |
|
|
|---------|---------|
|
|
| `m` | The master key (your seed) |
|
|
| `44'` | Purpose: "this is a BIP-44 wallet" |
|
|
| `1237'` | Coin type: "this is Nostr" (1237 is Nostr's registered coin type) |
|
|
| `0'` | Account: "account #0" |
|
|
| `0` | Change: "external/receive" (0) vs "internal/change" (1) |
|
|
| `0` | Address index: "address #0" |
|
|
|
|
Each segment derives a child key from the parent. Change any segment and you get a completely different key.
|
|
|
|
## The apostrophe (hardened vs unhardened)
|
|
|
|
The `'` after a number means **hardened**. It's the most important detail in the path.
|
|
|
|
### Without the apostrophe (unhardened)
|
|
|
|
```
|
|
m / 44' / 1237' / 0' / 0 / 0
|
|
^
|
|
no apostrophe = unhardened
|
|
```
|
|
|
|
Unhardened means: you can derive this child's **public key** from just the parent's **public key** — you don't need the private key.
|
|
|
|
This is useful for **watch-only wallets**: you can share the parent's extended public key with someone, and they can derive all the child public keys (addresses) without ever seeing your private key.
|
|
|
|
**The risk:** if a child **private key** leaks, and someone has the parent's extended public key, they can work backwards and derive **all sibling private keys**. So if address #5's private key leaks, addresses #0-4 and #6-99 are also compromised.
|
|
|
|
### With the apostrophe (hardened)
|
|
|
|
```
|
|
m / 44' / 1237' / 0' / 0' / 0'
|
|
^
|
|
apostrophe = hardened
|
|
```
|
|
|
|
Hardened means: you **need the parent's private key** to derive this child. You cannot derive it from the public key alone.
|
|
|
|
**The benefit:** if a child private key leaks, the attacker **cannot** derive sibling keys. Each hardened child is isolated. Compromising one doesn't compromise the others.
|
|
|
|
### Simple analogy
|
|
|
|
Imagine a building with floors and rooms:
|
|
|
|
- **Unhardened** = a glass door. Anyone with the floor key can see into all rooms on that floor. If someone picks the lock on room #5, they can figure out how to open rooms #0-4 and #6-99 too.
|
|
- **Hardened** = a steel door. You need the master floor key to open any room. Picking the lock on room #5 tells you nothing about the other rooms.
|
|
|
|
### Visual: what happens when one key leaks
|
|
|
|
**Unhardened** (no apostrophe) — one leak compromises ALL siblings:
|
|
|
|
```
|
|
m/44'/1237'/0'/0/0 ✓ safe
|
|
m/44'/1237'/0'/0/1 ✓ safe
|
|
m/44'/1237'/0'/0/2 ✓ safe
|
|
m/44'/1237'/0'/0/3 ✓ safe
|
|
m/44'/1237'/0'/0/4 ✓ safe
|
|
m/44'/1237'/0'/0/5 ✗ COMPROMISED (leaked)
|
|
m/44'/1237'/0'/0/6 ✗ COMPROMISED (derived from leak + parent pubkey)
|
|
m/44'/1237'/0'/0/7 ✗ COMPROMISED (derived from leak + parent pubkey)
|
|
...
|
|
m/44'/1237'/0'/0/99 ✗ COMPROMISED (derived from leak + parent pubkey)
|
|
|
|
Parent extended public key (m/44'/1237'/0'/0) is public
|
|
+ one child private key (address #5) leaks
|
|
= ALL 100 sibling private keys are compromised
|
|
```
|
|
|
|
**Hardened** (with apostrophe) — one leak only affects that one key:
|
|
|
|
```
|
|
m/44'/1237'/0'/0'/0' ✓ safe
|
|
m/44'/1237'/0'/0'/1' ✓ safe
|
|
m/44'/1237'/0'/0'/2' ✓ safe
|
|
m/44'/1237'/0'/0'/3' ✓ safe
|
|
m/44'/1237'/0'/0'/4' ✓ safe
|
|
m/44'/1237'/0'/0'/5' ✗ COMPROMISED (leaked)
|
|
m/44'/1237'/0'/0'/6' ✓ safe (cannot be derived without parent PRIVATE key)
|
|
m/44'/1237'/0'/0'/7' ✓ safe (cannot be derived without parent PRIVATE key)
|
|
...
|
|
m/44'/1237'/0'/0'/99' ✓ safe (cannot be derived without parent PRIVATE key)
|
|
|
|
One child private key (address #5) leaks
|
|
= ONLY address #5 is compromised
|
|
= siblings are safe because hardened derivation requires the parent PRIVATE key
|
|
```
|
|
|
|
## Why NIP-06 uses unhardened last segments
|
|
|
|
NIP-06 (Nostr's key derivation standard) uses `m/44'/1237'/<account>'/0/0` — the first three segments are hardened, the last two are unhardened.
|
|
|
|
This is because NIP-06 copied the BIP-44 pattern from Bitcoin, where:
|
|
- The **account** segment is hardened (so different accounts are isolated)
|
|
- The **change** and **address** segments are unhardened (so watch-only wallets can derive addresses without the private key)
|
|
|
|
For Bitcoin, this makes sense: you want to share your extended public key with a payment processor so they can generate receive addresses for you.
|
|
|
|
For Nostr, it's less useful — but it means Nostr tools can derive your public keys from your extended public key, which some key management software uses.
|
|
|
|
## What this means for n_signer
|
|
|
|
n_signer always holds your private key and derives everything itself. You never share extended public keys with anyone. So:
|
|
|
|
- **Unhardened segments give you no benefit** — you don't need watch-only derivation
|
|
- **Unhardened segments add risk** — the child key compromise vulnerability
|
|
|
|
### Recommendation
|
|
|
|
**Harden everything** if you don't need NIP-06 compatibility:
|
|
|
|
```
|
|
m/44'/1237'/0'/0'/0' ← all hardened, maximum isolation
|
|
m/44'/1237'/0-99'/0'/0' ← all hardened, 100 isolated agent keys
|
|
```
|
|
|
|
**Use NIP-06 paths** if you want compatibility with standard Nostr tools:
|
|
|
|
```
|
|
m/44'/1237'/0'/0/0 ← NIP-06 standard (last two unhardened)
|
|
m/44'/1237'/0-99'/0/0 ← NIP-06 compatible, 100 agent keys
|
|
```
|
|
|
|
## Common path patterns
|
|
|
|
### Standard Nostr (NIP-06)
|
|
|
|
```
|
|
m/44'/1237'/0'/0/0
|
|
```
|
|
|
|
One key. The default Nostr key that tools like `nak keygen` produce.
|
|
|
|
### Multiple Nostr accounts (NIP-06)
|
|
|
|
```
|
|
m/44'/1237'/0'/0/0 ← account 0
|
|
m/44'/1237'/1'/0/0 ← account 1
|
|
m/44'/1237'/2'/0/0 ← account 2
|
|
```
|
|
|
|
Change the account segment (hardened) to get different Nostr identities.
|
|
|
|
### Multiple Nostr agents (hardened, maximum isolation)
|
|
|
|
```
|
|
m/44'/1237'/0'/0'/0' ← agent 0
|
|
m/44'/1237'/1'/0'/0' ← agent 1
|
|
m/44'/1237'/2'/0'/0' ← agent 2
|
|
```
|
|
|
|
Same as above but with the last two segments hardened. Each agent is fully isolated — compromising one doesn't compromise the others.
|
|
|
|
### Range and wildcard syntax (n_signer wizard)
|
|
|
|
In n_signer's role wizard, you can use range syntax or wildcard for the variable segment:
|
|
|
|
```
|
|
m/44'/1237'/0-99'/0/0 ← agents 0-99, NIP-06 compatible
|
|
m/44'/1237'/0-99'/0'/0' ← agents 0-99, all hardened
|
|
m/44'/1237'/*'/0'/0' ← any agent index, all hardened (wildcard)
|
|
```
|
|
|
|
- `0-99'` means "this segment can be any value from 0 to 99, hardened"
|
|
- `*'` means "this segment can be any non-negative integer, hardened" (wildcard — no range limit)
|
|
- `*` (without `'`) means "any non-negative integer, unhardened"
|
|
|
|
The client specifies the exact path (e.g. `m/44'/1237'/5'/0/0` for agent #5), and the server verifies it's within the role's allowed range (or accepts any value for `*`).
|
|
|
|
### SSH keys (ed25519)
|
|
|
|
```
|
|
m/44'/102001'/0'/0'/0'
|
|
```
|
|
|
|
SLIP-0010 derivation for ed25519. All segments are hardened (SLIP-0010 requires this for ed25519).
|
|
|
|
### Age / x25519 keys
|
|
|
|
```
|
|
m/44'/102002'/0'/0'/0'
|
|
```
|
|
|
|
Key agreement keys for Age encryption or X25519 ECDH.
|
|
|
|
### Post-quantum keys
|
|
|
|
```
|
|
m/44'/102003'/0'/0'/0' ← ML-DSA-65 (signatures, FIPS 204)
|
|
m/44'/102004'/0'/0'/0' ← SLH-DSA-128s (signatures, FIPS 205)
|
|
m/44'/102005'/0'/0'/0' ← ML-KEM-768 (KEM, FIPS 203)
|
|
```
|
|
|
|
All hardened. The mnemonic-derived seed feeds a SHAKE-256 DRBG that replaces PQClean's `randombytes()` during keygen.
|
|
|
|
## Summary
|
|
|
|
| Concept | Simple explanation |
|
|
|---------|-------------------|
|
|
| `m` | The master key (your seed) |
|
|
| Numbers | Which branch to take at each level |
|
|
| `'` (apostrophe) | "Hardened" — need private key to derive, isolates siblings |
|
|
| No `'` | "Unhardened" — can derive from public key, but siblings can be compromised |
|
|
| Path | A set of directions from the master key to a specific key |
|
|
| Different path | Different key (always, no exceptions) |
|
|
|
|
**Golden rule:** If you don't need watch-only derivation (and n_signer doesn't), harden everything.
|