About

This DID method spec conforms to the requirements in the DID specification currently published by the W3C Credentials Community Group. For more information about DIDs and DID method specifications, please see the DID Primer and DID Spec.

This document defines a "peer" DID Method that can be used independent of any central source of truth. The method is intended to be cheap, fast, scalable, and secure. It is suitable for most private relationships between people, organizations, and things. DIDs associated with this method are also promotable to a more public context. That is, blockchains with different DID methods could choose to graft some or all peer DIDs into their namespace(s) with no risk of accidental collision, and no loss of meaning. A particular peer DID would have a recognizable and consistent identity in all of them.

We expect that peer-to-peer relationships in every blockchain ecosystem can benefit by offloading pairwise and n-wise relationships to peer DIDs.

 

Introduction

Most introductions to DIDs describe them as identifiers that are rooted in a public source of truth like a blockchain, a database, a distributed filesystem, or similar. This publicness lets arbitrary parties resolve the DIDs to an endpoint and keys. It is an important feature for many use cases.

However, the vast majority of relationships between people, organizations, and things are simpler. When Alice(Corp|Device) and Bob want to interact, there are exactly and only 2 parties in the world who should care: Alice and Bob. We call these parties "peers" because there is an equality rather than a hierarchy to them, at least insofar as their DID management is concerned. Letting the world resolve the identifiers and keys that only peers care about is unnecessary, and it represents a privacy and security risk as well as a problem of cost, scale, and performance.

Terminology note: as used in this spec, "peer DID" means a DID that uses the "did:peer" DID method we're defining. Such DIDs are often pairwise, but "pairwise DID" is not a synonym, since peer DIDs can also be used to model n-wise relationships. Similarly, "peer DID" is not intended to be a synonym for a DID that is not publicly resolvable. Many different approaches to such DIDs might exist; "peer DID" is just the name for the approach described here.

Pairwise, N-wise

The omnipresence of peer relationships raises the possibility of DIDs that are only resolvable and usable by peers within the context of a given relationship. Such pairwise or n-wise identifiers can still have all the other characterstics that make DIDs useful -- DID Documents, endpoints, keys, authorizations, interoperability, and tooling.

Guarantees

This spec uses a protocol, rather than a public oracle, as the root of trust. It is worthy of trust because it guarantees the following properties:

  1. DIDs are associated with a key pair at the moment of creation. This prevents a category of man-in-the-middle attacks where an attacker could rotate a DID's keys at the outset of a relationship unbeknownst to peers.
  2. DIDs have an acceptable level of uniqueness. This is NOT a guarantee that DIDs will never be reused by their owner, NOR is it a guarantee that collusion cannot subvert uniqueness. Thus, it is not a uniqueness upon which deep trust can be based. Rather, it is a guarantee that good behavior will not produce accidental collisions. In this sense, it is rather like the uniqueness offered by NATing mechanisms in IPv4. It provides enough uniqueness that a DID can be used as an index in a database or as a routing target in DID communication. It also makes it possible for blockchains to "adopt" a peer DID by mapping it into their namespace, without incurring the risk of ambiguity. Any time a peer DID is discovered to be less than unique, a true problem exists and systems can fairly raise an exception.
  3. The values of DIDs are securely random. This prevents attackers from discovering patterns in DIDs that might undermine privacy.
  4. Parties to a relationship can prove the orderly and authorized evolution of their keys to one another.

Advantages

Peer DIDs are not suitable for anywise use cases, which are usually public by intent. However, peer DIDs have certain virtues that make them desirable for private relationships between a small number of enumerable parties:

  • They have no transaction costs, making them essentially free to create, store, and maintain.
  • They scale and perform entirely as a function of participants, not with any central system's capacity.
  • Because they are not persisted in any central system, there is no trove to protect.
  • Because only the parties to a given relationship know them, there is no concern about personal data and privacy regulations due to third party data controllers or processors.
  • Because they are not beholden to any particular blockchain, they have minimal political or technical baggage.
  • They can be mapped into the namespaces of other DID ecosystems, allowing a peer DID to have predictable meaning in 1 or more other blockchains. This solves a problem with blockchain forks fighting over the ownership of a DID, and promotes interoperability.

