Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test Encapsulation/ Decapsulation Failure Scenarios #50

Merged
merged 4 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 68 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,22 @@ make ubsan_test -j # Run tests with UndefinedBehaviourSanitizer enabled
```

```bash
PASSED TESTS (9/9):
PASSED TESTS (15/15):
2 ms: build/test.out ML_KEM.ML_KEM_1024_KeygenEncapsDecaps
3 ms: build/test.out ML_KEM.ML_KEM_512_KeygenEncapsDecaps
3 ms: build/test.out ML_KEM.ML_KEM_1024_EncapsFailureDueToNonReducedPubKey
3 ms: build/test.out ML_KEM.ML_KEM_1024_DecapsFailureDueToBitFlippedCipherText
3 ms: build/test.out ML_KEM.ML_KEM_512_DecapsFailureDueToBitFlippedCipherText
3 ms: build/test.out ML_KEM.ML_KEM_768_KeygenEncapsDecaps
3 ms: build/test.out ML_KEM.PolynomialSerialization
4 ms: build/test.out ML_KEM.ML_KEM_768_KeygenEncapsDecaps
4 ms: build/test.out ML_KEM.ML_KEM_1024_KeygenEncapsDecaps
41 ms: build/test.out ML_KEM.ML_KEM_512_KnownAnswerTests
63 ms: build/test.out ML_KEM.ML_KEM_1024_KnownAnswerTests
64 ms: build/test.out ML_KEM.ML_KEM_768_KnownAnswerTests
226 ms: build/test.out ML_KEM.CompressDecompressZq
284 ms: build/test.out ML_KEM.ArithmeticOverZq
4 ms: build/test.out ML_KEM.ML_KEM_512_EncapsFailureDueToNonReducedPubKey
4 ms: build/test.out ML_KEM.ML_KEM_768_DecapsFailureDueToBitFlippedCipherText
4 ms: build/test.out ML_KEM.ML_KEM_768_EncapsFailureDueToNonReducedPubKey
27 ms: build/test.out ML_KEM.ML_KEM_512_KnownAnswerTests
45 ms: build/test.out ML_KEM.ML_KEM_768_KnownAnswerTests
60 ms: build/test.out ML_KEM.ML_KEM_1024_KnownAnswerTests
243 ms: build/test.out ML_KEM.CompressDecompressZq
304 ms: build/test.out ML_KEM.ArithmeticOverZq
```

In case you're interested in running timing leakage tests using `dudect`, execute following
Expand Down Expand Up @@ -379,9 +385,11 @@ cd
git clone https://github.com/itzmeanjan/kyber.git && pushd kyber && git submodule update --init && popd
# Or do single step cloning and importing of submodules
git clone https://github.com/itzmeanjan/kyber.git --recurse-submodules
# Or clone and then run tests, which will automatically bring in dependencies
git clone https://github.com/itzmeanjan/kyber.git && pushd kyber && make -j && popd
```

- Write your program while including proper header files ( based on which variant of ML-KEM you want to use, see [include](./include) directory ), which includes declarations ( and definitions ) of all required ML-KEM routines and constants ( such as byte length of public/ private key, cipher text etc. ).
- Write your program while including proper header files ( based on which variant of ML-KEM you want to use, see [include](./include/ml_kem/) directory ), which includes declarations ( and definitions ) of all required ML-KEM routines and constants ( such as byte length of public/ private key, cipher text etc. ).

