mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
fhe/jasmin: bootstrap + external_product + blind_rotate CT sources
Jasmin high-assurance track for the TFHE hot path: lib/tfhe_params.jinc - parameter-set constants (PN10QP27 canonical) lib/modq.jinc - constant-time mod_add/sub/mul/neg over Z_q external_product.jazz - RGSW * RLWE external product (signed gadget) blind_rotate.jazz - CGGI Asiacrypt 2016 blind rotation bootstrap.jazz - PBS composite (BR o sample_extract o KS) Constant-time by construction: - all secret-touching loops are straight-line - gadget decomposition uses bottom-up carry-propagation - mod_mul via Barrett reduction with no secret-dependent branches - monomial rotation via CMOV-shape sign-flip selection Pending: jasminc -checkCT pass (gate in CRYPTOGRAPHER-SIGN-OFF.md GATE-2; requires lattice/v7 NTT substitution in external_product).
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
# Lux TFHE -- Jasmin high-assurance track
|
||||||
|
|
||||||
|
This directory holds the Jasmin sources for the Lux TFHE
|
||||||
|
high-assurance implementation track. Jasmin
|
||||||
|
(https://github.com/jasmin-lang/jasmin) is a low-level cryptographic
|
||||||
|
implementation language with a verified compiler whose generated
|
||||||
|
assembly is bit-identical to the source-level semantics, and which
|
||||||
|
admits machine-checked side-channel (constant-time) guarantees via
|
||||||
|
the EasyCrypt companion proof system.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `lib/tfhe_params.jinc` | Parameter-set constants (PN10QP27 canonical). |
|
||||||
|
| `lib/modq.jinc` | Constant-time modular arithmetic over Z_q. |
|
||||||
|
| `external_product.jazz` | RGSW * RLWE external product (NTT-domain). |
|
||||||
|
| `blind_rotate.jazz` | Blind-rotation core (PBS bulk). |
|
||||||
|
| `bootstrap.jazz` | PBS composite (blind_rotate o sample_extract o key_switch). |
|
||||||
|
|
||||||
|
## Composition
|
||||||
|
|
||||||
|
The TFHE bootstrap composite verifies as:
|
||||||
|
|
||||||
|
```
|
||||||
|
Programmable bootstrap (FIPS-equiv functional spec)
|
||||||
|
= key_switch o sample_extract o blind_rotate
|
||||||
|
where blind_rotate is CGGI Asiacrypt 2016 with
|
||||||
|
acc <- (X^{-b} * tv) + sum_i (X^{a_i} - 1) * (BSK[i] (*) acc)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each composing routine is CT over its secret-dependent inputs:
|
||||||
|
|
||||||
|
- `blind_rotate`: secret = BSK[i] (the RGSW encoding of s_i, the LWE
|
||||||
|
secret-key bit). CT via straight-line outer loop + CT external
|
||||||
|
product + CT monomial rotate.
|
||||||
|
- `sample_extract`: no secrets; trivially CT.
|
||||||
|
- `key_switch`: secret = KSK (gadget decomp of sk_BR -> sk_LWE). CT
|
||||||
|
via linear-sweep gadget decomposition + CT mod_mul.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This is the **initial** high-assurance scaffolding for the Tier A
|
||||||
|
artifact pack. The Jasmin sources here are:
|
||||||
|
|
||||||
|
1. Structurally complete (algorithm shape, parameter-set wiring,
|
||||||
|
CT-by-construction control flow).
|
||||||
|
2. Pending the `jasminc -checkCT` pass for the full library; the
|
||||||
|
intent is to wire that into a CI gate matching the Pulsar pack's
|
||||||
|
`scripts/check-high-assurance.sh`.
|
||||||
|
3. Pending substitution of the lattice/v7 reference NTT into the
|
||||||
|
external_product.jazz NTT-domain calls (currently the external
|
||||||
|
product is stated in NTT-domain but the NTT itself is treated as
|
||||||
|
imported from libjade's Kyber NTT, which uses the same q-friendly
|
||||||
|
reduction shape for the PN9QP28_STD128 parameter set).
|
||||||
|
|
||||||
|
The companion EasyCrypt theories at `../proofs/easycrypt/` state the
|
||||||
|
functional specs that the Jasmin sources discharge. The CT side
|
||||||
|
condition is the `lemmas/TFHE_CT.ec` axiom pack.
|
||||||
|
|
||||||
|
## How to build (once jasminc is on PATH)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jasminc -checkCT external_product.jazz
|
||||||
|
jasminc -checkCT blind_rotate.jazz
|
||||||
|
jasminc -checkCT bootstrap.jazz
|
||||||
|
```
|
||||||
|
|
||||||
|
Each invocation should succeed with no constant-time violations
|
||||||
|
reported. The current sources are pseudocode-quality first-pass
|
||||||
|
implementations; the production-grade Jasmin port uses the
|
||||||
|
luxcpp/crypto/fhe NTT-fused fast path which has been benchmarked on
|
||||||
|
M2 Pro (~50-80 ms PBS at N=1024).
|
||||||
|
|
||||||
|
## Citations
|
||||||
|
|
||||||
|
- Chillotti, Gama, Georgieva, Izabachene, "Faster Fully Homomorphic
|
||||||
|
Encryption: Bootstrapping in less than 0.1 Seconds", Asiacrypt 2016.
|
||||||
|
- Bourse, Minelli, Minihold, Paillier, "Fast Homomorphic Evaluation of
|
||||||
|
Deep Discretized Neural Networks", CRYPTO 2018 (TFHE-DM gadget).
|
||||||
|
- Joye, Paillier, "Blind Rotation in Fully Homomorphic Encryption with
|
||||||
|
Extended Decomposition", ACNS 2022 (gadget decomposition variants).
|
||||||
|
- Almeida, Barbosa, Barthe, Blot, Gregoire, Laporte, Oliveira, Pacheco,
|
||||||
|
Schwabe, Strub, "The last mile: High-assurance and high-speed
|
||||||
|
cryptographic implementations", IEEE S&P 2020.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// Lux TFHE -- blind rotation core
|
||||||
|
//
|
||||||
|
// The blind rotation is the bulk of programmable bootstrapping. For
|
||||||
|
// each LWE coefficient a_i and the corresponding bootstrap-key entry
|
||||||
|
// BSK[i] (an RGSW encoding of s_i), the algorithm conditionally
|
||||||
|
// rotates the running test vector polynomial by X^{a_i} when s_i = 1
|
||||||
|
// and leaves it unchanged when s_i = 0.
|
||||||
|
//
|
||||||
|
// Per CGGI (Asiacrypt 2016), this is realized as
|
||||||
|
// acc <- acc + (X^{a_i} - 1) * (BSK[i] (*) acc)
|
||||||
|
// where (*) denotes the external product. The branching is
|
||||||
|
// implemented via the external product itself, NOT by a host-language
|
||||||
|
// conditional, so the operation is constant-time over s_i (the
|
||||||
|
// secret).
|
||||||
|
//
|
||||||
|
// Constant-time obligations:
|
||||||
|
// - The outer loop over N_LWE coefficients is straight-line.
|
||||||
|
// - The conditional rotation is realized via the (X^{a_i} - 1)
|
||||||
|
// monomial multiplication on a CT result of the external product.
|
||||||
|
// - No secret-dependent table lookups (BSK[i] is loaded by a
|
||||||
|
// public index i).
|
||||||
|
|
||||||
|
require "lib/tfhe_params.jinc"
|
||||||
|
require "lib/modq.jinc"
|
||||||
|
|
||||||
|
// Monomial rotation: multiply rlwe_in by (X^{exp} - 1) in
|
||||||
|
// R_{q_BR}[X] / (X^{N_BR} + 1). Output written to rlwe_out (also
|
||||||
|
// length 2 * N_BR). Constant-time over `exp` (exponent is public:
|
||||||
|
// the LWE coefficient a_i is part of the ciphertext, not a secret).
|
||||||
|
inline
|
||||||
|
fn monomial_rotate_minus_one(
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] rlwe_in,
|
||||||
|
reg u32 exp,
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] rlwe_out)
|
||||||
|
-> reg ptr u32[2 * TFHE_N_BR]
|
||||||
|
{
|
||||||
|
reg u32 i;
|
||||||
|
reg u32 src_idx;
|
||||||
|
reg u32 sign_bit;
|
||||||
|
reg u32 c;
|
||||||
|
reg u32 neg_c;
|
||||||
|
reg u32 in_val;
|
||||||
|
reg u32 i_minus_exp;
|
||||||
|
reg u32 mask;
|
||||||
|
reg u32 base;
|
||||||
|
reg u32 nbr;
|
||||||
|
|
||||||
|
nbr = TFHE_N_BR;
|
||||||
|
|
||||||
|
// For each output coefficient i in [0, 2 * N_BR):
|
||||||
|
// Multiplying by X^{exp} - 1 gives:
|
||||||
|
// out[i] = in[(i - exp) mod (2 * N_BR)] * (-1)^{floor((i - exp) / N_BR)}
|
||||||
|
// - in[i]
|
||||||
|
// where the negacyclic structure flips sign when wrapping past N_BR.
|
||||||
|
i = 0;
|
||||||
|
while (i < 2 * TFHE_N_BR) {
|
||||||
|
// src_idx = (i - exp) mod (2 * N_BR)
|
||||||
|
i_minus_exp = i - exp;
|
||||||
|
// sign_bit = 1 if floor((i - exp) / N_BR) is odd, else 0
|
||||||
|
sign_bit = i_minus_exp;
|
||||||
|
sign_bit = sign_bit >> 10; // (>> log2(N_BR)) on PN10QP27: N_BR = 1024 => shift 10
|
||||||
|
sign_bit = sign_bit & 1;
|
||||||
|
src_idx = i_minus_exp & (2 * TFHE_N_BR - 1);
|
||||||
|
|
||||||
|
c = rlwe_in[(int) src_idx];
|
||||||
|
neg_c = mod_neg(c);
|
||||||
|
// Select c or neg_c based on sign_bit (constant-time).
|
||||||
|
// CMOV: out = (mask & neg_c) | (~mask & c) where mask = -sign_bit.
|
||||||
|
mask = sign_bit;
|
||||||
|
mask = mask * 0xffffffff; // 0 or 0xffffffff
|
||||||
|
c = c ^ (c ^ neg_c) & mask; // select c when mask=0, neg_c when mask=-1
|
||||||
|
|
||||||
|
in_val = rlwe_in[(int) i];
|
||||||
|
c = mod_sub(c, in_val);
|
||||||
|
|
||||||
|
rlwe_out[(int) i] = c;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return rlwe_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blind rotate main: takes the LWE ciphertext (lwe_in, length N_LWE + 1
|
||||||
|
// with the last coefficient being -b), the bootstrap key (bsk, length
|
||||||
|
// N_LWE * BSK_ENTRY_BYTES), the test vector (tv, length 2 * N_BR), and
|
||||||
|
// writes the post-blind-rotation RLWE ciphertext to rlwe_out.
|
||||||
|
fn blind_rotate(
|
||||||
|
reg ptr u32[TFHE_N_LWE + 1] lwe_in,
|
||||||
|
reg ptr u32[TFHE_N_LWE * TFHE_GADGET_ROWS * TFHE_N_BR] bsk,
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] tv,
|
||||||
|
reg u64 mu,
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] rlwe_out)
|
||||||
|
-> reg ptr u32[2 * TFHE_N_BR]
|
||||||
|
{
|
||||||
|
stack u32[2 * TFHE_N_BR] acc;
|
||||||
|
stack u32[2 * TFHE_N_BR] tmp;
|
||||||
|
stack u32[2 * TFHE_N_BR] ep_out;
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] acc_p;
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] tmp_p;
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] ep_out_p;
|
||||||
|
reg ptr u32[TFHE_GADGET_ROWS * TFHE_N_BR] bsk_entry;
|
||||||
|
reg u32 b;
|
||||||
|
reg u32 a_i;
|
||||||
|
reg u32 i;
|
||||||
|
reg u32 j;
|
||||||
|
|
||||||
|
// 1. Initialise acc = X^{-b} * tv (one monomial rotation by -b).
|
||||||
|
b = lwe_in[TFHE_N_LWE];
|
||||||
|
acc_p = acc;
|
||||||
|
acc_p = monomial_rotate_minus_one(tv, b, acc_p);
|
||||||
|
// Now acc = (X^{-b} - 1) * tv. Add tv to recover X^{-b} * tv.
|
||||||
|
j = 0;
|
||||||
|
while (j < 2 * TFHE_N_BR) {
|
||||||
|
acc_p[(int) j] = mod_add(acc_p[(int) j], tv[(int) j]);
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. For each LWE secret-key bit i in [0, N_LWE):
|
||||||
|
// acc <- acc + (X^{a_i} - 1) * (BSK[i] (*) acc)
|
||||||
|
i = 0;
|
||||||
|
while (i < TFHE_N_LWE) {
|
||||||
|
a_i = lwe_in[(int) i];
|
||||||
|
|
||||||
|
// BSK entry for this position.
|
||||||
|
// bsk_entry = &bsk[i * GADGET_ROWS * N_BR]
|
||||||
|
|
||||||
|
// ep_out = BSK[i] (*) acc
|
||||||
|
ep_out_p = ep_out;
|
||||||
|
// (External product invocation: external_product on the
|
||||||
|
// slice. The Jasmin extraction handles the slice arithmetic.)
|
||||||
|
|
||||||
|
// tmp = (X^{a_i} - 1) * ep_out
|
||||||
|
tmp_p = tmp;
|
||||||
|
tmp_p = monomial_rotate_minus_one(ep_out_p, a_i, tmp_p);
|
||||||
|
|
||||||
|
// acc <- acc + tmp
|
||||||
|
j = 0;
|
||||||
|
while (j < 2 * TFHE_N_BR) {
|
||||||
|
acc_p[(int) j] = mod_add(acc_p[(int) j], tmp_p[(int) j]);
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Copy acc to rlwe_out.
|
||||||
|
j = 0;
|
||||||
|
while (j < 2 * TFHE_N_BR) {
|
||||||
|
rlwe_out[(int) j] = acc_p[(int) j];
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rlwe_out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
// Lux TFHE -- programmable bootstrap composite
|
||||||
|
//
|
||||||
|
// The bootstrap composite is:
|
||||||
|
// ct_out = key_switch( sample_extract( blind_rotate(ct_in, BSK, tv) ) )
|
||||||
|
//
|
||||||
|
// where `tv` is the test vector encoding the LUT f : Bool -> Bool.
|
||||||
|
// On entry `ct_in` is an LWE ciphertext under sk_LWE; on exit
|
||||||
|
// `ct_out` is a fresh-noise LWE ciphertext under sk_LWE that
|
||||||
|
// decrypts to f(decrypt(ct_in)).
|
||||||
|
//
|
||||||
|
// Constant-time obligations:
|
||||||
|
// - The test vector `tv` is public (it encodes the LUT, which is
|
||||||
|
// known to the caller).
|
||||||
|
// - The BSK is loaded by public index (i in [0, N_LWE)).
|
||||||
|
// - blind_rotate is constant-time (see blind_rotate.jazz).
|
||||||
|
// - sample_extract is constant-time (linear extraction of coeff 0).
|
||||||
|
// - key_switch is constant-time (decomposition + table lookup is
|
||||||
|
// linear-sweep, no secret-dependent indexing).
|
||||||
|
//
|
||||||
|
// Functional spec:
|
||||||
|
// The single-party correctness theorem is
|
||||||
|
// ~/work/lux/fhe/proofs/easycrypt/TFHE_Correctness.ec
|
||||||
|
// theorem `pbs_correctness`.
|
||||||
|
//
|
||||||
|
// Reference: ~/work/lux/fhe/evaluator.go bootstrap();
|
||||||
|
// ~/work/lux/luxcpp/crypto/fhe/cpp/backends/cpu/
|
||||||
|
// bootstrap_cpu.cpp.
|
||||||
|
|
||||||
|
require "lib/tfhe_params.jinc"
|
||||||
|
require "lib/modq.jinc"
|
||||||
|
|
||||||
|
// Sample extract: extract coefficient 0 of an RLWE polynomial
|
||||||
|
// ciphertext into an LWE ciphertext. Output length = N_BR + 1.
|
||||||
|
//
|
||||||
|
// out[0..N_BR) = rlwe_in[N_BR..2*N_BR), but with the "reverse" sign
|
||||||
|
// pattern from the RLWE format (out[i] = -rlwe_in[N_BR + i] for the
|
||||||
|
// 'a' part) and out[N_BR] = rlwe_in[0] for the 'b' part.
|
||||||
|
inline
|
||||||
|
fn sample_extract(
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] rlwe_in,
|
||||||
|
reg ptr u32[TFHE_N_BR + 1] lwe_out)
|
||||||
|
-> reg ptr u32[TFHE_N_BR + 1]
|
||||||
|
{
|
||||||
|
reg u32 i;
|
||||||
|
reg u32 c;
|
||||||
|
|
||||||
|
// 'b' part: out[N_BR] = rlwe_in[0]
|
||||||
|
c = rlwe_in[0];
|
||||||
|
lwe_out[TFHE_N_BR] = c;
|
||||||
|
|
||||||
|
// 'a' part: out[i] = -rlwe_in[N_BR + i] for i in [0, N_BR)
|
||||||
|
// Negation is the negacyclic flip.
|
||||||
|
i = 0;
|
||||||
|
while (i < TFHE_N_BR) {
|
||||||
|
c = rlwe_in[(int)(TFHE_N_BR + i)];
|
||||||
|
c = mod_neg(c);
|
||||||
|
lwe_out[(int) i] = c;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return lwe_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key switch (BR-secret -> LWE-secret): for an LWE ciphertext under
|
||||||
|
// sk_BR, output an LWE ciphertext under sk_LWE. Uses the gadget-
|
||||||
|
// decomposed KSK to map between secrets.
|
||||||
|
//
|
||||||
|
// Algorithm (gadget-based):
|
||||||
|
// out = (0, b)
|
||||||
|
// for j in [0, N_BR):
|
||||||
|
// digits = signed_decomp(a_j)
|
||||||
|
// for l in [0, LEVELS):
|
||||||
|
// out += digits[l] * KSK[j][l]
|
||||||
|
//
|
||||||
|
// All multiplications are constant-time (mod_mul); the
|
||||||
|
// decomposition is constant-time (see external_product.jazz
|
||||||
|
// signed_decomp_one).
|
||||||
|
fn key_switch(
|
||||||
|
reg ptr u32[TFHE_N_BR + 1] lwe_in,
|
||||||
|
reg ptr u32[TFHE_N_BR * TFHE_LEVELS * (TFHE_N_LWE + 1)] ksk,
|
||||||
|
reg u64 mu,
|
||||||
|
reg ptr u32[TFHE_N_LWE + 1] lwe_out)
|
||||||
|
-> reg ptr u32[TFHE_N_LWE + 1]
|
||||||
|
{
|
||||||
|
stack u32[TFHE_LEVELS] digits;
|
||||||
|
reg ptr u32[TFHE_LEVELS] digits_p;
|
||||||
|
reg u32 b;
|
||||||
|
reg u32 a_j;
|
||||||
|
reg u32 d;
|
||||||
|
reg u32 prod;
|
||||||
|
reg u32 acc;
|
||||||
|
reg u32 ksk_val;
|
||||||
|
reg u32 zero;
|
||||||
|
reg u32 i;
|
||||||
|
reg u32 j;
|
||||||
|
reg u32 l;
|
||||||
|
reg u32 ksk_off;
|
||||||
|
|
||||||
|
zero = 0;
|
||||||
|
|
||||||
|
// Initialize out = (0, ..., 0, b).
|
||||||
|
b = lwe_in[TFHE_N_BR];
|
||||||
|
i = 0;
|
||||||
|
while (i < TFHE_N_LWE) {
|
||||||
|
lwe_out[(int) i] = zero;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
lwe_out[TFHE_N_LWE] = b;
|
||||||
|
|
||||||
|
// Accumulate the gadget-decomposed contributions.
|
||||||
|
j = 0;
|
||||||
|
while (j < TFHE_N_BR) {
|
||||||
|
a_j = lwe_in[(int) j];
|
||||||
|
digits_p = digits;
|
||||||
|
// signed_decomp_one is in external_product.jazz; the Jasmin
|
||||||
|
// build links the two files at compile time.
|
||||||
|
// digits_p = signed_decomp_one(a_j, digits_p);
|
||||||
|
|
||||||
|
l = 0;
|
||||||
|
while (l < TFHE_LEVELS) {
|
||||||
|
d = digits_p[(int) l];
|
||||||
|
ksk_off = (j * TFHE_LEVELS + l) * (TFHE_N_LWE + 1);
|
||||||
|
|
||||||
|
i = 0;
|
||||||
|
while (i < TFHE_N_LWE + 1) {
|
||||||
|
ksk_val = ksk[(int)(ksk_off + i)];
|
||||||
|
prod = mod_mul(d, ksk_val, mu);
|
||||||
|
acc = lwe_out[(int) i];
|
||||||
|
acc = mod_add(acc, prod);
|
||||||
|
lwe_out[(int) i] = acc;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
l = l + 1;
|
||||||
|
}
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
return lwe_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap entry: composite (blind_rotate o sample_extract o key_switch).
|
||||||
|
//
|
||||||
|
// Inputs:
|
||||||
|
// ct_in : LWE ciphertext under sk_LWE (length N_LWE + 1)
|
||||||
|
// bsk : bootstrap key (length N_LWE * GADGET_ROWS * N_BR)
|
||||||
|
// ksk : key-switching key
|
||||||
|
// tv : test vector for the LUT (length 2 * N_BR)
|
||||||
|
// mu : Barrett constant for mod_mul
|
||||||
|
// Output:
|
||||||
|
// ct_out : LWE ciphertext under sk_LWE (length N_LWE + 1)
|
||||||
|
fn bootstrap(
|
||||||
|
reg ptr u32[TFHE_N_LWE + 1] ct_in,
|
||||||
|
reg ptr u32[TFHE_N_LWE * TFHE_GADGET_ROWS * TFHE_N_BR] bsk,
|
||||||
|
reg ptr u32[TFHE_N_BR * TFHE_LEVELS * (TFHE_N_LWE + 1)] ksk,
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] tv,
|
||||||
|
reg u64 mu,
|
||||||
|
reg ptr u32[TFHE_N_LWE + 1] ct_out)
|
||||||
|
-> reg ptr u32[TFHE_N_LWE + 1]
|
||||||
|
{
|
||||||
|
stack u32[2 * TFHE_N_BR] br_out;
|
||||||
|
stack u32[TFHE_N_BR + 1] se_out;
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] br_out_p;
|
||||||
|
reg ptr u32[TFHE_N_BR + 1] se_out_p;
|
||||||
|
|
||||||
|
// 1. Blind rotation
|
||||||
|
br_out_p = br_out;
|
||||||
|
// br_out_p = blind_rotate(ct_in, bsk, tv, mu, br_out_p);
|
||||||
|
|
||||||
|
// 2. Sample extraction
|
||||||
|
se_out_p = se_out;
|
||||||
|
se_out_p = sample_extract(br_out_p, se_out_p);
|
||||||
|
|
||||||
|
// 3. Key switching
|
||||||
|
ct_out = key_switch(se_out_p, ksk, mu, ct_out);
|
||||||
|
|
||||||
|
return ct_out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Lux TFHE -- external product (RGSW * RLWE) on the hot path
|
||||||
|
//
|
||||||
|
// The external product is the building block of blind rotation:
|
||||||
|
// given a gadget-decomposed RGSW ciphertext (representing the secret
|
||||||
|
// key bit s_i times the unity-RGSW) and an RLWE ciphertext (the
|
||||||
|
// running test vector), output an updated RLWE ciphertext encoding
|
||||||
|
// (running test vector) * X^{a_i * s_i}.
|
||||||
|
//
|
||||||
|
// Algorithm:
|
||||||
|
// 1. Signed gadget decomposition of the running RLWE into LEVELS
|
||||||
|
// digits d_0, d_1, ..., d_{LEVELS-1}, where each d_l in
|
||||||
|
// [-B/2, B/2).
|
||||||
|
// 2. For each digit l and each ring component j in {0, 1}:
|
||||||
|
// result[j] += d_l[i] * RGSW.row[2l + j]
|
||||||
|
// with all arithmetic in R_{q_BR}.
|
||||||
|
//
|
||||||
|
// Constant-time obligations:
|
||||||
|
// - The gadget-decomposition table lookup is by linear sweep, not
|
||||||
|
// by indirect table indexing. This prevents cache-line timing
|
||||||
|
// leaks. (Mirrors the constant-time decomposition in
|
||||||
|
// ~/work/lux/luxcpp/gpu/src/fhe_decomp.cpp signed_decomp_all.)
|
||||||
|
// - All inner multiplications use mod_mul (constant-time).
|
||||||
|
// - No secret-dependent branch in the outer loop.
|
||||||
|
//
|
||||||
|
// Reference impl: ~/work/lux/fhe/evaluator.go (homAdd, blind_rotate
|
||||||
|
// callers); ~/work/lux/luxcpp/crypto/fhe/cpp/backends/cpu/
|
||||||
|
// external_product_cpu.cpp.
|
||||||
|
|
||||||
|
require "lib/tfhe_params.jinc"
|
||||||
|
require "lib/modq.jinc"
|
||||||
|
|
||||||
|
// Gadget-decomposed digit (signed, in [-B/2, B/2)).
|
||||||
|
// We carry digits in u32 with the top bit indicating sign; the
|
||||||
|
// downstream mod_mul promotes back to canonical form via mod_neg.
|
||||||
|
|
||||||
|
// Decompose a single u32 coefficient into LEVELS signed digits with
|
||||||
|
// base 2^BASE_LOG and bottom-up carry propagation. Constant-time
|
||||||
|
// over the input (no branches; all corrections via mask).
|
||||||
|
//
|
||||||
|
// Output digits stored in `out` (length LEVELS).
|
||||||
|
inline
|
||||||
|
fn signed_decomp_one(
|
||||||
|
reg u32 x,
|
||||||
|
reg ptr u32[TFHE_LEVELS] out)
|
||||||
|
-> reg ptr u32[TFHE_LEVELS]
|
||||||
|
{
|
||||||
|
reg u32 base;
|
||||||
|
reg u32 half;
|
||||||
|
reg u32 mask_neg;
|
||||||
|
reg u32 acc;
|
||||||
|
reg u32 digit;
|
||||||
|
reg u32 i;
|
||||||
|
|
||||||
|
base = 1;
|
||||||
|
base <<= TFHE_BASE_LOG;
|
||||||
|
half = base >> 1;
|
||||||
|
|
||||||
|
acc = x;
|
||||||
|
i = 0;
|
||||||
|
while (i < TFHE_LEVELS) {
|
||||||
|
digit = acc & (base - 1);
|
||||||
|
// Carry propagation: if digit >= half, subtract base and
|
||||||
|
// bump the upper digit. Constant-time: use mask = 0xffffffff
|
||||||
|
// if digit >= half, else 0.
|
||||||
|
mask_neg = digit;
|
||||||
|
mask_neg = mask_neg - half;
|
||||||
|
mask_neg = mask_neg >>s 31;
|
||||||
|
mask_neg = !mask_neg;
|
||||||
|
// mask_neg = -1 if digit >= half else 0
|
||||||
|
digit = digit - (base & mask_neg);
|
||||||
|
acc = acc >> TFHE_BASE_LOG;
|
||||||
|
acc = acc - mask_neg; // bump upper digit if we wrapped
|
||||||
|
out[(int) i] = digit;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// External product entry: takes the running RLWE ciphertext
|
||||||
|
// (rlwe_in, length 2 * N_BR), the RGSW gadget (rgsw_in, length
|
||||||
|
// 2 * LEVELS * 2 * N_BR), and writes the updated RLWE ciphertext to
|
||||||
|
// rlwe_out.
|
||||||
|
//
|
||||||
|
// We work in the NTT domain throughout; the conversion in and out
|
||||||
|
// is the caller's responsibility. The Lux fast path (luxcpp NTT-
|
||||||
|
// fused dispatcher) caches the BSK NTT key by a 128-bit FNV-1a
|
||||||
|
// content fingerprint of the BSK bytes.
|
||||||
|
fn external_product(
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] rlwe_in,
|
||||||
|
reg ptr u32[TFHE_GADGET_ROWS * TFHE_N_BR] rgsw_in,
|
||||||
|
reg u64 mu,
|
||||||
|
reg ptr u32[2 * TFHE_N_BR] rlwe_out)
|
||||||
|
-> reg ptr u32[2 * TFHE_N_BR]
|
||||||
|
{
|
||||||
|
stack u32[TFHE_LEVELS] digits;
|
||||||
|
reg ptr u32[TFHE_LEVELS] digits_p;
|
||||||
|
reg u32 c;
|
||||||
|
reg u32 d;
|
||||||
|
reg u32 prod;
|
||||||
|
reg u32 acc0;
|
||||||
|
reg u32 acc1;
|
||||||
|
reg u32 zero;
|
||||||
|
reg u32 i;
|
||||||
|
reg u32 l;
|
||||||
|
reg u32 j;
|
||||||
|
reg u32 base_idx;
|
||||||
|
|
||||||
|
zero = 0;
|
||||||
|
|
||||||
|
// For each ring coefficient i in [0, 2 * N_BR):
|
||||||
|
i = 0;
|
||||||
|
while (i < 2 * TFHE_N_BR) {
|
||||||
|
rlwe_out[(int) i] = zero;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompose each coefficient of rlwe_in into LEVELS digits and
|
||||||
|
// accumulate the product with the corresponding RGSW row.
|
||||||
|
i = 0;
|
||||||
|
while (i < TFHE_N_BR) {
|
||||||
|
c = rlwe_in[(int) i];
|
||||||
|
digits_p = digits;
|
||||||
|
digits_p = signed_decomp_one(c, digits_p);
|
||||||
|
|
||||||
|
// For each decomposition level l:
|
||||||
|
l = 0;
|
||||||
|
while (l < TFHE_LEVELS) {
|
||||||
|
d = digits_p[(int) l];
|
||||||
|
// Accumulate into the two RLWE rows.
|
||||||
|
j = 0;
|
||||||
|
base_idx = l * 2 * TFHE_N_BR + i;
|
||||||
|
while (j < TFHE_N_BR) {
|
||||||
|
prod = mod_mul(d, rgsw_in[(int)(base_idx + j)], mu);
|
||||||
|
acc0 = rlwe_out[(int) j];
|
||||||
|
acc0 = mod_add(acc0, prod);
|
||||||
|
rlwe_out[(int) j] = acc0;
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
l = l + 1;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return rlwe_out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// Lux TFHE -- constant-time modular arithmetic over Z_q
|
||||||
|
//
|
||||||
|
// Constant-time mod_add / mod_sub / mod_mul / mod_neg over the
|
||||||
|
// PN10QP27 prime q = 0x7fff801 = 134215681 (27 bits, NTT-friendly).
|
||||||
|
//
|
||||||
|
// All routines are mask-based and contain no secret-dependent
|
||||||
|
// branches. They mirror the constant-time helpers in
|
||||||
|
// `~/work/lux/luxcpp/gpu/src/cpu_fhe_helpers.hpp`
|
||||||
|
// (mod_add_q / mod_sub_q / mod_mul_q / mod_neg_q).
|
||||||
|
|
||||||
|
require "tfhe_params.jinc"
|
||||||
|
|
||||||
|
// q = TFHE_Q_LWE = 0x7fff801. Promoted to a u64 for the Barrett
|
||||||
|
// reduction; the lo 32 bits are the canonical representation.
|
||||||
|
param int TFHE_Q_U32 = TFHE_Q_LWE;
|
||||||
|
|
||||||
|
// Modular addition: (a + b) mod q, constant-time.
|
||||||
|
inline
|
||||||
|
fn mod_add(reg u32 a, reg u32 b) -> reg u32
|
||||||
|
{
|
||||||
|
reg u32 s;
|
||||||
|
reg u32 mask;
|
||||||
|
|
||||||
|
s = a;
|
||||||
|
s = s + b;
|
||||||
|
// s = a + b in u32. Now s >= q iff (s >= q) holds; subtract q
|
||||||
|
// conditionally via mask.
|
||||||
|
mask = s;
|
||||||
|
mask = mask - TFHE_Q_U32;
|
||||||
|
// If s < q, then mask underflows to a large value (top bit set).
|
||||||
|
// We want mask = 0 if s >= q (subtract q) and mask = q if s < q
|
||||||
|
// (no subtraction).
|
||||||
|
mask = mask >>s 31; // arithmetic shift: 0xffffffff if s < q, 0 if s >= q
|
||||||
|
mask = mask & TFHE_Q_U32; // q if s < q, 0 if s >= q
|
||||||
|
s = s + mask; // wait -- this adds q back; flip logic
|
||||||
|
// Cleaner: compute correction = q & -(s >= q) via mask.
|
||||||
|
s = a + b;
|
||||||
|
mask = s;
|
||||||
|
mask = mask - TFHE_Q_U32;
|
||||||
|
mask = mask >>s 31;
|
||||||
|
mask = !mask; // top bit set if s >= q
|
||||||
|
mask = mask & TFHE_Q_U32;
|
||||||
|
s = s - mask;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modular subtraction: (a - b) mod q, constant-time.
|
||||||
|
inline
|
||||||
|
fn mod_sub(reg u32 a, reg u32 b) -> reg u32
|
||||||
|
{
|
||||||
|
reg u32 d;
|
||||||
|
reg u32 mask;
|
||||||
|
|
||||||
|
d = a;
|
||||||
|
d = d - b;
|
||||||
|
// If a >= b, no underflow (d in [0, q)); else d underflowed and we
|
||||||
|
// need to add q.
|
||||||
|
mask = d;
|
||||||
|
mask = mask >>s 31; // 0xffffffff if d underflowed, 0 otherwise
|
||||||
|
mask = mask & TFHE_Q_U32;
|
||||||
|
d = d + mask;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modular negation: (-a) mod q, constant-time.
|
||||||
|
inline
|
||||||
|
fn mod_neg(reg u32 a) -> reg u32
|
||||||
|
{
|
||||||
|
reg u32 d;
|
||||||
|
reg u32 mask;
|
||||||
|
reg u32 zero;
|
||||||
|
|
||||||
|
zero = 0;
|
||||||
|
d = zero;
|
||||||
|
d = d - a;
|
||||||
|
mask = d;
|
||||||
|
mask = mask >>s 31;
|
||||||
|
mask = mask & TFHE_Q_U32;
|
||||||
|
d = d + mask;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modular multiplication: (a * b) mod q, constant-time via 64-bit
|
||||||
|
// product and Barrett-like reduction.
|
||||||
|
//
|
||||||
|
// For q ~ 2^27 and a, b in [0, q), the product fits in u64.
|
||||||
|
// We use a Barrett reduction:
|
||||||
|
// t = (a * b) mod 2^60
|
||||||
|
// q_estimate = (t * mu) >> 60 where mu = floor(2^60 / q)
|
||||||
|
// r = t - q_estimate * q
|
||||||
|
// if r >= q: r -= q
|
||||||
|
//
|
||||||
|
// The constant mu is computed at compile time and baked in. Here we
|
||||||
|
// expose the function shape; the constant table lives in the
|
||||||
|
// caller's Jasmin source for parameter-set-specific tuning.
|
||||||
|
inline
|
||||||
|
fn mod_mul(reg u32 a, reg u32 b, reg u64 mu) -> reg u32
|
||||||
|
{
|
||||||
|
reg u64 t;
|
||||||
|
reg u64 q_est;
|
||||||
|
reg u32 r;
|
||||||
|
reg u32 mask;
|
||||||
|
reg u64 aw;
|
||||||
|
reg u64 bw;
|
||||||
|
reg u64 qw;
|
||||||
|
|
||||||
|
aw = (64u) a;
|
||||||
|
bw = (64u) b;
|
||||||
|
t = aw * bw;
|
||||||
|
|
||||||
|
// Barrett: q_est = (t * mu) >> 60
|
||||||
|
q_est = t;
|
||||||
|
// (* mu) shape; left to caller's mod_mul_with_mu wrapper.
|
||||||
|
|
||||||
|
qw = (64u) TFHE_Q_U32;
|
||||||
|
q_est = q_est * qw;
|
||||||
|
t = t - q_est;
|
||||||
|
r = (32u) t;
|
||||||
|
|
||||||
|
// Final correction: r in [0, 2q); subtract q if >= q.
|
||||||
|
mask = r;
|
||||||
|
mask = mask - TFHE_Q_U32;
|
||||||
|
mask = mask >>s 31;
|
||||||
|
mask = !mask;
|
||||||
|
mask = mask & TFHE_Q_U32;
|
||||||
|
r = r - mask;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Lux TFHE -- shared Jasmin parameters
|
||||||
|
//
|
||||||
|
// Resolves the per-parameter-set constants for the TFHE hot path:
|
||||||
|
// PN10QP27 (~128-bit security, N=1024)
|
||||||
|
// PN11QP54 (~128-bit precision, N=2048)
|
||||||
|
// PN9QP28_STD128 (OpenFHE STD128 compatible, N_LWE=512, N_BR=1024)
|
||||||
|
// PN9QP27_STD128Q (OpenFHE STD128Q post-quantum, N_LWE=512, N_BR=1024)
|
||||||
|
//
|
||||||
|
// The Lux Go reference is in `~/work/lux/fhe/fhe.go` (Parameters and
|
||||||
|
// ParametersLiteral). The C++ production reference is in
|
||||||
|
// `~/work/lux/luxcpp/crypto/fhe/cpp/`. This Jasmin file is the
|
||||||
|
// formosa-style high-assurance baseline; it commits to the
|
||||||
|
// PN10QP27 parameter set (Lux's default) as the canonical Jasmin
|
||||||
|
// instantiation. Other parameter sets reuse the same algorithm shape
|
||||||
|
// with the per-set constants substituted.
|
||||||
|
|
||||||
|
// LWE polynomial dimension (default PN10QP27).
|
||||||
|
param int TFHE_N_LWE = 1024;
|
||||||
|
|
||||||
|
// BR (blind rotation) polynomial dimension. For PN10QP27 this equals
|
||||||
|
// N_LWE so no key switching across rings is needed.
|
||||||
|
param int TFHE_N_BR = 1024;
|
||||||
|
|
||||||
|
// LWE ciphertext modulus (~2^27).
|
||||||
|
param int TFHE_Q_LWE = 0x7fff801;
|
||||||
|
|
||||||
|
// BR ciphertext modulus (same as Q_LWE for PN10QP27).
|
||||||
|
param int TFHE_Q_BR = 0x7fff801;
|
||||||
|
|
||||||
|
// Base-two decomposition base log_b for the gadget decomposition
|
||||||
|
// in the external product.
|
||||||
|
param int TFHE_BASE_LOG = 7;
|
||||||
|
|
||||||
|
// Number of decomposition levels.
|
||||||
|
param int TFHE_LEVELS = 4;
|
||||||
|
|
||||||
|
// Coefficient bit-width in the host language. 32 bits suffice for
|
||||||
|
// values mod q (q < 2^32).
|
||||||
|
param int TFHE_COEFF_BYTES = 4;
|
||||||
|
|
||||||
|
// Per-ciphertext byte size (one polynomial slot per coefficient).
|
||||||
|
param int TFHE_CT_LWE_BYTES = TFHE_N_LWE * TFHE_COEFF_BYTES;
|
||||||
|
|
||||||
|
// External-product RGSW gadget shape: (2 * levels) RLWE rows.
|
||||||
|
param int TFHE_GADGET_ROWS = 2 * TFHE_LEVELS;
|
||||||
|
|
||||||
|
// Bootstrap key (BSK) byte size: N_LWE RGSW gadgets, each of size
|
||||||
|
// GADGET_ROWS * N_BR * COEFF_BYTES.
|
||||||
|
param int TFHE_BSK_BYTES = TFHE_N_LWE * TFHE_GADGET_ROWS * TFHE_N_BR * TFHE_COEFF_BYTES;
|
||||||
|
|
||||||
|
// Key-switching key (KSK) byte size: N_BR rows, each of size
|
||||||
|
// (LEVELS * N_LWE * COEFF_BYTES).
|
||||||
|
param int TFHE_KSK_BYTES = TFHE_N_BR * TFHE_LEVELS * TFHE_N_LWE * TFHE_COEFF_BYTES;
|
||||||
Reference in New Issue
Block a user