flow
flow copied to clipboard
FLIP: Capability Controllers
Description
Cadence encourages a capability-based security model as described on the Flow doc site. Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being make this concept even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with.
This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.
🔍 Inspect: https://vercel.com/onflow/flow-docs/J5dNbSkiW9GF1KvNN9tup7dnp3ZT
âś… Preview: https://flow-docs-git-janez-capability-controllers-onflow.vercel.app
I am super excited about this, thanks for this FLIP @janezpodhostnik
Few comments and ideas I have.
-
Capabilities as Resource makes little sense in my opinion. As I can Proxy the capability with resource. ( nested resource in another resource, and give capability to this resource to someone else )
-
I think Capability should be just
struct(address, capabilityID)
withborrow
method. ( which should point to CapCon basically at address capabilityID, but maybe to prevent later confusion with capabilities / controllers we can merge both conceptually.
Technically AuthAccount.capabilities can have different type like [&AuthCapability]
can allow revoke, restore etc methods, while PublicAccount.capabilities can be [Capability] ) here: AuthCapability is CapCon
-
Adding a description field to CapCon
-
Getting rid of
private
path is great, what if we get rid ofpublic
too ? ( I am a bit hater of Path's in general ) ( For Public capabilities )
Something like this as a result:
Issue:
authAccount.issueCapability<&{HasCount}>(public: True, description: "HasCount Public")
This will create CapCon ( AuthCapability )
in user Storage which publicAccount.capabilities
will contain a Capability
to this CapCon ( AuthCapability ) also AuthAccount.capabilities
will contain a &AuthCapability
to this.
Use:
let publicAccount = getAccount(issuerAddress)
let countCapabilities = publicAccount.capabilities.filter<&{HasCount}>()
let countRef = countCapabilities[0]!.borrow()
countRef.count
For Private:
var countCap = authAccount.issueCapability<&{HasCount}>(public: False, description: "HasCount Capability to Janez")
What do you think ?
@bluesign thank you for the comments!
Capabilities as Resource makes little sense in my opinion. As I can Proxy the capability with resource. ( nested resource in another resource, and give capability to this resource to someone else )
That is true, but the difference in this scenario is that if:
- capability is a value: giving a capability to A who then copies to B then revoking the capability revokes this capability for A and B which might be surprising
- capability is a resource: if A passes the capability to B ( by giving B a reference to a capability or by indirection) it might be more explicit that B's capability depends on A's capability
Adding a description field to CapCon
I like this, but should probably be an optional. Not sure it is a good idea to force descriptions.
Technically AuthAccount.capabilities can have different type like [&AuthCapability] can allow revoke, restore etc methods, while PublicAccount.capabilities can be [Capability] ) here: AuthCapability is CapCon
This might be easier to understand! What I'm thinking though, would this make it easier for people to make mistakes (by accidentally giving people the access to &AuthCapability). I have to think through this.
Getting rid of private path is great, what if we get rid of public too ? ( I am a bit hater of Path's in general ) ( For Public capabilities )
I like your example, but I have a problem with it. The public domain paths sort of serve as a name... so when you publicAccount.capabilities.filter<&{HasCount}>()
and you get back two capabilities, how do you know which one to choose? By description? that seems error prone.
I think this is a great improvement. To me handling capabilities always felt complex and abstracting that logic with a nice API would be awesome. I'm just brainstorming but how about if the API also allows listing all capability controllers account has? not just for a specific storage path. Honestly just an idea since I don't use cadence enough to know if that is actually used a lot, but just thinking to have an option where you can check what a certain account "supports".
hey @janezpodhostnik
Thanks for the answers, sorry I missed the answers when you posted, thanks to @sideninja I just saw.
Resource vs Struct discussion, I think we have just opposite perspective, which is very nice, I think both sides have cons/pros. My main perspective here was, giving a resource, creating a false sense of security and uniqueness. Resources have inherited "unique" from their definition, which can give more assurances than it should be. But I am ok with resources, maybe just would need to document.
I like your example, but I have a problem with it. The public domain paths sort of serve as a name... so when you publicAccount.capabilities.filter<&{HasCount}>() and you get back two capabilities, how do you know which one to choose? By description? that seems error prone.
The problem here is in my opinion: Paths are more error prone. Actually this is more of a business logic problem than technical.
Imagine this: if I am a wallet developer, user has 2 FlowVaults, what should wallet do ?
- Use primary one linked at default Path ( don't allow to use other Vault )
- Enumerate Wallets, show them with Paths ( or better with descriptions )
Now for me obvious better solution is the second one, but I feel Flow pushing people to use first one.
@bluesign just made me aware of this.
Will this not break a lot of code. Today I store Capabilities inside structs very often and this change will make this impossible. Since a Capability is a resource and I cannot store a reference to it.
Or am I missing something obivious here?
Will this not break a lot of code.
It will break a lot of code in theory. We're figuring out a proposal first that we can all agree would be useful, then we'll discuss more about the feasibility of upgrades and migrations to address the breaking changes. We still have a lot of control over upgrades and migrations, so it might not be as terrible as it sounds. Still needs to be discussed a lot though
Right now there is no event emitted when storage is stored/loaded or when a link/unlink operation happends. It would be very powerful if that is the case.
One constant problem that is an edge case you need to handle in cadence is the posibility for blackhat to craft his own transaction and unlink something. If you do not think about this beforehand then your entire system can grind down to a halt. Now having the posibility to listen to those events to see when it happends will atleast make the situation a little better.
Also knowing "who has set up a collection of the given type" would also be possible if there was events emitted.
@bjartek it would be nice, I suggested that before ( https://github.com/onflow/cadence/issues/1059 )
But I think there are some technical limitations preventing that to be implemented. Events per block, events per transaction etc. TPS vs usability trade-offs
This FLIP introducing new capability functions may be a good spot to add support for run-time types as part of capability functions: https://github.com/onflow/cadence/issues/1617
This is to allow for the following to work:
let publicAccount = getAccount(issuerAddress)
let runTimeType: Type = Type<&{NonFungibleToken.CollectionPublic}>()
let countRef = publicAccount.borrowCapability<runTimeType>(/public/hasCount)! // or publicAccount.borrowCapability(/public/hasCount, type: runtimeType)
countRef.count
Is this something that would be feasible to include here?
@aishairzay
Borrowing using a run-time type will not be possible, as the type checker needs to be able to statically know what the result of the function is going to be.
However, a function to create a capability using a run-time type could be added, as the type does not need to be statically known, just at run-time.
The latest updates on your projects. Learn more about Vercel for Git ↗︎
Name | Status | Preview | Updated |
---|---|---|---|
flow-docs | âś… Ready (Inspect) | Visit Preview | Aug 22, 2022 at 2:05PM (UTC) |
@janezpodhostnik Could you please provide a summary of the current discussion, i.e. what the current sentiment is, and if and what any outstanding questions are?
Hi @janezpodhostnik, @turbolent asked me to comment on this FLIP. I’ll share some thoughts, but I’m sure I’m missing some nuances due to relative unfamiliarity with Cadence, so please feel free to correct my misconceptions!
First, this is a wonderfully clear document. The example code with private and public sharing of capabilities was especially helpful.
Second, I tried to summarize what I saw as the problems and pain points below. Is there anything I’m getting wrong or missing? I'll follow up with another comment.
Problems
- Confusing and awkward syntax: Capabilities syntax is currently confusing and awkward to use
- Accidental Relinking: Relinking can be done accidentally by linking with the same path after it has been unlinked
- Redirecting is not explicit: redirecting is done by linking again with the same path but it can be indistinguishable from accidental relinking even though the user's intent is very different
-
Shared Access To Capabilities: If an issuer creates a capability and sends it to Person A, Person A can share access to the capability merely by creating a copy and giving it to Person B without asking the issuer’s permission
- Not always a problem: Sometimes sharing without having to ask permission is desirable, such as a tip jar capability that allows anyone to give money
- However, other times shared access is undesirable:
- The issuer might not want Person B to have access, because they believe Person B is likely to abuse the access or use it differently than intended
- Person A might worry that Person B will misuse the capability, causing both of their access to be cut off entirely because the issuer couldn’t tell who was responsible.
- The community as a whole might suffer because being unable to manage access well could cause hesitancy in sharing access to anyone. (For example, a wiki that can’t tell who is responsible for vandalism can’t easily allow the public to edit.)
- Note: as we know, sharing access can’t actually be prevented entirely in practice, because people can always let other people log in as them
- Revoking a particular person’s access is difficult: Unlinking a path revokes all copies of the capability using that path. In order to revoke only for a particular person, you would have to create multiple private linked paths (e.g. /private/hasCountAlice, /private/hasCountBob)
- No recording of policy or intention when creating a capability: there is currently no way to record why a path was created other than including hints in the path name, making it difficult to know why a certain capability was issued
- Must Create Receivers: Must create a receiver for each new capability type, with its own public path
My comment started to get too long, so I'm dividing it up. :)
Cutting off access to capabilities
There’s some prior literature in capability-based security on cutting off access to capabilities because of misuse:
- The paper Delegating Responsibility in Digital Systems: Horton’s “Who Done It?”
- The related talk by Mark Miller at Activity Pub 2019: ​​https://youtu.be/NAfjEnu6R2g?t=573
I’ll attempt to summarize here, but one helpful distinction from the paper is proactive control vs reactive control:
- Proactive control: prevent bad things from happening, or limit the damage when they do
- Reactive control: cut off future access, punish
Capabilities support proactive control well, since access and damage can be limited because (when done well) authority is already limited to the least amount needed to carry out the request. But, capabilities in their basic form don’t provide any information about who is to blame for misusing access, which is required for reactive control.
To add reactive control to capability-based security, the paper suggests an approach called HORTON. Here’s a simplified explanation of HORTON, using the same scenario as in my previous comment, where an issuer creates a capability for Person A, and Person B asks Person A for access:
- Person A says to the issuer: “I’d like to share my access to the capability with Person B, could you create separate access for Person B?”
- The issuer creates a new capability for Person B, and tags the capability with Person B’s identifier (for Cadence, this is probably an address)
- The issuer sends the new capability to Person B directly (or through Person A, but in that case we must guarantee that Person A isn’t able to use Person B’s new capability, so that Person B isn’t unfairly blamed for A’s behavior.)
- If the capability that was given to Person B is misused, the issuer can revoke that capability specifically, without necessarily cutting off access for Person A.
- The issuer can find all capabilities tagged with Person B’s address, and choose to revoke all of those as well.
- The issuer has recorded how Person B was introduced (through A) and therefore might want to reconsider access for Person A too.
- Similarly, Person B can revoke access in the opposite direction. Person B has tagged their capabilities with the particular issuer, and if B decides that those capabilities are untrustworthy, B can choose to shut off their own access to the issuer by finding all their capabilities tagged with the issuer’s address.
I’ll follow up in later comments to connect this to CapCons, but I wanted to post this separately in case it triggered any ideas itself.
I don't think we can have reasonable Reactive control
on blockchain where new identity can be created instantly. I think ( at least for me ) most important part of the capabilities were being anonymous
and simple
.
- I give you this access ( linking some private path)
- I can revoke it anytime I want ( unlink it )
- you do whatever you want with it ( copy & give to someone & destroy )
- if this is abused, I will revoke it
This is a very basic social contract. Main problem in my opinion was very complicated book keeping.
- linking is complicated
- revoking is complicated
- keeping track what capabilities are given, complicated
- keeping track of what capabilities are used, complicated
- giving private capability is complicated ( needed receiver )
Also I just read the HORTON ( simplified version thanks to @katelynsills ) but I totally don't agree with the solution.
I mean when I want to delegate rights, I also want to delegate responsibility. Otherwise it is a big scaling problem for me. for sure there are some use cases that can require this ( central authority to give away rights ) but I don't think this is the common scenario.
Overall, CapCons seem like a great idea. Revocation of the capability itself is much better than unlinking the path.
Saying "no one can send a message to capability X until I explicitly restore access" is a lot more robust than "there's now nothing linked at path X, unless I accidentally link something there again." Also, I think explicit revocation is easier to understand because it matches the user's intention.
CapCons are similar to an already existing pattern in capability-based security, called the Revocable Forwarder (page 4 in the Security Picture Book).
However, there's a difference between Cadence capabilities and the Revocable Forwarder pattern. In the Revocable Forwarder pattern, the Revoker object is a capability. In Cadence, what we're calling "capabilities" is just one of many capability patterns: the Facet pattern - basically a subset of the methods/fields of some other, more powerful object. (Image from page 3 of the Security Picture Book)
The Facet pattern is simple, but limited. For example, you often want to express other kinds of attenuations that can't be achieved with the facet pattern alone: rate-limiting, filtering, withdrawal limits, etc.
The reason I bring this up is because I think CapCons should be thought of as capabilities in the capability-based security sense, even if they aren't Cadence Capabilities aka facets. I can imagine scenarios in which you want to 1) allow someone else to revoke the access, 2) prove to everyone that you can't revoke access or redirect access to something else (to make a credible commitment in a smart contract, for instance). I don't think CapCons should be non-storage objects, for this reason.
Additionally, I think in the very near future, users will want to build things like DAO treasuries that require delegation, rate-limiting, and withdrawal limits. For a DAO treasury, there might be one Vault, but different "departments" with particular withdrawal limits according to their budgets. Furthermore, each department should be able to independently create new capabilities that further subdivide the budget, say, into a particular DAO member's daily travel expenses. In other words, there is a tree of delegated capabilities, with each lower level not exceeding the budget above. Doing something like this easily is really where capability-based security shines, so my hope is that it would be easy to do in Cadence.
My guess is that in Cadence you might be able to do this with a combination of Resources and capabilities. Is this right? If we expand the idea of capabilities in Cadence to include CapCons and other non-facet capabilities, could it make it easier to create a delegated DAO treasury as described above?
In Cadence, what we're calling "capabilities" is just one of many capability patterns: the Facet pattern
To be honest, I don't have many experience here, can you give some other examples ? Actually we are not the Facet pattern
as capabilities, Cadence capabilities are currently Forwarder pattern
according to the book.
I think we should think: Facet ia a kind of FilteredForwarder
, and FilteredForwarder is the building block.
The Facet pattern is simple, but limited. For example, you often want to express other kinds of attenuations that can't be achieved with the facet pattern alone: rate-limiting, filtering, withdrawal limits, etc.
I think thinking in term as Forwarder Pattern is more correct. We have made simple Capability Chains with already existing cadence capabilities that achieve said things.
FilteredForwarder( object, verb, args, filter)
:here filter is some function as: shouldPass(object, verb, args) : Boolean
RateLimiter = FilteredForwarder( object, verb, args, rateLimitingFunction)
WithdrawalFacet = FilteredForwarder( object, verb, args, onlyallowwithdrawFunction )
AmountLimiter = FilteredForwarder( object, verb, args, amountLimitingFunction)
WithdrawalLimiter(N) = WithdrawalFacet + AmountLimiter(N)
( this + operator shows that they are chained )
My guess is that in Cadence you might be able to do this with a combination of Resources and capabilities. Is this right? If we expand the idea of capabilities in Cadence to include CapCons and other non-facet capabilities, could it make it easier to create a delegated DAO treasury as described above?
Actually you can even make DAO without using any capability at all. But basically Resource containing a capability
is the best design pattern we have so far.
The patterns described in this picturebook are simple because they discard the modern fascination with the identities of the participants.
This is an issue too actually, I am not sure how I feel to link capabilities with identities, although I am against, having some support for this also can be nice for people who has fascination with the identities of the participants. :)
@katelynsills thanks for this security picturebook reference.
You're very welcome @bluesign!
The Facet pattern is a transparent forwarder for a subset of methods/fields, so I think we might be saying the same thing in different terms.
Re: resources, it sounds like all the behavior is put in resources, so if you wish do something like a capability delegation tree, you need resources sandwiched by capabilities in order to do it properly. Is that right?
I am not sure how I feel to link capabilities with identities, although I am against, having some support for this also can be nice for people who has fascination with the identities of the participants. :)
Yup! HORTON comes from the capability community, too, so it's not weakening capability-based security, just adding some reactive control on top.
Also, I know you were saying that on blockchains, new identity can be created cheaply, but I don't think this prevents reactive control entirely, since presumably there was some kind of implicit reputation associated with the identity that got access originally. This implicit reputation is burned when that identity is cut off and a new one is made. In other words, there was some reason that Alice gave Bob or Charlie access in the first place, rather than making the capability public for any random address. Even if Bob and Charlie can create new identities very cheaply, they still have to convince Alice to regrant them access (i.e. rebuild their reputation). The pickier Alice is, the harder it is to regain access.
(My apologies for the multiple comments! This FLIP seems like a big change and so there's a lot to discuss, and it seems easier to break up into separate comments.)
Should Cadence Capabilities be Resources?
I agree with @bluesign that capabilities shouldn’t be resources, because:
- Oftentimes you do want people to be able to copy a capability and give it to someone else. There are two reasons why you might want to:
- non-subtractive goods
- wanting to entirely delegate responsibility, as @bluesign points out
- Forcing the creation of a new resource when it is not needed seems like it would be costly on-chain.
- We can solve the problem of clunky or unintuitive revocation in another way, by making it easier to track intention and manage policy through the wallet and perhaps a few changes to Cadence
Non-subtractive goods
To back up a bit, when we’re creating tokens on a blockchain and deciding how they can be used, we’re creating digital goods with property rights. I'm probably not saying anything that isn't already known here, but there are various kinds of goods in economics, as defined by two dimensions: excludable/non-excludable and rivalrous/non-rivalrous (also known as subtractability).
- Excludability: How easy is it to gate-keep access?
- Subtractability: If one person uses the good, does it subtract from or diminish other people’s use?
This table gives a few examples in each quadrant:
Resources in a capability-based security system provide platform level enforcement of exclusive ownership, and by default, exclusive use. This is very cool, considering how hard this is to do in the real world, but it’s important to note that it only fits some kinds of goods, and not others.
For something like a tip jar receiver, the simplest and most desirable thing to do is to give everyone access and allow other people to share access. We don’t necessarily want or need to log anything or revoke anything, because misuse doesn’t really exist. This is because usage of tip jar is not subtractive: me giving a tip does not make it harder for you to give a tip. In fact, anyone using the tip jar is giving money, so the more usage the better.
Delegating responsibility for misuse
Another reason we might want to share access is, as @bluesign pointed out, “when I want to delegate rights, I also want to delegate responsibility”. In the DAO treasury example I mentioned above, the DAO might want to allow a department to temporarily hire a designer without the treasury having to give permission, as long as it’s delegated from that department’s capability and therefore subject to the withdrawal limits or rate limits for that department. In this case, revoking access to the entire department if there is misbehavior is the appropriate answer, since it was their responsibility to sub-contract well.
What exactly is unintuitive?
The original proposal gives the example of Alice creating capabilities B and C and giving them to Bob and Charlie respectively. If Dan gets his access from Bob, and either Bob or Dan misuse the capability, it actually does make sense for Alice to revoke both Bob and Dan's access. I think this part is actually intuitive.
It only becomes unintuitive if we lack the information that Bob and Dan are accessing through the same capability:
- Bob has capability B
- Charlie has capability C
- Dan has capability B’
It's a lot more intuitive if instead we see something like the following, where it's clear to Dan that his usage is getting mixed with Bob’s. And same for Bob - by sharing, his usage is getting mixed with Dan’s. This is a huge difference compared to capability C!
Alternative solutions
So how do we accomplish this? First, I think policy/intention/timestamp/block height and other meta info about creating or sharing capabilities needs to be stored in the wallet or an app on the user's behalf. So, CapCons would not need the block height, but perhaps I'm missing something there.
Second, we need a way to represent that capability B is dependent on B' and vice versa. Making capabilities resources focused on disallowing copying, but perhaps we should allow copying and be able to record when a copy is sent to another address? I don't necessarily have good answers here as I'm not very familiar with what's going on under the hood for Cadence, but since capabilities are value types, could it be possible to view any other addresses holding the same capability? In other words, a map from capability id to an array of addresses? Maybe this is not constructed on-chain, but added in a block explorer or the wallet?
Third, for cases in which Alice really does want to manage Dan's access directly and punish him specifically for misuse, I think the HORTON approach makes sense. Potentially, this data could be entirely at the wallet level.
Lastly, there's still this clunky aspect of needing to create multiple private linked paths (e.g. /private/hasCountAlice, /private/hasCountBob) for each capability. Could we use the capability id directly? (It looks like this was previously suggested in the forum post). If the user's wallet is doing the heavy lifting of recording intention and purpose, and we have HORTON tagging of which address to blame for misuse, then I don't think we want to refer to capabilities by path. Especially if the wallet also allows the user to create their own nicknames for capabilities (in capability-based security, these are called "petnames").
If there aren't any paths, then there's a new problem - how do dapps know how to put together a transaction for the user to sign? If the contract hardcodes a path, that's easy, it's just "/storage/HelloAssetTutorial" or whatever the contract used. But if there aren't any paths, how does the dapp know which capability the user wants to use?
I think the File Input design could be helpful here. Rather than hardcoding that this website wants to use this file and having the website request the file by path, the website allows the user to choose. Importantly, the website cannot read the file system. The browser can read the file system, and the website merely gets the file that the user chose, as well as its filename as a string, if I remember correctly.
We can improve this by having the user's wallet know default capabilities/resources: "this is my main CryptoKitties Collection", "this is my checking account". Maybe something like this already exists? I could also imagine a more intelligent wallet learning over time what the user wants ("on this site, the user typically chooses X").
Btw, @bluesign, you had mentioned that giving private capabilities is complicated because the recipient must create receivers.
One thing that might be helpful is a public inbox (a public receiver for all types). Then it is no longer necessary, in theory, for the recipient to create a new receiver for each type. However, a public inbox introduces a spam issue.
The Petmail email protocol had an interesting solution to the spam issue. First, the recipient could specify rules for what they would allow in their public inbox, for instance, message size and content-type of the email. They also could require that any sender complete a captcha or other test, as contracted out to Ticket Servers. Updating this to a blockchain environment, you could imagine requiring a payment of FLOW tokens or some other token that indicates you've passed some sort of anti-sybil measure, which is burned after being received.
Second, any known sender could bypass the public inbox if the recipient gives them a private capability. This works because the recipient can cut off access in the case of misuse.
Thanks @katelynsills, very informative and in my opinion, in such a subject, multiple comments are totally fine.
Yup! HORTON comes from the capability community, too, so it's not weakening capability-based security, just adding some reactive control on top.
Yeah problem in here, when we gave the hammer to the developer, they use it in the ways that even Thor cannot imagine.
Updating this to a blockchain environment, you could imagine requiring a payment of FLOW tokens ...
Yeah this is totally possible, we just made a very similar general receiver contract ( called Lost & Found )
If the user's wallet is doing the heavy lifting of recording intention and purpose, and we have HORTON tagging of which address to blame for misuse, then I don't think we want to refer to capabilities by path. Especially if the wallet also allows the user to create their own nicknames for capabilities (in capability-based security, these are called "petnames").
This is a feature I would like to have.
I think the File Input design could be helpful here.
Totally agree here, this is the best alternative from UX perspective too.
Thank you all for all the comments :)
There were some good arguments for keeping capabilities as values, so I will change the flip to reflect that. As pointed out by Kate the real problem is identifying which capabilities are the same capabilities (copies of each-other) so that a user can more easily spot which capabilities will be invalidated together. I think adding some sort of description or intent to the capability itself might help with that. I don't think tracking capabilities off-chain would help with this problem, as that would require you to trust the wallet of the person who initially created the capability, to serve you the correct data.
Tracking/identifying/pet-naming capabilities off-chain is still possible and can be very useful, but this would currently require the developer (of the smart contract/transactions) to explicitly output (emit an event or something) the UUID of a newly created capability so that the off-chain machinery can hook on to. Maybe its worth exploring if creating a capability should trigger an event by default (or all the newly created UUIDs of capabilities should be in a single event). I think this would only help the owner/creator of the capability though, as off-chain sharing of this information with the consumer of the capability seems potentially dangerous to me.
The discussion on how to receive private capabilities is a bootstrapping discussion and this FLIP doesn't really change any part of that story. Having a good recipe on how to design your public receiver for bootstrapping would be very useful, but I don't think addressing that problem is really in the scope of this FLIP.
Delegation of issuing and revoking capabilities should be easier with the changes in this FLIP, but I want to explore this further and I will try to create some code samples for how that would look like.
The HORTON model is definitely something that should be possible to do within Cadence, but I am not sure that it should be the default model. Again, I will try to whip up some code samples on how that would look like.
I have prepared a delegation example: https://play.onflow.org/1dab1a55-5555-43d1-934c-991b24082720
Some observations: Its not that difficult to do currently (without this FLIP), but creating a new capability and revoking it requires that the delegated party (0x02) keeps track of which paths was used to create the capability, and making sure they are not reused. It also gives 0x02 to much power over storage of the delegator (0x01) as there is no good way to limit which private paths on 0x01 can be used by t0x02. 0x02 can also revoke any capabilities to any private paths, if it knows the path. These problems could be addressed by dynamically building paths, which is currently not possible.
With the changes from this FLIP, creating/revoking capabilities is more explicit and nicer. But the revocation still has a problem:
pub fun revokeFoo(capabilityID: UUID) {
let capcon = DelegationExample.account.getController(capabilityID: capabilityID)
capCon.revoke()
}
0x02 can revoke capabilities that have nothing to do with Foo
if it knows the UUID of it.
Seeing this I think capCons should also know the type of the capability...
Thoughts?
So I have done a lot of thinking about the HORTON model, and I'm still not sure I am on the right track.
As far as I can see it its a responsibility delegation problem.
Here is my interpretation of my understanding of the paper in Cadence terms.
Alice wants to delegate a Carols (cadence) capability to Bob. Alice, Bob and Carol are AuthAccounts.
The three have some requirements about this:
- Alice wants to delegate the transfer of a Carols capability to Bob
- Bob wants to know that capability they received is from Carol, and not from Alice
- Carol wants to make sure that only Bob can use the created capability and not Alice
- Bob wants to keep track of outgoing calls to carols capability
- Carol wants to keep track of incoming calls to their capability and who called
All these requirements can be satisfied:
-
(1.) We saw this in the example I created earlier.
-
(2.) Is trivial. Cadence capabilities have addresses of the target. After receiving the capability from Alice, Bob only needs to check that that address on the capability is Carol and not Alice.
-
(4.) Is also easy. A wrapper can be created around that capability that logs calls (or possibly better in this case: emits an event).
-
(5.) Is similar. Carol can actually give out a capability specially created for Bob pointing to an intermediate object (stub) that does logging/emitting. If it is called Carol can be sure it is Bob (or someone that bob gave this to; in either case, Bob is to blame).
-
(3.) is the tricky bit. when Alice asks Carol for a capability to give to Bob Carol needs to wrap the capability in a way that only Bob can unwrap it. I think the following wrapper could be used:
pub resource WrappedCapability { priv let unwraper: Address priv let cap: Capability<&AnyResource> pub fun unwrap(unwraper: AuthAccount): Capability<&AnyResource>? { if unwraper.address == self.unwraper { return self.cap } return nil } pub fun capabilityAddress(): Address { return self.cap.address } pub fun unwraperAddress(): Address { return self.unwraper } init(unwraper: Address, cap: Capability<&AnyResource>){ self.unwraper = unwraper self.cap =cap } }
Since an
AuthAccount
is passed we know that only the intended unwrapper can successfully retrieve the capability. Passing an auth account can be scary so Bob must be absolutely sure that the resource passed to him by Alice is actually aWrappedCapability
and that the capability target is Carol and that Bob is the expected unwrapper.
I think this satisfies the requirements of the HORTON model. Please let me know if that is not the case!
I also think that this is only partially related to this FLIP. The related part being the issuing of new capabilities which we already saw in the example I created here https://github.com/onflow/flow/pull/798#issuecomment-1215248629.
I think whats left for this FLIP is for me to change capabilities to values (and add a capability type to the capcon).
I will try to look at the security perspective. I think HORTON doc is based on trusted identities ( cost of identity is great ) But here on Flow, we have cheap identities.
If we assume C: Capability Carol gave to Alice, possible actors in the system with their roles.
Carol: Created C and gave to Alice, also doesn't mind people sharing C as long as they don't abuse Alice Good: Wants to share C to Bob Bob Good: Just wants to use C Alice Evil / Bob Evil: Wants to abuse C and get away with it
Now technically our customer is Carol for designing the system in my opinion.
Carol threat model is 'abuse of the shareable C'. That means she needs to be able to identify and block the attacker, but also should be able to find the threat source. That means we need to track C with original recipient (Alice) and current user ( Bob ). So Carol has a choice of blocking Bob or Alice depending on the situation. As Alice can create new identity X, then abuse C, Alice can never shift blame to X totally.
Here we should also allow Alice to revoke Bob's access to C ( which HORTON solves with proxy ). So basically Alice will proxy requests to C for Bob ( even she can use an identifier, considering there is an another proxy on Carol side )
But I think we need to move this proxy logic and bootstrapping into capability controllers maybe. ( technically with cadence even now we can apply HORTON, problem is making it standard way of doing it )
@bluesign A few thoughts:
-
I think if Carol can be sure that an issued capability will only be able to be used by the party it was issued to, Alice cannot create new identities to abuse a capability meant for Bob (this can be done by applying the
WrappedCapability
I mentioned). Alice can also not create a fake capability and give it to Bob stating it is from Carol, since capabilities have targets where it would be clearly visible that it is not targeting Carol. -
I'm not actually sure what to put into a stub/proxy in cadence. Some of the basic functionalities are already covered by the capability itself: if the capability is called the issuer can be sure who to blame, if the issuer used a
WrappedCapability
when issuing it, and the caller know what the capability is pointing at. Logging inside stubs/proxies doesn't really make sense in this context. I suppose you could use the stub/proxy to emit an event or write in some array (access log array), but I'm not sure how useful that is.
If you have some examples/ideas of:
- what would a proxy/stub contain
- what change would make bootstrapping easier
- how we could move proxy/stub/bootstrapping logic into CapCons/Capabilities
that would help me immensely with thinking about this.
I think if Carol can be sure that an issued capability will only be able to be used by the party it was issued to, Alice cannot create new identities to abuse a capability meant for Bob
I think here problem is different, let's say you allow me to create Mint capabilities and share those ( let's think as Minter / SubMinter )
- In perfect system, I should be able to create/delegate capability for Bob, without your interaction. So when some resource is minted, your proxy can see the caller is Bob. On abuse from Bob, you can revoke his capability. ( Without revoking mine ) But the problem here is, you don't verify Bob technically, so Bob can be my second persona.
My conclusion is even if we make sure Bob will use the capability, it still doesn't clear me from the blame. ( HORTON doesn't apply to us mainly )
what would a proxy/stub contain what change would make bootstrapping easier how we could move proxy/stub/bootstrapping logic into CapCons/Capabilities
WrappedCapability in your example was what I meant with proxy, if we can assign addresses allowed to get that capability, we can solve all in one go. ( this is best will be done with whitelist/blacklist approach I guess )
So this will allow me to use WrappedCapability ( which you gave me ) and wrap it again to give to Bob. So you will have access to caller Bob , with delegation from me information.
If capcon can access to this capability ( calling ), then it would be extra nice ( for logging the caller etc)
Hi @janezpodhostnik, the delegation example was very helpful!
I agree entirely with your concerns about it, specifically:
- It gives 0x02 too much power over storage of the delegator (0x01) as there is no good way to limit which private paths on 0x01 can be used by t0x02.
- 0x02 can revoke any capabilities to any of 0x01's private paths by guessing the path.
These seem like major security violations to me, enough to prohibit this particular pattern. It's a POLA issue, where the authority required is far beyond the minimum needed to accomplish the task. As I understand it (please let me know if I'm wrong!), the task is that 0x01 wants to allow 0x02 to give 0x03 a capability that is a facet of a resource that 0x01 holds. Importantly in this use case, 0x02 decides who to delegate to, not 0x01. But in this implementation, 0x01 is actually giving 0x02 the authority to revoke all of 0x01's capabilities and the authority to squat on (or otherwise mess with) all of 0x01's paths. The only thing that 0x02 can't do is create new capabilities in 0x01's account that target resources other than the Foo resource.
Given that "delegation is one of the main things that make capabilities powerful and useful", is there another way to delegate?
In the capability systems that I'm familiar with, revocable delegation looks more like this:
- 0x01 creates an object Foo.
- 0x01 creates a revocable capability ("A") that forwards to Foo.
- 0x01 passes A to 0x02 by sending to 0x02's receiver.
- 0x02 can create another revocable capability ("B") that forwards to A, not Foo. (B is stored in 0x02's account, not 0x01, so 0x02 is not given any access to 0x01's storage.)
- 0x02 sends B to 0x03 by sending to 0x03's receiver.
- 0x03 can call B.foo()
- 0x02 can use the "adminFacet" (basically capCon) of B to revoke B
- 0x03 gets an error calling B.foo(), since it is revoked
It seems like the POLA violation in the delegation example you shared occurs because the FooDelegate Resource accesses 0x01's account rather than creating the capability for 0x03 in 0x02's account. Additionally, I had a question about why the capability for 0x03 ("B") needs to target Foo rather than the capability that 0x02 holds ("A").
To make sure that 0x02 doesn't have access to 0x01's storage, we could create a naive solution: that 0x02 passes in their AuthAccount. However, this is correctly deemed an anti-pattern, so we can't actually do it:
pub resource FooDelegate : IFooDelegate{
pub fun newFoo(acc: AuthAccount, path: PrivatePath): Capability<&Foo>? {
return acc.link<&Foo>(path, target: DelegationExample.fooStorage)
}
pub fun revokeFoo(acc: AuthAccount, path: PrivatePath){
acc.unlink(path)
}
}
But it's an informative example, because you should be able to fix the POLA issues inherent passing in an AuthAccount, by attenuating the AuthAccount to have only the authority you want - in this case, creating a specific link to a path that you pre-specify. However, only doing that opens up 0x01 to attack by 0x02, because if acc
is a custom object passed by 0x02, and 0x01 is passing a reference to Foo to acc.link
, 0x02 could steal Foo if acc
isn't just linking.
This is where I got stuck, but I think there must be a better way. Here's what I'm hoping to do:
- Make sure Capability B for 0x03 is created in 0x02's account, not 0x01's, so 0x02 has no access to 0x01's storage
- Make sure Capability B points to Capability A, not Foo, so that the delegation goes through 0x02.
@janezpodhostnik and @bluesign, did I capture the use case correctly? Any ideas on how to accomplish this?
I will look at the HORTON questions and respond there too.
@janezpodhostnik , could you clarify what you mean by delegating the transfer of Carol’s capability to Bob? I think I might be confused, and I want to distinguish between three cases:
-
Bob wants access to a capability that Alice has; Alice sends the capability (or a direct copy of it) to Bob (“Direct sharing”)
- Alice is to blame for Bob’s actions from Carol’s perspective
- Alice can’t revoke Bob’s access very easily without revoking her own
- Bob gets the same authority as Alice
-
Bob wants access; Alice creates a new capability that is revocable that forwards to the capability she has; Alice sends this new capability to Bob (“Delegation but sharing blame”)
- Alice is still to blame for Bob’s actions from Carol’s perspective
- Alice can revoke Bob’s access easily without revoking her own
- Bob’s capability might have attenuated authority compared to the one Alice has
-
Bob wants access; Alice asks Carol to create a separate capability for Bob that forwards to Carol’s resource directly (doesn’t go through Alice at all); Carol records that Alice was the one who intro’d Bob (Horton)
-
Bob is to blame for Bob’s actions from Carol’s perspective
- Although, if there are enough Bad Bobs introduced by Alice, Carol might start to prohibit Alice from inviting more people.
- The authority to use the capability is separated from the authority to invite new people to use the capability.
- Alice can’t revoke Bob’s access, because Bob has direct access to what Carol provides
- Alice can’t attenutate Bob’s access for the same reason
-
Bob is to blame for Bob’s actions from Carol’s perspective
I think I might have been confused in responding to your delegation example, because I wasn’t sure which case the example was tackling. In the example, Alice/0x02 is able to revoke Bob’s/0x03’s access, so it’s not Case 3 (“Horton”), but it’s not Case 1 (“Direct Sharing”) or 2 (“Delegation but sharing blame”) either.