Groups

Most of this doc is framed by a pairwise Alice:Bob context. This sort of pair (no matter whether its members are people, IoT devices, or institutions) will be the most common peer relationship in the SSI landscape. Groups larger than 2 can also have a peer-style relationship--but not all groups will be modeled that way. Therefore, applying this method to groups should be done thoughtfully.

Some groups, such as Doctor+Hospital+Patient, are clearly n-wise. Each party can enumerate all the other parties, and each party uses the same identifier in all directions within the group. N-wise groups are peer groups by definition: (see HIPE 0014 for help on the SSI notation used in this diagram).

N-wise groups often exist alongside pairwise relationships. For example, a 3-wise business partnership between Alice, Bob, and Carol is probably preceded by pairwise relationships between each of the parties. When the business is founded, a new 3-wise relationship is created, with the parties allocating new DIDs, keys, and DID Docs to this relationship independent of the ones they use in their pairwise friendships. This keeps business and personal relationships separate. If a new partner is added, the 3-wise relationship can become 4-wise; if a partner dies, the partnership can lose an active participant, but continue to recognize that party in the relationship history. This shows the close affinity but separation between pairwise and n-wise. For more details, see this exploratory doc.

Groups can also be modeled with a hub-and-spoke model. That model is commonly used in group chats, for example: each member of the group chat sends and receives via a central service, which in turn broadcasts to all other members of the group. This hub-and-spoke model is actually just a pairwise variant, because the relationship is between a member and the hub; all other relationships are only indirect:

We do not cover hub-and-spoke groups explicitly below. They are compatible with this method, if reanalyzed as pairwise. Otherwise, the protocol may need adaptation.

Enforcement

In centralized systems, security is enforced at the center. This is so obvious that we take it for granted--you can't access a database unless you log in first, and it's the database that enforces this.

Despite their other decentralized features, blockchains are no different in this respect. If a blockchain accepts updates to a DID Doc, then the blockchain must guarantee that those updates are only made by authorized parties. Thus, most DID methods imagine a blockchain parsing the authorization section of a DID Doc, and rejecting mischief from hackers.

However, in a peer relationship, there IS no centralized authority. This leads to an interesting inversion of responsibility that must be understood: Bob enforces Alice's authorization policy, and Alice enforces Bob's.

This might seem wrong--shouldn't Alice enforce her own security? But it is quite rational. Who cares whether the agents he is dealing with truly belong to Alice and are authorized by her? Bob does. And if one of Alice's agents gets hacked and attempts to subvert the Alice:Bob relationship, who is the uncontaminated party that can refuse to cooperate with the rogue agent? Bob is.

Another way to think about this is that, within the Alice:Bob relationship, Bob acts as a substitute for a centralized resource that Alice's agents try to access. In such a mental model, of course Bob would be a logical place to enforce access rules for Alice.

Core Characteristics

Namestring

The namestring that shall identify this DID method is: peer

A DID that uses this method MUST begin with the following prefix: did:peer. Per the DID specification, this string MUST be in lowercase. The remainder of the DID, after the prefix, is the NSI specified below.

Early feedback on this method suggested that we embed it beneath the namespace of a particular blockchain, as in did:sov:peer or did:v1:nym. However, this DID method is not captive to any particular blockchain, does not take its resolution rules from a parent method, and does not require anchoring or reference to a blockchain to be valid. Furthermore, any direct or indirect anchoring of a peer DID to a specific blockchain is driven by circumstance and changeable at any time. For example, a peer DID could specify that it is using a dead drop on blockchain 1, then change to blockchain 2, then change to blockchains 3 and 4 at the same time. Therefore, "peer" belongs at the top of the DID namespace. How this method may be used in conjunction with various blockchains is discussed later.

Target System(s)

This DID method applies to any identity management implementation that meets the following two requirements:

Namespace Specific Identifier (NSI)

The Peer DID scheme is defined by the following ABNF:

