-
Notifications
You must be signed in to change notification settings - Fork 18
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
Way to connect delegation chains #130
Comments
I have wrote a draft of how we could address it (at least in specific context) https://github.com/web3-storage/specs/pull/7/files @expede would love your input, if you are able to take a look. |
[For those just joining us, this is part of the larger story about delegating "everything" including having future delegations to Alice "automatically forwarded" to Bob] @Gozala Yes, I think that option 2 (in proofs) is the correct place. Were we to ship this feature, it would need to get baked pretty deeply into UCAN. By definition, it adds a bit of complexity for delegation alignment, but I think that's unavoidable given that's core to the proposed system. |
@expede I spend bit more time thinking about this and writing about a related things storacha/specs#12 which made me realize that there are actually two things that are worth untangling here:
Let me try to clarify what I mean by specific examples Non linear proofsLet’s consider scenario where const prf1 = {
iss: ”did:key:zAlice”,
aud: “did:key:zBob”,
att: [{ can: “inbox/add”, with: “did:key:zAlice” }],
…
} Independently we have const prf2 = {
iss: “did:key:zBob”,
aud: “did:key:zMallory”,
att: [
{ can: “*”, with: “did:key:zBob” },
{ can: “*”, with: “did:key:zAlice” }
],
…
} It seems reasonable that Mallory would be able to compose two into as it proves that:
And therefor {
iss: “did:key:zMallory”,
aud: “did:dns:ucan.xyz”,
att: [{ can: “inbox/add”, with: “did:key:zAlice”, nb: { msg: “hello” } }],
prf: [
prf2,
prf1,
]
} I think ☝️is very intuitive and making it invalid goes against expectations. There had been handful of cases where we have came up with a design that intuitively made sense and only later recognized that it is not going to work because we did not have linear proof chain. We had to resort to alternative involving more coordination to form a linear chain. This also brings me to another point. Requiring linear chain imposes more coordination among participating actors, which seems like unnecessary overhead. I can not imagine a case where this limitation would be better than limiting delegation, specifically I would like to propose lifting this requirement and amend validation logic. I suggest treating principal misaligned proofs as implicit proofs in delegation chain for matching Imposed resource hierarchyI’m noticing that most issues we run seem to stem from the fact that we don’t have hierarchy in resources while most of fissions stuff seem to have it. Example #130 is basically attempts to address delegation for a resource that is not known yet. What’s important however is not the fact that resource is not known yet, but that it’s not going to be under the own resource which is also why it’s hard to express. I have realized that we could technically address #130 with ☝️without necessarily extending spec to define a forwarding resource schema: {
iss: "did:key:zService",
aud: "did:key:zZzzz",
exp: null,
att: [{ with: "did:key:zAlice", can: "inbox/send", nb: { msg: “hello” } }],
prf: [
// did:key:zAlice -> did:key:zW3
{
iss: "did:key:zAlice",
aud: "did:key:zW3",
exp: null,
att: [{ with: "did:key:zAlice", can: "*" }],
},
// did:key:zW3 -> did:key:zService
{
iss: "did:key:zW3",
aud: "did:key:zService",
exp: null,
att: [{ with: "did:*", can: "*" }],
},
],
} I do still think that defining and spec-ing unattenuated delegation is good idea. With all the above I also think that |
Yeah, I agree with you, at least in principal! I think that revocation and memoization get harder if we aren't including the proof CIDs in the credential directly. How would you handle these cases? |
I'm not totally sure that I follow the topic here. Do you mean the idea that we discussed last week or the week before where Alice can "automatically forward" all capabilities directed to Bob, and also revoke things with Alice in the chain? If so, I like the idea! It sounds like the pattern has a name ("powerbox") in the existing literature. We just need to add it to the (very early, very WIP) 0.10 spec. Or are you saying that you don't like that idea anymore? |
In regards to memoization, you can not really do it by the CID of the UCAN, because some capability may validate while other could fail. Instead we have to memoize I'm not sure I understand how this affects revocation though. |
Sorry for not been clearer here. I've just realized that bunch of the problems we've seem to be running into would not have existed if we had more of hierarchical layout like WNFS. I could simply link all delegated resources under some path and delegation from the parent path would've covered those resources as well. Since we do not have such hierarchy and our resources are pretty much DIDs it becomes tricky to delegate things I may have authority over without explicitly listing each one, or resorting to something like
I was trying to untangle two things from that idea, "non linear proofs" and "forwarding resource" notation. It seems that even with just former we may be able to address bunch of limitations we're running to even if we did not have later. |
I think it makes it harder, but not impossible! At minimum it changes who has to track provenance. Let me rubber duck in text a bit here: In the existing chain structure, you know that if you've checked UCAN_C, you'd also checked its proofs UCAN_B and UCAN_A. If the issuer of UCAN_A revokes UCAN_C, this is a very straightforward marking of UCAN_C invalid everywhere. This can be gossiped very lightweightly by CID: "hey everyone, UCAN_C is invalid! I signed off on it and I'm in the chain!". Under this proposal, you now have to say something like "you can't use UCAN_A to prove UCAN_C". We now gossip about the tuple At some stage, someone has to check "which UCANs were used to build a delegation graph when I checked this last?", and possibly update them. These graphs can get pretty complex. In the current system, a delegation chain like this...
...can instead look like this...
☝️ This is by design — it's the core of this proposal! It is a lot more states. We have the equivalent of 7 of today's An advantage (kind of like having all of the I guess we'd also check all of the At the end of this, you'd then have essentially a synthesized UCAN that looks like today's UCANs. This proposal is a generalization of a few parts, which does mean added complexity (there's exponentially more possible synthesized UCANs for the same number of delegations), but I an also see it being pretty convenient with enough tooling 🤷♀️ FormatThe format change would be pretty simple: remove the Current FeelsI'm increasingly in favour of this proposal! |
The UCAN Cinematic Universe. The UCU 🦸 |
wait I thought issuer could only revoke tokens it issued and not the tokens downstream, well it could indirectly of course by revoking own token. Is my interpretation incorrect or am I misunderstanding quoted point above ? |
I don’t think this accounts for rights amplification, where you end up with more of braided chain and not as straight forward revocations. In practice today you already need to track set of cids that could invalidate a proof |
I'm not saying that they're all straight lines. I'm claiming that a UCAN that under v0.9 is a straight line will always have to be treated as if it's part of a graph under this proposal. |
You can revoke tokens as far downstream as you want, as long as your DID is in the proof chain. Here's a diagram from the current spec: https://github.com/ucan-wg/spec/#661-example |
I’m not sure I understand why (but then I also thought you could only revoke what you’ve issued). Couldn’t you just revoke either UCAN_A or UCAN_C which would atomically invalidate the combination ? |
Perhaps both? The quoted point...
...says that it's very each to track provenance today, since it's directly in the chain. With this proposal, some more of that tracking gets pushed to each recipient of a UCAN graph. You also have to pass around more information to revoke a UCAN, because now it depends on exploring all possible delegation histories when someone revokes a downstream UCAN. This isn't necessarily a problem! I'm just pointing out an instance of the law of conservation of complexity and exploring what it would look like to implement and use. |
I guess it’s along the lines of downstream revocation. You may not want to revoke all of the things just individual use |
Exactly! |
Yes, that's the idea! But know to do that, you have to explore every possible path through a UCAN's history that you know about. If I (the issuer of |
I don’t think tracking is getting much more complicated, or at least I don’t see why. We already have to track cid sets to account for amplification. With this proposal you may have to track for additional cid if it ends up used by prover. Verification does get complicated indeed a& so does targeted revocations |
I agree that it's probably not a big deal |
I think I had convinced myself when I did my rubber ducking session earlier in the thread. Shall we just mark this for inclusion in 0.10 and see what others think? |
Porting back here from ucan-wg/invocation#1 (comment) In UCAN v0.10, we're going to drop the prf field so that you can construct a graph out of whatever proofs you happen to have, not what was in the chain at the time / what was delegated to you. We're dropping prf field ? Where are we going to stick the proofs then ? Maybe I need to back up: my understanding of where we got to with #130 was that you could do things like this:
...and substitute any UCAN as a proof as long as it matches the scoping and principal alignment rules. If I have UCAN_Z, I could give someone any of the 7 possible proof chains that make this up. In order to have that freedom, we need to pull the CIDs out of the signed UCAN payload, otherwise we limit ourselves to whatever is in the payload. Is that not your current picture, @Gozala? We'd probably put them in the CAR file or HTTP headers, and let the recipient figure out how to construct the graph. This is what I was saying the other day about it increasing the amount of work for them, but ultimately it's not THAT different from what they do today. The difference is that it happens on a per-transport basis, which is already the case with CIDs in the prf field. Oh I see you mean not remove the field, but rather remove it from the signature payload To be honest, I was not proposing that just allowing to stick proofs that aren't principal aligned. That way they could be utilized down the chain. That said I think removing it from the signature is intriguing and I think it makes sense. Awesome 🎉 Also FWIW, when I explained this as context for some other design decisions yesterday to @QuinnWilton, she was surprised that it didn't already work like that (it's how it worked in her head as someone who's heard about UCAN but never used them in production) I would really prefer to keep them inside UCAN, even if omitted from the signature. That way there is information about graph roots. let's move this back to the Issue on the core spec |
No rush to write this at midnight PST, but can you expand on why? I had assumed that you were passing around UCANs in a CAR file today. You can then sort them by issuer/audience, and create the graphs. How does having the roots in the UCAN paylaod make consuming this easier? |
We omit some of the blocks we have transmitted prior. So you may have CAR that leaves out bunch of proofs that you need for the invocation, but your search space now is whole network because who knows which proofs were omitted ? We could in theory index all of the proofs we encounter by issuer, audience, resource, ability and then try to fill the gaps from there, but I don't think it's better than UCAN telling you everything you need to know to validate. |
Okay, so for my own understanding: should I be able to add more proofs after the fact if the proofs written into my UCAN are all revoked/expired?
|
I'm also realizing after discussions in ucan-wg/invocation#1 I'm realizing that context in which I was seeking this feature was invocations and not delegations. I am also starting to understand why you've mentioned removing |
Ah! Indeed, it is a different situation for delegation and invocation — agreed! |
For invocation, for at least the |
While I think removing
Removing Question of whether |
Yup that all does make sense 👍 This clearly needs more consideration before we put it in the v0.10 PR. I think I need to sketch out how this would work RE this GH Issue under both assumptions. The repathing option is pretty nice to enable autoforwarding, but I agree that it increases what we're asking the validator to do. In some ways it's the same thing as having all of the proofs in the This is some of what I think I was failing to communicate that I wasn't thrilled about the complexity of on our call earlier this week. We must have had different pictures in our heads. Here we are a few days later am I've become comfortable with it after talking it through with a few Fission folks, and thinking that you were advocating for at least the equivalent of no Anyhow! Action item is to mock up what a UCAN chain would look like for "forwarding" under both options 👍 ✍️ |
Catching up here, first reaction is that this seems quite dangerous? Currently in the UCAN spec we have a wildcard resource mentioned,
Let's suppose we have a delegation chain as follows,
Now One way to get around this could be to recommend ppl to not use wildcards, but it seems inevitable that people will need to use these at some point. Concrete example of when using a wildcards is very useful. Using Ceramic here as an example.
While it's possible to do two separate requests to WalletConnect, one for DID and one for resource delegation, this makes UX much worse. |
I've actually come around to see the other side of this. One telling use case I have in mind is In order to provide a good user experience we further want to further delegate permissions from the By allowing connecting of delegation chains as you describe above, new state proofs could be generated in the background, while the user only needs to delegate to the session key once. |
I love the idea of decoupling proofs from authorization!
"bafy...UCAN Alice->Bob"
{
"cap": {"/": "bafy...someTask"},
"iss": "Alice's DID",
"aud": "Bob's DID",
"except": ["Malory's DID"] // describes that can't be delegated to Malory
} UCAN spec currently does not allow to express restrictions on who the downstream audience may be though. Is it of value to have? Side-thought. On invocations, when a proof chain is required, should it be up to the invoker to provide it? |
Another side-thought, on how UCANs and Invocation spec can take on their corresponding auth and invocation responsibilities.
(perhaps supplemented with proofs as:)
should then Taking another spin on this example, an invocation then could be expressed as: {
// bare description of capability/operation, can be used in "await"
"bafy...updateDns": {
"dns:example.com?TYPE=TXT": {
"crud/update": {
"input": {
"value": {"ok/await": "bafy...someInstruction"}
}
}
}
},
// authorization of capability/operation, a UCAN
"bafy...updateDns Task Invoker->Executor": {
"ucv": "0.2.0",
"cap": {"/": "bafy...updateDns"},
"fct": {"maxRetries": 2}, // a way to express "how" attenuations (meta)
"iss": "Invoker's DID",
"aud": "Executor's DID",
"sig": "Invoker's signature over the above fields"
},
// authorization of invocation of capability/operation
"bafy...updateDns Task Invoker->Executor Invocation": {
"v": "invocation@1.0.0"
"run": {"/": "bafy...updateDns Task Invoker->Executor"}, // we kinda accidentaly baked-in proof here, eh!
"sig": "Invoker's signature over the above fields",
"prf": ["bafy...updateDns Task Invoker->Executor"] // proofs are at the level of Invocations, yay! (except that in "run" :sweat_smile: ). Also they're left out of "sig"
},
// Executor delegates execution to Executor2
// first he isues
// authorization of capability/operation, a UCAN
"bafy...updateDns Task Executor->Executor2": {
"ucv": "0.2.0",
"cap": {"/": "bafy...updateDns"},
"fct": {"maxRetries": 2}, // we need to repeat it, eh! would be nicer to have it inside "cap"
"iss": "Executor's DID",
"aud": "Executor2's DID",
"sig": "Executor's signature over the above fields"
},
// then he issues
// authorization of invocation of capability/operation
"bafy...updateDns Task Invoker->Executor->Executor2 Invocation": { // another invocation? eh! there should be just one
"v": "invocation@1.0.0"
"run": {"/": "bafy...updateDns Task Invoker->Executor"},
"sig": "Executor's signature over the above fields",
"prf": ["bafy...updateDnsTask Invoker->Executor",
"bafy...updateDnsTask Executor->Executor2"]
},
"bafy...updateDns Task Invoker->Executor->Executor2 Invocation Receipt": {
"ran": {"/": "bafy...updateDns Task Invoker->Executor->Executor2 Invocation"},
"out": "DNS updated!",
"iss": "Executor2's DID",
"sig": "Executor2's signature over the above fields"
}
} We see there are some "meh"s about the approach above, we can do better! UCAN is designed to support multiple Capabilities. "bafy...updateDns": {
"rsc": "dns:example.com?TYPE=TXT", // resource
"op": "crud/update", // operation
"input": {"someKey": "someValue"}
} Task as UCAN includes Auth Hey, this is accidental, we'd want to auth separately! Additionally, Task as UCAN leaves Facts (Meta, meant to attenuate with "how") outside of Capability. In order to cater for both of these problems we could extend our above data model to support Meta. "bafy...updateDns Task": {
"rsc": "dns:example.com?TYPE=TXT",
"op": "crud/update",
"input": {"someKey": "someValue"},
"meta": {"maxRetries": 2} // attenuating with "how"
} And then we can authorize it via UCAN, yay! "bafy...updateDns Task UCAN Invoker->Executor": {
"ucv": "0.2.0",
"cap": {"/": "bafy...updateDns Task"},
"iss": "Invoker's DID",
"aud": "Executor's DID",
"sig": "Invoker's signature over the above fields"
} Invocations include Proofs "bafy...updateDns Task Invocation": {
"v": "invocation@1.0.0"
"task": {"/": "bafy...updateDns Task"},
"iss": "Invoker's DID",
"sig": "Invoker's signature over the above fields"
} Then Executor, in order to delegate, would issue another UCAN "bafy...updateDns Task UCAN Executor->Executor2": {
"ucv": "0.2.0",
"cap": {"/": "bafy...updateDns Task"}
"iss": "Executor's DID",
"aud": "Executor2's DID",
"sig": "Executor's signature over the above fields"
} and hand over to delegatee the original Invocation with extended proofs. The actual proofs used to execute are captured in Receipt for that Invocation. "bafy...updateDns Task Invocation Receipt": {
"v": "invocation@1.0.0",
"ran": {"/": "bafy...updateDns Task Invocation"},
"out": "DNS updated!",
"iss": "Executor2's DID",
"prf": [{"/": "bafy...updateDns Task UCAN Invoker->Executor"},
{"/": "bafy...updateDns Task UCAN Executor->Executor2"}],
"sig": "Executor2's signature over the above fields"
} In sum we have:
Goes without saying, this may not be the best solution, if not all to crazy. 😄 |
@andrewzhurov yes this is the current plan in various states of clarity in the WIP PRs 👍 |
I find it increasingly appealing how we could have data model of Capability / Task (what's refered to as CAN above) reusable across UCAN and Invocation spec. Task's data model seems handy for describing single capability. (Note, it's WIP) So a WASM Executor can authorize Invoker to run arbitrary WASM "bafy...runWasm CAN": {
"op": "wasm/run",
}
"bafy...runWasm UCAN Executor -> Invoker": {
"cap": {"/": "runWasm CAN"},
"iss": "Executor's DID",
"aud": "Invoker's DID"
} then Invoker is able to further attenuate "arbitrary wasm" down to some concrete expression "bafy...runWasm2+2": {
"op": "wasm/run",
"rsc": {"/": "bafy...wasmMathModule"},
"input": [2, 2]
} authorizes Executor to run it "bafy...runWasm2+2 UCAN Invoker->Executor": {
"cap": {"/": "bafy...runWasm2+2"},
"iss": "Invoker's DID",
"aud": "Executor's DID"
} and expresses invocation intent "bafy...runWasm2+2 Invocation": {
"run": {"/": "bafy...runWasm2+2"},
"iss": "Invoker's DID"
} having supplied to Executor the Invocation and the UCAN, "bafy...runWasm2+2 Invocation Receipt": {
"ran": {"/": "bafy...runWasm2+2 Invocation"},
"out": 4,
"iss": "Executor's DID",
"prf": [{"/": "bafy...runWasm UCAN Executor->Invoker"},
{"/": "bafy...runWasm2+2 UCAN Invoker->Executor"}]
} Whether to have Task and Instruction separate, as in the linked data model, or merged, as in example from the comment above, is unclear, seems to be a tradeoff. Conversely, having "how" and "what" expressed at the same level allows to await on specific "how". But with no regard to exact data model, having CAN describe single capability, reusable across UCAN and Invocation seems increasingly appealing to me. Curious to learn your take on it, fellas. |
Sorry for joining the discussion late. I'm still trying to get my head around the issues. I want to return to the very first entry in this discussion that deals with connecting delegation chains involving offline private keys. I don't understand the problem. In that example,
My understanding is that offline keys are very long lived, e.g., 10s of years, and are used to sign certificates to more transient keys, typically a year (but Google is pushing for 90 days). The public keys of those certificates are then used in things like TLS. That being the case, Alice can delegate to did:key:zW3-2023. Better yet, she can establish a secure connection to the service using did:key:zW3-2023 and delegate to a public key the service generates specifically for this delegation. Doing things this way means there's no need to connect delegation chains. I'm far from an expert in such matters, so I'm likely missing something. What is it? |
Pulling piece of discussion from storacha/w3up#182 (comment) after realizing that
did:dns
has nothing to do with actual problem.Ok so let's consider following scenario:
did:key:zAlice
delegates todid:key:zW3
service.did:key:zW3
private key is not in any computer it's on piece of paper in safe deposit box.did:key:zW3
has a delegatedid:key:zService
which acts on it's behalfdid:key:zAlice
, but it can not because delegation can not be signed bydid:key:zService
In other words we have two delegation chains that if we were to connect we'd get a valid delegation chain. We could construct delegation like
However above delegation would not be valid because:
with: *
or a way to describe resources delegated to an issuer.ucan:*
could be expanded to imply not only all proofs but also all siblings that delegate to theiss
?with: *
be a thing ?aud
in second proof isdid:key:zW3
and notdid:key:zService
.iss
, because you are providing proof on their behalf. It would only work one level deep, but that seems ok.The text was updated successfully, but these errors were encountered: