Open dlongley opened 2 years ago
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?
@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".
Looping in @mavarley @OR13 @troyronda for awareness.
can I get a TLDR?
TL;DR - Remove lots of optionality to make ZCAPs easier to implement and interoperate.
Here's my summary:
capabilityDelegation
property, but instead contains a controller
property.invoker
(VM URL) property is replaced with controller
property (URI, e.g. DID).capability
property) .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)?
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.
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:
A root zcap MUST have an
@context
field that is a string with the valuehttps://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-encodedN+1
times whereN
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 anid
parameter) and the capability action (via anaction
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 beread
orwrite
. 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 ofcapabilityInvocation
.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 thecapability
property, and the action to be taken in thecapabilityAction
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 controllerX
for the collectionhttps://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 thecontroller
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 itscontroller
) into:Delegated Capability
A delegated zcap looks like this:
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 inproof
). 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. Theid
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 why10
/ 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 theinvocationTarget
either matches theinvocationTarget
in the parent capability or, if invocation target attenuation [link] is permitted, that it has theinvocationTarget
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 JavaScriptnew Date().toISOString()
code can produce such a date representation, though it is preferred to remove millisecond precision vianew 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 particularcontroller
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 theallowedAction
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 athttps://foo.example/bars/123
can be delegated with an attenuation such that the delegated ZCAP has an invocation target ofhttps://foo.example/bars/123/bazzes/456
. This could be further delegated and attenuated with a ZCAP with an invocation target ofhttps://foo.example/bars/123/bazzes/456?day=tuesday
and then again withhttps://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 acapability
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 ofurn: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.jsTODO: 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