peer-did = "did:peer:" keyfmtchar ":" idstring
keyfmtchar = "1"
idstring = 21*22base58char
base58char = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "A" / "B" / "C"
    / "D" / "E" / "F" / "G" / "H" / "J" / "K" / "L" / "M" / "N" / "P" / "Q"
    / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z" / "a" / "b" / "c"
    / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "m" / "n" / "o" / "p"
    / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z"

All Peer DIDs are base58 encoded values. The underlying number represented in base58 is a cryptographic public key or a derivative (e.g., hash) thereof, as determined by keyfmtchar. At the time of this writing, only one keyfmtchar is defined. This is "1", denoting a format where idstring is the upper 16 bytes of an Ed25519 public key. This gives idstring a length of either 21 or 22 base58 characters. This spec may be updated to include other keyfmtchar variants for SecP256, Curve448, or similar. Such updates might change the length of idstring as well.

The encoding of idstring uses most alphas and digits, omitting 0 (zero), O (capital o), I (capital i), and l (lower L) to avoid readability problems. Regardless of the length of idstring, peer DIDs are case-sensitive and may not be case-normalized, even though the prefix is always lower-case.

Namestring Generation Method

The unique numeric value underlying a Peer DID MUST be generated by using a cryptographically robust algorithm with secure randomness to create an Ed25519 keypair (or, when additional formats are supported, an analogous keypair on a different curve), and then selecting as the NSI a subset of the public verification key that is at least 128 bits, and that is dependent on keyfmtchar. For keyfmtchar = "1", the NSI = the most significant 16 bytes of the public verification key.

In this way, the DID can be known to begin its existence already associated with keys, and the owner of the new DID is guaranteed to be the only entity in the world who possesses the private key. This guarantees the initial integrity of the DID's chain of custody.

Examples

A convenient regex to match peer DIDs is:

^did:peer:1:[1-9A-HJ-NP-Za-km-z]{21,22}$.

A valid peer DID might be: did:peer:1:YTEFYzByfU2RwJPyULfLLn.

Protocol

Roles

We call this the "peer" DID method because it enables peers--parties with no relative hierarchy--to exchange DIDs without recourse to a central authority. Thus, we expect only one role in our protocol: peer. And this is the case, at a high level.

However, at another level of detail, each peer may interact through various pieces of software and hardware that proxy them. They may need to reveal a modest amount of information about such agents to achieve cooperative security. For example, Alice may control 3 devices, and may wish to protect herself from hacking by saying that at least 2 of her existing devices must agree before a new device can be added or an existing device can be revoked. This requires her to share her M-of-N policy with Bob, with enough information about agent keys that he can validate a multi-party digital signature. Thus, our protocol must occasionally contemplate details about this more granular level of each peer's sovereign domain.

Messages

Peers interact via messages. This is a more general claim than saying that peers expose an API, because it doesn't assume that peers are directly callable by each other. It must be possible to exchange DIDs in this method in a very asynchronous fashion (e.g., one party sends a message while the other party is offline; a response is returned when the original sender may not be actively listening).

Message transmission is transport-agnostic. That is, the messages can flow over any combination of HTTP, bluetooth, NFC, email, AMPRNet (TCP/IP over ham radio), snail mail, sneakernet, intermediaries, or future protocols we have not yet invented.

Messages are JSON. They are compatible with basic concepts of JSON-LD, but no deep knowledge of JSON-LD is required to understand them, and no dependency on JSON-LD libraries is necessary. The specifics of JSON format are discussed below.

Messages are serialized and encrypted in a standard way for transport. This method associates security guarantees with the message envelope rather than the transport, and is aligned philosophically with the approach of IETF's Message Level Security initiative. However, the first version of this spec predates MLS's maturity, so the wire format uses JWEs with some extensions, instead. The extensions allow the same message to be encrypted once for multiple recipients and sent to them all in an efficient manner:

These extensions are under review by the maintainers of JWE/JWT and are likely to be mainstreamed eventually. The particulars are detailed in a doc that explains how agents in the Hyperledger Indy ecosystem have implemented the format - but the format is easy to implement without any Indy dependencies, and its design has no connection to the Indy blockchain. See this implementation in pure javascript, and this one in pure python, in addition to Indy's C-callable implementation in Rust.

All JSON messages in this protocol are part of a "message family" named connections. This family is identified by the following DID reference:

did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0

Of course, subsequent evolutions of the message family will replace `1.0` with an appropriate update per semver rules.

CRUD Operations

Create (Register)

Although the cryptographic operation of creating a peer DID and its first keypair can be done at any time and does not require a message, registering a peer DID with the other party or parties in a relationship involves a specific sequence of messages known as the connection protocol. The protocol is formally documented in a HIPE; only a summary is offered here. It has 3 steps:

  1. One of the parties (the inviter) sends an invitation to connect. The invitation is NOT encrypted, and it does NOT require the recipient (the invitee) to have special software that understands this protocol. Instead, it is a specially formatted URI that can be a deep link, helping the invitee to learn about SSI, get an SSI-capable app, and otherwise onboard into the ecosystem. If the invitee already has SSI-aware software, the URI can be recognized by a registered handler, short-circuiting the URI fetch and the onboarding/ learning process. This invitation URI may be transmitted by email, SMS, QR code, or any other convenient method. If it is important to the inviter that it be secured, the inviter can share the invitation on a secure channel.
  2. The invitee sends an encrypted connection request back to the inviter, using the endpoint that the inviter supplied in the invitation. This connection request includes the invitee's DID Doc, DID, and keys.
  3. The inviter sends an encrypted connection response the other direction. This response includes the inviter's DID Doc, DID, and keys.

