Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formalize the use of the endpoint ATTRIB and rendering the DID Document #296

Merged
merged 7 commits into from
Jun 24, 2021
191 changes: 119 additions & 72 deletions spec/did-method-spec-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,60 +125,22 @@ <h3>Create (Register)</h3>
in that case it is assumed that the key has all authorisations.
</p>
<p>For example:</p>
<p>To create a DID, you must submit a transaction that looks like this: <br>
<p>To create a DID, you must submit a <a href="https://hyperledger-indy.readthedocs.io/projects/node/en/latest/requests.html#nym">"NYM"</a> transaction that looks like this: <br>
<pre>
{
"submitterId": &lt;Trust Anchor DID&gt;,
"signature": &lt;signature over this transaction from the Trust Anchor&gt;,
"reqId": &lt;A nonce for this transaction&gt;,
"operation": {
"type": "NYM",
"did": &lt;new DID that is being registered&gt;,
"document": {
"publicKey": [{
"id": &lt;a valid unique identifier&gt;,
"type": &lt;ed25519, defined in appendix&gt;,
"publicKeyBase58": &lt;Key material encoded&gt;,
"authorizations": ["ALL"]
}],
"authentication": [{
"type": &lt;type of DID authentication&gt;,
"publicKey": "&lt;reference to a publicKey object&gt;",
}],
"service": [{
"type": &lt;agentService, emailService, apiService, dnsService, etc&gt;,
"serviceEndpoint": &lt;A URI for the endpoint&gt;
}],
"role":&lt;Optional; Enumeration for roles, an integer; if left empty then no special role is assigned&gt;
}
}
'operation': {
'type': &lt;Transaction type -- NYM &gt;,
'dest': &lt;new DID that is being registered&gt;,
'role': &lt;Role given to the new DID -- based on network config&gt;,
'verkey': &lt;Key material encoded&gt;,
},

'identifier': 'L5AD5g65TDQr1PPHHRoiGf' &lt;Trust Anchor DID&gt;,
'reqId': &lt;A nonce for this transaction&gt;,
'protocolVersion': 2,
'signature': &lt;signature over this transaction from the Trust Anchor&gt;
}

Example
{
"submitterId": "did:sov:29wksjcn38djfh47ruqrtcd5",
"signature": "1qaz2wsx3edc4rfv5tgb6yhn7ujm8iklop==",
"reqId": "okn987yhbgFtErDsCXsw",
"operation": {
"type": "NYM",
"did": "did:sov:mnjkl98uipsndg2hdjdjuf7",
"document": {
"publicKey": [{
"id": "key1"
"type": "ED25519SignatureVerification",
"publicKeyBase58": "..."
}],
"authentication": [{
"type": "ED25519SigningAuthentication",
"publicKey": "key1"
}],
"service": [{
"type": "agentService",
"serviceEndpoint":"https://www.sovrin.org/agents"
}]
}
}
}
</pre> The transaction must be signed by a Trust Anchor and must provide an un-registered DID and a document of data
about that DID. Possible outcomes from the create operation include:
<ul>
Expand All @@ -187,27 +149,11 @@ <h3>Create (Register)</h3>
<li>SUCCESS: "Contains the sequence number of the transaction and the merkle proof"</li>
</ul>
</p>
<p>The DID document is rendered as follows. The exact data structure persisted on the ledger does not have to match the following and can vary for each node.
<pre>
{
"id": "did:sov:mnjkl98uipsndg2hdjdjuf7",
"publicKey": [{
"id": "key1"
"type": "ED25519SignatureVerification",
"publicKeyBase58": "...",
"authorizations": ["all"]
}],
"authentication": [{
"type": "ED25519SigningAuthentication",
"publicKey": "key1"
}],
"service": [{
"type": "agentService",
"serviceEndpoint":"https://www.sovrin.org/agents"
}]
}
</pre>
</p>

<h4>DID Service Endpoint</h4>
<p>By convention, Indy DID Controllers often create an Indy ATTRIB object (using the <a href="https://hyperledger-indy.readthedocs.io/projects/node/en/latest/requests.html#attrib">"ATTRIB"</a>
transaction) related to the NYM with a raw value of "endpoint", and a URL that represents the service endpoint for the DID. As noted in the "Read" section of this specification
such an "endpoint" ATTRIB is read when resolving a DID and it's data included in the resulting DID Document.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"it's data" is a misspelling; should be without the apostrophe: "its data"


