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)}")