```cpp
// main.cpp
Expand Down Expand Up @@ -445,6 +453,57 @@ ML-KEM-1024 Routines | `ml_kem_1024::` | `include/ml_kem/ml_kem_1024.hpp`
> [!NOTE]
> ML-KEM parameter sets are taken from table 2 of ML-KEM draft standard @ https://doi.org/10.6028/NIST.FIPS.203.ipd.

All the functions, in this Kyber header-only library, are implemented as `constexpr` functions. Hence you should be able to evaluate ML-KEM key generation, encapsulation or decapsulation at compile-time itself, given that all inputs are known at compile-time. I present you with following demonstration program, which generates a ML-KEM-512 keypair and encapsulates a message, producing a ML-KEM-512 cipher text and a fixed size shared secret, given `seed_{d, z, m}` as input - all at program compile-time. Notice, the *static assertion*.

```cpp
// compile-time-ml-kem-512.cpp
//
// Compile and run this program with
// $ g++ -std=c++20 -Wall -Wextra -pedantic -I include -I sha3/include -I subtle/include main.cpp && ./a.out
// or
// $ clang++ -std=c++20 -Wall -Wextra -pedantic -fconstexpr-steps=4000000 -I include -I sha3/include -I subtle/include main.cpp && ./a.out

#include "ml_kem/ml_kem_512.hpp"

// Compile-time evaluation of ML-KEM-512 key generation and encapsulation, using NIST official KAT no. (1).
constexpr auto
eval_encaps() -> auto
{
using seed_t = std::array<uint8_t, ml_kem_512::SEED_D_BYTE_LEN>;

// 7c9935a0b07694aa0c6d10e4db6b1add2fd81a25ccb148032dcd739936737f2d
constexpr seed_t seed_d = { 124, 153, 53, 160, 176, 118, 148, 170, 12, 109, 16, 228, 219, 107, 26, 221, 47, 216, 26, 37, 204, 177, 72, 3, 45, 205, 115, 153, 54, 115, 127, 45 };
// b505d7cfad1b497499323c8686325e4792f267aafa3f87ca60d01cb54f29202a
constexpr seed_t seed_z = {181, 5, 215, 207, 173, 27, 73, 116, 153, 50, 60, 134, 134, 50, 94, 71, 146, 242, 103, 170, 250, 63, 135, 202, 96, 208, 28, 181, 79, 41, 32, 42};
// eb4a7c66ef4eba2ddb38c88d8bc706b1d639002198172a7b1942eca8f6c001ba
constexpr seed_t seed_m = {235, 74, 124, 102, 239, 78, 186, 45, 219, 56, 200, 141, 139, 199, 6, 177, 214, 57, 0, 33, 152, 23, 42, 123, 25, 66, 236, 168, 246, 192, 1, 186};

std::array<uint8_t, ml_kem_512::PKEY_BYTE_LEN> pubkey{};
std::array<uint8_t, ml_kem_512::SKEY_BYTE_LEN> seckey{};
std::array<uint8_t, ml_kem_512::CIPHER_TEXT_BYTE_LEN> cipher{};

std::array<uint8_t, ml_kem_512::SHARED_SECRET_BYTE_LEN> shared_secret{};

ml_kem_512::keygen(seed_d, seed_z, pubkey, seckey);
(void)ml_kem_512::encapsulate(seed_m, pubkey, cipher, shared_secret);

return shared_secret;
}

int
main()
{
// This step is being evaluated at compile-time, thanks to the fact that my ML-KEM implementation is `constexpr`.
static constexpr auto computed_shared_secret = eval_encaps();
// 500c4424107df96b01749b95f47a14eea871c3742606e15d2b6c91d207d85965
constexpr std::array<uint8_t, ml_kem_512::SHARED_SECRET_BYTE_LEN> expected_shared_secret = { 80, 12, 68, 36, 16, 125, 249, 107, 1, 116, 155, 149, 244, 122, 20, 238, 168, 113, 195, 116, 38, 6, 225, 93, 43, 108, 145, 210, 7, 216, 89, 101 };

// Notice static_assert, yay !
static_assert(computed_shared_secret == expected_shared_secret, "Must be able to compute shared secret at compile-time !");
return 0;
}
```

See example [program](./examples/ml_kem_768.cpp), where I show how to use ML-KEM-512 API.

```bash
Expand Down
47 changes: 47 additions & 0 deletions tests/test_helper.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#pragma once
#include "ml_kem/internals/math/field.hpp"
#include "ml_kem/internals/rng/prng.hpp"
#include <array>
#include <cassert>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <string_view>

