Merge pull request #50 from itzmeanjan/test-encap-decap-failures

Test Encapsulation/ Decapsulation Failure Scenarios
This commit is contained in:
Anjan Roy
2024-06-21 22:45:43 +04:00
committed by GitHub
5 changed files with 323 additions and 9 deletions

View File

@@ -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
@@ -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
@@ -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

View File

@@ -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.
@@ -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);
}

View File

@@ -1,4 +1,5 @@
#include "ml_kem/ml_kem_1024.hpp"
#include "test_helper.hpp"
#include <gtest/gtest.h>
// For ML-KEM-1024
@@ -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()));
}

View File

@@ -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
//
@@ -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()));
}

View File

@@ -1,4 +1,5 @@
#include "ml_kem/ml_kem_768.hpp"
#include "test_helper.hpp"
#include <gtest/gtest.h>
// For ML-KEM-768
@@ -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()));
}