Skip to content

Side Channels and Confidential Contracts

Introduction

Confidential contracts must be developed carefully to avoid side-channel information leakage attacks. This doc describes side channels in the context of confidential smart contracts and discusses application-level mitigations that can be used by developers.

What are "side channels"?

Side channels are unintended communication channels over which information is transmitted or passed. That is, otherwise well-written software accidentally leaks information due to its normal operation. Preventing side-channel information leakage in general is possible but difficult; furthermore, it is an active area of research––many of the known techniques are still somewhat expensive.

To understand side channels better, let's look at some examples.

Examples

There are some highly publicized side channel information leaks, including ones involving Intel SGX. See the additional info section for links to relevant articles. Some side channels are peculiar to smart contracts.

For the sake of discussion, let us suppose we had a Solidity contract, a contrived example, that contained the following code:

contract SecretContract {
  bool private secretFlag;
  bytes32[2**20] private bigArray;

  constructor(bool secret) {
    secretFlag = secret;
  }

  function doSomething(...) returns (...) {
    ...
    if (secretFlag) {
      for (uint i = 0; i < bigArray.length; i++) {
        bigArray[i] = bigArray[i] + 1;
      }
    }
  }
}

Communication with the contract is through web3c, so Alice, the creator of the contract, can confidentially supply the secret argument to the constructor: the creator knows, through enclave attestation, cryptographic key exchange, and secure channel establishment, that she is supplying it to an audited execution engine that will run the contract code, and that no eavesdropper can learn of the secret value. Furthermore, active adversaries will not be able to alter the data in transit without being detected.

This seems confidential, but what happens when Bob invokes the doSomething method? How could he indirectly infer the value of the secretFlag? He has at least 4 ways:

  • The returned value(s) may be a function of the flag. Even if the value(s) have different statistical distributions depending on secretFlag's value, with enough calls (gas cost) Bob can infer, to whatever level of confidence needed, the flag's value.

  • The amount of time required to run the transaction depends on the value of secretFlag. In the example, we run a 2**20 iteration loop if and only if secretFlag is set, and anyone monitoring the execution time of the compute workers will be able to infer secretFlag's value.

  • The amount of energy consumed while running the contract call will also be significant. Even in cases where the total iteration count is not (noticeably) dependent on confidential values, researchers have conjectured that when there is a consistent statistical bias in the number of bits flipped, that might be detectable with enough statistical samples.

  • When a smart contract runs, its execution consumes gas. And the gas consumption is reported back to the caller. This means that all those updates to storage––reading bigArray[i], incrementing it, and storing the new value back––will be an easily distinguishable signal as to whether secretFlag was set or not. As a matter of fact, Bob doesn't even have to spend all the gas needed to make the subcontract call like legitimate users; instead, he could make the subcontract call to doSomething, supplying only a small amount of gas that would suffice only if secretFlag were false, and see if the subcontract call reverts due to running out of gas.

There are other ways that information could leak. Let us take a look at the SecretBallot example from the confidential voting contract discussed in the Confidential Smart Contracts page. That code used a mapping to hold the number of votes each candidate received:

contract SecretBallot {
    ...
    bytes32[] public candidateNames;
    ...
    mapping (bytes32 => uint16) private votesReceived;
    ...
    function voteForCandidate(bytes32 candidate) public {
        ...
        votesReceived[candidate] += 1;
        hasVoted[msg.sender] = true;
        totalVotes += 1;
    }
    ...
}

Solidity mapping uses keccak256 (SHA3) on the concatenation of the keys and the location of the mapping (docs) to determine the starting location for the value object. If the code for a contract like this is available (which it would be, for auditing), then the location of votesReceived is known. Since the list of candidates is public, the location where the number of votes received for each candidate can also be determined.

What does this mean? If an adversary can observe the interface between the compute engine where the Ethereum bytecodes are executed and the persistent storage (which is a key-value store), even if the values stored are encrypted, the pattern of storage locations accessed reveals the vote tally as the votes are cast, before the ballot concludes.

Mitigation Techniques

Constant Time / Resource Computation

For timing and power side channels, there are two standard approaches. The first is to design the algorithms such that the amount of resources used (time or power) is independent of the input. Many cryptographic scheme implementations work hard to be constant time (measuring power usage is often considered out of scope) in order to eliminate this class of side channels. The same approach could work for gas usage as well.

The second approach is to randomize the input. This works well for algorithms that are "randomized self-reducible", that is, an input can be mapped to a random other input, use a non-side-channel-free algorithm on that random input, and use the self-reduction property on the output to figure out what the output for the original input should have been.

This may appear to not apply to smart contracts since smart contract requires deterministic execution, and this technique requires that the randomization be kept secret. However, the contract author could supply the contract with high-entropy seed data for a cryptographically secure pseudorandom number generator (CSPRNG), and as long as the execution of the CSPRNG does not itself reveal timing or power characteristics that leak information about the key material, use the CSPRNG to randomly pick the self-reduction mapping.

Oblivious Remote Access Memory (ORAM)

A general solution to eliminating information leakage from memory access patterns is the Oblivious Remote Accessed Memory abstraction. Here, the basic idea is that the ORAM scheme provides an abstract memory-like interface (oblivious memory) on top of the actual RAM or persistent storage. Each ORAM read access turns into several accesses of underlying (encrypted) storage, so that an adversary would not know which of those accesses were for the actual contents that the client algorithm wanted to access, and which were misdirection. Further, after the access, the ORAM scheme writes encryped data back out to the underlying storage to swap data around, so that an adversary cannot infer when the algorithm is accessing the same ORAM memory location.

Application-specific Oblivious Data Structures

Instead of an oblivious memory abstraction layer for all of memory, application-specific data structures often suffices. In the secret ballot example, we could implement the vote counting as something along these lines:

contract SecretBallot {
    ...
    uint256[] private encodedVotesReceived;
    ...
    constructor(bytes32[] _candidateNames) public {
        ballotCreator = msg.sender;
        candidateNames = _candidateNames;
        votesReceived.length = candidateNames.length;
    }
    ...
    function voteForCandidate(bytes32 candidate) public {
        ...
        int pos = findCandidate(candidate); // position in candidateNames
        require(pos != -1);  // is valid name
        for (uint ix = 0; ix < encodedVotesReceived.length; ix++) {
          int addend = uint256(1) << ((ix == pos) ? 0 : 128);
          encodedVotesReceived[ix] += addend;
        }
    }
    ...
    function totalVotesFor(bytes32 candidate) public returns (uint128) {
        int pos = findCandidate(candidate);
        require(pos != -1);
        require(votingEnded);
        return uint128(encodedVotesReceived[pos]);
    }
    ...
}

Since the storage interface is a key-value interface, with 256-bit keys and 256-bit values, under the assumption that writes will re-encrypt the memory location, this version of voteForCandidate will change and rewrite every 256-bit location in the array. Here, we assume that ((ix == pos) ? 0 : 128) is compiled into a bytecode sequence that executes in constant time (or that the difference is not measurable). NB: it is necessary to change the 256-bit encoded value even if the memory encryption scheme used is probabilistic, because the EVM implementation will not charge for a SSTORE operation if the cleartext value is unchanged at the storage externalities interface.

Literature

Statistical Analysis Techniques For Extracting Information From Side Channels

Oblivious RAM

Additional Info

Side-channel attacks