fhe/ct/dudect: harness for Encrypt + Decrypt + Bootstrap CT

Empirical constant-time analysis harness mirroring the Pulsar
pack at ~/work/lux/pulsar/ct/dudect/:

  encrypt_ct.go   + dudect_encrypt.c    - cgo bridge + C main loop
  decrypt_ct.go   + dudect_decrypt.c    - the CT-critical routine
  bootstrap_ct.go + dudect_bootstrap.c  - PBS composite

CT population (operational framing):
  - Both classes are VALID inputs to the routine under test
  - Class A is a fixed input; class B is uniformly drawn from a
    pre-built pool of K independent valid inputs
  - Any timing difference is a real secret-content signal

All three cgo shared libraries build clean:
  GOWORK=off go build -buildmode=c-shared -tags tfhe_encrypt_ct
  GOWORK=off go build -buildmode=c-shared -tags tfhe_decrypt_ct
  GOWORK=off go build -buildmode=c-shared -tags tfhe_bootstrap_ct

dudect.h fetched on demand via fetch.sh (not committed).
dudect_compat.h supplies _mm_mfence/__rdtsc on AArch64 hosts.

Submission-grade run pending (GATE-1 in CRYPTOGRAPHER-SIGN-OFF.md).
This commit is contained in:
Hanzo AI
2026-05-19 08:41:20 -07:00
parent 9ac3dad316
commit 3ceb29aa27
10 changed files with 959 additions and 0 deletions
+119
View File
@@ -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
+70
View File
@@ -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).
+110
View File
@@ -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 <stdint.h>
#include <stddef.h>
*/
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() {}
+115
View File
@@ -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 <stdint.h>
#include <stddef.h>
*/
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() {}
+106
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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;
}
+97
View File
@@ -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 (<emmintrin.h>, <x86intrin.h>) 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 <stdint.h>
/*
* 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 <mach/mach_time.h>
/*
* 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 */
+106
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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;
}
+111
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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;
}
+91
View File
@@ -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 <stdint.h>
#include <stddef.h>
*/
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() {}
+34
View File
@@ -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)"