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:
- based on the deployer's public key obtained from the (so far public)
rho:deploy:params
system process (amd further callingRevAddress!fromPubKeyBytes
) - by off-chain inspecion and hardcoding arbitrary rev-addresses and par-ing the attacks targeted at different deployers
- by arranging a known victim to call a contract that has their RevAddress hardcoded
- by asking the victim to call the contract and provide their RevAddress as parameter
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.
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.
So far, the following ideas for remediation strategies were brought up:
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:
- extract the 'creator' of the
identity
unforgable name - 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.
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:
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...
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
).
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
.
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.
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:
The
authKeyForDeployer
method would then check that$IDENTITY_PROOF
was actually signed by the deployer before returning an auth key.