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

Apply 2022 Updates to match implementation experience #39

Open
dlongley opened this issue Mar 8, 2022 · 8 comments
Open

Apply 2022 Updates to match implementation experience #39

dlongley opened this issue Mar 8, 2022 · 8 comments

Comments

@dlongley
Copy link
Contributor

dlongley commented Mar 8, 2022

The following are some raw notes on how ZCAPs have evolved after getting significant implementation experience. These changes need to make their way into the next major revision of the spec. Many of them involve greatly simplifying ZCAPs.

Updates

We should rename ZCAP-LD to just ZCAP. ZCAPs can be modeled as JSON that is JSON-LD compatible -- which is simpler and different from making it a "JSON-LD native" format.

Something else that was learned from implementation experience and a desire to remove optionality -- is that root objects should not themselves "be ZCAPs", but rather should have a dedicated root ZCAP that represents them -- which can be referenced via the ID: urn:zcap:root:<encodeURIComponent(invocationTarget URL for root object)>.

More details follow...

Root and Delegated Capabilities

There are root zcaps and delegated zcaps. Both are expressed in JSON. Both can be invoked at a verifier to take an action. A verifier is a system capable of checking a capability invocation to ensure it is valid.

Root Capability

A root zcap looks like this:

{
  "@context": [
    "https://w3id.org/zcap/v1"
  ],
  "id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Ffoo",
  "controller": "did:key:example",
  "invocationTarget": "https://example.com/foo"
}

A root zcap MUST have an @context field that is a string with the value https://w3id.org/zcap/v1. This field makes zcaps JSON-LD compatible, but does not mean that any other JSON-LDisms are permitted. In other words, zcaps are JSON-based, but the JSON has been chosen carefully such that it can be interpreted properly as JSON-LD as well. Other JSON-LD representations that deviate from the JSON expression of a zcap are not permitted.

By enabling JSON-LD compatibility, DI proofs (Data Integrity Proofs, formerly known as Linked Data proofs) are used instead of JOSE-based signatures to keep zcap sizes small by eliminating the need to encapsulate zcaps via base64 encoding. This is particularly important for expressing capability chains [TODO: link to capability chains], where ancestor zcaps would be base64-encoded N+1 times where N is the length or position in the chain. If neither of these approaches were used, a novel signature encapsulation mechanism would have to be invented to get the same benefits. Instead, reuse of existing work is preferred.

Additionally, JSON-LD compatibility also enables CBOR-LD to be used to express zcaps – further reducing size via semantic compression. [Note: [See invoking a root zcap section]. When invoking a root zcap, a capability invocation proof is not added to the root zcap itself, but rather to another document that is acceptable to an API. This means that no additional contexts (e.g., cryptosuite contexts) ever need to be added to a root zcap. Further work on the Data Integrity 1.0 spec could also alleviate the need for additional cryptosuite contexts entirely.]

A root zcap MUST have an id that is a string that expresses a URN. This ID can always be dereferenced by the verifier system if it is a valid root zcap for a particular endpoint. The ID of a root zcap SHOULD [Note: this should become a MUST if it covers all use cases, to keep things simple] have the following format:

urn:zcap:root:${encodeURIComponent(invocationTarget)}

This identifier format makes it clear that the identifier for the root zcap for the root invocation target of invocationTarget.

A root zcap MUST have an invocationTarget that is a string that expresses a URI. The invocation target identifies where the zcap may be invoked and identifies the target object that the root zcap expresses authority for.

A root zcap MUST have a controller that is a string or an array of strings that each express a URI that identifies a controller for the root zcap. The controller (or controllers) may take any actions with the invocation target (that are supported by the verifier) by invoking the root zcap. The controller (or controllers) may create delegated zcaps from the root zcap [TODO: link to delegated zcap section].

Note: A root zcap MUST NOT have any other fields.

A root zcap can be invoked by referencing only its ID because the verifier can (and MUST) always dereference the zcap locally using a trusted dereferencing mechanism. This is because a root zcap does not have a capability delegation proof; it is the root of trust for a capability chain [TODO: link to capability chains].

