2

If I have a function f(x, y) and g(x, y) and f and g a are constant time, is the following code vulnerable to side channel attacks (assuming the conditional is constant time as well)

if (conditional) {
  f(a, b);
  g(b, a);
else {
  f(b, a);
  g(a, b);
}
DannyNiu
  • 10,640
  • 2
  • 27
  • 64
Rabool
  • 23
  • 2

3 Answers3

2

Yes. The if-else statements illustrated in the OP would usually compile to something similar to this:

JUMP-IF   ElseLabel, Condition-Register;
MOV       Arg1, a
MOV       Arg2, b
CALL      f
MOV       Arg1, b
MOV       Arg2, a
CALL      g
JUMP-TO   EndOfElseLabel
ElseLabel:
MOV       Arg1, b
MOV       Arg2, a
CALL      f
MOV       Arg1, a
MOV       Arg2, b
CALL      g
EndOfElseLabel:

It is the consensus of the cryptography community that any non-deterministic code paths constitute side channel, such as the one above where assembly code may take either path depending on the condition. This is exacerbated considering (among many reasons) the CPU implementations would often carry out predictive branching - code paths are executed behind-the-scene with outcome chosen at a time deferred to when the condition is actually computed.

DannyNiu
  • 10,640
  • 2
  • 27
  • 64
1

Yes, the code shown is potentially vulnerable to timing attack, the simplest form of side-channel attack, to determine the value of condition. More generally, essentially any use of a variable quantity in C or Rust can leak it's value by side-channels, e.g. power analysis.

Among the few uses of a variable quantity that is generally* safe from timing attack in compiled languages (like C or Rust) on modern CPUs are bitwise and arithmetic operators & | ^ ~ + - when operating on integer types; casting among these types; shift (<< >>) of such quantity if the shift count is non-secret (including a public constant shift). Also, reading from or writing to a variable (or a vector at a non-secret offset) is safe.

With this in mind, assuming a and b are of unsigned type, and condition is 0 or 1 and of type ìnt, this code is provably equivalent to the one in the question and is less unlikely to be constant-time:

unsigned ai = a ^ b;
unsigned bi = ((-(unsigned)conditional) & ai) ^ a; // b if conditional, a otherwise
ai ^= bi;                                          // a if conditional, b otherwise
f(ai, bi);
g(bi, ai);

This produces the correct result because (-(unsigned)conditional) has all it's bit at 0 if conditional is 0, and all it's bit at 1 if conditional is 1. This is an insurance of the C language.

This solves the data-dependent timing in the code shown, but is still potentially vulnerable to other side-channel attacks, e.g. power analysis. And of course there remains the potential for data-dependent timing inside the code of f and g.


* It's still advisable to check the generated code, especially when using signed quantities. An example is uint64_t j = i where i is an ìnt8_t variable: duration conceivably could depend on the sign of i on some combinations of CPU and compiler.

fgrieu
  • 149,326
  • 13
  • 324
  • 622
0

You can try

f(conditional ? a : b, conditional ? b : a)
g(conditional ? b : a, conditional ? a : b)

Then check the assembler code to see that the compiler is using conditional instruction.

gnasher729
  • 1,350
  • 7
  • 9