Bignum arithmetic notes / 12

Constant-time C and security boundaries

The arithmetic theorem says what value is computed. The side-channel theorem asks what the execution trace reveals.

A bignum function used on secrets should have control flow and memory access independent of secret data. This is a stricter requirement than mathematical correctness.

The same leakage contract reappears in elliptic scalar multiplication and side-channel safe group operations, where secret scalar bits control point operations unless the implementation prevents it.

C hazards

Hazard Rule
signed overflow never use signed types for limb arithmetic
shift by word size invalid in C, even for unsigned types
strict aliasing avoid type-punning limb arrays
variable-length secret loops fix loop bounds by public sizes
secret table index scan and mask-select
early exit comparison only for public inputs

Example: bad comparison

int bn_cmp_public(const limb_t *a, const limb_t *b, uint32_t n) {
    for (uint32_t i = n; i-- > 0;) {
        if (a[i] > b[i]) return 1;
        if (a[i] < b[i]) return -1;
    }
    return 0;
}

This is fine for public moduli or public encodings. It is not fine if a and b are secret values, because the first differing limb leaks.

Example: masked table lookup

static uint32_t ct_eq_u32(uint32_t x, uint32_t y) {
    uint32_t z = x ^ y;
    z |= 0u - z;
    return (z >> 31) ^ 1u;
}

void table_select(limb_t *r, const limb_t *table,
                  uint32_t rows, uint32_t n, uint32_t idx) {
    for (uint32_t j = 0; j < n; j++) r[j] = 0;
    for (uint32_t i = 0; i < rows; i++) {
        uint32_t eq = ct_eq_u32(i, idx);
        uint32_t mask = 0u - eq;
        for (uint32_t j = 0; j < n; j++) {
            r[j] = (limb_t)(r[j] | (table[i*n + j] & mask));
        }
    }
}

Precondition: idx < rows, rows is public, and idx has already been reduced to the table range. Out-of-range indices must be rejected or normalized before this helper is called. The source has fixed memory traversal. Whether the compiler preserves the intended constant-time behavior is a toolchain and audit question.

Undefined behavior boundary

Unsigned arithmetic wraps modulo \(2^w\). That is defined. But these are undefined or invalid:

int32_t s = INT32_MAX;
s = s + 1;          /* undefined */
uint32_t x = 1;
x <<= 32;           /* invalid shift when width is 32 */

A proof over \(\mathbb Z/2^w\mathbb Z\) only applies to C operations that actually implement that ring.

Compiler boundary

Compilers optimize under the C abstract machine, not the cryptographic leakage model. A source-level branchless idiom can still compile into a branch on some targets, and a memory wipe can be removed if the compiler proves the bytes are dead.

Use disassembly, constant-time analysis tools, or verified primitives for high-assurance deployments.

API design

Separate functions by leakage contract:

int bn_cmp_public(const limb_t *a, const limb_t *b, uint32_t n);
void bn_sub_secret(limb_t *r, const limb_t *a, const limb_t *b, uint32_t n);
void bn_select_secret(limb_t *r, const limb_t *x, const limb_t *y, uint32_t n, uint32_t bit);

Names should make misuse harder. Do not hide variable-time behavior behind a neutral name such as bn_cmp when callers may pass secrets.

Mathematical correctness is not enough

If RSA exponentiation branches on private exponent bits, the output is still \(m^d\bmod N\). The arithmetic theorem is true, while the implementation leaks the exponent. Cryptographic correctness is a conjunction of value correctness, input validation, randomness conditions, and leakage constraints.