Breaking Bad: How Compilers Break Constant-Time~Implementations
arxiv.org
arxiv.org
All optimizing compilers perform optimizations based on the notion of an abstract machine, which determines what semantics of the original program must be conserved. And the abstract machine for a given language is usually provided by the language that is being implemented (except for e.g. languages that just give up and say "give me C's abstract machine").
You definitely don't want compiler backends trying to "guess" when some defensive programming technique is being used, so that they can compile it conservatively. That's a dead-end pathway to madness. You need languages to give you the power to explicitly say "this operation is constant time, don't mess with it". And in practice they do give you this power, via inline assembly. Of course inline assembly is suboptimal for a lot of reasons, and we can certainly try to do better, but this is a language design problem, not an optimizing compiler problem.
It would be nice for processor makers to explicitly specify that the common arithmetic instructions will take time which is constant in terms of their argument data.
https://www.intel.com/content/www/us/en/developer/articles/t...
See the comment above by "cokernel_hacker", which provides links to the Intel and Arm documentation of this feature.
> "However, we want to note that libsodium is the smallest library that we test with the most limited support for cryptographic primitives. For example, libsodium does not support the elliptic curve secp256; instead,they do all of their elliptic curve operations on curve25519. Therefore, it may be unfair to compare to larger libraries with support for more cryptographic primitives."
https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Common-Functio...
It doesn't do anything like that.
« We found binaries with issues in all optimization levels as depicted in Figure 3. We observe notable drops for LLVM's and GCC's -O0. We believe that fewer enabled optimizations also lead to fewer compiler-induced issues. We discuss in Section VII that optimizations are one of the reasons compilers introduce side channels; thus fewer optimizations could mean defensive programming techniques are less likely to get inter-fered with. However, we note that this cannot be considered a solution to compiler-induces issues as we still observe some issues with -O0. We discuss these further in Section VI-D. »
int a = read_a_value();
a = constant_time_add(a+1);
do_something_with(a);
In many languages, the compiler is free to insert entirely new operations between these statements so long as the output isn’t affected. Or the compiler could reuse a register that contained a to do something else and do something awful like zeroing that register in non-constant time.I think that new types are a better solution. A type could be specified such that the compiler must not use its value in a non-constant time operation, nor may the compiler leak it into an initialized value.
Not for writing whole programs in, but for writing fairly small amounts of code that performs arithmetic and compiles to libraries with C linkage. No VM, not even any memory allocation (make the caller do that in advance with fixed-size areas).
Basically the "portable assembler" that people keep using C for despite its explicit promises in the standard to not be that.
actual assembler is just such a language, as is being discussed here.
I’ve always thought it was strange that assembly wasn’t used for some of these primitives, since you don’t control the codegen, but letting the compiler provide intrinsics would solve the problem without requiring every project to write their own assembly routines.
And that's not just for constant-time implementations, sometimes it's a lot more simpler than that. Some security scheme against power consumption side-channel attacks, such as dual-rail with precharge logic, require that registers are zeroed before being written to. A compiler seeing code such as:
x = 0 /* first write, zeroing */
x = sensitive_data /* second write, actual value */
will of course consider that the first write is never read and optimize it away, thus breaking the security scheme… Then, there's also the need to make sure that the register your using is always the same (because leakage profile may differ a lot between the different registers of a single device). All the more reasons to write critical cryptographic code in directly in assembly.[1] https://pablo.rauzy.name/research.html#phd
[2] A ~fun anecdote: I'm a computer scientist, but most people at that lab were in electronics, and for them assembly code was considered high level because it was already software!
Intel has DOIT: https://www.intel.com/content/www/us/en/developer/articles/t...
The idea is that the processor will not take shortcuts that take advantage of the values it is processing. For example, a 64-bit division cannot shortcut if the operands are both small, etc.
[1] See for example the work we did in this paper: Formally Proved Security of Assembly Code Against Power Analysis: A Case Study on Balanced Logic https://eprint.iacr.org/2013/554
Anyway, IMO, CPU designers seem more aware of security implications than compiler developers. I expect more attention to those things in the future, not less.
edit: Anyway, if your threat model includes "attacker can discern differences in power at uop-granularity and make meaningful correlations", you are probably doomed at the outset and you should not have used an out-of-order machine in the first place.
I'm not up to date on GPU architectures, bit I wouldn't be surprised of they do this sort of stuff.
Ultimately security focused languages that provide operational guarantees with C linkage are probably necessary. I don’t think it’s enough to not optimize C.
I wrote AEGIS implementations in it: https://github.com/jedisct1/aegis-jasmin and it was a really great experience.
It seems to me that the entire stack is built on this assumption, making life incredibly difficult the other 0.01% of the time.
Nobody asks such a question and not all would answer it the same.
Id rather take 5% perf. regression to my cpp code if it meant that compile time halves and errors quality will be better
1. authentication request arrives
2. schedule response at a later time, generously leaving time for doing the next step
3. do part of the work that is sensitive to timing attacks
4. respond at scheduled time
Since there is lots of cryptography used to keep users out of their own hardware (or stolen hardware) these days, this is important.
And it's not hard. Godbolt.org is free for everyone.
Checking if the code branches is super easy, you don't need a super smart runtime analysis tool to figure out if there's a branch. Ditto for everything else you want to check.
1) Allow any block of code to be defined as constant-time. The compiler is forbidden any optimizations that could break this. Constant-time routines can only call other routines that are likewise declared as constant-time.
2) Allow any operation to be defined as important. The compiler can't remove it as redundant.