nips/102.md
2024-09-16 14:47:18 -04:00

5.3 KiB

NIP-102

Subkey Attestation and Management

draft optional

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.

Attestation

A subkey attestation is created by signing the message:

nip102:<hex-subkey1-pubkey>

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 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:

{
  "inbox_keys": ["<hex-subkey1-pubkey>", "<hex-subkey2-pubkey>"],
  "revoked_subkeys": ["<hex-subkey3-pubkey>"]
}

Event Signing

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.

{
  "pubkey": "<hex-subkey1-pubkey>",
  "kind": 1,
  "tags": [
    ["I", "<hex-account0-pubkey>"],
    ["Ia", "<subkey1-attestation>"]
  ]
}

Event Validation

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.

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).

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:

{
  "kind": 1,
  "content": "GM",
  "I": "<hex-account0-pubkey>",
  "Ia": "<subkey1-attestation1>"
}

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.

Clients that do not support this NIP will continue to treat subkey1 and account0 as different accounts.

Stage 2:

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.

Reference Code

import secp256k1

def create_attestation(account_privkey, subkey_pubkey):
    privkey = secp256k1.PrivateKey(bytes.fromhex(account_privkey))
    signature = privkey.ecdsa_sign(bytes.fromhex(subkey_pubkey))
    return privkey.ecdsa_serialize(signature).hex()

def verify_attestation(account_pubkey, subkey_pubkey, attestation):
    pubkey = secp256k1.PublicKey(bytes.fromhex(account_pubkey), raw=True)
    signature = pubkey.ecdsa_deserialize(bytes.fromhex(attestation))
    return pubkey.ecdsa_verify(bytes.fromhex(subkey_pubkey), signature)

# Example usage
account_privkey = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
account_pubkey = secp256k1.PrivateKey(bytes.fromhex(account_privkey)).pubkey.serialize().hex()
subkey_pubkey = "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"

attestation = create_attestation(account_privkey, subkey_pubkey)
# attestation is "30440220198c94e388c3a5d7eed7f66ea83dd60a0156ba612c1d5067286ace5c641cbb600220739ca9cd3f3780f28c3a98df954736e323d3f23905bfea4482365055b2fb9fe5"

print(f"Attestation is valid: {verify_attestation(account_pubkey, subkey_pubkey, attestation)}")