Full Report
Cryptography is a fragile beast. It's powerful but can break with any small mistakes. One of these "small mistakes" is around comparisons. If one operation takes a different amount of time than another, this leaks some information. In the case of cryptography, this can lead to complete breakage. Because of this, a lot of cryptography is constant-time - meaning that all code-paths should take the same amount of time. This constant-time thing is great! However, the code you write is different than the code that is executed. Sometimes, the compiler will optimize these constant-time operations away. Compilers will remove redundant operations, vectorized loops and restructure algorithms to be faster. Naturally, this is the enemy of constant-time cryptography code. In a recent research paper, 19 libraries across five compilers added vulnerabilities. So, what's the solution to the optimization issue? This is what the article discusses. in LLVM, they added __builtin_ct_select to a family of intrinsic. This function contains a special intermediate representation that says "this must be constant-time" and "do not do optimizations on this". According to the authors, this fixed almost all of the constant-time issues discussed in the paper. Many cryptography libraries hand-write assembly code to prevent optimizations. Of the features that don't, some were still vulnerable. Several projects have expressed strong interest in using this feature instead of inline assembly or hoping/praying the optimizer doesn't add a bug. How does the constant-time work on different platforms? On x86-64, cmov is used for conditional moves. On i386, which doesn't have this instruction, masked arithmetic is performed with bitwise operations. On ARM, CSEL is used. On AAarch64, masked arithmetic is performed with bitwise operations. On all other architectures, bitwise arithmetic is ton. Overall, a great blog post and addition to the LLVM compiler. Great work on this!
Analysis Summary
# Best Practices: Protecting Cryptographic Code with Constant-Time Intrinsics
## Overview
These practices address **timing side-channel attacks**, where a compiler’s optimization of source code introduces branching or non-constant execution times based on secret data. Even if a developer writes "constant-time" C code, the compiler may optimize it into variable-time machine code (e.g., converting a bitwise mask into a conditional jump). These recommendations leverage the new LLVM `__builtin_ct_select` hardware-backed primitives to ensure cryptographic operations remain secure after compilation.
## Key Recommendations
### Immediate Actions
1. **Identify Secret-Dependent Branches:** Audit cryptographic implementations for code where execution flow (if/else) or memory access (array indexing) depends on secret keys or private data.
2. **Verify Compiler Versions:** Determine if your toolchain uses LLVM (Clang). Prepare to upgrade to **LLVM 22** (upcoming release) to access native constant-time intrinsics.
3. **Replace Hand-Written Assembly:** Where applicable, identify complex inline assembly used to bypass optimizers and mark those sections for replacement with safer, more maintainable intrinsics.
### Short-term Improvements (1-3 months)
1. **Integrate `__builtin_ct_select`:** Replace manual bitwise masking and ternary operators (`? :`) applied to secrets with the new intrinsic to force the compiler to use constant-time CPU instructions (like `cmov` or `csel`).
2. **CI/CD Pipeline Validation:** Incorporate Binary Analysis tools to check that sensitive code paths do not contain conditional branches (`jcc` on x86) in the final binary.
### Long-term Strategy (3+ months)
1. **Standardize Cryptographic Primitives:** Move toward a centralized library of primitives (e.g., `ct_lookup`, `ct_compare`) that use LLVM intrinsics to ensure uniformity across the organization.
2. **Monitor Language Support:** For non-C projects, track the adoption of these LLVM primitives in Rust (`core::intrinsics`) or Swift to migrate away from less reliable optimization barriers like `optnone`.
## Implementation Guidance
### For Small Organizations
- **Focus on Libraries:** Rather than writing custom crypto, use well-vetted libraries (like *boringssl* or *ring*) that are already adopting these LLVM safeguards.
- **Update Compilers:** Ensure developers are not using outdated, unpatched versions of Clang/LLVM.
### For Medium Organizations
- **Refactor Vulnerable Patterns:** Use `__builtin_ct_select` specifically for secret-dependent table lookups and conditional assignments.
- **Training:** Educate developers on why "readable" C code is often transformed into "insecure" machine code by the optimizer.
### For Large Enterprises
- **Enforce Policy via Intrinsics:** Mandate the use of `__builtin_ct_expr` for all expressions involving private keys to create a compiler-enforced "safety zone."
- **Architectural Cross-Platform Review:** Validate that the compiler is correctly lowering these intrinsics to the appropriate hardware instructions (e.g., `CSEL` for ARM, `cmov` for x86) across all supported deployment environments.
## Configuration Examples
### Constant-Time Selection
Instead of using a standard ternary operator which the compiler may turn into a branch:
c
// INSECURE: Compiler may optimize to a branch
uint64_t val = secret_condition ? a : b;
// SECURE: Forced constant-time selection using LLVM intrinsic
uint64_t val = __builtin_ct_select(secret_condition, a, b);
### Constant-Time Lookups
To perform a table lookup without leaking the index via power or timing analysis:
c
// Example of a secure lookup pattern using the new intrinsic
uint64_t result = 0;
for (size_t i = 0; i < 16; i++) {
result = __builtin_ct_select(i == secret_idx, table[i], result);
}
## Compliance Alignment
- **NIST FIPS 140-3:** Supports requirements for side-channel resistance in cryptographic modules.
- **ISO/IEC 19790:** Relevant for security requirements for cryptographic modules regarding non-invasive attack mitigation.
- **CWE-385:** Directly addresses "Information Exposure Through Timing Discrepancy."
## Common Pitfalls to Avoid
- **Over-reliance on "Volatile":** Using `volatile` or inline assembly "memory" clobbers is often insufficient and can lead to fragile code that breaks when compiler versions change.
- **Trusting the Ternary:** Never assume `(condition ? a : b)` is constant-time; modern compilers almost always optimize this into a conditional jump if they perceive a performance gain.
- **Ignoring Architecture:** Being unaware that certain old architectures (like i386) do not have `cmov` instructions; ensure your compiler is using bitwise masking fallbacks for these targets.
## Resources
- **LLVM Project:** `https[:]//github[.]com/llvm/llvm-project`
- **Trail of Bits Technical RFC:** `https[:]//discourse[.]llvm[.]org/t/rfc-constant-time-coding-support/87781`
- **Breaking Bad Research (ETH Zürich):** `https[:]//arxiv[.]org/pdf/2410.13489.pdf`