// Given a hex encoded string of length 2*L, this routine can be used for parsing it as a byte array of length L.
Expand All @@ -30,3 +34,46 @@ from_hex(std::string_view bytes)

return res;
}

// Given a valid ML-KEM-{512, 768, 1024} public key, this function mutates the last coefficient
// of serialized polynomial vector s.t. it produces a malformed (i.e. non-reduced) polynomial vector.
template<size_t pubkey_byte_len>
static inline constexpr void
make_malformed_pubkey(std::span<uint8_t, pubkey_byte_len> pubkey)
{
constexpr auto last_coeff_ends_at = pubkey_byte_len - 32;
constexpr auto last_coeff_begins_at = last_coeff_ends_at - 2;

// < 16 -bit word >
// (MSB) ---- | ---- | ---- | ---- (LSB)
// | 12 -bits of last coeff, to be mutated | Most significant 4 -bits of second last coeff |
const uint16_t last_coeff = (static_cast<uint16_t>(pubkey[last_coeff_begins_at + 1]) << 8) | static_cast<uint16_t>(pubkey[last_coeff_begins_at + 0]);

constexpr uint16_t hi = ml_kem_field::Q << 4; // Q (=3329) is not a valid element of Zq. Any value >= Q && < 2^12, would work.
const uint16_t lo = last_coeff & 0xfu; // Don't touch most significant 4 -bits of second last coefficient
const uint16_t updated_last_coeff = hi ^ lo; // 16 -bit word s.t. last coefficient is not reduced modulo prime Q

pubkey[last_coeff_begins_at + 0] = static_cast<uint8_t>(updated_last_coeff >> 0);
pubkey[last_coeff_begins_at + 1] = static_cast<uint8_t>(updated_last_coeff >> 8);
}

// Given a ML-KEM-{512, 768, 1024} cipher text, this function flips a random bit of it, while sampling choice of random index from input PRNG.
template<size_t cipher_byte_len, size_t bit_sec_lvl>
static inline constexpr void
random_bitflip_in_cipher_text(std::span<uint8_t, cipher_byte_len> cipher, ml_kem_prng::prng_t<bit_sec_lvl>& prng)
{
size_t random_u64 = 0;
prng.read(std::span<uint8_t, sizeof(random_u64)>(reinterpret_cast<uint8_t*>(&random_u64), sizeof(random_u64)));

const size_t random_byte_idx = random_u64 % cipher_byte_len;
const size_t random_bit_idx = random_u64 % 8;

const uint8_t hi_bit_mask = 0xffu << (random_bit_idx + 1);
const uint8_t lo_bit_mask = 0xffu >> (std::numeric_limits<uint8_t>::digits - random_bit_idx);

const uint8_t selected_byte = cipher[random_byte_idx];
const uint8_t selected_bit = (selected_byte >> random_bit_idx) & 0b1u;
const uint8_t selected_bit_flipped = (~selected_bit) & 0b1;

cipher[random_byte_idx] = (selected_byte & hi_bit_mask) ^ (selected_bit_flipped << random_bit_idx) ^ (selected_byte & lo_bit_mask);
}
69 changes: 69 additions & 0 deletions tests/test_ml_kem_1024.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "ml_kem/ml_kem_1024.hpp"
#include "test_helper.hpp"
#include <gtest/gtest.h>

// For ML-KEM-1024
Expand Down Expand Up @@ -36,3 +37,71 @@ TEST(ML_KEM, ML_KEM_1024_KeygenEncapsDecaps)
EXPECT_TRUE(is_encapsulated);
EXPECT_EQ(shared_secret_sender, shared_secret_receiver);
}

