{"id":"282ca634a65007864c3f8cbce50470e26bf875849c5c6c2a31745df908175686","pubkey":"da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd","created_at":1775035824,"kind":30817,"tags":[["d","nip-identity-trees"],["title","NIP-IDENTITY-TREES: Purpose-Tagged Identity Derivation (nsec-tree)"]],"content":"NIP-IDENTITY-TREES\n====================\n\nPurpose-Tagged Identity Derivation (nsec-tree)\n-------------------------------------------------\n\n`draft` `optional`\n\n## Motivation\n\nNostr users who want to maintain separate identities — one for social posting, one for commerce, one for a specific application — must today manage multiple independent nsecs. There is no standard way to derive these from a single secret, so users either reuse one identity (losing privacy) or accumulate a growing collection of unrelated keys with no recovery path if any are lost.\n\nNIP-06 solves a related problem: deterministic derivation of a single Nostr identity from a BIP-39 mnemonic. This NIP is complementary, not competing. It extends the same mnemonic path — using a different account index to avoid collision — and adds a second entry point for users who already have an nsec and do not use a mnemonic wallet. Both entry points produce a **tree root** from which an unlimited number of child keypairs can be derived deterministically.\n\nEach child is scoped to a human-readable **purpose string** and a numeric index. A `\"social\"` child and a `\"commerce\"` child are entirely independent secp256k1 keypairs; an observer cannot tell they share a root. A user who loses access to their wallet can recover all children by re-running derivation over their known purpose strings. Optional **linkage proofs** allow selective, verifiable disclosure that a child belongs to a particular master identity, without revealing other children.\n\n**This NIP requires no relay changes, no client changes, and no new event kinds. Child keys are ordinary Nostr keypairs.** Any existing Nostr client can use a child identity without knowing it was derived. The derivation happens entirely client-side, using only HMAC-SHA256 and — optionally — standard BIP-32 key derivation.\n\n## Notation\n\n| Symbol | Meaning |\n|--------|---------|\n| `HMAC-SHA256(key, msg)` | RFC 2104 HMAC with SHA-256 |\n| `utf8(s)` | UTF-8 encoding of string `s` |\n| `uint32_be(n)` | 4-byte unsigned big-endian integer |\n| `\\|\\|` | Byte concatenation |\n| `0x00` | Single null byte |\n| `secp256k1_x_only(sk)` | BIP-340 x-only public key from private key `sk` |\n\n## Overview\n\nThe following diagram illustrates the derivation flow:\n\n![Purpose-Tagged Identity Derivation Flow](https://raw.githubusercontent.com/forgesworn/nip-drafts/main/images/identity_trees-1.png)\n\n```mermaid\nflowchart TD\n    classDef green fill:#1b3d2d,stroke:#16c79a,color:#f0f0f0\n    classDef yellow fill:#2d2d1b,stroke:#f5a623,color:#f0f0f0\n    classDef blue fill:#1b2d3d,stroke:#0f3460,color:#f0f0f0\n    classDef red fill:#3d1b1b,stroke:#e94560,color:#f0f0f0\n    classDef purple fill:#2d1b3d,stroke:#9b59b6,color:#f0f0f0\n\n    MNEMONIC([BIP-39 Mnemonic<br/>12 or 24 words]):::blue\n    NSEC([Existing nsec]):::blue\n\n    MNEMONIC -- \"BIP-32 at<br/>m/44'/1237'/727'/0'/0'\" --> ROOT\n    NSEC -- \"HMAC-SHA256<br/>key=nsec, msg='nsec-tree-root'\" --> ROOT\n\n    ROOT[Tree Root Secret<br/>+ Master Pubkey]:::purple\n\n    ROOT -- \"HMAC-SHA256<br/>purpose='social', index=0\" --> SOCIAL([Social Identity<br/>npub1abc...]):::green\n    ROOT -- \"HMAC-SHA256<br/>purpose='commerce', index=0\" --> COMMERCE([Commerce Identity<br/>npub1def...]):::green\n    ROOT -- \"HMAC-SHA256<br/>purpose='nostr:persona:work', index=0\" --> PERSONA([Work Persona<br/>npub1ghi...]):::green\n\n    ROOT -. \"Blind proof<br/>BIP-340 Schnorr\" .-> PROOF{Linkage Proof<br/>master owns child}:::yellow\n    SOCIAL -. \"verified\" .-> PROOF\n\n    style ROOT stroke-width:3px\n```\n\n## Tree Root Derivation\n\nTwo entry points produce a 32-byte **tree root secret**. The tree root is a valid secp256k1 private key; its x-only public key (BIP-340) is the **master pubkey**, used in linkage proofs and recovery.\n\nThe two paths intentionally produce different tree roots from the same underlying key material. Users must choose one entry point and use it consistently.\n\n### Mnemonic Path\n\n```\nBIP-39 mnemonic (with optional passphrase)\n  -> BIP-32 seed\n  -> derive child at m/44'/1237'/727'/0'/0'  (all five levels hardened)\n  -> 32-byte private key = tree_root\n```\n\nThe derivation path `m/44'/1237'/727'/0'/0'` uses NIP-06's coin type (`1237'`) but a different account index (`727'`) to avoid collision with NIP-06's own identity at `0'`. All levels are hardened because the tree root is used only as an HMAC secret and linkage proof signer — never for extended public key derivation.\n\n```\nmaster_pubkey = secp256k1_x_only(tree_root)\n```\n\n### Nsec Path\n\nThe nsec is not used directly as the HMAC key. An intermediate HMAC creates one-way separation between the signing key and the derivation key, following the HKDF-Extract pattern:\n\n```\ntree_root = HMAC-SHA256(key = nsec_bytes, msg = utf8(\"nsec-tree-root\"))\n```\n\nWhere `nsec_bytes` is the raw 32-byte private key decoded from the bech32 `nsec`, and `\"nsec-tree-root\"` is the fixed 14-byte ASCII label (`6e7365632d747265652d726f6f74`).\n\n```\nmaster_pubkey = secp256k1_x_only(tree_root)\n```\n\nThis ensures the tree root cannot be reversed to recover the nsec, and that compromising a child key does not expose the nsec. The nsec remains usable as a standalone signing key without dual-purpose risk.\n\n## Child Key Derivation\n\nAll child keys are derived from the tree root via HMAC-SHA256:\n\n```\nmessage      = utf8(\"nsec-tree\") || 0x00 || utf8(purpose) || 0x00 || uint32_be(index)\nchild_privkey = HMAC-SHA256(key = tree_root, msg = message)\nchild_pubkey  = secp256k1_x_only(child_privkey)\n```\n\nThe HMAC message is constructed by concatenating:\n\n| Component | Encoding | Bytes |\n|-----------|----------|-------|\n| Domain prefix | `utf8(\"nsec-tree\")` | 9 bytes: `6e7365632d74726565` |\n| Separator | `0x00` | 1 byte |\n| Purpose | `utf8(purpose)` | Variable |\n| Separator | `0x00` | 1 byte |\n| Index | `uint32_be(index)` | 4 bytes, big-endian |\n\nThe null byte separators prevent concatenation ambiguity between purpose strings and indices.\n\n### Curve Order Handling\n\nHMAC-SHA256 output is 256 bits. If the output, interpreted as an unsigned integer, is greater than or equal to the secp256k1 curve order `n`, it is not a valid private key (probability ≈ 3.7×10⁻³⁹). In this case the implementation MUST increment the index by one and retry. The returned index reflects the **actual index used**, not the originally requested index.\n\n```\ncurrent_index = requested_index\nwhile current_index <= 0xFFFFFFFF:\n    msg       = utf8(\"nsec-tree\") || 0x00 || utf8(purpose) || 0x00 || uint32_be(current_index)\n    candidate = HMAC-SHA256(key = tree_root, msg = msg)\n    if candidate < n:\n        return (candidate, current_index)\n    current_index += 1\nerror(\"index overflow\")\n```\n\nIf incrementing would exceed `0xFFFFFFFF`, the derivation MUST fail. In practice this is impossible.\n\n## Purpose Strings\n\nPurpose strings MUST satisfy all of the following:\n\n1. Non-empty — minimum 1 byte when UTF-8 encoded\n2. Maximum 255 bytes when UTF-8 encoded\n3. No embedded null bytes (`0x00`)\n4. At least one non-whitespace character\n5. Case-sensitive, byte-exact — `\"Social\"` and `\"social\"` are different purposes\n\nRecommended format: lowercase, colon-namespaced (e.g. `\"social\"`, `\"commerce\"`, `\"trott:rider\"`, `\"402:api:v2:prod\"`). The colon convention lets applications claim namespaces without a central registry.\n\n## Linkage Proofs\n\nLinkage proofs allow the tree root owner to prove that a child identity belongs to them. Two types exist.\n\n### Blind Attestation\n\nProves ownership without revealing the derivation slot (purpose or index):\n\n```\nattestation = \"nsec-tree:own|\" || hex(master_pub) || \"|\" || hex(child_pub)\nsignature   = schnorr_sign(utf8(attestation), tree_root)\n```\n\n### Full Attestation\n\nProves ownership and reveals the derivation slot:\n\n```\nattestation = \"nsec-tree:link|\" || hex(master_pub) || \"|\" || hex(child_pub) || \"|\" || purpose || \"|\" || decimal(index)\nsignature   = schnorr_sign(utf8(attestation), tree_root)\n```\n\nIn both cases `hex(...)` is lowercase hex (64 characters for an x-only pubkey) and `decimal(index)` is the index in decimal ASCII with no leading zeroes.\n\n### Verification\n\nReconstruct the canonical attestation string from the proof's fields and compare it to the provided attestation. Then verify the BIP-340 Schnorr signature against the master pubkey:\n\n```\nexpected = canonical_attestation_from_fields(proof)\nif expected != proof.attestation:\n    return false\nreturn schnorr_verify(proof.signature, utf8(proof.attestation), master_pub)\n```\n\n### Proof Structure (JSON)\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `masterPubkey` | string | Lowercase hex x-only pubkey (64 chars) |\n| `childPubkey` | string | Lowercase hex x-only pubkey (64 chars) |\n| `purpose` | string? | Present in full proofs only |\n| `index` | number? | Present in full proofs only |\n| `attestation` | string | The signed message (UTF-8) |\n| `signature` | string | BIP-340 Schnorr signature (lowercase hex, 128 chars) |\n\n## Recovery\n\nSince derivation is deterministic, the same root always produces the same children. To recover all child identities, scan known purpose strings at indices `0` through `N-1`. A gap limit of N=20 is recommended, following BIP-44 convention: stop scanning a purpose once 20 consecutive indices have produced no known activity.\n\n## Compatibility\n\n**NIP-06:** Complementary. nsec-tree uses NIP-06's coin type (`1237'`) at a different account index (`727'`). Both derivations can coexist under the same mnemonic without collision.\n\n**NIP-07 / NIP-46:** Complementary. A signer can hold the tree root and derive child keys on demand, reducing the amount of key material that must be stored or transferred.\n\n**NIP-26:** Unrelated. NIP-26 delegated signing authority using tags embedded in events, requiring relay and client support. nsec-tree operates entirely client-side and produces ordinary keypairs with no protocol-level delegation mechanism.\n\n**Linked subkeys (PR #1810):** Complementary. If adopted, nsec-tree can generate the derived keys that linked subkeys would publicly associate.\n\n**NIP-4e (Decoupling Encryption from Identity, PR #1647):** Philosophically aligned. NIP-4e argues signing and encryption should use separate per-device keys. nsec-tree extends this principle to signing identities themselves — separate purpose-scoped keys from a single root, with cryptographic unlinkability between them.\n\n**NIP-102 (Subkey Attestation, PR #1450), NIP-0b (On-Behalf-Of, PR #1482), NIP-41 (Identity Management, PR #1032):** All three address publicly linked subkeys for device management or key rotation. nsec-tree solves a different problem: deriving multiple *unlinkable* identities for privacy, not linking device keys for recovery. The proposals are complementary — nsec-tree generates keys, subkey NIPs can publicly associate them when the user *chooses* to reveal the relationship.\n\n**NIP-D8 (Key Rotation, PR #2114):** Explicitly rejects HD subkey schemes due to validation cost. nsec-tree sidesteps this concern because child keys are ordinary Nostr keypairs — no chain validation is required. Verifiers only see a standalone pubkey unless the user presents a linkage proof.\n\n**FROSTR:** Threshold signing (t-of-n FROST) for Nostr. Solves multi-device key *security* (no single device holds the full secret) but does not address multi-identity derivation. Complementary — a FROSTR group could hold an nsec-tree root.\n\n## Security Considerations\n\n**Master compromise.** If the tree root leaks, all child keys are derivable. There is no forward secrecy — the tree is fully deterministic. Protect the master secret with the same rigour as any Nostr nsec.\n\n**Unlinkability.** Without a linkage proof, no observer can determine whether two child keys share a root. The derivation is entirely private; purpose strings, indices, and the tree root are HMAC inputs, never exposed in outputs.\n\n**One-way derivation.** Child keys cannot be reversed to recover the tree root. The tree root cannot be reversed to recover the nsec or mnemonic. Each derivation layer is a one-way HMAC-SHA256 operation.\n\n**Relay correlation.** nsec-tree provides cryptographic unlinkability. Network-level unlinkability (IP addresses, timing, relay sets) is an operational concern outside the scope of this NIP.\n\n**Zeroisation.** Implementations SHOULD zero secret material after use. In garbage-collected languages, string encodings (bech32 nsec) cannot be reliably zeroed; security-sensitive code should work with raw byte arrays.\n\n**No custom cryptography.** All primitives are standard: HMAC-SHA256 (RFC 2104), BIP-32 key derivation, BIP-340 Schnorr signatures.\n\n## Test Vectors\n\nAll conformant implementations MUST produce identical outputs for these inputs.\n\n### Vector 1 — nsec path, purpose \"social\", index 0\n\n**Input:**\n\n```\nnsec_bytes:  0101010101010101010101010101010101010101010101010101010101010101\npurpose:     \"social\"\nindex:       0\n```\n\n**Tree root derivation:**\n\n```\ntree_root:   8d2db9ce9548534e7ae924d05e311355e3a12744214c88e65b39fa2bf2df6d6f\nmaster_pub:  8c03e047ae60c01e942a8337e71d17e3517fcc63ee6ceff8173bbd23fabe649d\n```\n\n**Child derivation:**\n\n```\nmessage:      6e7365632d7472656500736f6369616c0000000000  (21 bytes)\nchild_priv:   98e98b476eab3c2bcb5020e4a679a41b74eebfb30a07944c4361c906501265e7\nchild_pub:    cdc4cd2a01ba1b8afd3299b66c38d13043a19acb687c334f0527cffaf464b372\nactual_index: 0\n```\n\n### Vector 4 — mnemonic path, purpose \"social\", index 0\n\n**Input:**\n\n```\nmnemonic:    \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\"\npassphrase:  (none)\npurpose:     \"social\"\nindex:       0\n```\n\n**Tree root derivation** (BIP-32 path `m/44'/1237'/727'/0'/0'`):\n\n```\ntree_root:   cc92d213b5eccd19eb85c12c2cf6fd168f27c2cc347c51a7c4c62ac67795fc65\nmaster_pub:  3eb14b67cc942c5388e03570b68d0887d40ff34af234662344e6c72a6298d656\n```\n\n**Child derivation:**\n\n```\nchild_priv:   f0e7c85f394df83212e108e60a7e226045742aa6d967ea1cfddf27ae65ac6ac8\nchild_pub:    1a4e31045ee7be1fc736954ffe7ea48fffc784865452a79545a027d0e712fc97\nactual_index: 0\n```\n\n## Reference Implementation\n\n- **TypeScript:** https://github.com/forgesworn/nsec-tree (ESM-only, 136 tests, frozen vectors)\n- **Rust:** https://github.com/forgesworn/heartwood (heartwood-core crate, 57 tests, byte-for-byte match)","sig":"5d7bd6ed0e32665400723942fb7b6a4b19ebf970b6fc74da775f20eb9add47dced0aaba5b74b0bdef9d33f87b228d3da8946c469cd34dca39cd8486c69d26c97"}