Constant-time code is a concept that has been developed in the area of cryptography since about 2005. It relates to the fact that implementations of cryptographic algorithms manipulate secret values, and if they do not do so with enough care, outsiders may obtain some information on such values through time-based side channels, e.g. by measuring the total time taken to perform a computation. Memory caches, as commonly used in modern computers, are a great source of such leaks. We thus call "constant-time" implementations that take care not to allow time-based side channels.
The concept of side channels is not restricted to cryptography. In fact, any piece of code that processes confidential data, in a context where attackers may make precise timing measures, is potentially vulnerable, and should use mitigation measures. In particular, implementations of security enclaves with Intel SGX or ARM TrustZone operate in a security model where side channel attacks are very effective, since the attacker is supposed to run his own code on the host system and can monitor cache accesses with great accuracy. Constant-time coding techniques are highly relevant to about anything that is implemented in an enclave.
This library, called CTTK, is a collection of constant-time implementations of primitive operations that may help in writing constant-time code, including non-cryptographic constant-time code.
It shall be noted that the expression "constant-time" is traditional but slightly confusing: constant-time code does not always execute in a constant amount of time; rather, this means that any variation in execution time is uncorrelated with secret information. For more information on constant-time coding, you may have a look at the Cryptography Coding Standard pages, and the BearSSL library.
This library uses the MIT license. In plain words, this means that you can reuse it in both opensource and proprietary projects; the only requirement is that you keep the license text with each source file, where it already is. This is for my protection: the license text basically says that whatever happens, it's not my fault, and you understand it.
This library is written and maintained by Thomas Pornin
<pornin@bolet.org>
. Any comments and suggestions are welcome. Be
warned that if you submit a patch or pull request, and I find it good,
then I will still rewrite it completely, because I am a raving maniac.
CTTK is still very early in its development. It probably contains some bugs. The API may change in future versions. I will do my best not to gratuitously break source or binary compatibility, but I cannot guarantee that it won't happen.
WARNING: IF YOU ARE TRYING TO USE THIS LIBRARY, IN ITS CURRENT STATE, FOR PRODUCTION CODE, THEN YOU ARE MOST CERTAINLY CRAZY. Of course, a solid dose of craziness is often needed to promote innovation; but it rarely ends well for the zealous early adopter.
A list of planned features and improvement can be seen in the TODO.md file.
To compile, type make
. This should work on any decent Linux or *BSD
system, both with GNU make and BSD make. If you use Microsoft Visual C
command-line tools on Windows, you may type nmake
, and it should work
too. You can tune compile-time options in the relevant file in the
conf
directory; these can also be added on the compilation command-line,
e.g.:
make BUILD=alt
will create a directory called alt
and put the compilation output in
that directory instead of the default directory build
.
There are some tunable configuration options in src/config.h
. Such
options may be set either in that file, or through command-line options
for compilation (in the CFLAGS
variable).
Compilation produces a static library (libcttk.a
on Unix-like systems,
cttks.lib
on Windows), a dynamic library (libcttk.so
on Unix-like
system, cttk.dll
on Windows), and a test executable (testcttk
), all
in the build directory, which is created when needed (its default name
is build
). The test executable can be run to perform some basic
self-tests.
There is no automated installation process yet; notably, in the context
of security enclaves such as SGX, a specific installation process would
be needed anyway. The external API is the cttk.h
file located in the
inc
directory.
If using the provided makefiles is inconvenient, the files may be integrated in any other build system. The dependencies are simple:
- Every
.c
file insrc
should be compiled into an object file. - Each such
.c
file includesinner.h
,config.h
, andcttk.h
. - There are no other dependencies.
CTTK is a C library. As a rule, C is a tricky language, full of pitfalls, in particular undefined behaviours that may break any implementation silently when changing the compiler version. More modern, safer languages such as Rust or Go are almost always preferable; and even for very low-level, bare metal processing, it makes sense to explore alternatives such as Forth.
On the other hand, C is still the lingua franca of programming languages, and a C compiler can be found for just about any hardware and software environment. Moreover, in an ideal world, languages that are under active development should integrate constant-time primitives as part of the standard language definition; a third-party library like CTTK makes sense only because C is a mostly frozen language.
CTTK is supposed to be usable from C++ as well, but with its C-like API. There is no support for operator overloading or templates.
The API is mostly documented in the cttk.h
file itself. An HTML
version of that documentation can be produced with
Doxygen; a configuration file (Doxyfile
)
is provided.
C has no namespace. Therefore, all external names provided by CTTK start with a specific prefix to help with avoiding name collisions.
All objects with external linkage (functions, global variables) have a
name that starts with cttk_
.
The header file (cttk.h
) also defines function and macro names that
start with cttk_
, CTTK_
or cti_
. These names only impact the
current translation unit, i.e. the files that include directly or
indirectly cttk.h
.
To use CTTK in your application, include the cttk.h
header. That file
pulls in a few standard library files that should be available on all C
implementations, including "freestanding" compilers for embedded
systems.
CTTK defines the cttk_bool
type to contain a boolean value (true or
false). This type is defined as a struct
so that it is NOT directly
usable to control conditional jumps, since these are, by definition,
not constant-time.
The cttk_true
and cttk_false
constants are provided for,
respectively, true and false values.
The cttk_bool_of_u32()
, cttk_bool_of_s32()
and cttk_bool_to_int()
functions can be used to convert between C "booleans" (i.e. integer
values 0 and 1) and CTTK booleans. Constant-time code should endeavour
to apply such conversions only when the boolean value is no longer
considered secret.
Boolean operations are implemented by cttk_not()
, cttk_and()
,
cttk_or()
, cttk_xor()
and cttk_eqv()
(this last one is also
known as the "XORNOT" operation).
Primitives for doing constant-time comparisons on native integers are
provided. In all names, s32
, u32
, s64
and u64
designate the C
types int32_t
, uint32_t
, int64_t
and uint64_t
, respectively.
Variants for all applicable types are provided, for the following
operations:
-
Multiplexer (selection of one of two operands, based on a
cttk_bool
value):cttk_s32_mux
,cttk_u32_mux
,cttk_s64_mux
,cttk_u64_mux
-
Comparison with zero (returns true if the operand is not zero):
cttk_s32_neq0
,cttk_u32_neq0
,cttk_s64_neq0
,cttk_u64_neq0
-
Comparison with zero (returns true if the operand is zero):
cttk_s32_eq0
,cttk_u32_eq0
,cttk_s64_eq0
,cttk_u64_eq0
-
Equality comparison between two integers:
cttk_s32_eq
,cttk_u32_eq
,cttk_s64_eq
,cttk_u64_eq
-
Inequality comparison between two integers:
cttk_s32_neq
,cttk_u32_neq
,cttk_s64_neq
,cttk_u64_neq
-
Ordering ("greater than"):
cttk_s32_gt
,cttk_u32_gt
,cttk_s64_gt
,cttk_u64_gt
-
Ordering ("greater or equal"):
cttk_s32_geq
,cttk_u32_geq
,cttk_s64_geq
,cttk_u64_geq
-
Ordering ("lower than"):
cttk_s32_lt
,cttk_u32_lt
,cttk_s64_lt
,cttk_u64_lt
-
Ordering ("lower or equal"):
cttk_s32_leq
,cttk_u32_leq
,cttk_s64_leq
,cttk_u64_leq
-
Generic comparison (returns -1, 0 or 1):
cttk_s32_cmp
,cttk_u32_cmp
,cttk_s64_cmp
,cttk_u64_cmp
-
Comparison with zero ("greater than zero"):
cttk_s32_gt0
,cttk_s64_gt0
-
Comparison with zero ("greater than or equal to zero"):
cttk_s32_geq0
,cttk_s64_geq0
-
Comparison with zero ("lower than zero"):
cttk_s32_lt0
,cttk_s64_lt0
-
Comparison with zero ("lower than or equal to zero"):
cttk_s32_leq0
,cttk_s64_leq0
-
Generic sign extraction (-1, 0 or 1):
cttk_s32_sign
,cttk_s64_sign
The cttk_u32_bitlength()
function computes the length, in bits, of an
unsigned 32-bit integer. The bit length of an integer x is the
smallest integer k such that x is lower than two raised to the power
k.
cttk_hextobin_gen
parses hexadecimal digits into binary.
cttk_bintohex_gen
performs the reverse operation: encoding binary
data into hexadecimal. These functions are tunable:
- Decoding may tolerate, or not, intervening whitespace.
- Decoding may accept, or not, input data with an odd number of
hexadecimal digits (a trailing
'0'
is then assumed to complete the last byte). - Encoding may use uppercase or lowercase letters.
The implementation protects the value of bytes and digits from outsiders. It cannot, however, hide the number of hexadecimal digits or the length, in bytes, of the binary data. If whitespace is accepted (and ignored), then location of whitespace within the source string may conceptually leak as well.
cttk_bintob64_gen
encode binary data into Base64 characters. Tunable
options are:
-
Output may be broken into lines. The line end is either an LF (0x0A), or a CR+LF sequence (0x0D 0x0A). Line length is either 76 or 64 data characters.
-
The padding characters (one or two "
=
" signs when the input length is not a multiple of 3) may be produced or omitted.
cttk_b64tobin_gen
parses Base64 characters into binary. The function
may tolerate (and ignore) whitespace characters; but it can also be
instructed to report errors on any extra character, including
whitespace. The padding characters are normally expected and processed,
but the decoder can also be configured to not use padding.
As for the hexadecimal decoder, the data byte contents are protected from outsiders. The data size, and position of data characters and whitespace within an incoming stream, may leak.
Not all CPU provide constant-time multiplication opcodes; see this page for details. CTTK provide some constant-time multiplication primitives:
-
cttk_mulu32()
: multiplication of two 32-bit unsigned integers, with a 32-bit result (the low 32 bits of the result). -
cttk_muls32()
: multiplication of two 32-bit signed integers, with a 32-bit result (truncation to the low 32 bits). Note that in plain C, overflows on signed integers trigger undefined behaviour; with CTTK, truncation is guaranteed. -
cttk_mulu32w()
: multiplication of two 32-bit unsigned integers, with a 64-bit result (uint64_t
). -
cttk_muls32w()
: multiplication of two 32-bit signed integers, with a 64-bit result (int64_t
). -
cttk_mulu64()
: multiplication of two 64-bit unsigned integers, with a 64-bit result (the low 64 bits of the result). -
cttk_muls64()
: multiplication of two 64-bit signed integers, with a 64-bit result (truncation to the low 64 bits). As forcttk_muls32()
, CTTK guarantees truncating behaviour.
The default implementation of these functions is (nominally)
constant-time on all architectures, but not very efficient. Some
compile-time options (see config.h
) can be used to force use of the
native multiplication operator, if you are certain that your code will
always run on hardware platforms that provide constant-time
multiplications (the gist of the Web page linked to above is that such a
bet is risky).
CTTK provides a constant-time implementation of big integers with a configurable size. In fact, several implementations are planned, for better performance on various architectures; application code should use the generic macros that will select the "right one" automatically.
A big integer value has the following characteristics:
-
It has a defined size which qualifies the space in which the value exists. The size is expressed in bits. If the size is n bits, then the value may range between
-2**(n-1)
and2**(n-1)-1
(where "**
" stands for exponentiation). All bit sizes, starting from 1, are supported. For instance, you can have 17-bit integers. Note that the size includes the sign bit; thus, 16-bit integers will range from -32768 to +32767. There is no upper limit for the bit size except available RAM. -
Each value may be either an integer in the defined range, or a NaN ("not a number"). A NaN is obtained whenever the mathematical result of an operation is not representable in the defined range (except for the operations that are explicitly defined as "truncating"). NaNs propagate: if an operand to an arithmetic operation is a NaN, then the result will also be a NaN.
-
The C type for a big integer is an array. The
cti_def
macro is used to declare a local variable or structure field that is large enough for big integers up to a given size. Since that type is an array, it is automatically passed by reference to called functions. -
To become usable, the big integer object must be initialized with
cti_init()
(alternatively, it may be declared and initialized as a local variable with thecti_definit
macro). Initialization sets the value to NaN but also encodes the actual size of the integer. Using an uninitialized big integer with any of the functions may trigger "bad things" such as buffer overflows. -
Big integers do not grow or shrink. They do not involve dynamic memory allocation either. Thus, there is no "release" function. They can be forgotten just like any plain local variable.
The following rules are applicable to all big integer functions:
-
The function name starts with
cti_
. -
All these function names are actually macros that map to the selected implementation functions.
-
Some of the functions may be inline functions, for better performance.
-
Destination operands come before source operands. This mimics mathematical notation (in "d = a + b", the destination is on the left) and established usage in the C standard library (e.g. the
memcpy()
function). -
All operand overlaps are allowed; that is, it is permitted to write things such as
cti_mul(x, x, y)
to perform a multiplication ofx
byy
and write the result inx
. -
Some operations internally need temporary buffers. These will be usually stack-allocated, but for large integers,
malloc()
is used. This is transparent to applications: the buffers are released before returning to the caller. For the benefit of embedded systems which might not have amalloc()
, it is possible to disable use of that function with the compile-time optionCTTK_NO_MALLOC
. Ifmalloc()
was disabled or failed to allocate the required memory, then the result will be set to NaN. -
If operand sizes do not match (both source and destination), then the result is set to NaN.
-
Constant-time protection is on the integer values, not the sizes. The "size" here is not the actual bit length of the value (that information is protected) but the representable range used by the containing variable. Similarly, the location in RAM of any value cannot be protected.
-
Whether an integer value is NaN or not is protected against time-based side channels. The
cti_isnan()
function returns the NaN status of an integer as acttk_bool
. -
For shift operators, default functions (e.g.
cti_lsh()
) protect the values of the source integer and the result value, but not the shift count. If the shift count is also secret, then alternate implementations are provided (cti_lsh_prot()
...) but they are substantially slower.
An example of the use of CTTK big integer code is shown below. It is an implementation of a function that computes the average of many 64-bit integers, and prints the result in decimal (with a precision of 12 digits in the fractional part). The individual values are considered secret, while the average is not. (This example is not meant to be realistic, but to demonstrate the usage syntax.)
#include <stdio.h>
#include <stdlib.h>
#include "cttk.h"
void
print_average(const uint64_t *values, uint64_t num)
{
/*
* We may have up to 2^64 values of 64 bits each, thus a sum
* of up to a bit less than 2^128. Since our big integers
* are signed, we need 129-bit integers.
*/
cti_definit(s, 129);
cti_definit(x, 129);
uint64_t u, hi, lo;
/*
* Compute the sum of all integers in s.
*/
cti_set_u32(s, 0);
for (u = 0; u < num; u ++) {
cti_set_u64(x, values[u]);
cti_add(s, s, x);
}
/*
* Divide the sum by the number of integers. The quotient
* will be the integral part of the average.
*/
cti_set_u64(x, num);
cti_divrem(x, s, s, x);
hi = cti_to_u64(x);
/*
* To get the fractional part, properly rounded to 12
* digits, we multiply the remainder by 10^12, then
* add num/2 (for rounding), and divide by num.
*/
cti_set_u64(x, 1000000000000);
cti_mul(s, s, x);
cti_set_u64(x, num >> 1);
cti_add(s, s, x);
cti_set_u64(x, num);
cti_div(x, s, x);
lo = cti_to_u64(x);
printf("avg = %llu.%012llu\n",
(unsigned long long)hi, (unsigned long long)lo);
}
An Oblivious RAM implementation allows array reads and writes in
constant-time, i.e. without leaking information about the exchanged
value or the access index. Currently, CTTK includes only a very basic
implementation that has cost O(N) for an array of size N: for every
read or write operation, the full array memory is touched. The relevant
functions are ctty_array_read()
and cttk_array_write()
.
Implementations of more efficient ORAM algorithms are planned, but not yet done.
Related functions are:
-
cttk_cond_copy()
: constant-time copy of bytes, conditionally to a boolean flag. -
cttk_cond_swap()
: constant-time exchange of two non-overlapping values, conditionally to a boolean flag. -
cttk_array_cmp()
,cttk_array_eq()
andcttk_array_neq()
: constant-time comparisons of arrays of bytes (equality and lexicographic order).