A verifier MAY dynamically generate a root zcap when it is dereferenced, e.g., basing the controller value on information obtained from a database or based on information submitted to the API (provided that the information can be validated / trusted). For examples, see [revocation] (controllers for the root zcap for a revocation endpoint SHOULD be constructed from the chain of the zcap that has been submitted for revocation, provided that that zcap itself has a root zcap that is expected by the verifier).

Invoking a root zcap

There can be multiple ways to invoke a root zcap, two are defined:

Using an HTTP signature in an HTTP request or by attaching an LD capability invocation proof to a document. The verifier will decide which of these methods is acceptable and what other information must be signed (in the case of an HTTP signature) or what documents may be submitted with attached capability invocation proof(s).

When invoking a root zcap using an HTTP signature, a capability-invocation header must be included that identifies the root zcap by ID (via an id parameter) and the capability action (via an action parameter) that is being invoked. The request URL identifies the intended invocation target, which either must match the invocation target in the root zcap or, if the verifier allows it, must have the root zcap's invocation target as a prefix. The capability action must be an action that is expected (supported) by the verifier at the request URL. The capability action SHOULD be read or write. The key used to create the HTTP signature must either be the private key paired with a verification method that matches the controller of the root zcap or a verification method controlled by the controller of the root zcap – and that is authorized for the purpose of capabilityInvocation.

When invoking using a DI proof, a capability invocation proof must be attached to a document that is acceptable by the API (this will be defined by the specific API being accessed). The capability invocation proof MUST include the intended invocationTarget, the root zcap ID in the capability property, and the action to be taken in the capabilityAction property. The same controller rules apply as in the HTTP signature case.

Dereferencing a Root Capability on a verifier system

It is expected that only verification systems will dereference root zcaps from their IDs. One model for new HTTP APIs is to store a controller value with every resource at the base of a hierarchy, e.g., store controller X for the collection https://foo.example/collections/123. When the root zcap for this collection is invoked or referenced via a delegated zcap invocation, the verifier can look up the controller property (or receive it from another system in some kind of decentralized setup) for that resource and include it in a "dynamically dereferenced" root zcap. In other words, the verifier sees the root zcap ID: urn:zcap:root:<encodeURIComponent(https://foo.example/collections/123> and dynamically converts that (after looking up its controller) into:

{
  "@context": [
    "https://w3id.org/zcap/v1"
  ],
  "id": "urn:zcap:root:https%3A%2F%2Ffoo.example%2Fcollections%2F123",
  // populated via a database or external system call
  "controller": "did:key:example",
  "invocationTarget": "https://foo.example/collections/123"
}

Delegated Capability

A delegated zcap looks like this:

{
  "@context": [
    "https://w3id.org/zcap/v1",
    "https://w3id.org/security/suites/ed25519-2020/v1"
  ],
  "id": "urn:uuid:cdc77118-6bfa-11ec-aceb-10bf48838a41",
  "parentCapability": "urn:zcap:root:https%3A%2F%2Fexample.com%2Ffoo",
  "controller": "did:key:example",
  "invocationTarget": "https://example.com/foo",
  "expires": "2021-11-03T18:33:51Z",
  "allowedAction": [
    "write",
    "read"
  ],
  "proof": {
    "type": "Ed25519Signature2020",
    "created": "2021-10-27T18:33:51Z",
    "verificationMethod": "did:key:z6MkfWKcvBiKCfNgz5UUGseNt37t4dguEvFgJ9XvX2UV6zB9#z6MkfWKcvBiKCfNgz5UUGseNt37t4dguEvFgJ9XvX2UV6zB9",
    "proofPurpose": "capabilityDelegation",
    "capabilityChain": [
      "urn:zcap:root:https%3A%2F%2Fexample.com%2Ffoo"
    ],
    "proofValue": "z3t9BCQyF21MDVYmLKc9zbLreqx4wBtQnUsd5aqyoWS5FfhapRz7QjPNLcgKAornUVmJR4ZjbGpuxRFnffxX1ZjtF"
  }
}

