security: fix FHE encryptor panic and NTT timing side-channel

F13: Add EncryptSafe(bool) (*Ciphertext, error) that returns errors instead of
panicking. Existing Encrypt(bool) is preserved but deprecated -- it now calls
EncryptSafe internally.

F07: Remove data-dependent branch (if hi == 0) from mulModBarrett. Always
execute the full 128-bit reduction path to eliminate timing side-channel.
This commit is contained in:
Hanzo AI
2026-04-07 17:40:11 -07:00
parent 3c6afe682a
commit 0a10cdc0c6
2 changed files with 22 additions and 12 deletions
+17 -5
View File
@@ -4,6 +4,8 @@
package fhe package fhe
import ( import (
"fmt"
"github.com/luxfi/lattice/v7/core/rlwe" "github.com/luxfi/lattice/v7/core/rlwe"
) )
@@ -23,9 +25,9 @@ func NewEncryptor(params Parameters, sk *SecretKey) *Encryptor {
} }
} }
// Encrypt encrypts a boolean value // EncryptSafe encrypts a boolean value, returning an error on failure.
// Note: Panics on error (should not happen with valid parameters) // Prefer this over Encrypt for production code paths.
func (enc *Encryptor) Encrypt(value bool) *Ciphertext { func (enc *Encryptor) EncryptSafe(value bool) (*Ciphertext, error) {
pt := rlwe.NewPlaintext(enc.params.paramsLWE, enc.params.paramsLWE.MaxLevel()) pt := rlwe.NewPlaintext(enc.params.paramsLWE, enc.params.paramsLWE.MaxLevel())
q := enc.params.QLWE() q := enc.params.QLWE()
@@ -46,10 +48,20 @@ func (enc *Encryptor) Encrypt(value bool) *Ciphertext {
ct := rlwe.NewCiphertext(enc.params.paramsLWE, 1, enc.params.paramsLWE.MaxLevel()) ct := rlwe.NewCiphertext(enc.params.paramsLWE, 1, enc.params.paramsLWE.MaxLevel())
if err := enc.encryptor.Encrypt(pt, ct); err != nil { if err := enc.encryptor.Encrypt(pt, ct); err != nil {
panic(err) // Should not happen with valid parameters return nil, fmt.Errorf("fhe encrypt: %w", err)
} }
return &Ciphertext{ct} return &Ciphertext{ct}, nil
}
// Encrypt encrypts a boolean value.
// Deprecated: Use EncryptSafe for production code. This method panics on error.
func (enc *Encryptor) Encrypt(value bool) *Ciphertext {
ct, err := enc.EncryptSafe(value)
if err != nil {
panic(err)
}
return ct
} }
// EncryptBit is an alias for Encrypt // EncryptBit is an alias for Encrypt
+5 -7
View File
@@ -333,15 +333,13 @@ func (e *NTTEngine) mulMod(a, b uint64) uint64 {
return div128(hi, lo, e.Q) return div128(hi, lo, e.Q)
} }
// mulModBarrett computes (a * b) mod Q using Barrett reduction // mulModBarrett computes (a * b) mod Q using Barrett reduction.
// For Q up to ~58 bits, we use a two-word Barrett with mu = floor(2^(2k)/Q) // Constant-time: always executes the full 128-bit path regardless of input.
// where k = number of bits in Q. This gives enough precision for the quotient.
func (e *NTTEngine) mulModBarrett(a, b uint64) uint64 { func (e *NTTEngine) mulModBarrett(a, b uint64) uint64 {
hi, lo := mul64(a, b) hi, lo := mul64(a, b)
if hi == 0 { // Always execute the full path to avoid timing side-channel on hi == 0
return lo % e.Q _, r := bits.Div64(hi%e.Q, lo, e.Q)
} return r
return div128(hi, lo, e.Q)
} }
// mul64 multiplies two 64-bit integers and returns 128-bit result as (hi, lo) // mul64 multiplies two 64-bit integers and returns 128-bit result as (hi, lo)