Skip to content

Instantly share code, notes, and snippets.

@ArturGajowy
Last active March 16, 2020 03:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ArturGajowy/d89803a7320cb65567f3b3f2229f908a to your computer and use it in GitHub Desktop.
Save ArturGajowy/d89803a7320cb65567f3b3f2229f908a to your computer and use it in GitHub Desktop.

Drain caller's vault attack - v3

Changes since previous version

  • The two outstanding proposals (B and D) have been refined.
  • Approach D further refined into two variants.
  • Pros and cons of each summarized.
  • Final recommendation of the Dev Team included.

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

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

We make the "deployer's RevVault OCAP", currently returned ambiently by RevVault!("deployerAuthKey", *ret), only available (at most) once per deploy from a URI-bound, platform-controlled unfograble name, bound to the URI rho:rev:auth. We prevent the attack by having the OCAP owner receive it before any 3rd-party contract is called.

new revAuth(`rho:rev:auth`) in {
  for (identity <- revAuth) {
   //...
  }
}

We require the flag --with-rev-auth in rnode deploy call to be passed for the OCAP to be passed by the platform in that deploy. The flag value is retained in DeployData propagated to validators.

The 'at most once receive' semantics is achieved by having the platform execute a linear (non-persistent) send on that name.

Preventing the 'flag passed, auth not received, back at square one' scenario

There is a risk that a deployer will pass the flag (or forget to not pass it) despite not receiving the OCAP in the deploy code, and thus allowing the 3rd-party contracts they call to receive it instead - from which point the original attack scenario follows.

To alleviate this, whenever the --with-rev-auth flag is passed, the validator the deploy is made at must require it to be exactly of the form shown above. Propagating a deploy that has code not fitting this template despite the --with-rev-auth flag being present in DeployData is a slashable offence.

For user's convenience, so that their deploy's don't hang when they use the template without passing the flag, we also reject deploys that fit exactly the above template but are not done with the flag passed.

Non-uniformity with current uri-bound names behavior

Currently, every URI-bound unforgable name (e.g. new stdout(`rho:io:stdout`) is bound to a static value, same for every deploy. This means everyone actaully uses the same stdout channel.

This must not be the case for revAuth(`rho:rev:auth`), or which a new unforgable name must be created just as if it were a non-URI-bound name. Care must be taken to propagate the RNG splits to the actual deploy code execution.

(Otherwise, one can pre-deply receives on the revAuth(`rho:rev:auth`) name and gather other deployer's OCAPs)

The platform must intercept/provide the innards of the RevVault contract

The OCAP for a presonal RevVault encompases an unforgable name _revVault, prviate to the RevVault contract. For the approach to work, the platform must know this name - which makes the implementaiton of both the contract and the approach B itself awkward and complex.

End-user usage

new revAuth(`rho:rev:auth`) in {
  for (auth <- revAuth) {
    //the above two lines need to happen before calling any ohter code
    
    new vaultCh, ret in {
      RevVault!("findOrCreate", "REV_ADDR", *vaultCh) |
      for (vault <- vaultCh) {
        vault!("transfer", "TARGET_REV_ADDR", 100, auth, *ret)
        //...
      }
    }
  }
}

D. Reify the deployer's / contract caller's identity as an unforgable value

All names in this chapter are tentative / subject to discusison

A new ground type Auth is introduced, instantiable only by the platform (similarly to unfograble names). Values of that type reflect the public key (and ohter deploy data as needed in future) from the deploy the value was constructed in. This reifies the deployer's identity in an unforgable on-chain token (value / OCAP).

Values of Auth are obtained, pending to our decision:

Variant D1

by using the DEPLOYER_AUTH keyword, with usage:

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

Variant D2

by declaring the new auth(`rho:deployer:auth`) unforgable name, with usage:

new auth(`rho:deployer:auth`), authKeyCh in {
  RevVault!("deployerAuthKey", auth, authKeyCh) |
  for (revAuthKey <- authKeyCh) {
    vault!("transfer", targetRevAddr, amount, revAuthKey, ret)
  }
}

In both variants, Auth value is resolved 'statically' (before execution; means "the identity of whoever wrote the DEPLOYER_AUTH keyword / declard the name"), making it impossible for Bob to access Alice's identity if they don't pass it to Bob explicitly / via value flow.

A malicious validator resolving an Auth value not corresponding to the current deployer can easily be slashed, and would be even without addtional rules by causing a different execution result.

Values of the Auth type are comparable (using ==) and can be used as part of other processes/names.

In the usage examples above, deployerAuthKey would resolve the corresponding RevAddress using a system contract toPubkeyBytes(Auth) and further passing the pubkey to RevAddress!fromPubKeyBytes.

We should also secure the getDeployData contract by it taking an Auth argument that needs to be the current deployer's for the contract to work, as the information in there is potentially sensitive.

Comparison and recommendation

Approach B:

  • uses a linear send to be immediately received in deploy code by the user, in combination with
    • a deploy flag,
    • forcing of how deploy code looks based on it in deploy validation (with and without the flag),
    • an additional slashing rule
  • has worse ergonomics due to requiring a flag
  • is targeted at the RevVault OCAP specifically
  • introduces additional cases where a deploy may fail (needed for safety of the approach)
  • requires implementation effort in multiple modules (node client, node server, rholang, casper, RevVault contract)
  • is harder to implement
  • requires close coupling of the platform and the RevVault contract
  • requires a change to how unforgable names are created

Approach D:

  • uses an on-chain unforgable token to refiy the deployer's identity
    • which allows for a (system/trusted) contract to get its caller's identity, if they wish to provide it as a parameter
  • requires no flags, no handling of the flags, no additional deploy failure scenarios
  • provides a RevVault-agnostic building block, opening way for further authenticaiton scenarios, including 3rd-party ones
  • requires implementation effort only in rholang and the RevVault contract
  • provides a clean implementation path, with little/no coupling
  • adds a new, unfograble ground type Auth
  • Variant 1: requires a new keyword DEPLOYER_AUTH
  • Variant 2: uses a uri-bound value auth(`rho:deployer:auth`)
    • this saves introduction of a keyword, but makes the type of processes corresponding to the new keyword usage non-uniform (it's either a GPrivate or GAuth). EDIT: this shouldn't be a problem though, as the only property of the new keyword we ever relied on is that the corresponding value is unforgable. This is retained. We can think of the type of values bound by new keyword to be data Unforgable = GPrivate bytes | GAuth publicKey.

Vote results / summary:

  • Chris: sligth preference of B
  • Artur: D
  • Ovidiu: D
  • Kent: sligth preference of D
  • Timm: D

Overall reccmendation:

We reccommend proceeding with one of the variants of D.

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