The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 when, and only when, they appear in all capitals, as shown here.
This specification defines the syntax and semantics of revoking a UCAN Delegation, and the ability to delegate this ability to others.
Using the principle of least authority such as certificate expiry and reduced capability scope SHOULD be the preferred method for securing a UCAN, but does not cover every situation. Revocation is a manual method for reversing a delegation. It cannot undo irreversible mutations (such as sending an email), but MAY limit misuse going forward. Revocation is the act of invalidating a UCAN after the fact, outside of the limitations placed on it by the UCAN's fields (such as its expiry).
Even when not in error at time of issuance, the trust relationship between a delegator and delegatee is not immutable. An agent can go rogue, keys can be compromised, and the privacy requirements of resources can (will!) change. While the UCAN Delegation approach recommends following the principle of least authority, unexpected conditions that require manual intervention do arise. These are exceptional cases, but are sufficiently important that a well defined method for performing revocation is nearly always desired in token and certificate systems.
UCAN delegation is designed to be local-first, partition-tolerant, cacheable, and latency-reducing. As such, fail-safe approaches are not suitable. Revocation is accomplished by delivery of an unforgeable message from a previous delegator.
UCAN Revocations are similar to block lists: they identify delegation paths that are retracted and no longer suitable for use. Revocation SHOULD be considered the last line of defense against abuse. Proactive expiry through time bounds or other constraints SHOULD be 8000 preferred, as they do not require learning more information than what would be available on an offline computer.
UCAN Revocation is a mechanism for invalidating a particular Delegation when used in conjunction with another Delegation in an Invocation proof chain. This is conceptually recursive, and more easily described in pictures:
flowchart RL
invoker((    Dan    ))
revoker((    Bob    ))
subject((    Alice    ))
subgraph Delegations
subgraph root [Root UCAN]
subgraph rooting [Root Issuer]
rootIss(iss: Alice)
rootSub(sub: Alice)
end
rootAud(aud: Bob)
end
subgraph del1 [Delegated UCAN]
del1Iss(iss: Bob) --> rootAud
del1Aud(aud: Carol)
del1Sub(sub: Alice)
del1Sub --> rootSub
end
subgraph del2 [INVALIDATED Delegation]
del2Iss(iss: Carol) --> del1Aud
del2Aud(aud: Dan)
del2Sub(sub: Alice)
del2Sub --> del1Sub
end
end
subgraph rev [Revocation]
revArg("arg: {revoke: cid(carol_to_dan)}")
revCmd("cmd: ucan/revoke")
revIss(iss: Bob)
revPrf("proofs")
end
subgraph inv [INVALIDATED Invocation]
invSub(sub: Alice)
invIss(iss: Dan)
invPrf("proofs")
end
revoker --> revIss
revArg:::revoked -.-> del2:::revoked
revIss -.-> del1Iss
revPrf:::revocation -.-> del1:::revocation
inv:::revoked
invPrf:::revoked
invIss --> del2Aud
invoker --> invIss
invSub --> del2Sub
rootIss --> subject
rootSub --> subject
invPrf --> Delegations
classDef revocation stroke:blue,fill:#76b0ff
classDef revoked stroke:red,fill:#ff7676,color:red
Revocation is the act of invalidating a proof in a delegation chain for some specific UCAN delegation by its CID. All UCAN capabilities are either claimed by direct authority over the Subject, or by delegation chain terminating in that direct ("root") authority. Each link in a delegation chain contains an explicit issuer (delegator) and audience (delegatee).
Revocations MUST be immutable and irreversible. Recipients of revocations SHOULD treat them as a monotonically-growing set. If a Revocation was issued in error, it MUST NOT be retracted — a new, unique UCAN delegation MAY be issued (e.g. by updating the nonce or changing the time bounds). This prevents confusion as the revocation moves through the network and makes revocation stores append-only and highly amenable to caching and gossip.
An Issuer of a particular Delegation in a proof chain MAY revoke that Delegation. Note that this is not always the same as revoking the Delegation they they Issued; any UCAN that contains a proof where the revoker matches the iss
field — even transitively in the delegation chain — MAY be revoked.
Revocation of a particular proof does not guarantee that the Agent can no longer access to the capability in question. If an Agent is able to construct a valid proof chain without relying on the revoked proof, they still have access to the capability. By real-world analogy, if Mallory has two tickets to a film, and one of them is invalidated by its serial number, she is still able to present the valid ticket to see the film.
flowchart TB
subgraph RA[Alice can revoke]
direction RL
AB["(Root)\niss: Alice\naud: Bob\niff: [X,Y,Z]"]
subgraph RB[Bob can revoke]
BC["iss: Bob\naud: Carol\niff: [X,Y]"]
BD["iss: Bob\naud: Dan\niff: [Y,Z]"]
subgraph RC[Carol can revoke]
CD["iss: Carol\naud: Dan\niff: [X,Y]"]
subgraph RD[Dan can revoke]
DE["iss: Dan\naud: Erin\niff: [X,Y,Z]"]
end
end
end
end
BD -->|proof| AB
BC -->|proof| AB
CD -->|proof| BC
DE -->|proof| CD
DE -->|proof| BD
Here Alice is the root Issuer. Alice MAY revoke any of the UCANs in the chain, Carol MAY revoke the two innermost, and so on. If the UCAN Carol -> Dan
is revoked by Alice, Bob, or Carol, then Erin will not have a valid chain for the X
capability, since its only proof is invalid. However, Erin can still prove the valid capability for Y
and Z
since the still-valid ("unbroken") chain Alice to Bob to Dan to Erin
includes them. Note that despite Y
being in the revoked Carol -> Dan
UCAN, it does not invalidate Y
for Erin, since the unbroken chain also included a proof for Y
.
UCAN revocation is designed to work in the broadest possible scenarios, and as such needs very weak constraints. UCAN revocation MAY operate in fully eventually consistent contexts, with single sources of truth, or among nodes participating in consensus. The format of the revocation does not change in these situations; it is entirely managed by how revocations are passed around the network. Weak assumptions enable UCAN to work with eventually consistent resources, such as CRDTs, Git forks, delay-tolerant replicated state machines, and so on.
These weak assumptions are often associated with being unable to guarantee delivery in a certain time bound. Weak assumptions can always be strengthened, but not vice-versa. For example, if a capability describes access to a resource with a single location or source of truth, sending a revocation to that specific agent enables confirmation in bounded time. This grants nearly identical semantics that many people turn to ACLs for, but with all of the benefits of capabilities during delegation and invocation.
Out of order delivery is typical of distributed systems. Further, a malicious user can otherwise delay revealing that they have a capability until the last possible moment in hopes of evading detection. Accepting revocations for resources that the agent controls prior to the delegation targeted by the revocation is received is thus RECOMMENDED.
The Agent that controls a resource MUST maintain a cache of Revocations for which it is the Subject. The Agent MAY additionally cache gossiped Revocations about other Subjects as part of a store and forward mechanism.
During validation of a UCAN delegation chain, the canonical CID of each UCAN delegation MUST be checked against the cache. If there's a match, the relevant Delegation MUST be ignored. Note that this MAY NOT invalidate the entire UCAN chain.
// Pseudocode
const delegators = invocation.prf.map(proof => proof.iss)
invocation.prf.forEach(delegation => {
// Is the proof in the revocation store?
store.lookup(delegation).then(revocation => {
// Is the revocation issuer in this proof chain?
if (delegators.includes(revocation.iss)) {
throw new Error("Invalidated via revocation by delegation issuer")
}
// Is the revocation based on a delegated revocation?
const cids = revocation.iff.filter(cav => !!cav.rev)
if (cids.length === 1 && invocation.prf.includes(cids[0])) {
throw new Error("Invalidated by delegated revocation")
}
})
})
Resources with a single source of truth SHOULD follow the typical approach of maintaining a revocation store at the same physical location as the resource. For example, a centralized server MAY have an endpoint that lists the revoked UCANs by canonical CID.
For eventually consistent data structures, this MAY be achieved by including the store directly inside the resource itself. For example, a CRDT-based file system SHOULD maintain the revocation store directly at a well-known path.
Since Revocations MUST NOT be reversible, a new Delegation SHOULD be issued if a Revocation was issued in error.
flowchart LR
Alice((   Alice   ))
Bob((   Bob   ))
Carol((   Carol   ))
Dan((   Dan   ))
del1{{Delegate\ncan: crud/read Alice's DB}}
del2{{Delegate\ncan: crud/read Alice's DB}}
del3{{Delegate\ncan: crud/read Alice's DB}}
newDel{{"Delegate\ncan: crud/read Alice's DB\n(Resissued) "}}
Alice === del1 ==> Bob === del2:::Revoked ===x Carol === del3 ==> Dan
Alice === newDel:::Reissued ===> Carol
rev>Revoke!]
Alice === rev:::Invocation
rev -.->|rev| del2
classDef Invocation stroke:#F00,fill:#F00,color:#000;
classDef Revoked stroke:#F00;
classDef Reissued stroke:green;
linkStyle 2 stroke:red
linkStyle 3 stroke:red
linkStyle 8 stroke:red
linkStyle 9 stroke:red
linkStyle 6 stroke:green
linkStyle 7 stroke:green
Revocations MAY be evicted once the UCAN that they reference expires or otherwise becomes invalid through its proactive mechanisms, such as expiry (exp
) plus some clock-skew buffer.
The authority to revoke some Delegation MAY be itself delegated to a Principal not in the delegation chain. The revoked Delegation SHOULD be referenced by its canonical CID.
Field | Value |
---|---|
can |
"ucan/revoke" |
args |
{"revoke": &Delegation} |
This is a Delegation of the ability to Revoke:
{
"iss": "did:web:alice.example.com",
"aud": "did:web:zelda.example.com",
"can": "ucan/revoke",
"args": {
"revoke": {"/": "bafkreiem4on23qnu2nn2jg7vwzxkns6sxi5faysq7ekwtjhugqga3vbhim"}
},
// ...
}
flowchart LR
Alice((   Alice   ))
Bob((   Bob   ))
Carol((   Carol   ))
Dan((   Dan   ))
Zelda((   Zelda   ))
del1{{Delegate\ncan: crud/read Alice's DB}}
del2{{Delegate\ncan: crud/read Alice's DB}}
del3{{Delegate\ncan: crud/read Alice's DB}}:::Revoked
delRev{{Delegate\ncan: ucan/revoke}}
Alice === del1 ==> Bob === del2 ===> Carol === del3 ===x Dan
Alice === delRev ===> Zelda
delRev -.->|cid| del2
rev>Revoke]
Zelda === rev:::Invocation ===> Alice
rev:::Invocation -.->|rev| del3
classDef Revoked stroke:#F00;
classDef Invocation stroke:#F00,fill:#F00,color:#000;
linkStyle 4 stroke:red
linkStyle 5 stroke:red
linkStyle 9 stroke:red
linkStyle 10 stroke:red
A revocation Action MUST take the following shape:
Field | Value |
---|---|
do |
"ucan/revoke" |
args |
See Arguments |
nonce |
"" |
Note that per UCAN Invocation, the nnc
field SHOULD is set to ""
since revocation is idempotent.
Being expressed as an Invocation means that Revocations MUST define an Action type for the command ucan/revoke
.
Field | Type | Required | Description |
---|---|---|---|
revoke |
&Delegation |
Yes | The CID of the UCAN Delegation that is being revoked |
path |
[&Delegation] |
No | A delegation path that includes the Revoker and the revoked Delegation |
Since all delegation chains MUST be rooted in a Delegation where the iss
and sub
fields are equal, the root Issuer is a priori in every delegation chain. This is not the case for sub-delegation. There are many paths through the authority network. For example, take the following delegation network:
flowchart LR
Alice -->|delegates| Bob -->|delegates| Dan -->|delegates| Erin
Bob -->|delegates| Carol -->|delegates| Erin
Alice -->|delegates| Mallory
Mallory is not in the delegation chain of Erin. This is fine, since the semantics of revocation merely state that she would assert that no delegation of hers may be used in the prf
field of an Invocation if it also includes the rev
Delegation. However, issuing spurious Revocations and requiring them to be stored is a potential DoS vector. Executors MAY require a delegation path witness be included to avoid this situation.
Unlike Mallory, Bob, Carol, and Dan can both provide valid delegation paths that include Delegations that they have issued. Bob has two paths (Alice -> Bob -> Dan -> Erin
or Alice -> Bob -> Carol -> Erin
), and either will suffice.
// DAG-JSON
{
"s": {"/": {"bytes": "7aEDQIscUKVuAIB2Yj6jdX5ru9OcnQLxLutvHPjeMD3pbtHIoErFpo7OoC79Oe2ShgQMLbo2e6dvHh9scqHKEOmieA0"}},
"p": {
"h": {"/": {"bytes": "NBIFEgEAcQ"}},
"ucan/i/1.0.0-rc.1": {
"iss": "did:plc:ewvi7nxzyoun6zhxrhs64oiz",
"sub": "did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z",
"do": "ucan/revoke",
"args": {
"revoke": {"/": "bafkreictzcfwelyww7zmjkl5nptyot24oilky2bppw42nui2acozhfmzqa"},
"path": [
{"/": "bafkreic4lzfu6gq6pxonmalbjzjumrs5p47plsolmccaz4qhgmzo24fagu"},
{"/": bafkreicc3jmhhtkzv26rb43cfx6ihyjlj2hixdfrkirglrermfo6cduelm""}
]
},
"nonce": {"/": {"bytes": ""}},
"meta": {
"comment": "bad behaviour"
},
"prf": [
{"/": "bafkr4idnrqfouibxdqpvh2lmkhgsbw5yabvjbiaea3fplrb4vxifaphvgy"},
]
}
}
}
Revocation lists are a fairly widely used concept.
SPKI/SDSI is closely related to UCAN. A different format is used, and some details vary (such as a delegation-locking bit), but the core idea and general usage pattern are very close. UCAN can be seen as making these ideas more palatable to a modern audience and adding a few features such as content IDs that were less widespread at the time SPKI/SDSI were written.
X.509 Certificate Revocation Lists defines two kinds of certificate invalidation: temporary ("hold") and permanent ("revocation"). This RFC also includes a field for indicating a reason for revocation. UCAN Revocation has no concept of a temporary hold on a capability, but this behavior MAY be emulated by revoking a credential and issuing a new UCAN with a nbf
field set to a time in the future.
ZCAP-LD is closely related to UCAN, but situated in the W3C-style linked data world (the "LD" in ZCAP-LD). Revocation in ZCAP-LD is only granted to those who have a special caveat on a capability. In contrast, UCAN capabilities MAY be revoked by anyone in the relevant delegation path.
OAuth 2.0 Revocation is very similar to UCAN revocation. It is largely concerned with the HTTP interactions to make OAuth revocation work. OAuth doesn't have a concept of sub-delegation, so only the user that has been granted the token can revoke it. However, this may cascade to revocation of other tokens, but the exact mechanism is left to the implementer.
While strictly speaking being about assertions rather than capabilities, Verfiable Credential Revocation spec follows a similar pattern to those listed above.
E-style object capabilities use active network connections with proxy agents to revoke delegations. Revocation is achieved by shutting down that proxy to break the authorizing reference. In many ways, UCAN Revocation attempts to emulate this behavior. Unlike UCAN Revocations, E-style object capabilities are fail-safe and thus by definition not partition tolerant.
Thank you Blaine Cook for the real-world feedback, ideas on future features, and lessons from other auth standards.
Thanks to Juan Caballero for the numerous questions, clarifications, and general advice on putting together a comprehensible spec.
Many thanks to Alan Karp for sharing his vast experience with capability-based authorization, patterns, and many right words for us to search for.
Thanks to Benjamin Goering for the many community threads and connections to W3C standards.
Many thanks to Christine Lemmer-Webber for her handwritten(!) feedback on the design of UCAN, spearheading the OCapN initiative, and her related work on ZCAP-LD.
Thanks to the entire SPKI WG for their closely related pioneering work.
We want to especially recognize Mark Miller for his numerous contributions to the field of distributed auth, programming languages, and computer security writ large.