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

Way to connect delegation chains #130

Open
Gozala opened this issue Nov 20, 2022 · 38 comments
Open

Way to connect delegation chains #130

Gozala opened this issue Nov 20, 2022 · 38 comments
Assignees
Labels
Milestone

Comments

@Gozala
Copy link
Contributor

Gozala commented Nov 20, 2022

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:

  1. User delegates some capability did:key:zAlice delegates to did:key:zW3 service.
  2. did:key:zW3 private key is not in any computer it's on piece of paper in safe deposit box.
  3. did:key:zW3 has a delegate did:key:zService which acts on it's behalf
  4. Service needs to redelegate capability received from did:key:zAlice, but it can not because delegation can not be signed by did: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

{
  iss: "did:key:zService",
  aud: "did:key:zZzzz",
  exp: null,
  att: [{ with: "did:key:zAlice", can: "*" }],
  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: "*", can: "*" }],
    },
  ],

However above delegation would not be valid because:

  1. We do not have with: * or a way to describe resources delegated to an issuer.
    • Perhaps ucan:* could be expanded to imply not only all proofs but also all siblings that delegate to the iss ?
    • Or we could make with: * be a thing ?
  2. It is violates principal alignment because aud in second proof is did:key:zW3 and not did:key:zService.
    • Perhaps spec should allow pushing misaligned proofs into a siblings with matching iss, because you are providing proof on their behalf. It would only work one level deep, but that seems ok.
    • Alternatively mismatched proofs could be moved into all the siblings, that way delegation several levels above could provide a proof on behalf of the issuer there.
@Gozala
Copy link
Contributor Author

Gozala commented Nov 20, 2022

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.

@expede
Copy link
Member

expede commented Nov 21, 2022

