Skip to content

Commit

Permalink
Merge pull request #26 from itzmeanjan/add-ct-tests
Browse files Browse the repository at this point in the history
Add Timing Leakage Detection Tests
  • Loading branch information
itzmeanjan authored Jan 27, 2024
2 parents a7d5520 + fe32d26 commit d91f770
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "gtest-parallel"]
path = gtest-parallel
url = https://github.com/google/gtest-parallel.git
[submodule "dudect"]
path = dudect
url = https://github.com/oreparaz/dudect.git
19 changes: 18 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ ASAN_FLAGS = -g -O1 -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsaniti
UBSAN_FLAGS = -g -O1 -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=undefined # From https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html

SHA3_INC_DIR = ./sha3/include
DUDECT_INC_DIR = ./dudect/src
I_FLAGS = -I ./include
DEP_IFLAGS = -I $(SHA3_INC_DIR)
DUDECT_DEP_IFLAGS = $(DEP_IFLAGS) -I $(DUDECT_INC_DIR)

SRC_DIR = include
DILITHIUM_SOURCES := $(wildcard $(SRC_DIR)/*.hpp)
BUILD_DIR = build
ASAN_BUILD_DIR = $(BUILD_DIR)/asan
UBSAN_BUILD_DIR = $(BUILD_DIR)/ubsan
DUDECT_BUILD_DIR = $(BUILD_DIR)/dudect

TEST_DIR = tests
DUDECT_TEST_DIR = $(TEST_DIR)/dudect
TEST_SOURCES := $(wildcard $(TEST_DIR)/*.cpp)
TEST_OBJECTS := $(addprefix $(BUILD_DIR)/, $(notdir $(patsubst %.cpp,%.o,$(TEST_SOURCES))))
ASAN_TEST_OBJECTS := $(addprefix $(ASAN_BUILD_DIR)/, $(notdir $(patsubst %.cpp,%.o,$(TEST_SOURCES))))
UBSAN_TEST_OBJECTS := $(addprefix $(UBSAN_BUILD_DIR)/, $(notdir $(patsubst %.cpp,%.o,$(TEST_SOURCES))))
DUDECT_TEST_SOURCES := $(wildcard $(DUDECT_TEST_DIR)/*.cpp)
DUDECT_TEST_BINARIES := $(addprefix $(DUDECT_BUILD_DIR)/, $(notdir $(patsubst %.cpp,%.out,$(DUDECT_TEST_SOURCES))))
TEST_LINK_FLAGS = -lgtest -lgtest_main
TEST_BINARY = $(BUILD_DIR)/test.out
ASAN_TEST_BINARY = $(ASAN_BUILD_DIR)/test.out
Expand All @@ -38,6 +44,9 @@ PERF_BINARY = $(BUILD_DIR)/perf.out

all: test

$(DUDECT_BUILD_DIR):
mkdir -p $@

$(ASAN_BUILD_DIR):
mkdir -p $@

Expand All @@ -53,6 +62,9 @@ $(SHA3_INC_DIR):
$(GTEST_PARALLEL): $(SHA3_INC_DIR)
git submodule update --init

$(DUDECT_INC_DIR): $(GTEST_PARALLEL)
git submodule update --init

$(BUILD_DIR)/%.o: $(TEST_DIR)/%.cpp $(BUILD_DIR) $(SHA3_INC_DIR)
$(CXX) $(CXX_FLAGS) $(WARN_FLAGS) $(OPT_FLAGS) $(I_FLAGS) $(DEP_IFLAGS) -c $< -o $@

Expand All @@ -71,6 +83,9 @@ $(ASAN_TEST_BINARY): $(ASAN_TEST_OBJECTS)
$(UBSAN_TEST_BINARY): $(UBSAN_TEST_OBJECTS)
$(CXX) $(UBSAN_FLAGS) $^ $(TEST_LINK_FLAGS) -o $@

$(DUDECT_BUILD_DIR)/%.out: $(DUDECT_TEST_DIR)/%.cpp $(DUDECT_BUILD_DIR) $(SHA3_INC_DIR) $(SUBTLE_INC_DIR) $(DUDECT_INC_DIR)
$(CXX) $(CXX_FLAGS) $(WARN_FLAGS) $(OPT_FLAGS) $(I_FLAGS) $(DUDECT_DEP_IFLAGS) -lm $(LINK_FLAGS) $< -o $@

test: $(TEST_BINARY) $(GTEST_PARALLEL)
$(GTEST_PARALLEL) $< --print_test_times

Expand All @@ -80,6 +95,8 @@ asan_test: $(ASAN_TEST_BINARY) $(GTEST_PARALLEL)
ubsan_test: $(UBSAN_TEST_BINARY) $(GTEST_PARALLEL)
$(GTEST_PARALLEL) $< --print_test_times

dudect_test_build: $(DUDECT_TEST_BINARIES)

$(BUILD_DIR)/%.o: $(BENCHMARK_DIR)/%.cpp $(BUILD_DIR) $(SHA3_INC_DIR)
$(CXX) $(CXX_FLAGS) $(WARN_FLAGS) $(OPT_FLAGS) $(I_FLAGS) $(DEP_IFLAGS) -c $< -o $@

Expand All @@ -102,5 +119,5 @@ perf: $(PERF_BINARY)
clean:
rm -rf $(BUILD_DIR)

format: $(DILITHIUM_SOURCES) $(TEST_SOURCES) $(BENCHMARK_SOURCES) $(BENCHMARK_HEADERS)
format: $(DILITHIUM_SOURCES) $(TEST_SOURCES) $(DUDECT_TEST_SOURCES) $(BENCHMARK_SOURCES) $(BENCHMARK_HEADERS)
clang-format -i $^
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
> [!CAUTION]
> This Dilithium implementation is conformant with Dilithium [specification](https://pq-crystals.org/dilithium/data/dilithium-specification-round3-20210208.pdf) and I also try to make it constant-time but be informed that it is not yet audited. **If you consider using it in production, be careful !**
> This Dilithium implementation is conformant with Dilithium specification @ https://pq-crystals.org/dilithium/data/dilithium-specification-round3-20210208.pdf. I also try to make it timing leakage free, using `dudect` (see https://github.com/oreparaz/dudect) -based tests, but be informed that this implementation is not yet audited. *If you consider using it in production, be careful !*
# dilithium
CRYSTALS-Dilithium: Post-Quantum Digital Signature Algorithm
Expand Down Expand Up @@ -89,6 +89,45 @@ PASSED TESTS (13/13):
365 ms: build/test.out Dilithium.ArithmeticOverZq
```

You can run timing leakage tests, using `dudect`, execute following

> [!NOTE]
> `dudect` is integrated into this library implementation of Dilithium DSA to find any sort of timing leakages. It checks for constant-timeness of most of the vital internal functions. Though it doesn't check constant-timeness of functions which use uniform rejection sampling, such as expansion of public matrix `A` or sampling of the vectors `s1`, `s2` or hashing to a ball etc.
```bash
# Can only be built and run on x86_64 machine.
make dudect_test_build -j

# Before running the constant-time tests, it's a good idea to put all CPU cores on "performance" mode.
# You may find the guide @ https://github.com/google/benchmark/blob/main/docs/reducing_variance.md helpful.

timeout 10m taskset -c 0 ./build/dudect/test_dilithium2.out
timeout 10m taskset -c 0 ./build/dudect/test_dilithium3.out
timeout 10m taskset -c 0 ./build/dudect/test_dilithium5.out
```

> [!TIP]
> `dudect` documentation says if `t` statistic is `< 10`, we're *probably* good, yes **probably**. You may want to read `dudect` documentation @ https://github.com/oreparaz/dudect. Also you might find the original paper @ https://ia.cr/2016/1123 interesting.
```bash
...
meas: 48.38 M, max t: +2.77, max tau: 3.99e-04, (5/tau)^2: 1.57e+08. For the moment, maybe constant time.
meas: 48.48 M, max t: +2.73, max tau: 3.93e-04, (5/tau)^2: 1.62e+08. For the moment, maybe constant time.
meas: 48.57 M, max t: +2.76, max tau: 3.96e-04, (5/tau)^2: 1.59e+08. For the moment, maybe constant time.
meas: 48.67 M, max t: +2.78, max tau: 3.99e-04, (5/tau)^2: 1.57e+08. For the moment, maybe constant time.
meas: 48.76 M, max t: +2.79, max tau: 3.99e-04, (5/tau)^2: 1.57e+08. For the moment, maybe constant time.
meas: 48.85 M, max t: +2.78, max tau: 3.97e-04, (5/tau)^2: 1.58e+08. For the moment, maybe constant time.
meas: 48.95 M, max t: +2.79, max tau: 3.98e-04, (5/tau)^2: 1.58e+08. For the moment, maybe constant time.
meas: 49.05 M, max t: +2.77, max tau: 3.95e-04, (5/tau)^2: 1.60e+08. For the moment, maybe constant time.
meas: 49.14 M, max t: +2.69, max tau: 3.84e-04, (5/tau)^2: 1.70e+08. For the moment, maybe constant time.
meas: 49.24 M, max t: +2.75, max tau: 3.92e-04, (5/tau)^2: 1.62e+08. For the moment, maybe constant time.
meas: 49.33 M, max t: +2.73, max tau: 3.89e-04, (5/tau)^2: 1.65e+08. For the moment, maybe constant time.
meas: 49.43 M, max t: +2.76, max tau: 3.93e-04, (5/tau)^2: 1.62e+08. For the moment, maybe constant time.
meas: 49.52 M, max t: +2.76, max tau: 3.92e-04, (5/tau)^2: 1.63e+08. For the moment, maybe constant time.
meas: 49.62 M, max t: +2.79, max tau: 3.97e-04, (5/tau)^2: 1.59e+08. For the moment, maybe constant time.
meas: 49.71 M, max t: +2.78, max tau: 3.94e-04, (5/tau)^2: 1.61e+08. For the moment, maybe constant time.
```

## Benchmarking

Benchmarking key generation, signing and verification algorithms for various instantiations of Dilithium digital signature scheme can be done, by issuing
Expand Down
1 change: 1 addition & 0 deletions dudect
Submodule dudect added at a18fde
118 changes: 118 additions & 0 deletions tests/dudect/test_dilithium2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "dilithium2.hpp"
#include <cstdio>

#define DUDECT_IMPLEMENTATION
#define DUDECT_VISIBLITY_STATIC
#include "dudect.h"

constexpr size_t SEED_LEN = 32; // Byte length of seed(s)

uint8_t
do_one_computation(uint8_t* const data)
{
constexpr uint32_t α = dilithium2::γ2 << 1;
constexpr uint32_t m = (field::Q - 1u) / α;
constexpr size_t w1bw = std::bit_width(m - 1u);

constexpr size_t doff0 = 0;
constexpr size_t doff1 = doff0 + 2 * SEED_LEN;

std::array<field::zq_t, dilithium2::l * ntt::N> vec{};
std::array<field::zq_t, vec.size()> vec_high{};
std::array<field::zq_t, vec.size()> vec_low{};
std::array<field::zq_t, vec_high.size()> vec_hint{};
std::array<uint8_t, (vec_high.size() * w1bw) / 8> encoded{};
std::array<field::zq_t, vec_high.size()> decoded{};
std::array<uint8_t, dilithium2::ω + dilithium2::l> encoded_hints{};
std::array<field::zq_t, vec.size()> decoded_hints{};

auto seed = std::span<const uint8_t, 2 * SEED_LEN>(data + doff0, doff1 - doff0);
const uint16_t kappa = (static_cast<uint16_t>(data[doff1 + 1]) << 8) | (static_cast<uint16_t>(data[doff1 + 0]) << 0);

uint8_t ret_val = 0;

sampling::expand_mask<dilithium2::γ1, dilithium2::l>(seed, kappa, vec);
ret_val ^= static_cast<uint8_t>(vec[0].raw() ^ vec[vec.size() - 1].raw());

polyvec::ntt<dilithium2::l>(vec);
ret_val ^= static_cast<uint8_t>(vec[0].raw() ^ vec[vec.size() - 1].raw());

polyvec::intt<dilithium2::l>(vec);
ret_val ^= static_cast<uint8_t>(vec[0].raw() ^ vec[vec.size() - 1].raw());

polyvec::highbits<dilithium2::l, α>(vec, vec_high);
ret_val ^= static_cast<uint8_t>(vec_high[0].raw() ^ vec_high[vec_high.size() - 1].raw());

polyvec::lowbits<dilithium2::l, α>(vec, vec_low);
ret_val ^= static_cast<uint8_t>(vec_low[0].raw() ^ vec_low[vec_low.size() - 1].raw());

polyvec::encode<dilithium2::l, w1bw>(vec_high, encoded);
ret_val ^= encoded[0] ^ encoded[encoded.size() - 1];

polyvec::decode<dilithium2::l, w1bw>(encoded, decoded);
ret_val ^= static_cast<uint8_t>(decoded[0].raw() ^ decoded[decoded.size() - 1].raw());

const auto z_norm = polyvec::infinity_norm<dilithium2::l>(vec);
ret_val ^= static_cast<uint8_t>(z_norm.raw());

polyvec::make_hint<dilithium2::l, α>(vec, vec_high, vec_hint);
ret_val ^= static_cast<uint8_t>(vec_high[0].raw() ^ vec_hint[vec_hint.size() - 1].raw());

const auto count_1 = polyvec::count_1s<dilithium2::l>(vec_hint);
ret_val ^= static_cast<uint8_t>(count_1);

bit_packing::encode_hint_bits<dilithium2::l, dilithium2::ω>(vec_hint, encoded_hints);
ret_val ^= encoded_hints[0] ^ encoded_hints[encoded_hints.size() - 1];

bit_packing::decode_hint_bits<dilithium2::l, dilithium2::ω>(encoded_hints, decoded_hints);
ret_val ^= static_cast<uint8_t>(decoded_hints[0].raw() ^ decoded_hints[decoded_hints.size() - 1].raw());

return ret_val;
}

void
prepare_inputs(dudect_config_t* const c, uint8_t* const input_data, uint8_t* const classes)
{
randombytes(input_data, c->number_measurements * c->chunk_size);

for (size_t i = 0; i < c->number_measurements; i++) {
classes[i] = randombit();
if (classes[i] == 0) {
std::memset(input_data + i * c->chunk_size, 0x00, c->chunk_size);
}
}
}

dudect_state_t
test_dilithium2()
{
constexpr size_t chunk_size = 2 * SEED_LEN + 2 + SEED_LEN;
constexpr size_t number_measurements = 1e5;

dudect_config_t config = {
chunk_size,
number_measurements,
};
dudect_ctx_t ctx;
dudect_init(&ctx, &config);

dudect_state_t state = DUDECT_NO_LEAKAGE_EVIDENCE_YET;
while (state == DUDECT_NO_LEAKAGE_EVIDENCE_YET) {
state = dudect_main(&ctx);
}

dudect_free(&ctx);

printf("Detected timing leakage in \"%s\", defined in file \"%s\"\n", __func__, __FILE_NAME__);
return state;
}

int
main()
{
if (test_dilithium2() != DUDECT_NO_LEAKAGE_EVIDENCE_YET) {
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}
118 changes: 118 additions & 0 deletions tests/dudect/test_dilithium3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "dilithium3.hpp"
#include <cstdio>

#define DUDECT_IMPLEMENTATION
#define DUDECT_VISIBLITY_STATIC
#include "dudect.h"

constexpr size_t SEED_LEN = 32; // Byte length of seed(s)

uint8_t
do_one_computation(uint8_t* const data)
{
constexpr uint32_t α = dilithium3::γ2 << 1;
constexpr uint32_t m = (field::Q - 1u) / α;
constexpr size_t w1bw = std::bit_width(m - 1u);

constexpr size_t doff0 = 0;
constexpr size_t doff1 = doff0 + 2 * SEED_LEN;

std::array<field::zq_t, dilithium3::l * ntt::N> vec{};
std::array<field::zq_t, vec.size()> vec_high{};
std::array<field::zq_t, vec.size()> vec_low{};
std::array<field::zq_t, vec_high.size()> vec_hint{};
std::array<uint8_t, (vec_high.size() * w1bw) / 8> encoded{};
std::array<field::zq_t, vec_high.size()> decoded{};
std::array<uint8_t, dilithium3::ω + dilithium3::l> encoded_hints{};
std::array<field::zq_t, vec.size()> decoded_hints{};

auto seed = std::span<const uint8_t, 2 * SEED_LEN>(data + doff0, doff1 - doff0);
const uint16_t kappa = (static_cast<uint16_t>(data[doff1 + 1]) << 8) | (static_cast<uint16_t>(data[doff1 + 0]) << 0);

uint8_t ret_val = 0;

sampling::expand_mask<dilithium3::γ1, dilithium3::l>(seed, kappa, vec);
ret_val ^= static_cast<uint8_t>(vec[0].raw() ^ vec[vec.size() - 1].raw());

polyvec::ntt<dilithium3::l>(vec);
ret_val ^= static_cast<uint8_t>(vec[0].raw() ^ vec[vec.size() - 1].raw());

polyvec::intt<dilithium3::l>(vec);
ret_val ^= static_cast<uint8_t>(vec[0].raw() ^ vec[vec.size() - 1].raw());

polyvec::highbits<dilithium3::l, α>(vec, vec_high);
ret_val ^= static_cast<uint8_t>(vec_high[0].raw() ^ vec_high[vec_high.size() - 1].raw());

polyvec::lowbits<dilithium3::l, α>(vec, vec_low);
ret_val ^= static_cast<uint8_t>(vec_low[0].raw() ^ vec_low[vec_low.size() - 1].raw());

polyvec::encode<dilithium3::l, w1bw>(vec_high, encoded);
ret_val ^= encoded[0] ^ encoded[encoded.size() - 1];

polyvec::decode<dilithium3::l, w1bw>(encoded, decoded);
ret_val ^= static_cast<uint8_t>(decoded[0].raw() ^ decoded[decoded.size() - 1].raw());

const auto z_norm = polyvec::infinity_norm<dilithium3::l>(vec);
ret_val ^= static_cast<uint8_t>(z_norm.raw());

polyvec::make_hint<dilithium3::l, α>(vec, vec_high, vec_hint);
ret_val ^= static_cast<uint8_t>(vec_high[0].raw() ^ vec_hint[vec_hint.size() - 1].raw());

const auto count_1 = polyvec::count_1s<dilithium3::l>(vec_hint);
ret_val ^= static_cast<uint8_t>(count_1);

bit_packing::encode_hint_bits<dilithium3::l, dilithium3::ω>(vec_hint, encoded_hints);
ret_val ^= encoded_hints[0] ^ encoded_hints[encoded_hints.size() - 1];

bit_packing::decode_hint_bits<dilithium3::l, dilithium3::ω>(encoded_hints, decoded_hints);
ret_val ^= static_cast<uint8_t>(decoded_hints[0].raw() ^ decoded_hints[decoded_hints.size() - 1].raw());

return ret_val;
}

void
prepare_inputs(dudect_config_t* const c, uint8_t* const input_data, uint8_t* const classes)
{
randombytes(input_data, c->number_measurements * c->chunk_size);

for (size_t i = 0; i < c->number_measurements; i++) {
classes[i] = randombit();
if (classes[i] == 0) {
std::memset(input_data + i * c->chunk_size, 0x00, c->chunk_size);
}
}
}

dudect_state_t
test_dilithium3()
{
constexpr size_t chunk_size = 2 * SEED_LEN + 2 + SEED_LEN;
constexpr size_t number_measurements = 1e5;

dudect_config_t config = {
chunk_size,
number_measurements,
};
dudect_ctx_t ctx;
dudect_init(&ctx, &config);

dudect_state_t state = DUDECT_NO_LEAKAGE_EVIDENCE_YET;
while (state == DUDECT_NO_LEAKAGE_EVIDENCE_YET) {
state = dudect_main(&ctx);
}

dudect_free(&ctx);

printf("Detected timing leakage in \"%s\", defined in file \"%s\"\n", __func__, __FILE_NAME__);
return state;
}

int
main()
{
if (test_dilithium3() != DUDECT_NO_LEAKAGE_EVIDENCE_YET) {
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}
Loading

0 comments on commit d91f770

Please sign in to comment.