Skip to content

Instantly share code, notes, and snippets.

@ArturGajowy
Last active April 10, 2019 13:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ArturGajowy/092619045d6c571662350a630dd942b5 to your computer and use it in GitHub Desktop.
Save ArturGajowy/092619045d6c571662350a630dd942b5 to your computer and use it in GitHub Desktop.

Drain caller's vault attack

Mechanism

All the entirely-on-chain authentication mechanisms I've seen us consider are susceptible to an attack, where the attacker deploys a contract that - when called by anybody / one of the targeted victims - drains the caller's vault. In the current version of the platform, such a contract would be:

//omiting RevVault contract resolution and similar details

contract drain() = {
  new victimAuthKeyCh, victimVaultCh, ret in {
  
    RevVault!("findOrCreate",  "$VICTIM_REV_ADDR", *victimVaultCh) |
    RevVault!("deployerAuthKey", *victimAuthKeyCh) |
    for (victimVault <- victimVaultCh; victimAuth <- victimAuthKeyCh)
      victimVault!("transfer", "$ATTACKER_REV_ADDR", 100, victimAuth, ret)
      //obviously one could lookup the balance and use it instead of '100' above
    }
    
  }
}

Any deployer calling such a contract can become a victim, because $VICTIM_REV_ADDR can be obtained in various ways:

  1. based on the deployer's public key obtained from the (so far public) rho:deploy:params system process (amd further calling RevAddress!fromPubKeyBytes)
  2. by off-chain inspecion and hardcoding arbitrary rev-addresses and par-ing the attacks targeted at different deployers
  3. by arranging a known victim to call a contract that has their RevAddress hardcoded
  4. by asking the victim to call the contract and provide their RevAddress as parameter

Importance

With the issue in place, no 3-rd party contract is safe to be called, by anyone. One has to scrutinizie each and every contract to the very bottom of their dependencies and ensure no contract downsrtream calls RevVault!("deployerAuthKey", *victimAuthKeyCh).

This must not remain the case, as in practice this restricts the set of 'safe' contracts to call to the ones authored by the deployer + the system contracts - severely affecting RChain's usability.

Cause

