Skip to content

security: branch-free modular primitives on decrypt hot path#1177

Open
BAder82t wants to merge 2 commits intodevfrom
security/ct-mod-decrypt
Open

security: branch-free modular primitives on decrypt hot path#1177
BAder82t wants to merge 2 commits intodevfrom
security/ct-mod-decrypt

Conversation

@BAder82t
Copy link
Copy Markdown
Collaborator

Summary

This PR removes data-dependent branches from modular arithmetic used during Decrypt. Branches whose direction depends on secret values can leak information through timing, so this change replaces them with branchless bitmask-based equivalents.

The new helpers also include a small inline-asm barrier for GCC and Clang to make it harder for the optimizer to rewrite the branchless form back into conditional branches at higher optimization levels.

What changes

New header — src/core/include/utils/constanttime.h

Adds four small inline helpers in namespace lbcrypto::ct. Each helper implements the same operation as the original conditional logic, but uses a mask derived from the sign bit or borrow bit instead of a branch.

Helper Replaces
AddIfNeg(x, m) x < 0 ? x + m : x
SubIfGE(x, m) x >= m ? x - m : x
ModSubFast(a, b, m) a - b mod m
SubIfAboveHalf(x, m, halfQ) centered-lift correction when x > halfQ

Call sites — src/core/include/math/hal/intnat/ubintnat.h

The existing public functions are preserved; only their internals change.

  • ModAddFast / ModAddFastEq now call ct::SubIfGE
  • ModSubFast / ModSubFastEq now call ct::ModSubFast
  • ModMulFastConst / ModMulFastConstEq now call ct::AddIfNeg

NTT butterflies — src/core/include/math/hal/intnat/transformnat-impl.h

The NTT butterflies previously had separate GCC and non-GCC implementations, each with its own conditional fixups such as:

if (hiVal >= modulus) hiVal -= modulus;
if (loVal < omegaFactor) loVal += modulus;

Replace the final sign-correcting ternary in ModMulFastConstEq with an
arithmetic-shift bitmask select, and add an inline-asm compiler barrier
on GCC/Clang to prevent aggressive optimization (notably -Ofast) from
re-folding the sequence back into a conditional branch.

ModMulFastConstEq is reached during decryption through
DCRTPolyImpl::ScaleAndRound, operating on secret-key-derived values.
A data-dependent branch at this point exposes a timing side channel on
targets without reliable branch predictors or under observation by a
co-resident attacker (SMT siblings, external power analysis). The
replacement computes

    signmask  = yprime >> (word_bits - 1)      // arithmetic shift
    corrected = yprime + (signmask & modulus)

which is the canonical branch-free sign-correction idiom. Correctness
is exactly equivalent for all valid yprime in the original range.

The compiler barrier follows the methodology demonstrated in the
PermNet-RM Reed-Muller encoder (Issaei, "PermNet-RM: The Unmasked
Butterfly"), which shows that without an opaque barrier, GCC -Ofast can
reintroduce the branch via value-range propagation.

Verified:
  - Existing core_tests (UTBinInt, UTBinVect::ModMulTest) pass.
  - Existing pke_tests (UTBFVRNS_DECRYPT 41 cases, UTCKKSRNS, etc.,
    58 tests across 5 suites) pass.
  - arithmetic correctness unchanged for the full signed-int input
    range.

Follow-up work (separate PR) will add a CI disassembly-grep harness
covering -O0..-Ofast and a ctgrind-based decrypt timing test.
Add utils/constanttime.h providing four branch-free helpers:
  - AddIfNeg(x, m)       : x + m if x < 0,  else x      (signed)
  - SubIfGE(x, m)        : x - m if x >= m, else x      (unsigned)
  - ModSubFast(a, b, m)  : (a - b) mod m, operands in [0, m)
  - SubIfAboveHalf(x, m, halfQ) : centered-lift below halfQ

Each helper is a small inline template: sign-mask/borrow-bit select
plus an inline-asm compiler barrier on GCC/Clang. Methodology follows
the PermNet-RM constant-time encoder (Issaei 2026), which demonstrates
that without an explicit barrier, aggressive optimization (notably
-Ofast) can reconstruct a data-dependent branch via value-range
propagation on constructs like `x >= 0 ? x : x + m`.

Refactor call sites in the decrypt hot path to use these helpers:

  ubintnat.h:
    - ModMulFastConst / ModMulFastConstEq     (sign correction)
    - ModAddFast / ModAddFastEq                (Barrett tail)
    - ModSubFast / ModSubFastEq                (borrow-guarded subtract)

  transformnat-impl.h (Cooley-Tukey & Gentleman-Sande butterflies):
    - ForwardTransformIterative
    - ForwardTransformToBitReverseInPlace    (mu-based and precon-based)
    - ForwardTransformToBitReverse           (mu-based and precon-based)
    - InverseTransformFromBitReverseInPlace  (mu-based and precon-based,
      including peeled first and final stages)

The NTT butterflies previously split into `#if defined(__GNUC__) &&
!defined(__clang__)` vs `#else` branches; both branches carried their
own conditionals. This consolidation replaces all of them with the
same branchless primitives, removing ~140 lines of duplicated logic
while making the constant-time guarantee uniform across compilers.

A data-dependent `if (omegaFactor != zero)` skip in two NTT variants
is also removed: the skip was an optimization that branched on a
secret-derived coefficient, and eliminating it is required for
constant-time behavior.

Verified on this branch:
  - core_tests              : 158/158 pass (17 suites, including
                              UTBinInt, UTBinVect::ModMulTest, UTNTT,
                              UTTrapdoor, Utilities)
  - pke_tests (decrypt-filtered) : 59/59 pass (6 suites, including
                              UTBFVRNS_DECRYPT 41 cases, CKKSRNS,
                              BGVRNS decrypt paths)

Follow-up (separate PR) will add a CI harness that disassembles the
decrypt object files across -O0..-Ofast and fails on any conditional-
jump mnemonic inside whitelisted functions, mirroring the PermNet-RM
CI approach.
@pascoec pascoec marked this pull request as draft April 29, 2026 20:33
@pascoec pascoec changed the base branch from main to dev April 29, 2026 20:35
@pascoec pascoec marked this pull request as ready for review April 29, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant