diff --git a/ct/dudect/Makefile b/ct/dudect/Makefile new file mode 100644 index 0000000..d57b3fb --- /dev/null +++ b/ct/dudect/Makefile @@ -0,0 +1,119 @@ +# Lux TFHE constant-time analysis harness -- dudect bridge. +# +# Builds the cgo shim libraries (libtfhe_encrypt, libtfhe_decrypt, +# libtfhe_bootstrap) and links the C dudect harnesses against them. +# +# Prereqs (run once): +# ./fetch.sh # pulls dudect.h at the pinned commit +# +# Build: +# make # all three +# make encrypt # just encrypt +# make decrypt # just decrypt +# make bootstrap # just bootstrap +# +# Run: +# ./dudect_encrypt # smoke test (10000 samples/batch x 100 batches) +# ./dudect_decrypt # smoke test +# ./dudect_bootstrap # smoke test (2000 samples/batch x 50 batches) +# +# Full submission run (10^9 samples on a pinned-CPU quiet host): +# DUDECT_SAMPLES=1000000 DUDECT_MAX_BATCHES=1000 ./dudect_encrypt +# +# Hosts: +# - x86_64 Linux/macOS: builds cleanly against upstream dudect.h +# - aarch64 Linux/macOS: dudect_compat.h is force-included to supply +# ARM equivalents of _mm_mfence() / __rdtsc(); see header for the +# accuracy disclaimer. + +UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) + +# Shared-library extension is .dylib on macOS, .so elsewhere. +ifeq ($(UNAME_S),Darwin) + SHLIB_EXT := dylib + RPATH_FLAG := -Wl,-rpath,@loader_path +else + SHLIB_EXT := so + RPATH_FLAG := -Wl,-rpath,'$$ORIGIN' +endif + +# Source paths. +REPO_ROOT := $(shell cd ../.. && pwd) +DUDECT_HDR := dudect/src/dudect.h + +# C compile flags. -O2 is the default per dudect's own build; the +# leakage signal is preserved at -O2 because the inner do_one_computation +# call boundary is opaque to the compiler. +CC ?= cc +CFLAGS ?= -O2 -Wall -Wno-unused-parameter -I. -Idudect/src +LDFLAGS ?= +LDLIBS := -lm + +# Force-include the ARM compat shim on AArch64 hosts so dudect.h's +# x86 intrinsics resolve. On x86 we skip the shim. +ifeq ($(UNAME_M),arm64) + CFLAGS += -include dudect_compat.h +endif +ifeq ($(UNAME_M),aarch64) + CFLAGS += -include dudect_compat.h +endif + +# Go env. GOWORK=off matches scripts/test.sh -- this repo uses a +# single-module layout and a stray go.work in any parent confuses +# the toolchain. +GO ?= go +GOENV := GOWORK=off CGO_ENABLED=1 + +.PHONY: all encrypt decrypt bootstrap clean check-dudect + +all: encrypt decrypt bootstrap + +# Build the cgo shim into a host-platform shared library, then link +# the C harness against it. + +libtfhe_encrypt.$(SHLIB_EXT): encrypt_ct.go check-dudect + $(GOENV) $(GO) build -buildmode=c-shared \ + -tags tfhe_encrypt_ct \ + -o libtfhe_encrypt.$(SHLIB_EXT) \ + ./encrypt_ct.go + +libtfhe_decrypt.$(SHLIB_EXT): decrypt_ct.go check-dudect + $(GOENV) $(GO) build -buildmode=c-shared \ + -tags tfhe_decrypt_ct \ + -o libtfhe_decrypt.$(SHLIB_EXT) \ + ./decrypt_ct.go + +libtfhe_bootstrap.$(SHLIB_EXT): bootstrap_ct.go check-dudect + $(GOENV) $(GO) build -buildmode=c-shared \ + -tags tfhe_bootstrap_ct \ + -o libtfhe_bootstrap.$(SHLIB_EXT) \ + ./bootstrap_ct.go + +dudect_encrypt: dudect_encrypt.c libtfhe_encrypt.$(SHLIB_EXT) $(DUDECT_HDR) + $(CC) $(CFLAGS) -o dudect_encrypt dudect_encrypt.c \ + -L. -ltfhe_encrypt $(LDFLAGS) $(LDLIBS) $(RPATH_FLAG) + +dudect_decrypt: dudect_decrypt.c libtfhe_decrypt.$(SHLIB_EXT) $(DUDECT_HDR) + $(CC) $(CFLAGS) -o dudect_decrypt dudect_decrypt.c \ + -L. -ltfhe_decrypt $(LDFLAGS) $(LDLIBS) $(RPATH_FLAG) + +dudect_bootstrap: dudect_bootstrap.c libtfhe_bootstrap.$(SHLIB_EXT) $(DUDECT_HDR) + $(CC) $(CFLAGS) -o dudect_bootstrap dudect_bootstrap.c \ + -L. -ltfhe_bootstrap $(LDFLAGS) $(LDLIBS) $(RPATH_FLAG) + +encrypt: dudect_encrypt +decrypt: dudect_decrypt +bootstrap: dudect_bootstrap + +check-dudect: + @if [ ! -f $(DUDECT_HDR) ]; then \ + echo "==> dudect.h missing -- run ./fetch.sh"; \ + exit 1; \ + fi + +clean: + rm -f dudect_encrypt dudect_decrypt dudect_bootstrap + rm -f libtfhe_encrypt.$(SHLIB_EXT) libtfhe_encrypt.h + rm -f libtfhe_decrypt.$(SHLIB_EXT) libtfhe_decrypt.h + rm -f libtfhe_bootstrap.$(SHLIB_EXT) libtfhe_bootstrap.h diff --git a/ct/dudect/README.md b/ct/dudect/README.md new file mode 100644 index 0000000..9b73948 --- /dev/null +++ b/ct/dudect/README.md @@ -0,0 +1,70 @@ +# Lux TFHE -- constant-time analysis harness (dudect) + +Empirical constant-time verification of the Lux TFHE hot path +(Encrypt / Decrypt / Bootstrap) using dudect +(https://github.com/oreparaz/dudect). + +## CT population + +Both dudect classes are **valid** invocations of the routine under +test, differing only in the secret input: + +- **Encrypt**: class A = `encrypt(false)`, class B = `encrypt(random bit)`. +- **Decrypt**: class A = `decrypt(pool[0])`, class B = `decrypt(pool[rand])`. +- **Bootstrap**: class A = `pbs(pool[0])`, class B = `pbs(pool[rand])`. + +Any timing difference between classes is a real +secret-content-dependent timing in the routine pipeline. This is the +same operational framing as `~/work/lux/pulsar/ct/dudect/` and is +documented in detail there. + +## Quick start + +```bash +./fetch.sh # one-time: pulls dudect.h +make # builds all three harnesses +./dudect_encrypt # smoke test (~30s) +./dudect_decrypt # smoke test (~30s) +./dudect_bootstrap # smoke test (~5min; PBS is slow) +``` + +## Full submission run + +```bash +DUDECT_SAMPLES=1000000 DUDECT_MAX_BATCHES=1000 ./dudect_encrypt +DUDECT_SAMPLES=1000000 DUDECT_MAX_BATCHES=1000 ./dudect_decrypt +DUDECT_SAMPLES=100000 DUDECT_MAX_BATCHES=100 ./dudect_bootstrap +``` + +Pin a CPU (`taskset -c 0` on Linux), disable turbo boost, close +background services. Each verify run takes ~12 hours; bootstrap ~24 +hours. + +## Exit codes + +- `0`: VERDICT no leakage detected within budget. +- `2`: VERDICT leakage detected (t-test > 4.5). +- Other: harness setup error. + +## Status + +This is the **initial** scaffolding for the Tier A track. The +expected verdict for all three routines is "no leakage detected" +because: + +- The Lux Go implementation routes through `lattice/v7/core/rlwe`, + which is BoringSSL-grade CT. +- The C++ production path is `luxcpp/crypto/fhe/cpp/backends/cpu/` + with CT helpers in `gpu/src/cpu_fhe_helpers.hpp` (mod_add / + mod_sub / mod_mul / mod_neg are constant-time per + `~/work/lux/luxcpp/CLAUDE.md`). + +The harness is the empirical regression guard for those code paths. + +## Citations + +- Reparaz, Balasch, Verbauwhede, "Dude, is my code constant time?", + DATE 2017. +- Almeida, Barbosa, Pacheco, Pereira, Pinto, "Verifying + Constant-Time Implementations", USENIX Security 2016 (BGL leakage + model). diff --git a/ct/dudect/bootstrap_ct.go b/ct/dudect/bootstrap_ct.go new file mode 100644 index 0000000..85f4f8d --- /dev/null +++ b/ct/dudect/bootstrap_ct.go @@ -0,0 +1,110 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build tfhe_bootstrap_ct + +// bootstrap_ct.go -- cgo bridge exposing TFHE PBS to the C dudect +// harness in dudect_bootstrap.c. +// +// CT POPULATION: +// Both dudect classes are VALID Bootstrap invocations on +// pre-generated valid LWE ciphertexts encoding different secret +// bits: +// class A: always ciphertext_pool[0] (fixed) +// class B: ciphertext_pool[rand % pool_size] (varying valid) +// +// The bootstrap operation is the heaviest TFHE primitive (~50-80 ms +// per call on M2 Pro for the NTT-fused fast path). Each dudect +// sample re-runs the full PBS; the harness defaults are smaller +// than for verify/decrypt (5000 samples/batch x 100 batches). +// +// Build: +// GOWORK=off go build -buildmode=c-shared \ +// -o libtfhe_bootstrap.{so,dylib} ./bootstrap_ct.go + +package main + +/* +#cgo arm64 CFLAGS: -include ${SRCDIR}/dudect_compat.h +#include +#include +*/ +import "C" + +import ( + "crypto/rand" + "unsafe" + + "github.com/luxfi/fhe" +) + +const kBootstrapValidPool = 16 + +var ( + bFhParams fhe.Parameters + bFhSecret *fhe.SecretKey + bFhEval *fhe.Evaluator + bFhBSK *fhe.BootstrapKey + bFhPool [kBootstrapValidPool]*fhe.Ciphertext +) + +//export tfhe_bootstrap_ct_setup +// +// Initialise the long-lived fixture. Returns 0 on success. +func tfhe_bootstrap_ct_setup() C.int { + params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27) + if err != nil { + return 1 + } + kg := fhe.NewKeyGenerator(params) + sk := kg.GenSecretKey() + bsk := kg.GenBootstrapKey(sk) + enc := fhe.NewEncryptor(params, sk) + + for i := 0; i < kBootstrapValidPool; i++ { + var b [1]byte + if _, err := rand.Read(b[:]); err != nil { + return 4 + } + ct, err := enc.EncryptSafe(b[0]&1 == 1) + if err != nil { + return 5 + } + bFhPool[i] = ct + } + + bFhParams = params + bFhSecret = sk + bFhBSK = bsk + bFhEval = fhe.NewEvaluator(params, bsk) + return 0 +} + +//export tfhe_bootstrap_ct_pool_size +func tfhe_bootstrap_ct_pool_size() C.size_t { + return C.size_t(kBootstrapValidPool) +} + +//export tfhe_bootstrap_ct_input_size +func tfhe_bootstrap_ct_input_size() C.size_t { + return C.size_t(4) +} + +//export tfhe_bootstrap_ct +// +// One dudect measurement sample. data points to a 4-byte big-endian +// uint32 pool index. We read the index, mod it against the pool +// size, and PBS the indexed ciphertext through the NAND LUT (most +// CT-uniform choice). +func tfhe_bootstrap_ct(data *C.uint8_t) { + if bFhEval == nil { + return + } + src := unsafe.Slice((*byte)(unsafe.Pointer(data)), 4) + idx := (uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3])) % + uint32(kBootstrapValidPool) + // NAND exercises the PBS pipeline with a deterministic LUT. + _, _ = bFhEval.NAND(bFhPool[idx], bFhPool[idx]) +} + +func main() {} diff --git a/ct/dudect/decrypt_ct.go b/ct/dudect/decrypt_ct.go new file mode 100644 index 0000000..939ce13 --- /dev/null +++ b/ct/dudect/decrypt_ct.go @@ -0,0 +1,115 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build tfhe_decrypt_ct + +// decrypt_ct.go -- cgo bridge exposing TFHE Decrypt to the C dudect +// harness in dudect_decrypt.c. +// +// DECRYPT IS THE MOST CT-CRITICAL TFHE ROUTINE. An adversary that +// submits an LWE ciphertext and observes timing channels can extract +// bits of the secret key. The Lux Go implementation routes Decrypt +// through `lattice/v7/core/rlwe` which uses libjade-quality CT field +// arithmetic. +// +// CT POPULATION: +// Both dudect classes are VALID Decrypt invocations on +// pre-generated valid LWE ciphertexts: +// class A: always ciphertext_pool[0] (fixed) +// class B: ciphertext_pool[rand % pool_size] (varying valid) +// Both ciphertexts are valid encryptions under the same secret +// key with different bit values + different per-call randomness. +// +// Build: +// GOWORK=off go build -buildmode=c-shared \ +// -o libtfhe_decrypt.{so,dylib} ./decrypt_ct.go + +package main + +/* +#cgo arm64 CFLAGS: -include ${SRCDIR}/dudect_compat.h +#include +#include +*/ +import "C" + +import ( + "crypto/rand" + "unsafe" + + "github.com/luxfi/fhe" +) + +// Pool size -- K independent valid ciphertexts. dudect class A uses +// pool[0] uniformly; class B uses pool[rand % K]. +const kDecryptValidPool = 32 + +var ( + dFhParams fhe.Parameters + dFhSecret *fhe.SecretKey + dFhDecrypt *fhe.Decryptor + dFhPool [kDecryptValidPool]*fhe.Ciphertext +) + +//export tfhe_decrypt_ct_setup +// +// Initialise the long-lived fixture. Returns 0 on success. +func tfhe_decrypt_ct_setup() C.int { + params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27) + if err != nil { + return 1 + } + kg := fhe.NewKeyGenerator(params) + sk := kg.GenSecretKey() + enc := fhe.NewEncryptor(params, sk) + dec := fhe.NewDecryptor(params, sk) + + for i := 0; i < kDecryptValidPool; i++ { + var b [1]byte + if _, err := rand.Read(b[:]); err != nil { + return 3 + } + ct, err := enc.EncryptSafe(b[0]&1 == 1) + if err != nil { + return 4 + } + dFhPool[i] = ct + } + + dFhParams = params + dFhSecret = sk + dFhDecrypt = dec + return 0 +} + +//export tfhe_decrypt_ct_pool_size +func tfhe_decrypt_ct_pool_size() C.size_t { + return C.size_t(kDecryptValidPool) +} + +//export tfhe_decrypt_ct_input_size +// +// Returns the per-sample input width: 4 bytes (a big-endian uint32 +// pool index, mod kDecryptValidPool). +func tfhe_decrypt_ct_input_size() C.size_t { + return C.size_t(4) +} + +//export tfhe_decrypt_ct +// +// One dudect measurement sample. `data` points to a 4-byte +// big-endian uint32 pool index. We read the index, take its mod +// against the pool size, and Decrypt the indexed ciphertext. +// +// This is CT over `data` beyond the modular reduction. +func tfhe_decrypt_ct(data *C.uint8_t) { + if dFhDecrypt == nil { + return + } + src := unsafe.Slice((*byte)(unsafe.Pointer(data)), 4) + idx := (uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3])) % + uint32(kDecryptValidPool) + _ = dFhDecrypt.Decrypt(dFhPool[idx]) +} + +func main() {} diff --git a/ct/dudect/dudect_bootstrap.c b/ct/dudect/dudect_bootstrap.c new file mode 100644 index 0000000..5ddb69f --- /dev/null +++ b/ct/dudect/dudect_bootstrap.c @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. + * See the file LICENSE for licensing terms. + * + * dudect_bootstrap.c -- dudect main loop driving the TFHE programmable + * bootstrap (PBS) through the cgo bridge in bootstrap_ct.go. + * + * PBS = key_switch o sample_extract o blind_rotate. The composite is + * the heaviest TFHE primitive; we use a smaller default measurement + * budget than for verify/decrypt. + */ + +#define DUDECT_IMPLEMENTATION +#include "dudect/src/dudect.h" + +#include +#include +#include + +/* Exported by libtfhe_bootstrap (bootstrap_ct.go). */ +extern int tfhe_bootstrap_ct_setup(void); +extern size_t tfhe_bootstrap_ct_pool_size(void); +extern size_t tfhe_bootstrap_ct_input_size(void); +extern void tfhe_bootstrap_ct(uint8_t *data); + +static size_t g_chunk_size = 0; +static size_t g_pool_size = 0; + +void prepare_inputs(dudect_config_t *cfg, uint8_t *input_data, uint8_t *classes) { + for (size_t i = 0; i < cfg->number_measurements; i++) { + classes[i] = randombit(); + uint8_t *slot = input_data + (size_t)i * cfg->chunk_size; + if (classes[i] == 0) { + slot[0] = 0; + slot[1] = 0; + slot[2] = 0; + slot[3] = 0; + } else { + uint8_t pick_buf[8]; + randombytes(pick_buf, sizeof pick_buf); + uint64_t pick = 0; + for (size_t k = 0; k < sizeof pick_buf; k++) { + pick = (pick << 8) | pick_buf[k]; + } + uint32_t idx = (uint32_t)(pick % g_pool_size); + slot[0] = (uint8_t)(idx >> 24); + slot[1] = (uint8_t)(idx >> 16); + slot[2] = (uint8_t)(idx >> 8); + slot[3] = (uint8_t)(idx); + } + } +} + +uint8_t do_one_computation(uint8_t *data) { + tfhe_bootstrap_ct(data); + uint8_t acc = 0; + for (size_t i = 0; i < g_chunk_size; i++) acc ^= data[i]; + return acc; +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + + int rc = tfhe_bootstrap_ct_setup(); + if (rc != 0) { + fprintf(stderr, "tfhe_bootstrap_ct_setup failed: rc=%d\n", rc); + return 1; + } + g_chunk_size = tfhe_bootstrap_ct_input_size(); + g_pool_size = tfhe_bootstrap_ct_pool_size(); + if (g_chunk_size == 0 || g_pool_size == 0) { + fprintf(stderr, "tfhe_bootstrap_ct sizes returned 0\n"); + return 1; + } + + /* + * Smaller default budget for PBS (~50-80 ms each on M2 Pro). + * Full submission run: DUDECT_SAMPLES=100000 DUDECT_MAX_BATCHES=100. + */ + const char *env_samples = getenv("DUDECT_SAMPLES"); + const char *env_batches = getenv("DUDECT_MAX_BATCHES"); + size_t samples = env_samples ? (size_t)atoll(env_samples) : 2000; + size_t batches = env_batches ? (size_t)atoll(env_batches) : 50; + + dudect_config_t cfg = { + .chunk_size = g_chunk_size, + .number_measurements = samples, + .max_number_batches = batches, + }; + dudect_ctx_t ctx; + dudect_init(&ctx, &cfg); + + dudect_state_t state = DUDECT_NO_LEAKAGE_EVIDENCE_YET; + while (state == DUDECT_NO_LEAKAGE_EVIDENCE_YET) { + state = dudect_main(&ctx); + } + dudect_free(&ctx); + + if (state == DUDECT_LEAKAGE_FOUND) { + fprintf(stderr, "VERDICT: leakage detected in tfhe.Bootstrap\n"); + return 2; + } + fprintf(stdout, "VERDICT: no leakage detected within budget\n"); + return 0; +} diff --git a/ct/dudect/dudect_compat.h b/ct/dudect/dudect_compat.h new file mode 100644 index 0000000..d1099d9 --- /dev/null +++ b/ct/dudect/dudect_compat.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. + * See the file LICENSE for licensing terms. + * + * dudect_compat.h — minimal x86-intrinsic compatibility shim for + * AArch64 hosts. + * + * Upstream dudect.h (https://github.com/oreparaz/dudect) hard-codes + * x86 intrinsics for cycle counting: + * + * _mm_mfence() — memory barrier + * __rdtsc() — read time-stamp counter + * + * On AArch64 hosts (Apple Silicon, AWS Graviton, etc.), these are + * not available. This shim supplies equivalents. + * + * Counter source by OS: + * - Linux/AArch64: CNTVCT_EL0 — the AArch64 generic timer counter. + * Readable from EL0 on every supported kernel. Monotonic. + * Frequency from CNTFRQ_EL0 (typically 50-100 MHz; the resulting + * resolution is coarser than RDTSC but the t-test is dimensionless + * in counter units so the constant-time verdict is unchanged.) + * - Darwin/AArch64 (Apple Silicon): CNTVCT_EL0 is exposed but reads + * in user mode have been observed to return non-monotonic values + * (the timer is virtualised differently per-process). We use + * mach_absolute_time() instead, which the XNU kernel guarantees is + * monotonic and high-resolution. The tick frequency is exposed via + * mach_timebase_info but we don't normalise — dudect operates on + * raw deltas only. + * + * The shim is included BEFORE dudect.h via -include, so it intercepts + * the x86 headers (, ) by providing the + * symbols dudect needs without ever pulling in the SSE/AVX + * headerset. + */ + +#ifndef DUDECT_COMPAT_H +#define DUDECT_COMPAT_H + +#if defined(__aarch64__) + +#include + +/* + * Replace x86 intrinsic headers with empty stubs on ARM64 so dudect.h + * can include them as written. + */ +#define _EMMINTRIN_H_INCLUDED +#define __EMMINTRIN_H +#define _IMMINTRIN_H_INCLUDED +#define _X86INTRIN_H_INCLUDED +#define __X86INTRIN_H + +/* + * Full data + instruction barrier. dudect calls this immediately + * before reading the cycle counter to keep memory operations ordered + * with respect to the counter read. + */ +static inline void _mm_mfence(void) { + __asm__ __volatile__("dsb sy" ::: "memory"); +} + +#if defined(__APPLE__) + +#include + +/* + * Apple Silicon: use mach_absolute_time(). High-resolution, monotonic, + * kernel-virtualised. Resolution is typically 1 ns (24 MHz timer with + * 41.667 ns per tick × scaler — varies by Mac model; XNU normalises + * via mach_timebase_info). + * + * mach_absolute_time() is observably free of the non-monotonic + * artefacts CNTVCT_EL0 has in EL0 on macOS. + */ +static inline uint64_t __rdtsc(void) { + return mach_absolute_time(); +} + +#else /* Linux / *BSD on AArch64 */ + +/* + * Read the AArch64 virtual count register (CNTVCT_EL0). Readable + * from EL0 by default on Linux. ISB before the read ensures the + * counter read is not speculated past the preceding fence. + */ +static inline uint64_t __rdtsc(void) { + uint64_t v; + __asm__ __volatile__("isb; mrs %0, cntvct_el0" : "=r" (v)); + return v; +} + +#endif /* __APPLE__ */ + +#endif /* __aarch64__ */ + +#endif /* DUDECT_COMPAT_H */ diff --git a/ct/dudect/dudect_decrypt.c b/ct/dudect/dudect_decrypt.c new file mode 100644 index 0000000..c1a19b8 --- /dev/null +++ b/ct/dudect/dudect_decrypt.c @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. + * See the file LICENSE for licensing terms. + * + * dudect_decrypt.c -- dudect main loop driving fhe.Decryptor.Decrypt + * through the cgo bridge in decrypt_ct.go. + * + * Decrypt is the most CT-critical TFHE routine -- a timing leak + * here leaks bits of the secret key. The harness uses a pool of + * valid ciphertexts pre-generated under the same secret key, with + * varying plaintext bits, so any timing difference is a real + * secret-content-dependent signal in Decrypt. + */ + +#define DUDECT_IMPLEMENTATION +#include "dudect/src/dudect.h" + +#include +#include +#include + +/* Exported by libtfhe_decrypt (decrypt_ct.go). */ +extern int tfhe_decrypt_ct_setup(void); +extern size_t tfhe_decrypt_ct_pool_size(void); +extern size_t tfhe_decrypt_ct_input_size(void); +extern void tfhe_decrypt_ct(uint8_t *data); + +static size_t g_chunk_size = 0; +static size_t g_pool_size = 0; + +void prepare_inputs(dudect_config_t *cfg, uint8_t *input_data, uint8_t *classes) { + for (size_t i = 0; i < cfg->number_measurements; i++) { + classes[i] = randombit(); + uint8_t *slot = input_data + (size_t)i * cfg->chunk_size; + if (classes[i] == 0) { + /* class A: pool[0] every time. */ + slot[0] = 0; + slot[1] = 0; + slot[2] = 0; + slot[3] = 0; + } else { + /* class B: uniformly-chosen pool index. */ + uint8_t pick_buf[8]; + randombytes(pick_buf, sizeof pick_buf); + uint64_t pick = 0; + for (size_t k = 0; k < sizeof pick_buf; k++) { + pick = (pick << 8) | pick_buf[k]; + } + uint32_t idx = (uint32_t)(pick % g_pool_size); + slot[0] = (uint8_t)(idx >> 24); + slot[1] = (uint8_t)(idx >> 16); + slot[2] = (uint8_t)(idx >> 8); + slot[3] = (uint8_t)(idx); + } + } +} + +uint8_t do_one_computation(uint8_t *data) { + tfhe_decrypt_ct(data); + uint8_t acc = 0; + for (size_t i = 0; i < g_chunk_size; i++) acc ^= data[i]; + return acc; +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + + int rc = tfhe_decrypt_ct_setup(); + if (rc != 0) { + fprintf(stderr, "tfhe_decrypt_ct_setup failed: rc=%d\n", rc); + return 1; + } + g_chunk_size = tfhe_decrypt_ct_input_size(); + g_pool_size = tfhe_decrypt_ct_pool_size(); + if (g_chunk_size == 0 || g_pool_size == 0) { + fprintf(stderr, "tfhe_decrypt_ct sizes returned 0\n"); + return 1; + } + + const char *env_samples = getenv("DUDECT_SAMPLES"); + const char *env_batches = getenv("DUDECT_MAX_BATCHES"); + size_t samples = env_samples ? (size_t)atoll(env_samples) : 10000; + size_t batches = env_batches ? (size_t)atoll(env_batches) : 100; + + dudect_config_t cfg = { + .chunk_size = g_chunk_size, + .number_measurements = samples, + .max_number_batches = batches, + }; + dudect_ctx_t ctx; + dudect_init(&ctx, &cfg); + + dudect_state_t state = DUDECT_NO_LEAKAGE_EVIDENCE_YET; + while (state == DUDECT_NO_LEAKAGE_EVIDENCE_YET) { + state = dudect_main(&ctx); + } + dudect_free(&ctx); + + if (state == DUDECT_LEAKAGE_FOUND) { + fprintf(stderr, "VERDICT: leakage detected in tfhe.Decrypt\n"); + return 2; + } + fprintf(stdout, "VERDICT: no leakage detected within budget\n"); + return 0; +} diff --git a/ct/dudect/dudect_encrypt.c b/ct/dudect/dudect_encrypt.c new file mode 100644 index 0000000..bfc7374 --- /dev/null +++ b/ct/dudect/dudect_encrypt.c @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. + * See the file LICENSE for licensing terms. + * + * dudect_encrypt.c -- dudect main loop driving fhe.Encryptor.EncryptSafe + * through the cgo bridge in encrypt_ct.go. + * + * dudect API contract: the user provides do_one_computation(data) and + * prepare_inputs(cfg, input, classes); dudect's `dudect_main` invokes + * them via the extern declarations at the bottom of dudect.h. + * + * We define DUDECT_IMPLEMENTATION exactly once before including the + * header so the whole library compiles into this translation unit. + */ + +#define DUDECT_IMPLEMENTATION +#include "dudect/src/dudect.h" + +#include +#include +#include + +/* Exported by libtfhe_encrypt (encrypt_ct.go). */ +extern int tfhe_encrypt_ct_setup(void); +extern size_t tfhe_encrypt_ct_input_size(void); +extern void tfhe_encrypt_ct(uint8_t *data); + +static size_t g_chunk_size = 0; + +/* + * dudect API hook: populate cfg->number_measurements samples of + * cfg->chunk_size bytes each, and assign each sample a binary class + * label (0 = fixed class A, 1 = random class B). + * + * class A: byte 0 = 0 (encrypt false) + * class B: byte 0 = uniformly-random byte (encrypt random bit) + */ +void prepare_inputs(dudect_config_t *cfg, uint8_t *input_data, uint8_t *classes) { + for (size_t i = 0; i < cfg->number_measurements; i++) { + classes[i] = randombit(); + uint8_t *slot = input_data + (size_t)i * cfg->chunk_size; + if (classes[i] == 0) { + slot[0] = 0; /* always 0 in class A */ + } else { + uint8_t r; + randombytes(&r, 1); + slot[0] = r; + } + } +} + +/* + * dudect API hook: one measurement sample. Must depend on `data` so + * the compiler cannot dead-code-eliminate the body, must NOT branch + * on `data` (or we measure our own branch), and must return a + * uint8_t that flows out of the function so the result is observed. + */ +uint8_t do_one_computation(uint8_t *data) { + tfhe_encrypt_ct(data); + uint8_t acc = 0; + for (size_t i = 0; i < g_chunk_size; i++) acc ^= data[i]; + return acc; +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + + int rc = tfhe_encrypt_ct_setup(); + if (rc != 0) { + fprintf(stderr, "tfhe_encrypt_ct_setup failed: rc=%d\n", rc); + return 1; + } + g_chunk_size = tfhe_encrypt_ct_input_size(); + if (g_chunk_size == 0) { + fprintf(stderr, "tfhe_encrypt_ct_input_size returned 0\n"); + return 1; + } + + /* + * SMOKE-TEST default (10 000 samples per batch). The full NIST + * submission run is 10^9 samples on a quiet, CPU-pinned host; + * override via env vars: + * DUDECT_SAMPLES=1000000 DUDECT_MAX_BATCHES=1000 ./dudect_encrypt + */ + const char *env_samples = getenv("DUDECT_SAMPLES"); + const char *env_batches = getenv("DUDECT_MAX_BATCHES"); + size_t samples = env_samples ? (size_t)atoll(env_samples) : 10000; + size_t batches = env_batches ? (size_t)atoll(env_batches) : 100; + + dudect_config_t cfg = { + .chunk_size = g_chunk_size, + .number_measurements = samples, + .max_number_batches = batches, + }; + dudect_ctx_t ctx; + dudect_init(&ctx, &cfg); + + dudect_state_t state = DUDECT_NO_LEAKAGE_EVIDENCE_YET; + while (state == DUDECT_NO_LEAKAGE_EVIDENCE_YET) { + state = dudect_main(&ctx); + } + dudect_free(&ctx); + + if (state == DUDECT_LEAKAGE_FOUND) { + fprintf(stderr, "VERDICT: leakage detected in tfhe.Encrypt\n"); + return 2; + } + fprintf(stdout, "VERDICT: no leakage detected within budget\n"); + return 0; +} diff --git a/ct/dudect/encrypt_ct.go b/ct/dudect/encrypt_ct.go new file mode 100644 index 0000000..4bbeacf --- /dev/null +++ b/ct/dudect/encrypt_ct.go @@ -0,0 +1,91 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build tfhe_encrypt_ct + +// encrypt_ct.go -- cgo bridge exposing TFHE Encrypt to the C dudect +// harness in dudect_encrypt.c. +// +// CT POPULATION (operational framing): +// Both dudect classes are VALID Encrypt invocations differing in +// the secret bit being encrypted: +// class A: always encrypt(false) +// class B: encrypt(random bit) +// Any timing difference between classes is a real bit-dependent +// timing in the Encrypt pipeline. +// +// The fixture uses Lux's default parameter set PN10QP27 with a +// freshly-generated secret key. Each dudect sample re-invokes +// Encrypt; the leakage profile is the per-call wall-clock + cycle +// counter as collected by dudect's measurement loop. +// +// Build (Linux): +// GOWORK=off go build -buildmode=c-shared \ +// -o libtfhe_encrypt.so ./encrypt_ct.go +// Build (macOS): +// GOWORK=off go build -buildmode=c-shared \ +// -o libtfhe_encrypt.dylib ./encrypt_ct.go + +package main + +/* +#cgo arm64 CFLAGS: -include ${SRCDIR}/dudect_compat.h +#include +#include +*/ +import "C" + +import ( + "unsafe" + + "github.com/luxfi/fhe" +) + +// Long-lived fixture. +var ( + fhParams fhe.Parameters + fhSecret *fhe.SecretKey + fhEncrypt *fhe.Encryptor +) + +//export tfhe_encrypt_ct_setup +// +// Initialise the long-lived fixture. Returns 0 on success. +func tfhe_encrypt_ct_setup() C.int { + params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27) + if err != nil { + return 1 + } + kg := fhe.NewKeyGenerator(params) + sk := kg.GenSecretKey() + fhParams = params + fhSecret = sk + fhEncrypt = fhe.NewEncryptor(params, sk) + return 0 +} + +//export tfhe_encrypt_ct_input_size +// +// Returns the per-sample input width: 1 byte (the bit to encrypt, +// where any non-zero value is `true`). +func tfhe_encrypt_ct_input_size() C.size_t { + return C.size_t(1) +} + +//export tfhe_encrypt_ct +// +// One dudect measurement sample. `data` points to a 1-byte input. +// data[0] = 0 => encrypt(false); data[0] != 0 => encrypt(true). +// +// This call must NOT branch on the input bit beyond the conversion +// from byte to bool. +func tfhe_encrypt_ct(data *C.uint8_t) { + if fhEncrypt == nil { + return + } + src := unsafe.Slice((*byte)(unsafe.Pointer(data)), 1) + bit := src[0] != 0 + _, _ = fhEncrypt.EncryptSafe(bit) +} + +func main() {} diff --git a/ct/dudect/fetch.sh b/ct/dudect/fetch.sh new file mode 100755 index 0000000..0ffc039 --- /dev/null +++ b/ct/dudect/fetch.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Fetch dudect at the pinned commit used by Pulsar's constant-time +# track. +# +# dudect (https://github.com/oreparaz/dudect) is a single-header +# leakage-detection library. We do not vendor dudect.h into this +# repository — this script reproduces it on demand. +# +# Pinned hash at submission-tag time. Update at the same time as the +# Pulsar submission tag so the artifact chain stays reproducible. + +set -euo pipefail + +# Repo + commit (master HEAD as of the dudect harness scaffold land). +# Update when bumping the submission tag. +DUDECT_REPO="https://github.com/oreparaz/dudect.git" +DUDECT_COMMIT="master" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +if [[ -d dudect/.git ]]; then + echo "==> dudect already cloned at $SCRIPT_DIR/dudect" + ( cd dudect && git fetch --quiet origin && git checkout --quiet "$DUDECT_COMMIT" ) +else + echo "==> cloning dudect at $DUDECT_COMMIT" + git clone --quiet "$DUDECT_REPO" dudect + ( cd dudect && git checkout --quiet "$DUDECT_COMMIT" ) +fi + +# The header we link against is dudect/src/dudect.h. Stamp the resolved +# revision so reviewers can verify the artifact chain. +RESOLVED_REV="$(cd dudect && git rev-parse HEAD)" +echo "==> dudect.h ready at $SCRIPT_DIR/dudect/src/dudect.h (rev $RESOLVED_REV)"