A delegated zcap is different from a root zcap primarily in that it has a parentCapability, an expiration date-time, and a capability delegation proof (found in proof). It is also different from a root zcap in that all delegated zcaps in a chain [link to capability chains] must be fully provided to the verifier when invoking a delegated zcap so that the verifier is not required to dereference them other than via the provided chain. Note: A verifier may still need to query a database for delegated zcaps by ID to perform revocation checks. However, a verifier MUST NOT be required to perform network requests or database queries to dereference delegated zcaps by ID when verifying the capability chain [link to capability chains], prior to inspecting it for potential revocations.

A delegated zcap MUST have an @context field with an array where the first value is the zcapld context and any subsequent values identify context(s) used to define vocabulary terms used in the capability delegation proof. [Note: Maximum array size should be defined in the spec along with rationale].

A delegated zcap MUST have an id that is a string that expresses a URI. The id SHOULD have the format:

urn:uuid:<uuid>

Using this format enables CBOR-LD to perform compression on the ID value and reduces correlation risk by making the ID semantically opaque.

A delegated zcap MUST have a parentCapability that is a string that expresses the ID of the parent zcap. The parent zcap may be another delegated zcap or the root zcap. A verifier MUST ensure that a delegated zcap was created by a controller of its parent capability by checking its capability delegation proof.

A delegated zcap can only be invoked by submitting the entire zcap. A delegated zcap MUST have a capability delegation proof and this proof MUST contain the delegation chain.

[target for link to capability chains]

A capability delegation chain MUST be an array that includes the root zcap using its ID (i.e., by reference only, not embedded) and every other delegated zcap in its ancestry must be referenced by ID except for the parent delegated zcap, which MUST be fully embedded. This ensures that delegated zcaps are of minimal size (other delegated zcaps in the chain are never repeated) and that every delegated zcap can be dereferenced directly from the chain without ever having to hit a network resource or similar. The capability delegation chain is ordered; the first entry MUST be the root zcap's ID and any other entries must be in the order of delegation from least recent to most recent.

A verifier MUST limit the length of the capability chain to prevent long chain attacks. A verifier SHOULD limit the length of the capability chain to 10. [exposition on why 10 / link to security section].

A root zcap MUST have an invocationTarget that is a string that expresses a URI. The invocation target identifies where the zcap may be invoked. A verifier MUST ensure that the invocationTarget either matches the invocationTarget in the parent capability or, if invocation target attenuation [link] is permitted, that it has the invocationTarget from the parent capability as a prefix. A prefix is defined as a base URI and parent path (and optional query) (i.e., / delimited and ?/& delimited) [more rigorous definition].

A delegated zcap MUST have a controller that is a string or an array of strings that each express a URI that identifies a controller for the delegated zcap. The controller (or controllers) may take any allowed actions [link] with the invocation target (that are supported by the verifier) by invoking the delegated zcap. The controller (or controllers) may create delegated zcaps from the delegated zcap. [Note: As with other data model sections in W3C specs, every property of a zcap should be called out in its own subsection along with the rules for the property.]

A delegated zcap MUST have an expires field that expresses an XSD date-time (Note: The JavaScript new Date().toISOString() code can produce such a date representation, though it is preferred to remove millisecond precision via new Date().toISOString().slice(0, -5) + 'Z').

A verifier MUST ensure that an invoked delegated zcap has not expired. A verifier MUST ensure that a delegated zcap's expiration date-time is not less restrictive than its parent capability's expiration date-time, if present (a root zcap does not have an expiration date-time).

A verifier SHOULD ensure that an invoked delegated zcap does not have an expiration date-time that is more than, e.g., three months in the future [TODO: exposition on why 3 months?]. This is because a verifier MUST store revoked zcaps [link to revocation] until they expire to prevent their use. A delegated zcap with an expiration date that is unreasonably far into the future will have to be stored for an unreasonable period of time to prevent its invocation. There are other mitigation strategies here such as considering all zcaps delegated from a particular controller as revoked or full key revocation.