[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 expede added the ⏭️ autoforwarding Autoforwarding label Nov 21, 2022
@Gozala
Copy link
Contributor Author

Gozala commented Nov 25, 2022

@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:

  1. Support for UCANs proofs that are not part of the chain
  2. Support for delegating a resource(s) without imposing hierarchy

Let me try to clarify what I mean by specific examples

Non linear proofs

Let’s consider scenario where did:key:zAlice shares some capability to did:key:zBob who (re)delegates capability to did:key:zMallory

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:

  1. did:key:zMallory can do anything with did:key:zAlice that did:key:zBob is able to2.
  2. did:key:zBob can inbox/add with did:key:zAlice.

And therefor did:key:zMallory is able to inbox/add with did:key:zAlice.

{
  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 did:key:zBob could restrict what it’s (re)delegating instead.

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 aud.

Imposed resource hierarchy

I’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 { with: “*”, can: “*” } is the most intuitive way. But than again we could address bunch of limitations by addressing non linear proofs even without addressing the other one.

@expede
Copy link
Member

expede commented Nov 26, 2022

Non linear proofs

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?

@expede
Copy link
Member

expede commented Nov 26, 2022

I’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.
[...]
I do still think that defining and spec-ing unattenuated delegation is good idea.

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?

@Gozala
Copy link
Contributor Author

Gozala commented Nov 26, 2022

Non linear proofs

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?

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 ucanCID + capabilityCID. Just as well we could add third component to include implicit proofs.

I'm not sure I understand how this affects revocation though.

@Gozala
Copy link
Contributor Author

Gozala commented Nov 26, 2022

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?

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 did:* which is barely legal, if it all.

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?

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.

@expede
Copy link
Member

expede commented Nov 26, 2022

I'm not sure I understand how this affects revocation though.

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 (UCAN_A, UCAN_C, signature) instead of the tuple (DID, UCAN_C, signature). We can infer the DID from UCAN_A so we're only adding info.

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...

UCAN_A
  |
  V
UCAN_B
  |
  V
UCAN_C
  |
  V
UCAN_Y
  |
  V
UCAN_Z

...can instead look like this...

    UCAN_A    UCAN_D
   /      \    /
  V        V  V
UCAN_B   UCAN_W
   \      /
    V    V
    UCAN_C   UCAN_E
   /      \   /
  V        V V
UCAN_X   UCAN_Y
   \      /
    V    V
    UCAN_Z

☝️ 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 UCAN_Zs in this UCAN multiverse. It has an upper bound on number of valid delegation chains that's the square of the number of nodes in the graph — $O(n^2)$.

An advantage (kind of like having all of the prfs together) is that a service could automatically "heal" a revoked UCAN by substituting another proof that they've seen before.

I guess we'd also check all of the nbf and exp, and take the smallest window? They're very unlikely to be strictly subsets now.

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 🤷‍♀️

Format

The format change would be pretty simple: remove the prf field, and pass around proofs in a separate data structure like the UCAN table from the bearer token spec: https://github.com/ucan-wg/ucan-as-bearer-token#23-example

Current Feels

I'm increasingly in favour of this proposal!

@expede
Copy link
Member

expede commented Nov 26, 2022

We have the equivalent of 7 of today's UCAN_Zs in this UCAN multiverse.

The UCAN Cinematic Universe. The UCU 🦸

@Gozala
Copy link
Contributor Author

Gozala commented Nov 27, 2022

If the issuer of UCAN_A revokes UCAN_C, this is a very straightforward marking of UCAN_C invalid everywhere.

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 ?

@Gozala
Copy link
Contributor Author

Gozala commented Nov 27, 2022

In the current system, a delegation chain like this...

UCAN_A
|
V
UCAN_B
|
V
UCAN_C
|
V
UCAN_Y
|
V
UCAN_Z

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

@expede
Copy link
Member

expede commented Nov 27, 2022

I don’t think this accounts for rights amplification

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.

@expede
Copy link
Member

expede commented Nov 27, 2022

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.

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

@Gozala
Copy link
Contributor Author

Gozala commented Nov 27, 2022

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 (UCAN_A, UCAN_C, signature) instead of the tuple (DID, UCAN_C, signature). We can infer the DID from UCAN_A so we're only adding info.

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 ?

@expede
Copy link
Member

expede commented Nov 27, 2022

Is my interpretation incorrect or am I misunderstanding quoted point above ?

Perhaps both?

The quoted point...

If the issuer of UCAN_A revokes UCAN_C, this is a very straightforward marking of UCAN_C invalid everywhere.

...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.

@Gozala
Copy link
Contributor Author

Gozala commented Nov 27, 2022

I guess it’s along the lines of downstream revocation. You may not want to revoke all of the things just individual use

@expede
Copy link
Member

expede commented Nov 27, 2022

You may not want to revoke all of the things just individual use

Exactly! Alice -> Bob -> Mallory. Bob is offline, and Mallory is misbehaving. Alice can invalidate Malory without invalidating Bob.

@expede
Copy link
Member

expede commented Nov 27, 2022

Couldn’t you just revoke either UCAN_A or UCAN_C which would atomically invalidate the combination ?

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 UCAN_A) invalidate UCAN_C, but they also can provide the path (from the above diagram) UCAN_D -> UCAN_W -> UCAN_C, which I am not the issuer on any token of, I would assume that UCAN_C is still valid.

@Gozala
Copy link
Contributor Author

Gozala commented Nov 27, 2022

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

@expede
Copy link
Member

expede commented Nov 27, 2022

I don’t think tracking is getting much more complicated

I agree that it's probably not a big deal

@expede
Copy link
Member

expede commented Nov 27, 2022

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?

@expede expede mentioned this issue Nov 27, 2022
15 tasks
@expede
Copy link
Member

expede commented Nov 27, 2022

☝️

Screenshot 2022-11-26 at 17 33 57

@expede expede added the v0.10 label Nov 27, 2022
@expede expede self-assigned this Nov 27, 2022
@expede expede added this to the v0.10 milestone Nov 27, 2022
@expede expede removed the 📋 v0.10 label Nov 27, 2022
@expede
Copy link
Member

expede commented Nov 30, 2022

Porting back here from ucan-wg/invocation#1 (comment)


@expede

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.


@Gozala

We're dropping prf field ? Where are we going to stick the proofs then ?


@expede

Maybe I need to back up: my understanding of where we got to with #130 was that you could do things like this:

    UCAN_A    UCAN_D
   /      \    /
  V        V  V
UCAN_B   UCAN_W
   \      /
    V    V
    UCAN_C   UCAN_E
   /      \   /
  V        V V
UCAN_X   UCAN_Y
   \      /
    V    V
    UCAN_Z

...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.


@Gozala

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.


@expede

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)


@Gozala

I would really prefer to keep them inside UCAN, even if omitted from the signature. That way there is information about graph roots.


@expede

let's move this back to the Issue on the core spec

@expede
Copy link
Member

expede commented Nov 30, 2022

I would really prefer to keep them inside UCAN, even if omitted from the signature.

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?

@Gozala
Copy link
Contributor Author

Gozala commented Nov 30, 2022

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.

@expede
Copy link
Member

expede commented Nov 30, 2022

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?

  REVOKED
     V
A ->[B]-> C -> E
|         ^
|         |
+--> D ---+
     ^
Alternate Proof
  Sent Later

@Gozala
Copy link
Contributor Author

Gozala commented Nov 30, 2022

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 prf from UCANs, if you provide bag of delegations with an invocation it could just go ahead and build a chain from there, no need to have pointers in the delegations themself.

