Firmware Security and the Importance of Constant-Time Programming (Part - 2)
Reproduced from - https://2wisebit.wordpress.com/2025/03/13/701/
Understanding Constant-Time String Comparison: A Deep Dive
Constant-time comparison of strings is an important security measure that helps maintain the consistent execution time for a string comparison operation, no matter what strings are being input. This supposedly minor detail goes a long way in avoiding timing attacks, which are a side-channel attack group that takes advantage of differences in execution time in order to glean sensitive information.
Why Constant-Time Comparison?
In typical string comparison, the operation tends to end as soon as a difference is encountered. The optimization, although effective in common usage, turns into a weakness in security-related situations. The duration for comparing two strings is directly proportional to the count of identical characters prior to the occurrence of the first difference. When the strings are equal, comparison takes the maximum time. When they have differences at the first character, it takes the least.
Constant-time comparison avoids this dependency. It ensures that the algorithm for comparison will always take the same amount of steps, irrespective of whether the strings are equal or not. It does this by comparing all the characters and adding up the results instead of short-circuiting at the first mismatch.
Importance of Constant-Time Comparison in Security
The major reason for constant-time comparison is to prevent timing attacks. Such attacks take advantage of the observation that various operations require different time periods to process on a computer. By monitoring these differences accurately, an attacker can deduce information about data being processed.
Following is an explanation of why this is essential in security:
Password Verification:
Consider a system that hashes a user-input password and compares it to a stored hash. In a non-constant-time comparison, an attacker can repeatedly try passwords, measuring the responses.
If the response time grows longer with each correctly input character, the attacker can guess the password character by character.
Constant-time comparison prevents this weakness by making the comparison take the same time every time, no matter what the input is.
Cryptographic Operations:
Cryptographic primitives typically consist of sensitive data comparisons, e.g., keys.
Information about the keys can be revealed through timing attacks, rendering the whole system insecure.
Implementations of constant time for cryptographic functions are therefore a must for avoidance of these types of attacks.
Other Sensitive Data:
All applications processing sensitive data like private keys, encryption keys, or authentication tokens should use constant-time comparison.
This serves to protect the attackers from deriving information regarding such secrets through timing attacks.
Example - Consider a non-constant-time string comparison
for (size_t i = 0; i < length; i++)
{
if (a[i] != b[i]) {
return 0; // Exit early if a difference is found
}
}
Variable Execution Time
What Happens with Early Exit in String Comparison?
Timing attacks exploit variations in the execution time of a program to infer sensitive information, such as passwords or cryptographic keys. An attacker can measure the time taken by the comparison function to infer information about the strings being compared.
Example: Password Verification
Suppose an attacker is trying to guess a password by comparing their guesses to a stored hash. The comparison function exits early if a mismatch is found. The attacker can use the following steps to deduce the password:
Example Scenario
By observing the timing differences, the attacker can deduce that "passwor" is closer to the correct password than "passw" or "passwo". Over multiple attempts, the attacker can reconstruct the entire password.
How Constant-Time Comparison Works (Conceptual)
The secret to constant-time comparison is to prevent any conditional branching or premature termination based on the result of comparison. This can normally be done by:
Comparing all characters: Rather than terminating at the first mismatch, all characters of the strings are compared.
With bitwise operations: Bitwise operations like XOR can be employed to add the result of comparison without exposing the intermediate results.
Conditional statements avoidance: Conditional statements that would control the flow of execution depending on comparison outcomes are not used.
By following these conventions, constant-time comparison makes the execution time constant in effect, essentially foiling timing attacks.
int constant_time_compare(const char * a, const char * b, size_t length)
{
volatile unsigned char result = 0;
for (size_t i = 0; i < length; i++) {
result |= a[i] ^ b[i];
}
// If result is 0, the strings are equal;
// otherwise, they are not.
return result == 0;
}
Why is it Constant Time?
Fixed Number of Operations
The loop iterates over every byte of the input strings, from the first to the last, without any possibility of exiting early. The loop always executes?length?iterations, performing the same operations (XOR and OR) for each byte. This ensures that the number of operations is constant for a given input length.
No Data-Dependent Branching
There are no conditional branches within the loop that depend on the input data. This prevents variations in execution time based on the content of the strings.
Consistent Memory Access Patterns
The memory access pattern (reading each byte of the strings) is consistent and does not depend on the data, minimizing the risk of timing variations due to cache effects.
Improving the Implementation of constant time execution
Prefetching
Prefetching can be done using compiler-specific intrinsics or pragmas. This is architecture and compiler-dependent, so the implementation may vary.
for (size_t i = 0; i < length; i++)
{
// Prefetch the next cache line
if (i % 64 == 0) { // Assuming a cache line size of 64 bytes
__builtin_prefetch( & a[i + 64], 0, 1);
__builtin_prefetch( & b[i + 64], 0, 1);
}
result |= a[i] ^ b[i];
}
Alignment
Ensuring alignment is typically done at the data allocation level. If you control the allocation of?a?and?b, you can use aligned memory allocation functions.
// Example of aligned allocation (outside the compare function)
char *aligned_alloc(size_t alignment, size_t size);
char *a = aligned_alloc(64, length); // Align to 64-byte boundary
char *b = aligned_alloc(64, length);
Batch Processing
Process data in chunks that fit into the cache. This requires careful handling to maintain constant-time properties.
int constant_time_compare(const char *a, const char *b, size_t length) {
volatile unsigned char result = 0;
size_t chunk_size = 64; // Process in chunks of 64 bytes
for (size_t i = 0; i < length; i += chunk_size) {
for (size_t j = 0; j < chunk_size && (i + j) < length; j++) {
result |= a[i + j] ^ b[i + j];
}
}
return result == 0;
}
Compiler Optimizations
Use compiler flags to control optimizations. This is done outside the code, typically in the build system or makefile.
gcc -fno-tree-vectorize -o compare compare.c
-fno-tree-vectorize:
Limitations and Challenges of Constant-Time Programming
Constant-time programming is a step in the right direction towards defense against timing attacks but also comes with some limitations and challenges. Constant-time implementations of algorithms need to consider performance, complexity, verification, and the compiler. These have been explained in detail below.
1. Overhead in Performance
Why Does Performance Overhead Arise
Constant-time programming tends to incur additional operations to prevent input data dependency on execution time. Some of them are:
Trade-Off Between Performance and Security
Minimizing Performance Overhead
2. Complexity
Why Constant-Time Code is Challenging
Simplifying Complexity
3. Verification
Why Verification is Difficult
Bypassing Verification Issues
4. Compiler Optimizations
Impact of Compiler Optimizations on Constant-Time Code
Preventing Compiler Optimizations
Previously -
https://www.dhirubhai.net/pulse/firmware-security-importance-constant-time-part-1-anupam-datta-hpnwe