// For ML-KEM-1024
//
// - Generate a valid keypair.
// - Malform public key s.t. last coefficient of polynomial vector is not properly reduced.
// - Attempt to encapsulate using malformed public key. It must fail.
TEST(ML_KEM, ML_KEM_1024_EncapsFailureDueToNonReducedPubKey)
{
std::array<uint8_t, ml_kem_1024::SEED_D_BYTE_LEN> seed_d{};
std::array<uint8_t, ml_kem_1024::SEED_Z_BYTE_LEN> seed_z{};
std::array<uint8_t, ml_kem_1024::SEED_M_BYTE_LEN> seed_m{};

std::array<uint8_t, ml_kem_1024::PKEY_BYTE_LEN> pubkey{};
std::array<uint8_t, ml_kem_1024::SKEY_BYTE_LEN> seckey{};
std::array<uint8_t, ml_kem_1024::CIPHER_TEXT_BYTE_LEN> cipher{};

std::array<uint8_t, ml_kem_1024::SHARED_SECRET_BYTE_LEN> shared_secret{};

ml_kem_prng::prng_t<256> prng{};
prng.read(seed_d);
prng.read(seed_z);
prng.read(seed_m);

ml_kem_1024::keygen(seed_d, seed_z, pubkey, seckey);

make_malformed_pubkey<pubkey.size()>(pubkey);
const auto is_encapsulated = ml_kem_1024::encapsulate(seed_m, pubkey, cipher, shared_secret);

EXPECT_FALSE(is_encapsulated);
}

// For ML-KEM-1024
//
// - Generate a valid keypair.
// - Encapsulate using public key, generate shared secret, at sender's side.
// - Cause a random bitflip in cipher text, at receiver's side.
// - Attempt to decapsulate bit-flipped cipher text, using valid secret key. Must fail *implicitly*.
// - Shared secret of sender and receiver must not match.
// - Shared secret at receiver's end must match `seed_z`, which is last 32 -bytes of secret key.
TEST(ML_KEM, ML_KEM_1024_DecapsFailureDueToBitFlippedCipherText)
{
std::array<uint8_t, ml_kem_1024::SEED_D_BYTE_LEN> seed_d{};
std::array<uint8_t, ml_kem_1024::SEED_Z_BYTE_LEN> seed_z{};
std::array<uint8_t, ml_kem_1024::SEED_M_BYTE_LEN> seed_m{};

std::array<uint8_t, ml_kem_1024::PKEY_BYTE_LEN> pubkey{};
std::array<uint8_t, ml_kem_1024::SKEY_BYTE_LEN> seckey{};
std::array<uint8_t, ml_kem_1024::CIPHER_TEXT_BYTE_LEN> cipher{};

std::array<uint8_t, ml_kem_1024::SHARED_SECRET_BYTE_LEN> shared_secret_sender{};
std::array<uint8_t, ml_kem_1024::SHARED_SECRET_BYTE_LEN> shared_secret_receiver{};

ml_kem_prng::prng_t<256> prng{};
prng.read(seed_d);
prng.read(seed_z);
prng.read(seed_m);

ml_kem_1024::keygen(seed_d, seed_z, pubkey, seckey);
const auto is_encapsulated = ml_kem_1024::encapsulate(seed_m, pubkey, cipher, shared_secret_sender);

random_bitflip_in_cipher_text<cipher.size()>(cipher, prng);
ml_kem_1024::decapsulate(seckey, cipher, shared_secret_receiver);

EXPECT_TRUE(is_encapsulated);
EXPECT_NE(shared_secret_sender, shared_secret_receiver);
EXPECT_EQ(shared_secret_receiver, seed_z);
EXPECT_TRUE(std::equal(shared_secret_receiver.begin(), shared_secret_receiver.end(), std::span(seckey).last<32>().begin()));
}
70 changes: 70 additions & 0 deletions tests/test_ml_kem_512.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "ml_kem/ml_kem_512.hpp"
#include "test_helper.hpp"
#include <gtest/gtest.h>
#include <span>

// For ML-KEM-512
//
Expand Down Expand Up @@ -36,3 +38,71 @@ TEST(ML_KEM, ML_KEM_512_KeygenEncapsDecaps)
EXPECT_TRUE(is_encapsulated);
EXPECT_EQ(shared_secret_sender, shared_secret_receiver);
}