@expede
Copy link
Member

expede commented Nov 30, 2022

Ah! Indeed, it is a different situation for delegation and invocation — agreed!

@expede
Copy link
Member

expede commented Nov 30, 2022

For invocation, for at least the run: "*" you need to point to some specific [&UCAN]. This could become run: [&UCAN] for the "all" case. There's some JSON examples here: ucan-wg/invocation#1 (comment) They may make more sense with the added context from this discussion 👍

@Gozala
Copy link
Contributor Author

Gozala commented Nov 30, 2022

While I think removing prf may have merit, I'm bit hesitant. I really like that UCANs are essentially stateless, so I can go and verify them on different nodes with different states and come to same conclusion

There is obviously some state like revocations and wall-clock

Removing prf in favor of communicating them out of band would undermine this. Which is why I think prf should stay, that way I can come to same conclusion whether it is valid or not regardless of what delegations I have seen / indexed. Agents are still in power of producing UCANs with alternative proofs that would be valid however.

Question of whether prf should be left out of signature or not is interesting. It would allow any actor to take original UCAN and update proofs as it pleased. I'm not sure I like it, because issuer is no longer in control. On the other hand if we keep prf in the signature, only actors to which issuer delegates would be able to recompose proofs, which I feel is a better choice.

@expede
Copy link
Member

expede commented Nov 30, 2022

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 prf field together, but more.

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 prf 😅

Anyhow! Action item is to mock up what a UCAN chain would look like for "forwarding" under both options 👍 ✍️

@oed
Copy link

oed commented Dec 2, 2022

Catching up here, first reaction is that this seems quite dangerous?

Currently in the UCAN spec we have a wildcard resource mentioned,

{ 
  "with": "ucan:*", 
  "can": "ucan/*" 
}

Let's suppose we have a delegation chain as follows,

DID_A -(*)-> DID_B -(*)-> DID_C

Now DID_C is malicious and want to make DID_B look bad. If DID_C writes to a resource controlled by DID_A this is easily detected by DID_B. However, if we allow substitution of the delegation chain a new DID_X could be created that delegates some capability to DID_Band DID_C composes this delegation chain to write to a resource DID_X controls. This would be very hard for DID_B to detect, but could potentially have reputational risk to them.
Note that this can happen with any resource that uses some sort of wildcard to abstract who the controller of a given resource is.

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.

  • App wants to request write access to users blog posts
  • App doesn't yet know the users DID
  • App uses WalletConnect to request the users address (DID) and access to their blog posts
    • WalletConnect supports (or will, not sure of status currently) requesting an address + SIWE at the same time
    • This means that we need to specify the resource in without knowing the users DID
    • In Ceramic this can be done using ceramic://*?model=<blog-post-schema-streamid>, which essentially means "delegate access to streams you control that conform to the given model schema"

While it's possible to do two separate requests to WalletConnect, one for DID and one for resource delegation, this makes UX much worse.

@oed
Copy link

oed commented Feb 6, 2023

I've actually come around to see the other side of this.

One telling use case I have in mind is did:nft. Here an NFT needs to delegate write permission to the current owner (in the form of did:pkh). Now an NFT doesn't have a way to create a signature, but we can generate a state proof from a blockchain (e.g. using eth_getProof on an EVM chain).

In order to provide a good user experience we further want to further delegate permissions from the did:pkh to a session key did:key in the users browser. Now this could be done by referencing the state proof in the prf field. However, these state proof are only valid for a specific block height. This means that new proofs need to be frequently generated, and the user thus would need to recreate the delegation from the did:pkh to the did:key frequently as well.

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.

@andrewzhurov
Copy link

andrewzhurov commented Oct 1, 2023

I love the idea of decoupling proofs from authorization!
An insight for me: a proof chain is only needed for invocation.
Leaving it out of auth allows for many wonders:

  1. More closely captures user's intent.
    Users want to authorize some capability, it's not their intent to restricting it to a specific authorization they have (and they may not even have it yet).

  2. Increased privacy.
    Users may not wish to accidentally share how they got that capability in the first place. Leaving out proofs allow for that.

  3. Users would be able to auth resources they don't have auth to (yet).
    I.e., allows for construction of auth graphs not only top-down, but in arbitrary order.
    Makes possible for scenarios such as.
    Alice: Hey, Bob, I've authed you to use storage service on my behalf. Btw, could you buy me that storage?
    And so Bob buys storage for Alice and uses it on her behalf.
    A rather strange use-case example, but cool that the new model allows for it.
    image

  4. Allows "healing" of capabilities. Explained above. There may be multiple auth paths for the same capability. When one's no more valid (revoked, expired), another's used in its place. I.e., proof chains are constructed on-the-fly.

  5. Allows upgradeability of auth graph. Without the need to reissue downstream nodes.
    E.g., in the example above, Alice -auth storage-> Bob holds and gets backed by auth issued by Storage Provider.
    E.g., to make for a more vivid example, having exceeded 1GB limit, Storage Provider instead of issuing another auth for the second 1GB could have revoked the first auth and issued 2GB auth for a discounted $1.5. And Alice would not need to reissue her auth to Bob based on that 2GB auth, as she didn't bake in the first 1GB auth in the first place. Neat!
    One use-case is UCAN rotation. Issuing short-lived, e.g., 1h UCANs and rotating it with a newer UCAN for a subsequent 1h of access.
    Another interesting use-case is restricting access access downstearm.
    In the mentioned example, when there is auth Alice -> Bob -> Malory, and Alice wants to prevent Malory from using her resources through Bob. Alice then could upgrade her auth to Bob to a new one that explicitly prevents usage by Malory.