Delegated zcaps MUST have expiration date-times to support good security hygiene practices and because zcaps support decentralized delegation. In order to revoke a zcap, it must be submitted to the verifier's revocation endpoint for the associated invocation target. If a delegated zcap has been lost / misplaced, it MUST eventually expire to avoid undesirable access.

A delegated zcap MAY have an allowedAction field that is a string or an array of strings that each express an action that the controller of the zcap may take when invoking the capability. A verifier MUST ensure that the allowedAction field in a delegated zcap is not less restrictive than the parent capability's, if present.

A delegated zcap MUST have a proof field that is an object or an array of objects that each express a DI proof. At least one of these proofs MUST be a zcap capability delegation proof [TODO: more details on this proof].

A capability delegator (one who creates a delegated zcap) may attenuate authority by setting a more restrictive expiration date-time, a more restrictive invocation target (via URL path- or query-based attenuation), or limiting the allowed actions. Taken together, the API that a verifier manages access is expected to have the flexibility required to model all desired authorization models.

URL Path- or Query-based Attenuation

Remove @context / vocab-based caveats and replace with:

A verifier will accept delegations (and invocations) where a suffix has been added to the parent zcap's invocation target (invoked zcap's invocation target). The suffix MUST start with / or ? if the invocation target prefix has no ? and & otherwise. This allows for fully customizable attenuations via HTTP API path and query parameters. For example, a ZCAP that can be invoked at https://foo.example/bars/123 can be delegated with an attenuation such that the delegated ZCAP has an invocation target of https://foo.example/bars/123/bazzes/456. This could be further delegated and attenuated with a ZCAP with an invocation target of https://foo.example/bars/123/bazzes/456?day=tuesday and then again with https://foo.example/bars/123/bazzes/456?day=tuesday&hour=12.

Invoking a delegated zcap

Just like with root zcaps, there can be multiple ways to invoke a delegated zcap, two are defined and are the same as with root zcaps with the following differences:

When invoking a delegated zcap using an HTTP signature, a capability-invocation header must be included that includes the full delegated zcap in a capability parameter by serializing it to JSON, gzipping the result, and then base64url-encoding the gzipped JSON.

When invoking using a DI proof, the capability property must express the full delegated zcap.

TODO: Add section on revocation detailing what verifiers MUST/SHOULD do to provide revocation endpoints for zcaps. A root invocation target SHOULD have a /zcaps/revocations subpath where a root zcap of urn:zcap:root:<the revocations path/the zcap ID to revoke> can be invoked. The verifier SHOULD set the controllers for that root zcap to all controllers in the (to be revoked) zcap's chain so that any controller in the chain can revoke the zcap. Link to example server-side revocation implementation:
https://github.com/digitalbazaar/ezcap-express/blob/main/lib/revoke.js

TODO: Detail algorithm for validation process:
https://github.com/digitalbazaar/zcapld/blob/4386185f784f552d6eaeb2c3c82959cb2e09762e/lib/CapabilityProofPurpose.js#L85

TODO: Detail how revocation works: Any controller in the chain of a delegated zcap may post that zcap to: <rootInvocationTarget>/zcaps/revocations/<zcapToRevokeId> using the root zcap: urn:zcap:root:encodeURIComponent(<rootInvocationTarget>/zcaps/revocations/<zcapToRevokeId>) to revoke. Example: https://github.com/digitalbazaar/ezcap-express/blob/main/lib/revoke.js

@cwebber
Copy link
Contributor

cwebber commented Mar 9, 2022

Hi! I'm glad zcap(-ld) has had real world implementation experience! I might be making some gradual changes as I read through this if I have time (ha ha I don't) but I'll start here:

urn:zcap:root:${encodeURIComponent(invocationTarget)}

I understand the push for a URI type that represents something that can't change, that makes sense to me. But... a new URN subscheme? That just encodes the same data? Why? What is achieved?

@dlongley
Copy link
Contributor Author

dlongley commented Mar 9, 2022

@cwebber,