// For ML-KEM-512
//
// - Generate a valid keypair.
// - Malform public key s.t. last coefficient of polynomial vector is not properly reduced.
// - Attempt to encapsulate using malformed public key. It must fail.
TEST(ML_KEM, ML_KEM_512_EncapsFailureDueToNonReducedPubKey)
{
std::array<uint8_t, ml_kem_512::SEED_D_BYTE_LEN> seed_d{};
std::array<uint8_t, ml_kem_512::SEED_Z_BYTE_LEN> seed_z{};
std::array<uint8_t, ml_kem_512::SEED_M_BYTE_LEN> seed_m{};

std::array<uint8_t, ml_kem_512::PKEY_BYTE_LEN> pubkey{};
std::array<uint8_t, ml_kem_512::SKEY_BYTE_LEN> seckey{};
std::array<uint8_t, ml_kem_512::CIPHER_TEXT_BYTE_LEN> cipher{};

std::array<uint8_t, ml_kem_512::SHARED_SECRET_BYTE_LEN> shared_secret{};

ml_kem_prng::prng_t<128> prng{};
prng.read(seed_d);
prng.read(seed_z);
prng.read(seed_m);

ml_kem_512::keygen(seed_d, seed_z, pubkey, seckey);

make_malformed_pubkey<pubkey.size()>(pubkey);
const auto is_encapsulated = ml_kem_512::encapsulate(seed_m, pubkey, cipher, shared_secret);

EXPECT_FALSE(is_encapsulated);
}

// For ML-KEM-512
//
// - Generate a valid keypair.
// - Encapsulate using public key, generate shared secret, at sender's side.
// - Cause a random bitflip in cipher text, at receiver's side.
// - Attempt to decapsulate bit-flipped cipher text, using valid secret key. Must fail *implicitly*.
// - Shared secret of sender and receiver must not match.
// - Shared secret at receiver's end must match `seed_z`, which is last 32 -bytes of secret key.
TEST(ML_KEM, ML_KEM_512_DecapsFailureDueToBitFlippedCipherText)
{
std::array<uint8_t, ml_kem_512::SEED_D_BYTE_LEN> seed_d{};
std::array<uint8_t, ml_kem_512::SEED_Z_BYTE_LEN> seed_z{};
std::array<uint8_t, ml_kem_512::SEED_M_BYTE_LEN> seed_m{};

std::array<uint8_t, ml_kem_512::PKEY_BYTE_LEN> pubkey{};
std::array<uint8_t, ml_kem_512::SKEY_BYTE_LEN> seckey{};
std::array<uint8_t, ml_kem_512::CIPHER_TEXT_BYTE_LEN> cipher{};

std::array<uint8_t, ml_kem_512::SHARED_SECRET_BYTE_LEN> shared_secret_sender{};
std::array<uint8_t, ml_kem_512::SHARED_SECRET_BYTE_LEN> shared_secret_receiver{};

ml_kem_prng::prng_t<128> prng{};
prng.read(seed_d);
prng.read(seed_z);
prng.read(seed_m);

ml_kem_512::keygen(seed_d, seed_z, pubkey, seckey);
const auto is_encapsulated = ml_kem_512::encapsulate(seed_m, pubkey, cipher, shared_secret_sender);

random_bitflip_in_cipher_text<cipher.size()>(cipher, prng);
ml_kem_512::decapsulate(seckey, cipher, shared_secret_receiver);

EXPECT_TRUE(is_encapsulated);
EXPECT_NE(shared_secret_sender, shared_secret_receiver);
EXPECT_EQ(shared_secret_receiver, seed_z);
EXPECT_TRUE(std::equal(shared_secret_receiver.begin(), shared_secret_receiver.end(), std::span(seckey).last<32>().begin()));
}
69 changes: 69 additions & 0 deletions tests/test_ml_kem_768.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "ml_kem/ml_kem_768.hpp"
#include "test_helper.hpp"
#include <gtest/gtest.h>