"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?
This would make invocations context-free, an important trait, brought to attention above by @Gozala. As well it'll put the cost of proof chain construction on the Invoker, sparing executor from it. And overall Invoker will be in charge of the invocation, he can rest worry-free, having provided fully-specified invocation, that it'll be executed as he expects.

@andrewzhurov
Copy link

Another side-thought, on how UCANs and Invocation spec can take on their corresponding auth and invocation responsibilities.
As @expede mentions here, invocation is akin to

run: [&UCAN]

(perhaps supplemented with proofs as:)

prf: [&UCAN, ...]

should then prf be extracted from the UCAN to the Invocation spec?

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.
Whereas for Task we need just one. We could tailor data model to fit better. Instruction data model from the Invocation spec fits great!

"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!
For that, we can use only the CAN part (Capability + Meta) of UCAN as Task.
I.e., UCAN - Auth = CAN = Task.:)

Additionally, Task as UCAN leaves Facts (Meta, meant to attenuate with "how") outside of Capability.
It'd be useful to have it inside of Capability in order to be able to gradually attenuate Capability with both "what" and "how".
Capability -> Capability -> Capability

In order to cater for both of these problems we could extend our above data model to support Meta.
And so in order to have a Task, attenuating the above "what" with "how", we'd issue a subsequent CAN:

"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
In order to deletage execution of an Invocation, Executor would need to issue another Invocation with proofs that authorize delegatee. Then we have two Invocations, only the last of which will be actually executed.
Perhaps what Invocation means to express is "I want that Task executed (and I don't actually care by whom)".
We could represent it as Invocation (without proofs) and have proofs come out-of-band.

"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"
}

Overall it looks like this
image

In sum we have:

  1. CAN that allows for attenuation of "what" and "how"
  2. CAN is used in both Invocation and UCAN
  3. UCAN is an independent auth edge
  4. UCANs come out-of-band
  5. UCAN graph / proof chain is assembled on-the-fly per execution of Invocation 🔥

Goes without saying, this may not be the best solution, if not all to crazy. 😄
How do you like it, folks?

@expede
Copy link
Member

expede commented Oct 5, 2023

should then prf be extracted from the UCAN to the Invocation spec?

@andrewzhurov yes this is the current plan in various states of clarity in the WIP PRs 👍

@andrewzhurov
Copy link

andrewzhurov commented Oct 14, 2023

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)
Adopting it for UCAN would give another suprising (at least for me) benefit of being able to authorize ability (e.g., "wasm/run") without limiting it to a particular resource (e.g., "wasm math module"). Which I realized taken a look at this example by @expede

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,
Executor runs the expression and produces Receipt

"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.
Having separate "what" / Instruction allows to await it with no regard to "how" it's been performed.
I.e., makes awaits "how"-agnostic.

Conversely, having "how" and "what" expressed at the same level allows to await on specific "how".
I.e., allows awaits to be "how"-specific.
You still are able to await only on "what", but it would be a bit more difficult to resolve suitable results from "what+how" receipts, as executor would first need to decouple "what" and "how" in order to detect that "what" matches the awaited "what".


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.

@alanhkarp
Copy link

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,

  1. User delegates some capability did:​key:zAlice delegates to did:​key:zW3 service. (Added by @alanhkarp: Why is did:​key:zAlice a capability? Isn't she signing a delegation UCAN with did:​key:zAlice?)
  2. did:​key:zW3 private key is not in any computer it's on piece of paper in safe deposit box.
  3. did:​key:zW3 has a delegate did:​key:zService which acts on it's behalf
  4. Service needs to redelegate capability received from did:​key:zAlice, but it can not because delegation can not be signed by did:​key:zService

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?

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

No branches or pull requests

5 participants