As 2., 3., and 4. clearly show, being able to determine the Deployer's RevAddress is not the crux of the problem. Rather, the problem lies in RevVault!("deployerAuthKey", *victimAuthKeyCh) being based on the ultimate-top-level-caller's (deployer's) identity rather than on the immediate-caller's one. Were the auth key provided by a hypothetic RevVault!("callerAuthKey", *authKeyCh) method, the auth key returned would be of the party actually writing the call to callerAuthKey and signing over the code that textually contains it.

Remediation strategies

So far, the following ideas for remediation strategies were brought up:

A. Base auth mechanisms on the immediate caller's identity

As suggested above, having a way of determining the identity of the immediate invoker of the auth mechanism (vs the top level deployer) would make this authentication scheme safe. The only way to obtain an auth token would be to resolve your own by calling callerAuthKey, or by having it provided by a 3rd party (wrapped in some attenuating contract, hopefully).

One can imagine having a special system contract / language keyword for that - but the detials of that would need to be workedout and would require storing the 'original deployer' identity in at least some of the rholang terms. Unforgable names could be a candidate, as the random number generator used for creating them is seeded with parts of DeployData: timestamp and deployers pulicKey. So unforgable names are based on their deployer's identity anyway. We could make that more explicit in the implementation, and make the auth as follows:

new identity(`rho:deployer:identity`), ret {
  RevVault!("callerAuthKey", identity, ret)
}

Undrneath, a system contract would be used to:

  1. extract the 'creator' of the identity unforgable name
  2. ensure that the identity hasn't been intercepted from a victim.

'2.' would be achieved by requiring the users to never share the identity(`rho:deployer:identity`) name and by checking that the passed unforgrable name is based on the rho:deployer:identity URI.

B. Make the deployer's identity only available once during a dpeloy

This would force obtaining the identity at the beginning of the deployment and require passing the identity explicitly wherever it's needed. This would be achieved by making the identity available from a linear send, that would need to be received by the user before they decide to call any 3rd party contracts. The identity would need to be passed explicitly to all authenticaiton mechanisms used.

new rootCh(`rho:deployer:identity`) in {
  for (identity <- identityCh) {
    //the above two lines need to happen before calling any ohter code
    
    new authKeyCh in {
      RevVault!("authKeyForDeployer", identity, authKeyCh) |
      for (revAuthKey <- authKeyCh) {
        //...
      }
    }
  }
}

Then, the attack illustrated before would not be possible, as trying to access identity twice / doing it from a drain contract called by a victim would result in the execution hanging due to there no longer being anything to read.

This works, as long as each user consumes the identity as the first thing they do during each deploy. To ensure that, we could:

B1. Require passing a flag for accessing the identity during a deploy

The rootCh(`rho:deployer:identity`) channel would only be populated when a --deploy-with-elevated-privileges flag is passed to the deploy client. This way, if a client forgets to consume, they're safe as long as they didn't pass the flag...

C. Restrict access to the identity channel syntactically

The new rootCh(`rho:deployer:identity`) declaration would only be legal at the top level of the deploy, and so would be the read from the corresponding channel. This guarantees access to identity in a scope where the caller is the deployer.

The send on the identity channel no longer needs to be linear, and the cahnnel can be passed down to trusted contracts if needed. Must not be leaked to 3rd parties though.

This has a disadvantage of special-casing two elements of the language semantics (new and receive).

D. Restrict access to the identity syntactically using an identity keyword

A new keyword - identity - would be introduced, with the semantics of a ground process of a new type Identity, values of which would only be created by the platform.

The identity value would be resolved 'statically' (before execution; would always mean "the identity of whoever wrote the identity keyword"), making it impossible for Bob to access Alice's identity if they don't pass it to Bob explicitly / via value flow.

Values of such type would be comparable and could be used as part of other processes.

One would access their vault as follows:

    new authKeyCh in {
      RevVault!("authKeyForIdentity", identity, authKeyCh) |
      for (revAuthKey <- authKeyCh) {
        vault!("transfer", targetRevAddr, amount, revAuthKey, ret)
      }
    }

authKeyForIdentity would resolve the corresponding RevAddress using a system contract toPubkey(identity) and further passing the pubkey to RevAddress!fromPubKeyBytes.

Early recommendation

Compared to other approaches, D requires similar amount of work as B (with very minor additions to language grammar and semantics), and both require much less than all other approaches.

In terms of ergonomics and usage safety, D is better than B in:

  • not requiring a flag that one can:
    • forget to pass and cause the execution to hang
    • forget to not pass and cause the identity to leak potentially by not consuming it
  • guaranteeing proper semantics of the obtained token no matter user discipline / flags passed
  • not causing the execution to hang in an attack scenario (in fact, the attack is no longer possible under D)

Notice that A, C and D are iterations along the same line: making sure the created auth key is for the immediate caller (original deployer) of the "create auth" method, vs the ultimate caller (current deployer). D is arguably the most clean and easiest of them to implement.

Further comparing B and D, we need to consider that:

  • B requires adding a top-level receive, and passing the received auth through all the intermediary contracts, for every single contract written so far that uses RevVault. This also affects future contracts, that would need to thread the root auth key to its actual usages.
  • D only requires adding a single identity / rootAuth keyword as the 2nd parameter of the auth method

Thus, my early recommendation is considering D for implementation.

@tymm
Copy link

tymm commented Apr 2, 2019

Pretty cool ideas!

I think there is another option where we add a new field to DeployData. The field would contain a signature over the current timestamp which then could be injected (at the time of the deployment) into the user's contract via a keyword (e.g. $IDENTITY_PROOF).
This would enable another way of getting an auth key for the immediate caller only.

It would look something like this:

new authKeyCh in {
  RevVault!("authKeyForDeployer", $IDENTITY_PROOF, authKeyCh) |
  for (revAuthKey <- authKeyCh) {
    //...
  }
}

The authKeyForDeployer method would then check that $IDENTITY_PROOF was actually signed by the deployer before returning an auth key.

@ArturGajowy
Copy link
Author

IDENTITY_PROOF; would contain a signature over the current timestamp

We don't need to use just the timestamp, we have a signature over the whole deploy. In either case, this is deploy-dependent, and it shouldn't be. It should be user-dependent. Let me explain:

Let's have a look at AuthKey.rho:

  contract AuthKey(@"make", @shape, ret) = {
    new authKey in {

      contract authKey(@"challenge", retCh) = {
        retCh!(bundle0{ (*_authKey, shape)})
      } |

      ret ! (bundle+{*authKey})
    }
  } |

  // Returns `true` iff the provided `key` is of the provided `shape`. Returns `false` otherwise.
  contract AuthKey(@"check", key, @shape, ret) = {
    new responseCh in {
      key!("challenge", *responseCh) |
      for (@response <- responseCh) {
        ret!(response == { bundle0{ (*_authKey, shape) } } )
      }
    }
  } |

and the way it's used:

  //in revVault!"transfer" :
  @AuthKey!("check", *authKey, (*_revVault, ownRevAddress), *authKeyValidCh) |
  @Either!("fromBoolean <-", *authKeyValidCh, "Invalid AuthKey", *authKeyValidEitherCh) |

For this to work, the authKey needs to be "of the same shape" for each call to "transfer". So the shape must be user-bound, not user's-deploy-bound.

@ArturGajowy
Copy link
Author

ArturGajowy commented Apr 8, 2019

The authKeyForDeployer method would then check that $IDENTITY_PROOF was actually signed by the deployer before returning an auth key.

If I get it right, you're proposing that the contract-specific "get auth" methods, like RevVault!("authKeyForDeployer", ... check the deployer's identity to match before returning the contract-specific key)?