// For ML-KEM-768
Expand Down Expand Up @@ -36,3 +37,71 @@ TEST(ML_KEM, ML_KEM_768_KeygenEncapsDecaps)
EXPECT_TRUE(is_encapsulated);
EXPECT_EQ(shared_secret_sender, shared_secret_receiver);
}

// For ML-KEM-768
//
// - Generate a valid keypair.
// - Malform public key s.t. last coefficient of polynomial vector is not properly reduced.
// - Attempt to encapsulate using malformed public key. It must fail.
TEST(ML_KEM, ML_KEM_768_EncapsFailureDueToNonReducedPubKey)
{
std::array<uint8_t, ml_kem_768::SEED_D_BYTE_LEN> seed_d{};
std::array<uint8_t, ml_kem_768::SEED_Z_BYTE_LEN> seed_z{};
std::array<uint8_t, ml_kem_768::SEED_M_BYTE_LEN> seed_m{};

std::array<uint8_t, ml_kem_768::PKEY_BYTE_LEN> pubkey{};
std::array<uint8_t, ml_kem_768::SKEY_BYTE_LEN> seckey{};
std::array<uint8_t, ml_kem_768::CIPHER_TEXT_BYTE_LEN> cipher{};

std::array<uint8_t, ml_kem_768::SHARED_SECRET_BYTE_LEN> shared_secret{};

ml_kem_prng::prng_t<192> prng{};
prng.read(seed_d);
prng.read(seed_z);
prng.read(seed_m);

ml_kem_768::keygen(seed_d, seed_z, pubkey, seckey);

make_malformed_pubkey<pubkey.size()>(pubkey);
const auto is_encapsulated = ml_kem_768::encapsulate(seed_m, pubkey, cipher, shared_secret);

EXPECT_FALSE(is_encapsulated);
}

// For ML-KEM-768
//
// - Generate a valid keypair.
// - Encapsulate using public key, generate shared secret, at sender's side.
// - Cause a random bitflip in cipher text, at receiver's side.
// - Attempt to decapsulate bit-flipped cipher text, using valid secret key. Must fail *implicitly*.
// - Shared secret of sender and receiver must not match.
// - Shared secret at receiver's end must match `seed_z`, which is last 32 -bytes of secret key.
TEST(ML_KEM, ML_KEM_768_DecapsFailureDueToBitFlippedCipherText)
{
std::array<uint8_t, ml_kem_768::SEED_D_BYTE_LEN> seed_d{};
std::array<uint8_t, ml_kem_768::SEED_Z_BYTE_LEN> seed_z{};
std::array<uint8_t, ml_kem_768::SEED_M_BYTE_LEN> seed_m{};

std::array<uint8_t, ml_kem_768::PKEY_BYTE_LEN> pubkey{};
std::array<uint8_t, ml_kem_768::SKEY_BYTE_LEN> seckey{};
std::array<uint8_t, ml_kem_768::CIPHER_TEXT_BYTE_LEN> cipher{};

std::array<uint8_t, ml_kem_768::SHARED_SECRET_BYTE_LEN> shared_secret_sender{};
std::array<uint8_t, ml_kem_768::SHARED_SECRET_BYTE_LEN> shared_secret_receiver{};

ml_kem_prng::prng_t<192> prng{};
prng.read(seed_d);
prng.read(seed_z);
prng.read(seed_m);

ml_kem_768::keygen(seed_d, seed_z, pubkey, seckey);
const auto is_encapsulated = ml_kem_768::encapsulate(seed_m, pubkey, cipher, shared_secret_sender);

random_bitflip_in_cipher_text<cipher.size()>(cipher, prng);
ml_kem_768::decapsulate(seckey, cipher, shared_secret_receiver);

EXPECT_TRUE(is_encapsulated);
EXPECT_NE(shared_secret_sender, shared_secret_receiver);
EXPECT_EQ(shared_secret_receiver, seed_z);
EXPECT_TRUE(std::equal(shared_secret_receiver.begin(), shared_secret_receiver.end(), std::span(seckey).last<32>().begin()));
}