<h3>Read (Resolve)</h3>
<p>A Sovrin DID record can be looked up directly by the DID using a standard Sovrin GET_NYM transaction that simply
Expand All @@ -227,10 +173,111 @@ <h3>Read (Resolve)</h3>
}
}
</pre>
<p>Anyone can query a DID record, by sending the above request. The response contains the DID document.</p>
<p>Anyone can query a DID record, by sending the above request. The response contains the data that can be used to create a DID document, as noted below.</p>

<h4>DID Service Endpoint</h4>

<p>When reading (resolving) a Sovrin DID, in addition to querying the DID record, a query MUST be performed for a related DID Service endpoint.
This is done by executing the <a href="https://hyperledger-indy.readthedocs.io/projects/node/en/latest/requests.html#get_attrib">"GET_ATTRIB"</a>
transaction. The format of the GET_ATTRIB request looks like
</p>
<pre>
{
"submitterId": &lt;Optional; DID of the author of this query&gt;,
"reqId": &lt;Optional; a nonce for this query&gt;,
"identifier": &lt;Required; The DID being read/resolved&gt;,
"operation": {
"raw": "endpoint" &lt;Required; the value must be endpoint&gt;
}
}
</pre>

<p>The relevant part of the response from the successful execution of such a GET_ATTRIB request is:</p>

<pre>
{
...
'data': '{"endpoint":{"endpoint":"https://example.com/endpoint"}}',
'dest': 'AH4RRiPR78DUrCWatnCW2w' &lt; The DID being read/resolved&gt;,
'raw': 'endpoint',
...
}
</pre>

<p>The inner value of "endpoint" ("https://example.com/endpoint" in the example above) is the service endpoint for the DID.
Anyone can query an ATTRIB by sending the above request. The service endpoint is used to create the service block of a DID document, as noted below.</p>

<h4>Resolver DID Document Format</h4>

<p>The DID Document is generated by a resolver (not by the Indy/Sovrin ledger) by taking the GET_NYM and optional GET_ATTRIB data and
rendering a DID Document as follows. See the notes following the rendering, and the next section that lists an earlier version
of the rendered document.
</p>

<pre>
{
"@context": [
"https://www.w3.org/ns/did/v1"
],
"id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM",
"verificationMethod": [
{
"type": "Ed25519VerificationKey2018",
"id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-1",
"publicKeyBase58": "9wvq2i4xUa5umXoThe83CDgx1e5bsjZKJL4DEWvTP9qe"
}
],
"service": [
{
"type": "endpoint",
"serviceEndpoint": "https://example.com/endpoint"
},
{
"id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#did-communication",
"type": "did-communication",
"priority" : 0,
"recipientKeys" : [ "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-1" ],
"routingKeys" : [ ],
"accept": [
"didcomm/aip2;env=rfc19"
],
"serviceEndpoint": "https://example.com/endpoint"
},
{
"id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#didcomm-1",
"type": "DIDComm",
"serviceEndpoint": "https://example.com/endpoint",
"routingKeys": [ ]
}
],
"authentication": [
{
"type": "Ed25519VerificationKey2018",
"id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-1",
"publicKeyBase58": "9wvq2i4xUa5umXoThe83CDgx1e5bsjZKJL4DEWvTP9qe"
}
],
"assertionMethod": [
{
"type": "Ed25519VerificationKey2018",
"id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-1",
"publicKeyBase58": "9wvq2i4xUa5umXoThe83CDgx1e5bsjZKJL4DEWvTP9qe"
}
]
}
</pre>

<h5>Notes</h5>