At first I thought this would make the rootAuthKey / identity leaks effectively harmless. It's not the case though, as having the leaked *victimsIdentity I can setup a contract that - once called in the victim's deploy - obtains the contract-specific vault successfully (RevVault!("authKeyForDeployer", *victimsIdentity, *ret)). Which brings us back to square 1.

So I'd advise against adopting this behaviur for "get auth" methods, as this would only lead to a false sense of safety w.r.t. rootAuthKey / identity leaks.

@tymm
Copy link

tymm commented Apr 8, 2019

It's not the case though, as having the leaked *victimsIdentity I can setup a contract that - once called in the victim's deploy - obtains the contract-specific vault successfully (RevVault!("authKeyForDeployer", *victimsIdentity, *ret)). Which brings us back to square 1.

A leaked $IDENTITY_PROOF/*victimsIdentity is useless for an attacker because the RevVault!("authKeyForDeployer", ... method would check it against the deploy data.
In the attacker's contract the authKeyForDeployer method would see that the signed timestamp in the deploy data is not the same as the signed timestamp provided as an argument to the authKeyForDeployer method and would therefore reject.

After thinking about it again using a signed nonce instead of signed timestamp might be better.

@ArturGajowy
Copy link
Author

Oh, I see. This could work 👍 We'd just need to have a way of obtaining the deployer from the identity_proof so that we can construct a user-bound (vs deploy-bound) RevVaultKey.

I see it as a nice refinement of D. We should use the deploy signature as the compared data (differs based on timestamp, user private key, and the code deployed) and carry the user id (public key) as part of the value corresponding to the keyword.

@cboscolo
Copy link

cboscolo commented Apr 9, 2019

I feel like this proposal is going in the wrong direction. The original REV Vault design used what we, unfortunately, called a "lockbox", but was really an object capability. Think of it as the "Spend Rev Vault" OCAP. It is an object that grants the ability to spend a specific REV Vault. If you possess it, you can pass it into the REV Vault transfer method to spend the REV in that REV Vault.

The question then is, how do we get this capability?

  1. Obtain it via a call to create new REV Vault. This is the approach to take when creating a REV Vault that can be spent with a multisig.
  2. We need a way to create one during the deploy that maps to the REV Address computed from the public key of the deployer. The original REV Vault design on the wiki took an approach that, although convenient, opened the door to this Drain conversation. To close the door, we should require the deployer of the rholang code explicitly "state" that they want to create a "Spend Rev Vault" OCAP. This OCAP would then be available to be passed into ONLY the REV Vault transfer call, or if they so desire, pass this OCAP to a trusted method that has other spend logic.

@ArturGajowy
Copy link
Author

ArturGajowy commented Apr 9, 2019

called a "lockbox", but was really an object capability

Now it's called an AuthKey, which I feel is closer to embody a capability. After all, a key embodies the capability to unlock a lock.

Think of it as the "Spend Rev Vault" OCAP.

I've mentioned in the discord that this is too specific of a capability. We need a I'm the deployer of this part of code capability for deriving from it the deployer's capability X. E.g. access my own POBox. Or access my own instance of a thing requiring authentication, that is not Rev related, and maybe even coded by community.

We need the POBox at the minimum. Without it, how do you pass voting rights of a multisig wrapper? (the voting rights are unforgable names - OCAPS. How do you safely store them for use by their intended users in latter deploys?)

require the deployer of the rholang code explicitly "state" that they want to create "Spend Rev Vault" OCAP

Notice that putting an rootAuth / identity keyword in one's code fits that description :) Further, it doesn't require passing node client flags that can be forgot/left turned on, which would lead to the OCAP leaking.

This OCAP would then be available to be passed into ONLY the REV Vault transfer call,

See above, this is too specific. At the same time, RevVault!("forDeployer", rootAuth, *ret) would return a restricted version of the OCAP (a subset, if you will) that only works with RevVaults.

or if they so desire, pass this OCAP to a trusted method that has other spend logic

Perfectly possible with the rootAuth / identity keyword. Moreover, the keyword would 'resolve' to a value of a type (c.f. Identity), whose values can only be created by the platform. So there's no other way of obtaining that value than writing that keyword in your code, or having it passed.

Further, having that Identity type, we'll be able to easily prevent leaks of Identity values using the type system that's been envisioned even before RCON3.

@cboscolo
Copy link

cboscolo commented Apr 9, 2019

I've mentioned in the discord that this is too specific of a capability.

In the context of an OCAP approach, this a rather absurd statement. We very much want to explicitly grant specific capability to do just this specific thing, "spend rev". This way, it can only be used to spend the REV of a particular REV Vault and nothing else. There may be other capabilities that we want to create and grant, but that is not int the scope of this conversation.

Furthermore, rootAuth / identity and I'm the deployer of this part of code, answer the "who" not the "what". It's not a capability, it is only identifying the public key that deployed the code.

Also as I said in Discord, calling it identity would be a gigantic mistake. It's an overused word, and rarely means what you think it means. In the context of deploying code to RCHain, it only identifies the public key, that signed the code, nothign more.

Finally, in your example:

At the same time, RevVault!("forDeployer", rootAuth, *ret) would return a restricted version of the OCAP (a subset, if you will) that only works with RevVaults.

Doesn't this just open up the "drain my REV Vault" problem that we are trying to solve?

@ArturGajowy
Copy link
Author

ArturGajowy commented Apr 9, 2019

There may be other capabilities that we want to create and grant, but that is not int the scope of this conversation.

They should, or we end up with a sub-par solution focused over just one use case. According to my understanding multisig won't happen without a safe POBox. We should at least address this.

It's not a capability, it is only identifying the public key that deployed the code.

As mentioned in discord, it is a capability: the ultimate of deployer-bound capabilities, to do everything a deployer can do. Based on it all the smaller deployer-specific capabilities are derived, e.g. for RevVault: RevVault!("forDeployer", rootAuth, *ret) - ret will hold the capability to spend deployer's vault.

Also as I said in Discord, calling it identity would be a gigantic mistake.

As mentioned in discord, we can come up with a better name.

Doesn't this just open up the "drain my REV Vault" problem that we are trying to solve?

No. The rootAuth keyword is resolved statically (before execution) to the auth of the deployer writing the keyword. If you write a contract having this line, you'll obtain your own (rev)auth.

@ArturGajowy
Copy link
Author

If you write a contract having this line, you'll obtain your own (rev)auth.

Even if I call it.

@cboscolo
Copy link

cboscolo commented Apr 9, 2019

lol, shortly after writing the above, I checked my twitter feed and this was at the top:

https://twitter.com/Steve_Lockstep/status/1115397519255597056

Steve is an identity & privacy researcher

@ArturGajowy
Copy link
Author

I'd love to hear what Steve has to say on our particular issue, vs a general philosophical claim (with which I agree BTW). Taking his words out of context and applying to an edge case like 'the capability to deploy this very code' is a poor argument even for an appeal to authority.

At the same time, while we're discussing a philosophical issue whether that's OCAP or not, approach B has 2 important issues mentioned in 'Early recommendation', one leading to the capability leaking (!).

Approach D literally makes obtaining a 3rd party's deployer-bound capability impossible (other than by having it passed by the 3rd party), and allows creating safe deployer-bound capabilities for 3rd party contracts based on it.

Let's stick to the pros and cons of the approaches.

@cboscolo
Copy link

It's not out of context at all. rootAuth is a "who" not a "what" [can it do]. Trying to repackaging it as a "what" by saying it is the ultimate capability doesn't change that. Agreed, though that we are ultimately arguing about the philosophical approach, not the pros and cons of this particular approach. Unfortunately, if we don't align on philosophy, it's going to be hard to agree on the design.

Based on the REV Vault discussion back in Warsaw, it was my understanding that we wanted our approach to RChain to be OCAP based, and not role-based. A criticism of the REV Vault design we came up with in Warsaw by those pushing this OCAP approach was that it was basically Ethereum's msg.sender approach repackaged. Your rootAuth approach is also basically msg.sender repackaged (hidden slightly with syntactic sugar.)

With an OCAP approach, the objects that provide access to a resource (like a REV Vault) need to be created on-chain, and usually by the entity that wants to control the access. But, a non-multisig REV Vault is an exception to this pattern because the REV Address is created off chain and the actual REV Vault is not created by the REV Address owner. So, in this special case, it makes sense that you can use the public key to obtain the object capability to spend the REV Vault associated with that public key. The original design did this using two different signatures, one to spend the REV Vault, and one to sign the deploy. It was this duel signature that we felt was cumbersome and wanted to streamline. So the question is, how can we streamline this without re-introducing the msg.sender design pattern?

I'm suggesting it be something explicit when signing the deploy that results in an object (perhaps via the deploy params) that can spend the deployer's REV Vault.

@ArturGajowy
Copy link
Author

ArturGajowy commented Apr 10, 2019

A criticism of the REV Vault design we came up with in Warsaw by those pushing this OCAP approach was that it was basically Ethereum's msg.sender approach repackaged.

For this to matter, we'd need to explore what is wrong with Ethereum's msg.sender in our context. Please do. What are the cons?


Also, before that, notice there's an important difference: with msg.sender, the author of a contract can always know the sender, and the sender can't prevent their identity being used, e.g. for discriminating access to a contract.

With rootAuth, the caller immediately knows which contracts grant access based on identity (they require passing rootAuth. The user gets to decide whether they want to share their identity, or not - which seems very much in line with Self Sovereign Identity your mentioned tweet refers to.

I can even explain how the user can decide to use an identity-detached proxy/authority (an unforgable name) for interacting with contracts, which is SSI-compliant again.

This is possible b/c rootAuth reifies the identity as an unforgable value (and values are processes, and each process can serve as a name), making it an OCAP token. Contrarily, msg.sender is an ambient authority to know the caller's identity.

So it is not "basically msg.sender repackaged (hidden slightly with syntactic sugar.)". The semantics are different, and there's no syntactic sugar in here. It's new syntax, and new semantics.


The above also answers your question:

how can we streamline this without re-introducing the msg.sender design pattern?


Granted, there is an ambient element to rootAuth, namely "obtaining your own rootAuth". It's the same with new though ("create an unforgable name, known usable by just me and those I share with"), and with obtaining the "spend capability based on public key" in approach B.

The difference between the three is B is unsafe and is based on an ambiently interceptible token (which is the crux of the 'Mechanism'). Putting on it a band aid of "only if the user adds a flag to have it, and forgets to intercept it first" doesn't change that.


You nicely explain why making an exception to OCAP discipline is mandated in our case:

REV Vault is an exception to this pattern because the REV Address is created off chain and the actual REV Vault is not created by the REV Address owner. So, in this special case, it makes sense that you can use the public key to obtain the object capability to spend the REV Vault associated with that public key

First, let's agree that "REV Vault is not created by the REV Address owner" has to be the case only b/c we need to charge for the execution. Even without charging though, they'd be unable to store their RevVault's unforgable name safely without a safe identity-based storage.

Then, notice:

REV Vault is an exception to this pattern because the REV Address is created off chain

that the public key is also created off-chain. Having a safe, language-level token corresponding to it not only allows a safe access to one's RevVault, but also to any other identity-based contract we can imagine.

We don't need to be resorting to exceptions though! The wiki page you've brought up on discord mentions 4 ways to obtain a reference, first of them being:

initial conditions: In the initial state of the computational world being described, object A may already have a reference to object B.

Object A is the deploy itself. Object B is the rootAuth. If you're partial on calling 'deploy' an object, notice the OCAP approach as described by the 4 rules is then impossible, because there's no entry point. That's also the only 'initial condition' we need.

EDIT: Which brings the question: how does the exception you describe fit the OCAP model rules?

@odeac
Copy link

odeac commented Apr 10, 2019

The original design did this using two different signatures, one to spend the REV Vault, and one to sign the deploy. It was this duel signature that we felt was cumbersome and wanted to streamline. So the question is, how can we streamline this without re-introducing the msg.sender design pattern?

The two signatures make sense as they have different purposes: one to authenticate the deploy such that nobody can execute random code on my behalf and the other to authenticate a specific wallet transaction.

In the light o the recent discussions I still think this is the secure way to move forward.

Also the original two-signature approach is simpler than the "simplified" solutions proposed above.

@tymm
Copy link

tymm commented Apr 10, 2019

After a brainstorming session with Artur, I have this revised and more detailed write-up of my proposal.
It requires the introduction of a new Rholang keyword called auth.

Getting an auth key looks like this:

new authKeyCh in {
  auth!("authKeyForDeployer", authKeyCh)
  for (revAuthKey <- authKeyCh) {
    //...
  }
}

At deploy-time the above Rholang code would get extended to (automatically by the client software):

new authKeyCh in {
  auth!("authKeyForDeployer", "signature-over-deploy-nonce", authKeyCh)
  for (revAuthKey <- authKeyCh) {
    // ...
  }
}

The "signature-over-deploy-nonce" string is a signature over a nonce that must be of the same value as a field called signedDeployNonce in the deploy data of the user's deployment.

At runtime the auth system contract would check that the signature value defined in code is the same as the one defined in the deploy data.
The auth system contract would only return an auth key if both values are the same.
A third-party contract that is called from the user's contract would need to know the signedDeployNonce value to successfully drain the user's vault.

This can only happen during a front-running attack where an evil validator withholds the victim's deployment to update a fraudulent contract that is called by the victim.
This scenario can be countered by making deployments invalid in which the signedDeployNonce argument to the auth system contract is not the same as the signedDeployNonce value in the deploy data. This is why auth needs to be a Rholang keyword - there has to be a statical code analysis that checks that the second argument to the auth system contract is of the same value as the signedDeployNonce field in the deploy data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment