Remove HD keys in favor of attestations

This commit is contained in:
Vinny Fiano 2024-09-16 14:38:50 -04:00
parent 843c33b3cc
commit 2308e84a89

110
102.md
View File

@ -1,109 +1,107 @@
NIP-102 NIP-102
======= =======
Hierarchical Deterministic Key Management Subkey Attestation and Management
----------------------------------------- ------------------------------
`draft` `optional` `draft` `optional`
This NIP defines a way to separate identity from authentication using hierarchical deterministic (HD) keys. This allows people to use one key pair to issue independent key pairs for different apps. If a key is compromised, the root key pair can publish an event revoking that key as of a specific time. This NIP defines a way to separate identity from authentication using subkey attestations. This allows the use of one keypair to issue independent keypairs for different apps. If any subkey key is compromised, the root keypair can publish an event revoking the key.
HD keys (BIP-32, BIP-39, BIP-44) provide a means of creating new key pairs from a parent in such a way that the new pubkey can be verified to be a child of the parent's pubkey. A message signed by this new key can be rapidly and locally verified as an authentic subkey of the claimed parent. ## Attestation
NIP-06 declares the default nostr derivation path to be `m/44'/1237'/<account>'/0/0`. A subkey attestation is created by signing the message:
``` ```
recovery phrase -> seed -> xpriv nip102:<hex-subkey1-pubkey>
-> m/44'/1237'/0' -> Account0
-> m/44'/1237'/0'/0/0 -> Account0 Subkey0
-> m/44'/1237'/0'/1/0 -> Account0 Subkey1
-> m/44'/1237'/0'/2/0 -> Account0 Subkey2
``` ```
## Management Event This signature is then added to events to demonstrate authorization to post on behalf of the original key.
The parent key can publish a `Kind 10102` event for subkey management. This event lists subkeys that have been revoked, as well as those that are currently active, and a preference for how valid subkeys that are not listed should be treated. This is only a preference, as it may not always be immediately available to clients and relays. The parent key can publish a `Kind 10102` event for subkey management. This event lists attestations that have been revoked, as well as those that should be used to encrypt messages so that they can be read using preferred clients. These declarations are only a preference, as they may not be available in a timely manner.
Content: Content:
```json ```json
{ {
"inbox_keys": ["<hex-subkey0-pubkey>", "hex-subkey2-pubkey"], "inbox_keys": ["<hex-subkey1-pubkey>", "<hex-subkey2-pubkey>"],
"revoked_keys": ["<hex-subkey1-pubkey>"], "revoked_subkeys": ["<hex-subkey3-pubkey>"]
"other_keys": ["<non-derived-subkey>"]
} }
``` ```
## Signing ## Event Signing
When signing events using a subkey, the account pubkey and subkey derivation path will be included as a well known attribute. Relays and clients should validate the subkey->parent relationship, and immediately discard messages with invalid claims. When signing events using a subkey, the account pubkey will be included as a well-known attribute. The event will also include an attestation of the subkey's authorization signed by the main key. Relays and clients should validate this attestation, and discard messages that make invalid claims.
```json ```json
{ {
"pubkey": "<hex-subkey1-pubkey>", "pubkey": "<hex-subkey1-pubkey>",
"kind": 1, "kind": 1,
"tags": [ "tags": [
["I", "<hex-account0-pubkey>", "<subkey1-derivation-path>"], ["I", "<hex-account0-pubkey>"],
["Ia", "<subkey1-attestation>"]
] ]
} }
``` ```
For clients that don't implement NIP-102, we can have the account key publish a `Kind 10102` key management event using the subkey at the time of creation: ## Event Validation
Content: An implementing client's subscriptions will be slightly different. For each subscription using `authors`, a second subscription will be registered specifying `#I` instead of `authors`. This will collect all events published by subkeys of the authors, each of which will be checked for a valid attestation in `Ia`. This can be done at the same time the event signature is validated. As the client finds new values of `I`, it will request `Kind 10102` events with an author of `I` in order to check for revocations. When a `Kind 10102` event with `revoked_attestations` is received, use this index to find events that are no longer valid.
```json
{
"account_key": "<hex-account0-pubkey>",
"derivation_path": "<subkey1-derivation-path>"
}
```
## Migration After validation, the client will treat an event as if it were signed by the pubkey specified in `I`. This includes honoring NIP-9 deletion requests for events authored by `account0` as well as those having an `I` of `account0` (eg, a subkey acting on behalf of `account0`).
Most current keys do not use BIP-39 derivation and will need to publish a `Kind 10102` event pointing to a new pubkey: If `subkey1` publishes an event without an `I` attribute, it should be treated as having an author of `subkey1` and NOT `account0`. `subkey1` may have its own profile, etc, separate from `account0` that is used in these circumstances.
## Destructive Events
`NIP-09` deletion requests should be honored for events authored by `account0` or having an `I` of `account0`.
## Adoption
### Stage 1:
Client support is required for basic functionality, so we start when the first client implements this spec.
The client will be able to sign messages for the keypair `account0`. A new keypair `subkey1` is generated, and an attestation that `subkey1` is authorized to post on behalf of `account0` is signed by `account0`. For brevity we will say this is a key rotation, and the client now switches to signing messages using `subkey1` with `subkey1-attestation`. The client publishes the message:
```json ```json
{ {
"other_keys": ["<non-derived-subkey>"] "kind": 1,
"content": "GM",
"I": "<hex-account0-pubkey>",
"Ia": "<subkey1-attestation1>"
} }
``` ```
The new key must publish the opposite `Kind 10102` event claiming the parent: All instances of the client will create secondary subscriptions with `#I` instead of `authors`. Valid events will be merged with those authored by the key in `I`, though destructive events (deletions / replacements) will be deferred until a `Kind 10102` has been retrieved for the pubkey `I`.
```json Clients that do not support this NIP will continue to treat `subkey1` and `account0` as different accounts.
{
"account_key": ["<parent-pubkey>"]
}
```
## Behavior ### Stage 2:
All searches for the account pubkey should return those signed by any account subkey. If an event is replaceable, it is up to the client or relay as to whether both messages are maintained, or if only the latest across all subkeys is maintained. In any situation where data will be lost, a reasonable effort should be made to locate the most recent revocation list before proceeding. Relays should ensure that `I` is indexed, and may drop events with invalid `Ia` attestations or have been revoked with the expectation that these will be discarded by clients anyway. Replacement events should ignore `I` and maintained per signing key until there is sufficient client support. In the interim conforming clients will merge these locally.
Do not attempt to resolve `other_keys`, as this could grow exponentially. Always work up from the subkey, as each key can only have a single `account_key`. Limit any search to a reasonable constant maximum depth, maybe 8. If the maximum depth is reached, use the original key instead of the most recently found.
## Reference Code ## Reference Code
```python ```python
from bip_utils import Bip39SeedGenerator, Bip32Slip10Secp256k1 import secp256k1
account0_derivation_path = "m/44'/1237'/0'" def create_attestation(account_privkey, subkey_pubkey):
subkey0_derivation_path = "0/0" privkey = secp256k1.PrivateKey(bytes.fromhex(account_privkey))
signature = privkey.ecdsa_sign(bytes.fromhex(subkey_pubkey))
return privkey.ecdsa_serialize(signature).hex()
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" def verify_attestation(account_pubkey, subkey_pubkey, attestation):
master_xpriv = "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu" pubkey = secp256k1.PublicKey(bytes.fromhex(account_pubkey), raw=True)
account0_xpub = "xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN" signature = pubkey.ecdsa_deserialize(bytes.fromhex(attestation))
subkey0_xpub = "xpub6Gf5o5yEF14TykSmvZBzS9wFSgnqvPsxit1v4CaaNf6S6S5mm169FRN3QkCsVsDm8NNaN8eGbQg9vR43BD9UqQTrfWFmRKoWep2gxQpFh3Q" return pubkey.ecdsa_verify(bytes.fromhex(subkey_pubkey), signature)
seed = Bip32Slip10Secp256k1.FromSeed(Bip39SeedGenerator(mnemonic).Generate()) # Example usage
account0 = seed.DerivePath(account0_derivation_path) account_privkey = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
subkey0 = account0.DerivePath(subkey0_derivation_path) account_pubkey = secp256k1.PrivateKey(bytes.fromhex(account_privkey)).pubkey.serialize().hex()
subkey_pubkey = "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"
def is_subkey(pubkey, subkey, derivation_path): attestation = create_attestation(account_privkey, subkey_pubkey)
parent = Bip32Slip10Secp256k1.FromExtendedKey(pubkey) # attestation is "30440220198c94e388c3a5d7eed7f66ea83dd60a0156ba612c1d5067286ace5c641cbb600220739ca9cd3f3780f28c3a98df954736e323d3f23905bfea4482365055b2fb9fe5"
if parent.DerivePath(derivation_path).PublicKey().ToExtended() == subkey:
return True
else:
return False
print(f'subkey0_xpub is child of account0_xpub: {is_subkey(account0_xpub, subkey0_xpub, subkey0_derivation_path)}') print(f"Attestation is valid: {verify_attestation(account_pubkey, subkey_pubkey, attestation)}")
``` ```