<p>The following are notes about the example DID Document above:
<ul>
<li>The DID Document text is boilerplate except for the "did:sov..." identifier, the public key ("publicKeyBase58) and the references to the service endpoint ("serviceEndpoint").</li>
<li>If there is no result from the GET_ATTRIB transaction, the "service" block is not included in the document.</li>
<li>The three entries in the service block cover the three "eras" of DIDComm, AIP 1.0 (type "endpoint"), AIP 2.0 (type "did-communication") and DIDComm V2 (type "DIDComm").</li>
<li>Because there is just one endpoint specified, the service blocks cannot make use of the DIDComm routing capabilities for messaging based on the DIDDoc.</li>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there is just one endpoint specified

I don't understand this comment. I see several different places where serviceEndpoint is given in the service array. All of them have the same value, but I don't understand why that would matter. Can the sentence be clarified?

Copy link
Contributor Author

@swcurran swcurran Jun 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daniel -- I'm trying not to limit what we're doing here. Currently, the convention that Indy users are following is to have an ATTRIB with a raw value of `{"endpoint":{"endpoint":"http://example.com/didService"}}. There are 313 such instances (as I type this) on Sovrin MainNet today.

If you look at the Universal Resolver, it's output includes that information, in the form of the simple "endpoint" service block. What this PR does is just expand the single "endpoint" we have based on the convention being used, and provide proper DIDComm service blocks for AIP 2.0 and DIDComm V2.

Assuming we don't change the convention, we have no where to specify a mediator/routingKeys -- hence that is hard-wired to be an empty array.

What we really want is did:indy -- but that's not happening fast enough...

Make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. Thank you for the clarification. That helps a lot.

With this PR, would it be possible to start up a new convention (without writing any new code in the ledger--only in clients) where the raw value would also include routingKeys data, and this would then be returned by the UR? If so, then maybe it would be better to NOT insert an empty array for routingKeys, and just let the data stored by the ledger be returned. That way we could start defining endpoints without routing keys if we were motivated to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really like to the see the focus put on did:indy for that, so I personally would prefer not. I suppose it would take off the pressure to get did:indy implemented. I'd love to know how much people would use this if we did it now. Easy enough though -- like this I think:

{
  "endpoint": {
    "endpoint": "http://example.com/didService",
    "routingKeys": [ "did:key:12345", "did:key:67890" ]
  }
}

BTW -- my reply to you above was terrible. Just noticed my opening line was the opposite of intended. I said "I'm trying not to limit", but meant to say "I'm trying not to limit". Doh :rofl

Copy link
Contributor

@dhh1128 dhh1128 Jun 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the logic of limiting coding effort -- but what I'm proposing is not extra coding. It's actually less coding. Instead of hard-coding some logic into UR that always adds an empty routingKeys, I'm suggesting that we just ignore the issue entirely. If someone writes an ATTRIB that includes routingKeys inside raw, then UR returns it because the ledger returns it. If they don't have an ATTRIB with routingKeys, UR doesn't emit one, either. All the current ATTRIBs on the record would lack routingKeys and thus be invalid DIDComm endpoints -- and so would new ones written to the ledger using the status quo convention. But by doing nothing, we'd make it possible to adopt a new convention if we were forced to -- without any more changes to the did:sov spec, the ledger, or the UR.

My suggestion is motivated by a lack of confidence in the timeline for did:indy. If we had did:indy today, I wouldn't suggest it at all, because we'd have a better alternative. Perhaps I'm being pessimistic to want to leave us a pressure release valve. Or perhaps there are reasons not to do this that I haven't considered. I wasn't advocating super strongly -- just thinking out loud. I'm content to have the PR merged as-is, if others feel that's the best tradeoff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not as simple as you say. First, the resolver has to know about each raw type (e.g. "endpoint", "routingKeys") and specifically make a call to the ledger to ask for each value. There is no way to know what ATTRIBs a client has written and we don't want the resolver to have to ask for a whole bunch of ATTRIBs on the off chance a DID controller used a specific set of ATTRIBs. As such, I would not want to add any other ATTRIBs as part of this process. "did:indy" is needed for that.

As noted, we could add an extra format for "endpoint" to add routingKeys, but the resolvers would have to add support for that format (update all existing deployments) and clients would have to know about it to use it. Since these endpoints are likely used only during connection establishment, and will be replaced by peer DIDs, the lack of mediators could be worked around.

I agree that "did:indy" won't be around for awhile. However, I think this will be sufficient for the institutional issuers we have today -- the main use case we have for DIDs on Indy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, got it. I had forgotten that each raw value was a separate call. Then I agree with your conclusion.

</ul>
</p>

<h3>Update (Replace)</h3>
<p>To replace the DID document, the owner or guardian (guardianship ends once ownership begins) of the DID should send the following
transaction using a key referenced in the <strong>authentication</strong> property.
Expand Down