These three steps have been carefully defined to achieve a number of important goals:

  • Because the invitation can be sent to someone who does not know about DIDs or SSI at all, and because it can be sent over insecure channels, establishing DID relationships has an extremely low bar. This is vital to the virality of SSI.
  • The invitation identifies an endpoint over which the connecting can occur, but it does not communicate peer keys or a peer DID from the inviter, and the endpoint that the inviter uses (as well as the inviter's DID and keys) are only transmitted once security and privacy are guaranteed.
  • The protocol can be used by parties using any DID method. One of the parties can be using a peer DID, both can, or neither can.
  • The invitation can be sent by a third party, allow introductions (Alice introduces Carol to Bob). However, an introducer is prevented from seeing the secure and private connection built by the two parties.

Read (Resolve)

(This operation, as well as Update and Delete, are documented in greater detail in a HIPE about connection management protocols. Here, we offer only a summary.)

A peer DID can be resolved to a DID Document by sending a state_request message from one peer to another:

{
  "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/state_request",
  "@id": "6a4986dd-f50e-4ed5-a389-718e61517207",
  "for": "did:peer:rjHiqAzCbsNYhMZDTUASHg",
  "as_of_time": "2019-07-23 18:05:06.123Z"
}
            

The as_of_time property is optional and often omitted; if so, the DID Doc at the current time is returned.

A state_response message is returned. It includes a DID Doc and some additional metadata, and looks like this:

{
  "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/relmgmt/1.0/state_response",
  "@id": "1517207d-f50e-4ed5-a389-6a4986d718e6",
  "~thread": { "thid": "6a4986dd-f50e-4ed5-a389-718e61517207" },
  "for": "did:peer:1:rjHiqAzCbsNYhMZDTUASHg",
  "did_doc": {
      "@context": "https://w3id.org/did/v1",
      "id": "did:peer:1:qQk1twjzCmEMgLDRNmo7oS",
      "publicKey": [
        {"id": "routing", "type": "Ed25519Verkey2018",  "owner": "did:peer:1:rjHiqAzCbsNYhMZDTUASHg","publicKey": "4x6qAfCNrqQqEB3nS7Zfu7K8HH5gYEeNc3z7PYXmd54d"},
        {"id": "1", "type": "Ed25519Verkey2018",  "owner": "did:peer:1:rjHiqAzCbsNYhMZDTUASHg","publicKey": "Ar5P8bBr3vXMguTw3U14S6mN2rxrDsYV8Tt75FZ2ZTu4"}
      ],
      "authentication": [
        {"type": "Ed25519Verkey2018", "publicKey": "#1"}
      ],
      "service": [
        {"type": "Agency", "serviceEndpoint": "did:sov:Av1e1Cpu2MavT6QN8nuLJ4" }
      ]
  },
  "as_of_time": "2019-07-23 18:05:06.123Z"
}        

Note the use of ~thread in the response, with thid equal to the @id property of the previous request, to connect the two messages. This is a standard message threading feature of DID Communication, and also shows up in other message interactions described here.

Update

The owner of a peer DID can update their associated DID Document with anyone who knows the DID--one or more agents of the peer, or agents of the owner itself--by sending a sync_state message to another peer. An example of this type of message is:

{
  "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/relmgmt/1.0/sync_state",
  "@id": "e61586dd-f50e-4ed5-a389-716a49817207",
  "for": "did:peer:1:rjHiqAzCbsNYhMZDTUASHg",
  "base_hash": "d48f058771956a305e12a3b062a3ac81bd8653d7b1a88dd07db8f663f37bf8e0",
  "base_hash_time": "2019-07-23 18:05:06.123Z",
  "deltas": [
    {
      "ops": [
        {
          "op": "add_key",
          "fragment": {
            "id": "6", "type": "Ed25519Verkey2018",
            "owner": "did:peer:1:EMmo7oSqQk1twmgLDRNjzC",
            "publicKey": "DjbU8jgf1MjGWu6hGwr4N4EoAfhfTjutjWc8fgdxb3QP"
          }
        },
        {
          "op": "remove_key",
          "fragment": {"id": "5"}
        }
      ],
      "delta_time": "2019-07:23 19:00:01",
      "result_hash": "058771956a305e12a3b062a3ac81d48fbd8653d7f663f37bf8e0b1a88dd07db8",
      "proofs": [
        {
          "by": "#4",
          "signature": "MIIB6gYJKoZIhvcNAQcCoIIB2zCCAdcCAQE...wYDVQQKEwR"
        }
      ]
    }
  ]
}
                

This message does not follow a simple request~response pattern, and does not have "update" in its name, because either party may know details that the other party lacks. The sender picks a point in time, delta_time, where it believes it and the receiver were probably in sync; it then describes the deltas that it know about, that have happened since.

If the recipient already has the same state, it replies with an ACK.

If the recipient knew about a subset of the delta, but not all of it, it applies what is left of the delta, and sends an ACK.

If the recipient has a more evolved state, the recipient sends a reply that is a new sync_state message informing the sender of hitherto unknown information. As with the ACKs, this new message is known to be a reply to the original sync_state because its ~thread decorator identifies the previous message's @id as its thid.

If the recipient does not recognize the base_hash, it selects a hash from a point in time earlier than base_hash_time and sends back a new sync_state message with that earlier base.

If the recipient detects a conflict, it attempts to merge states. If the merge is successful, it sends a new sync_state that shows the merge. If the merge is not successful, then a merge conflict exists, and the merge conflict policy for the sovereign domain of the associated DID is invoked. See "Merges and Merge Conflicts".

Delete

In a self-sovereign paradigm, abandoning a relationship can be done unilaterally, and does not require formal announcement; one party can simply stop communicating with the other. Since there is no public record of the relationship to delete, no further action is strictly required. Indeed, sometimes a formal announcement is impossible, if one of the parties is offline.

However, it is best practice to announce that the relationship is being abandoned. This should cause the other party to clean up by removing the DID, DID Doc, and associated metadata from its local cache. It should also prevent the other party from trying to communicate in the future. Formally terminating a connection happens by sending a leave message to a peer:

{
  "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connection/1.0/leave",
  "@id": "c17147d2-ada6-4d3c-a489-dc1e1bf778ab",
  "~please_ack": {}
}
                

If Bob receives a message like this, he should assume that Alice no longer considers herself connected, and take appropriate action. This could include destroying data about Alice that he has accumulated over the course of their relationship, removing her peer DID and its public key(s) and endpoints from his wallet, and so forth. The nature of the relationship, the need for a historical audit trail, regulatory requirements, and many other factors may influence what's appropriate; the protocol simply requires that the message be understood to have permanent termination semantics.

The ~please_ack decorator is optional. It asks the receiver to send an acknowledgement that it has processed the message. If used, the receiver should send an ACK that looks like this:

{
"@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/ack",
"@thread": { "thid": "c17147d2-ada6-4d3c-a489-dc1e1bf778ab" }
}

Security Considerations

Secure communication

All the messages in this protocol (except for a connection invitation that requires no security, by design) must be sent encrypted, using the encryption format specified in DIDComm's wire format. This gives strong guarantees about the confidentiality and integrity of exchanged data, regardless of the transport mechanism used to transmit the messages.

Cryptonyms

Because peer DIDs are generated from their initial public key, they cannot be created without the creator controlling them. As mentioned earlier, this prevents man-in-the-middle attacks at the time of creation. Since keys must be created from keys generated by a secure random number generator, they are guaranteed to be unpredictable and globally unique at creation time.

Key Authorizations

The keys authorized to participate in a peer relationship may not all have the same privileges. All keys have the privilege of rotating themselves. Keys that have the privilege of adding keys can only add keys with equal or lesser privileges than themselves. All other authorizations on keys must be specified in the authorization section of a peer DID doc. Its structure is:

“authorization”: [ list of grants ]
grant = { "let": recipient , “do”: privs }
recipient block = one of the following:
    “key”: keyref, e.g., “#1”
    "group":
    “and”: [ list of recipient block ]
    “or”: [ list of recipient block ]
    “m_of_n”: {“m”: number, “n”: [ list of recipient block ]}
privs = a string that enumerates the privileges being granted. This string is list-like, but is not modeled using a JSON list or dict because of some specialized syntax that conflicts with canonicalization requirements. Its format is space-delimited list of privileges, where each privilege is an op code name. The list must be sorted in ascending alphabetical order to aid normalization.

Basically, authorizations are a series of grants. Grants identify the recipient, which is either a key or a combination of keys, and the privileges that the recipient is receiving.

An example might be:

What this says is:

Key Management

Keys used to control peer DIDs, or keys authorized to communicate and update the DID Docs for peer DIDs, should be managed according to best practices for DKMS, as described in the DKMS spec.

Privacy Considerations

Peer DIDs should remain pairwise or n-wise, not being reused across relationships. This allows proper isolation to defeat correlation. It also enables granular exercise of sovereignty in a robust, multidimensional identity.

Grafting Peer DIDs Into Another DID Method Namespace

Because peer DIDs are guaranteed to be globally unique at the moment of creation, their 128-bit NSI will not exist on any other blockchain. Blockchain-based DID methods can register a peer DID Doc using their own method. As the DID value, they can either combine their own method prefix with peer DID's NSI ("did:peer:abc123" --> "did:xyz:abc123"), or they can create a child namespace for peer DIDs beneath their own ("did:peer:abc123" --> did:xyz:peer:abc123").

If a peer DID is registered and grafted into another DID namespace, using either method, a decision must be made about which version of the DID Doc is normative: the one registered on that blockchain, one registered on another blockchain, or one not registered on a blockchain at all. Ideally, there would be no difference between the DID Docs stored in each place. This could be achieved by freezing the DID Doc at the moment when it is cross registered.

One reason why this grafting and cross-registering feature is interesting is that peer DIDs could be supported by existing DID tools (e.g., the universal resolver) that require a blockchain, without any retooling.

Reference Implementations

The wire format for DID Communication encryption has three independent implementations--one in libindy, one in python with no Indy dependencies, and one in javascript with no Indy dependencies.

The connection protocol that creates and registers peer DIDs, including support for DID resolution after connection, has been fully implemented by half a dozen different organizations, as of March 2019. One of these organizations is not using libindy. The connection management protocols that allow update of DIDs are in various states of implementation. An up-to-date summary of implementation status, including links to the implementations, can be found in the indy-agent repo on github.

There is a formal test suite for the protocols as well, in the same github repo.

Resources

Developers maintaining this spec and its reference implementations are often found on RocketChat a chat.hyperledger.org, #indy and #indy-sdk. You might also connect with them via the Hyperledger Indy mailing list at indy@lists.hyperledger.org, or in Hyperledger and W3C working groups, or at the semi-annual Internet Identity Workshop conferences.