Thought experiment around bringing dids to nostr.
I have no idea whether this will work, or if it's a good idea. What has me interested is:
- Could provide a means for key rotation while using the same pubkey as identity
- Could allow for interop between different protocols / systems
- Could enable use of Verifiable Credentials
- e.g. CashApp could issue cashtag Verifiable Credentials to DIDs. This would basically allow people to have a "blue checkmark" or, more generally speaking, some trusted way to say "this pubkey is linked to this cashtag".
- e.g. could introduce stuff like payments on nostr via CashApp simply by having a pubkey present their cashtag VC to a CashApp api endpoint. VC can be used as a form of authentication, since the VC was issued by CashApp
- Could allow for less reliance on DNS if desired
DIDs, like URLs are fully qualified URIs. Similar to a URL, DIDs can be resolved. resolving a URL like https://snort.social/
returns html. So what does resolving a DID return? A DID document
What is a DID document? it's a JSON object that contains information about the DID e.g.
- how to contact this DID,
- pubkeys this DID uses to sign messages,
- asymmetric encryption keys that can be used to create shared keys and encrypt messages for the DID. Example:
{
"@context": [
"https://www.w3.org/ns/did/v1",
],
"id": "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48",
"verificationMethod": [
{
"id": "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48#nip06-0",
"type": "SchnorrSecp256k1VerificationKey2019",
"controller": "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48",
"publicKeyHex": "e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48"
}
],
"service": [
{
"id": "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48#nostr-relays",
"type": "Relay",
"serviceEndpoint": ["wss://relay.damus.io", "wss://relay.nostr.info"]
},
{
"id": "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48#ln",
"type": "LightningNode",
"serviceEndpoint": "ip://024bfaf0cabe7f874fd33ebf7c6f4e5385971fc504ef3f492432e9e3ec77e1b5cf@52.1.72.207:9735"
}
],
"keyAgreement": [
{
"id": "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48#keyagreement",
"type": "X25519KeyAgreement2023",
"controller": "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48",
"publicKeyHex": "75d92cea4ab8ef28a0a14acf103d6b8a2bb026120d62d1817fa5a4b11f534038"
}
]
}
π‘ TODO: think of more
service
examples
π‘ TODO: figure out
id
property forkeyAgreement
so what do the properties in the DID document mean?
property | description | notes |
---|---|---|
verificationMethod |
includes crypto keys that can be used for various purposes, such as to verify digital signatures | the key shown in the example above happens to be the decoded nostr pubkey |
service |
lists services that can be used to interact with a DID | the example includes the nostr relays that this DID publishes to. Could also include any other service e.g. a lightning node |
keyAgreement |
lists keys that can be used to generate shared keys for encryption/decryption purposes |
π‘ there are several other properties that can exist on DID documents. more info on that here
This NIP proposes the following:
- how to generate a nostr did
- a new event kind
9325
for publishing, patching, and recovering a DID. - how to resolve a nostr DID
did:nostr:<nostr_hex_pubkey>
- generate a new (or use an existing) nostr pubkey according to BIP340 as mentioned in nip01.
nostr_hex_pubkey
is represented as 32-bytes lowercase hex-encoded public key - prefix the
nostr_hex_pubkey
withdid:nostr:
did:nostr:41e791de6a6f6f0b3c820c2db179c0679e2c228ae6ecb9583cb48b3e1ff354b6
π‘ if desired nostr dids can be displayed using the bech32 encoded
npub
format described in nip19 e.g.did:nostr:npub1u2...
. As stated in nip19: The bech32 encodings should not be used to represent a did in nostr events.
the base DID document can be derived without sending a publish
message to a relay. It's not all that useful in and of itself, but forms the foundation of DID resolution.
π‘ TODO: write out steps to derive base DID Document by reading reference implementation
// from example.ts in reference implementation
const did = pubKeyToDid(kp.public);
console.log(deriveDidDoc(did));
Output
{
"id": "did:nostr:41e791de6a6f6f0b3c820c2db179c0679e2c228ae6ecb9583cb48b3e1ff354b6",
"verificationMethod": [
{
"id": "did:nostr:41e791de6a6f6f0b3c820c2db179c0679e2c228ae6ecb9583cb48b3e1ff354b6#nip06-0",
"type": "SchnorrSecp256k1VerificationKey2019",
"controller": "did:nostr:41e791de6a6f6f0b3c820c2db179c0679e2c228ae6ecb9583cb48b3e1ff354b6",
"publicKeyHex": "41e791de6a6f6f0b3c820c2db179c0679e2c228ae6ecb9583cb48b3e1ff354b6"
}
]
}
π‘ the
verificationMethod
in the example above is a schnorr pubkey. SchnorrSecp256k1VerificationKey2019 is used to describe the type of verification method. Honestly not entirely sure what the motiviation is behind including the year (aka2019
).
This event can be used to publish, patch, and recover a DID. every 9325
message should contain the o
(e.g op) and d
(e.g. did
) tags
{
"kind": 9325,
"tags": [
["o", "publish | patch | recover"],
["d", "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48"]
]
}
{
"kind": 9325,
"pubkey": "e3932a8cd4ec81e1e9a41467470ec9db817accf21e1e8b939525e84da6786d9a",
"created_at": 1676957260,
"tags": [
["d","did:nostr:e3932a8cd4ec81e1e9a41467470ec9db817accf21e1e8b939525e84da6786d9a"],
["o","publish"]
],
"content": "{\"r\":\"683c867bf3dc5e7993fdd0715771d36eb6f00b8a8645795c332246ea5f19c7d5\",\"patches\":[{\"op\":\"add\",\"path\":\"/service\",\"value\":[{\"id\":\"did:nostr:e3932a8cd4ec81e1e9a41467470ec9db817accf21e1e8b939525e84da6786d9a#nostr-relays\",\"type\":\"NostrRelay\",\"serviceEndpoint\":[\"wss://relay.damus.io\"]}]}]}",
"sig": "b1bae7a64d84ce63e40dfa24d299892116694ae71215f357ddad0a0091455e0b985b45ccca6f9354223dc3f0243a9eabcb6d874ff85fac7051493fee8aab2ef9"
}
- should only ever be 1
publish
event for a given DID - should be the first event of kind
9325
for a given DID - the
o
tag should have a value ofpublish
- the
d
tag should be present and contain the relevant DID content
is a stringified json object that contains the following properties:
Property | Description |
---|---|
r |
double hashed (sha256) encoded recovery pubkey |
patches |
an array of JSON patch ops that are applied on top of the base DID Document |
event.pubkey
should match theid
of the DID found in thed
tag.- standard nostr
sig
verification
{
"content": "{\"r\":\"0df32bb15f48f60c5d7e035bdfbc67507a8576109a422dd860ad1a7b4935b3e6\"}",
"created_at": 1676957711,
"kind": 9325,
"pubkey": "b424a3e0ea03a1450feef4cdba3dd74892c3e18726f079f89db42d597dfceb4c",
"tags": [
["d","did:nostr:89abca8adb7d5db8a4b4e90b0c0eb97dc6f3a957fc219a83bf925fb9c7bd4331"],
["o", "recover"],
["e", "bd513634c49754383d9d4110ebeaa1433467c5b70da7ae2d2873cabcd5445451", "wss://relay.damus.io", "reply"]
],
"sig": "1f3fdaf79dcf9fb7ab51834c80b28abc5e6a3850f0185bb5b616f4a5e133f188dc609be10462e551ea984fe403a32a4b0b8b492d0c9c45f63d4fdd4d38d8c46d",
}
- the
o
tag should have a value ofrecover
- the
d
tag should be present and contain the relevant DID - should contain a nip10 marked
e
tag pointing to the most recent event of kind9325
for the relevant DID- π‘ If more than one
e
tag is allowed, it may be helpful to include an additional markedroot
e
tag that points back to the initial publish event - π‘ If more than one
e
tag is allowed, it may be helpful to include an additional markedmention
e
tag that points back to the most recentrecover
event for the relevant DID
- π‘ If more than one
content
is a stringified json object that contains the following properties:
Property | Description |
---|---|
r |
new double hashed (sha256) recovery key |
π‘ Note: any
r
should not be used more than once. Recovering should always include a newr
that can be used for subsequent recoveries
sha256(sha256(event.pubkey))
should matchJSON.parse(previousEvent.content).r
of the most recentrecover
event if one exists or the initialpublish
event if no otherrecover
events exist
{
"kind": 9325,
"tags": [
["d", "did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48"]
["e", "event id of most recent 9325 event for did"]
["o", "patch"],
],
"content": "{\"patches\":[{\"op\":\"add\",\"path\":\"/service\",\"value\":[{\"id\":\"did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48#nostr-relays\",\"type\":\"NostrRelay\",\"serviceEndpoint\":[\"wss://relay.damus.io\"]}]}]}"
}
content
is a stringified json object that contains the following properties:
Property | Description |
---|---|
patches |
an array of JSON patch ops |
Given a DID (e.g. did:nostr:e2bdaa90e96a4fafb9f1c36f9b378e4bbd6fea26e5d47063e7b30aa15de37d48
):
- Derive the base DID document by following these steps
- fetch all events of kind
9325
for the DID being resolved - order the events using
created_at
- ensure that first event is a
publish
. perform integrity checks listed here - for each event thereafter:
- ensure that the id provided in the marked event tag matches the id of the previous event
- if
patch
: apply patches to DID doc - if
recover
: perform integrity checks listed here- optional: include revealed pubkey as
verificationMethod
in DID doc
- optional: include revealed pubkey as
- clients or relays making use of the
#d
tag should perform necessary integrity checks before trusting that the event came from the listed DID - if possible, relays should perform integrity checks on a
9325
event prior to storing it