I understand the push for a URI type that represents something that can't change, that makes sense to me. But... a new URN subscheme? That just encodes the same data? Why? What is achieved?

So this is related to our finding that not every resource can be represented as a zcap itself (i.e., "as its own root zcap"). Having the option for some resources to work that way whilst others do not was an unnecessary complexity and was potentially confusing, especially to people unfamiliar with open world / semweb / linked data concepts. Therefore, it is much simpler to draw a hard line and say that a resource is always a different entity from its "root zcap".

But, where then, should its root zcap live / how can we reference it? And how does one establish an appropriate, trusted binding between the two? Again -- having too many options here creates complexity which is the enemy of interoperability (among other things). We found that we could create a very simple solution by creating a new URI scheme that says that the "root zcap" for the resource at URL X can simply and always be referenced via the URI: urn:zcap:root:<encodeURIComponent(X)>. X here is also the root "invocationTarget" -- where the zcap can be invoked to, e.g., read/write the resource. We allow for arbitrary attenuations through URL path hierarchies and queries -- delegated zcaps can have attenuated invocation targets off of the root resource / invocation target.

We also found that, in practice, this root zcap URI need only be dereferenced on verification systems -- and that the root zcap can be easily and dynamically generated "on the fly". There's no need for storage of root zcaps -- and more rigorous checks can be performed when the URI scheme is well known. In implementations, all that is needed when verifying zcap invocations (including checking the whole delegation chain) is a function that can provide the "root controller" for the resource -- to fill the controller property on the dynamically generated root zcap. Of course, the controller needs to come from somewhere (e.g., database or another trusted system).

So, this is borne out of removing optionality in the name of making things simpler to understand and implement -- and to better promote interoperability.

Note that another related finding was that delegated ZCAPs should always be fully embedded in the delegation chain. Allowing optionality to reference them and require verifiers to retrieve them from the network had little to no practical benefit. Removing this simplifies implementations such that they never need to retrieve a ZCAP from "the network".

@msporny
Copy link
Contributor

msporny commented Mar 9, 2022

Looping in @mavarley @OR13 @troyronda for awareness.

@OR13
Copy link

OR13 commented Mar 9, 2022

can I get a TLDR?

@dlongley
Copy link
Contributor Author

TL;DR - Remove lots of optionality to make ZCAPs easier to implement and interoperate.

@clehner
Copy link
Member

clehner commented Mar 10, 2022

Here's my summary:

  • Change from invokers (verification method URLs) to controllers.
    • Root document no longer has verification material in a capabilityDelegation property, but instead contains a controller property.
    • Delegation invoker (VM URL) property is replaced with controller property (URI, e.g. DID).
  • Ensure the delegation chain is included in the invocations/delegations, so that delegations do not need to be dereferenced.
    • Delegation id must be a UUID.
    • Delegation document includes previous delegation document (unless previous is the root document).
    • Invocation document includes delegation document (rather than the id of the delegation document), in the proof (capability property) .
  • Make the root document into essentially a function mapping from an invocation target URL to a set of controllers.
    • Root document id is limited to a URN wrapping a URL (invocation target).
    • The root document is expected to be dynamically generated as a function of the URL.
  • Specify use of HTTP Signatures for invocation on HTTP URL invocation targets.
  • Specify attenuation of invocation target URL by path and query.
  • Specify use and attenuation of expiration date and allowed actions set.
  • Detail a possible mechanism for revocation.
    • Invocation of a URL derived from the target URL

@clehner
Copy link
Member

clehner commented Mar 24, 2022

I think using controller instead of invoker makes sense. (I had wondered why delegation was to verification method URLs rather than e.g. DIDs).

Could delegations use a content-hash id instead of a UUID? This would enable including parent delegations in a zcap's signature payload indirectly, via hashes. Or is desired not to reintroduce loading additional resources (even hash-identified ones that could be provided alongside the payload)?

@dlongley
Copy link
Contributor Author

Or is desired not to reintroduce loading additional resources (even hash-identified ones that could be provided alongside the payload)?

This. We want to keep it really simple -- everything you need is always there, no need to load / fetch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants