krypteia-silentops 0.2.0

Side-channel countermeasure toolkit: constant-time primitives, dudect-style timing leakage verifier, and shared SCA helpers for the krypteia workspace.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Cédric Mesnil <cslashm@pm.me>

//! ARMv6-M Thumb constant-time primitives (Cortex-M0/M0+).
//!
//! The M0/M0+ has NO IT blocks, NO conditional execution, NO csel.
//! Only 16-bit Thumb instructions + a few 32-bit (BL, MRS, MSR).
//!
//! Constant-time selection must use pure bitwise operations:
//!   result = (a & mask) | (b & ~mask)
//! where mask is all-ones or all-zeros derived from the condition.
//!
//! Targets: STM32F0, STM32L0, Raspberry Pi Pico (RP2040), some Secure Elements.

use core::arch::asm;

/// Constant-time select for Cortex-M0/M0+ (ARMv6-M: no IT blocks, no cmov).
///
/// Branch-free mask select `result = b ^ ((a ^ b) & mask)`, where
/// `mask = 0xFFFFFFFF` iff `condition != 0` and `0x0` otherwise. ARMv6-M
/// (thumb1) has only 2-operand data-processing forms (`ANDS/EORS Rdn, Rm`),
/// so the 3-operand `ands/bics/orrs` variants — which require thumb2 and did
/// not assemble on M0 — are replaced by the `mov`/`eors`/`ands` sequence
/// below. Constant instruction count; no branch, no conditional execution, no
/// data-dependent memory access.
#[inline(always)]
pub fn ct_select_u8(a: u8, b: u8, condition: u8) -> u8 {
    let result: u32;
    unsafe {
        asm!(
            // Build mask: all-ones if condition != 0, all-zeros if == 0.
            "negs {mask}, {cond}",        // mask = -condition
            "asrs {mask}, {mask}, #31",   // mask = 0x00000000 or 0xFFFFFFFF
            // Select: out = b ^ ((a ^ b) & mask)  (all 2-operand thumb1 ops).
            "mov  {t}, {a}",              // t = a
            "eors {t}, {b}",              // t = a ^ b
            "ands {t}, {mask}",           // t = (a ^ b) & mask
            "mov  {out}, {b}",            // out = b
            "eors {out}, {t}",            // out = b ^ ((a ^ b) & mask)
            a = in(reg) a as u32,
            b = in(reg) b as u32,
            cond = in(reg) condition as u32,
            mask = out(reg) _,
            t = out(reg) _,
            out = out(reg) result,
            options(pure, nomem, nostack),
        );
    }
    result as u8
}

/// Constant-time select for i16 (NTT polynomial coefficients).
#[inline(always)]
pub fn ct_select_i16(a: i16, b: i16, condition: u8) -> i16 {
    let result: u32;
    unsafe {
        asm!(
            "negs {mask}, {cond}",
            "asrs {mask}, {mask}, #31",
            "mov  {t}, {a}",
            "eors {t}, {b}",
            "ands {t}, {mask}",
            "mov  {out}, {b}",
            "eors {out}, {t}",
            a = in(reg) a as u32,
            b = in(reg) b as u32,
            cond = in(reg) condition as u32,
            mask = out(reg) _,
            t = out(reg) _,
            out = out(reg) result,
            options(pure, nomem, nostack),
        );
    }
    result as i16
}

/// Constant-time select for i32 (ML-DSA coefficients).
#[inline(always)]
pub fn ct_select_i32(a: i32, b: i32, condition: u8) -> i32 {
    let result: u32;
    unsafe {
        asm!(
            "negs {mask}, {cond}",
            "asrs {mask}, {mask}, #31",
            "mov  {t}, {a}",
            "eors {t}, {b}",
            "ands {t}, {mask}",
            "mov  {out}, {b}",
            "eors {out}, {t}",
            a = in(reg) a as u32,
            b = in(reg) b as u32,
            cond = in(reg) condition as u32,
            mask = out(reg) _,
            t = out(reg) _,
            out = out(reg) result,
            options(pure, nomem, nostack),
        );
    }
    result as i32
}

/// Constant-time equality of two `u32`s: returns 1 if `a == b`, 0 otherwise.
#[inline(always)]
pub fn ct_eq_u32(a: u32, b: u32) -> u8 {
    let diff = a ^ b;
    let mask = (diff | diff.wrapping_neg()) >> 31;
    ((mask as u8) ^ 1) & 1
}

/// Constant-time equality: returns 1 if `a == b` (all bytes), 0 otherwise.
///
/// No early exit — always processes all bytes.
#[inline(never)]
pub fn ct_eq(a: &[u8], b: &[u8]) -> u8 {
    if a.len() != b.len() {
        return 0;
    }
    let mut diff = 0u8;
    for i in 0..a.len() {
        diff |= a[i] ^ b[i];
    }
    // Branchless: (diff == 0) → 1, else → 0
    // Use: result = 1 - min(diff, 1) via shift
    let d32 = diff as u32;
    let result: u32;
    unsafe {
        asm!(
            "negs {t}, {d}",       // t = -d
            "orrs {t}, {d}",       // t = (-d) | d  (MSB set iff d != 0); 2-operand thumb1
            "lsrs {t}, {t}, #31",  // t = 0 if d==0, 1 if d!=0
            "movs {out}, #1",
            "subs {out}, {out}, {t}", // out = 1 - t
            d = in(reg) d32,
            t = out(reg) _,
            out = out(reg) result,
            options(pure, nomem, nostack),
        );
    }
    result as u8
}

/// Constant-time conditional copy: if `condition != 0`, copy `src` to `dst`.
///
/// If `condition == 0`, `dst` is untouched. Always reads both `src` and `dst`
/// (no address-dependent memory access).
#[inline(never)]
pub fn ct_copy(dst: &mut [u8], src: &[u8], condition: u8) {
    let len = dst.len().min(src.len());
    for i in 0..len {
        dst[i] = ct_select_u8(src[i], dst[i], condition);
    }
}

/// Constant-time slice select: writes `a` to `out` if `condition != 0`,
/// else writes `b`. Both `a` and `b` are read in full byte-by-byte; no
/// secret-dependent control flow or memory access. Length used is
/// `out.len().min(a.len()).min(b.len())`.
#[inline(never)]
pub fn ct_select_bytes(out: &mut [u8], a: &[u8], b: &[u8], condition: u8) {
    let len = out.len().min(a.len()).min(b.len());
    for i in 0..len {
        out[i] = ct_select_u8(a[i], b[i], condition);
    }
}

/// Secure zeroization: writes zeros that cannot be elided by the optimizer.
///
/// Uses `core::ptr::write_volatile` and a compiler fence.
#[inline(never)]
pub fn ct_zeroize(buf: &mut [u8]) {
    for byte in buf.iter_mut() {
        unsafe { core::ptr::write_volatile(byte, 0) };
    }
    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}

/// Secure zeroization for i16 slices (polynomial coefficients).
#[inline(never)]
pub fn ct_zeroize_i16(buf: &mut [i16]) {
    for val in buf.iter_mut() {
        unsafe { core::ptr::write_volatile(val, 0) };
    }
    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}