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

Prepare PQC KEM KAT Tests for Classic McEliece #3892

Merged
merged 1 commit into from
Jan 18, 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
34 changes: 13 additions & 21 deletions src/tests/test_frodokem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,43 +31,35 @@ namespace Botan_Tests {
#if defined(BOTAN_HAS_FRODOKEM)

namespace {
class Frodo_KAT_Tests_Impl {
class Frodo_KAT_Tests final : public Botan_Tests::PK_PQC_KEM_KAT_Test {
public:
using public_key_t = Botan::FrodoKEM_PublicKey;
using private_key_t = Botan::FrodoKEM_PrivateKey;
Frodo_KAT_Tests() : PK_PQC_KEM_KAT_Test("FrodoKEM", "pubkey/frodokem_kat.vec") {}

static constexpr const char* algo_name = "FrodoKEM";
static constexpr const char* input_file = "pubkey/frodokem_kat.vec";

public:
Frodo_KAT_Tests_Impl(std::string_view algo_spec) : m_mode(algo_spec) {}

decltype(auto) mode() const { return m_mode; }
private:
Botan::FrodoKEMMode get_mode(const std::string& mode) const { return Botan::FrodoKEMMode(mode); }

bool available() const { return m_mode.is_available(); }
bool is_available(const std::string& mode) const final { return get_mode(mode).is_available(); }

auto map_value(std::span<const uint8_t> value) const {
std::vector<uint8_t> map_value(const std::string&, std::span<const uint8_t> value, VarType var_type) const final {
if(var_type == VarType::SharedSecret) {
return {value.begin(), value.end()};
}
auto xof = Botan::XOF::create_or_throw("SHAKE-256");
xof->update(value);
return xof->output<std::vector<uint8_t>>(16);
}

auto rng_for_keygen(Botan::RandomNumberGenerator& rng) const {
Botan::FrodoKEMConstants consts(m_mode);
Fixed_Output_RNG rng_for_keygen(const std::string& mode, Botan::RandomNumberGenerator& rng) const final {
Botan::FrodoKEMConstants consts(get_mode(mode));
return Fixed_Output_RNG(rng, consts.len_sec_bytes() + consts.len_se_bytes() + consts.len_a_bytes());
}

auto rng_for_encapsulation(Botan::RandomNumberGenerator& rng) const {
Botan::FrodoKEMConstants consts(m_mode);
Fixed_Output_RNG rng_for_encapsulation(const std::string& mode, Botan::RandomNumberGenerator& rng) const final {
Botan::FrodoKEMConstants consts(get_mode(mode));
return Fixed_Output_RNG(rng, consts.len_sec_bytes() + consts.len_salt_bytes());
}

private:
Botan::FrodoKEMMode m_mode;
};

class Frodo_KAT_Tests : public Botan_Tests::PK_PQC_KEM_KAT_Test<Frodo_KAT_Tests_Impl> {};

std::vector<Test::Result> test_frodo_roundtrips() {
auto& rng = Test::rng();

Expand Down
34 changes: 14 additions & 20 deletions src/tests/test_kyber.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,47 +110,41 @@ BOTAN_REGISTER_TEST("kyber", "kyber_pairwise", KYBER_Tests);

namespace {

class Kyber_KAT_Tests_Impl {
class Kyber_KAT_Tests final : public PK_PQC_KEM_KAT_Test {
public:
using public_key_t = Botan::Kyber_PublicKey;
using private_key_t = Botan::Kyber_PrivateKey;
Kyber_KAT_Tests() : PK_PQC_KEM_KAT_Test("Kyber", "pubkey/kyber_kat.vec") {}

static constexpr const char* algo_name = "Kyber";
static constexpr const char* input_file = "pubkey/kyber_kat.vec";

public:
Kyber_KAT_Tests_Impl(std::string_view algo_spec) : m_mode(algo_spec) {}

decltype(auto) mode() const { return m_mode; }
private:
Botan::KyberMode get_mode(const std::string& mode) const { return Botan::KyberMode(mode); }

bool available() const { return m_mode.is_available(); }
bool is_available(const std::string& mode) const final { return get_mode(mode).is_available(); }

std::vector<uint8_t> map_value(std::span<const uint8_t> value) const {
std::vector<uint8_t> map_value(const std::string& mode,
std::span<const uint8_t> value,
VarType var_type) const final {
if(var_type == VarType::SharedSecret) {
return {value.begin(), value.end()};
}
// We use different hash functions for Kyber 90s and Kyber "modern", as
// those are consistent with the requirements of the implementations.
std::string_view hash_name = m_mode.is_modern() ? "SHAKE-256(128)" : "SHA-256";
std::string_view hash_name = get_mode(mode).is_modern() ? "SHAKE-256(128)" : "SHA-256";

auto hash = Botan::HashFunction::create_or_throw(hash_name);
const auto digest = hash->process(value);
return {digest.begin(), digest.begin() + 16};
}

auto rng_for_keygen(Botan::RandomNumberGenerator& rng) const {
Fixed_Output_RNG rng_for_keygen(const std::string&, Botan::RandomNumberGenerator& rng) const final {
const auto seed = rng.random_vec(32);
const auto z = rng.random_vec(32);
return Fixed_Output_RNG(Botan::concat(seed, z));
}

auto rng_for_encapsulation(Botan::RandomNumberGenerator& rng) const {
Fixed_Output_RNG rng_for_encapsulation(const std::string&, Botan::RandomNumberGenerator& rng) const final {
return Fixed_Output_RNG(rng.random_vec(32));
}

private:
Botan::KyberMode m_mode;
};

class Kyber_KAT_Tests : public Botan_Tests::PK_PQC_KEM_KAT_Test<Kyber_KAT_Tests_Impl> {};

} // namespace

BOTAN_REGISTER_TEST("kyber", "kyber_kat", Kyber_KAT_Tests);
Expand Down
120 changes: 70 additions & 50 deletions src/tests/test_pubkey_pqc.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,11 @@
#include "test_rng.h"

#include <botan/hash.h>
#include <botan/pk_algs.h>
#include <botan/internal/fmt.h>

namespace Botan_Tests {

namespace detail {

template <typename T>
concept PQC_KEM_KAT_Test_Implementation =
std::derived_from<typename T::private_key_t, Botan::Private_Key> &&
std::derived_from<typename T::public_key_t, Botan::Public_Key> &&
std::convertible_to<decltype(T::input_file), std::string> &&
std::convertible_to<decltype(T::algo_name), std::string> &&
requires(
T impl, Botan::RandomNumberGenerator& rng, std::span<const uint8_t> crypto_artefact, std::string_view algo_spec) {
{ T(algo_spec) } -> std::same_as<T>;
{ impl.rng_for_keygen(rng) } -> std::same_as<Botan_Tests::Fixed_Output_RNG>;
{ impl.rng_for_encapsulation(rng) } -> std::same_as<Botan_Tests::Fixed_Output_RNG>;
{ impl.map_value(crypto_artefact) } -> std::same_as<std::vector<uint8_t>>;
{ impl.available() } -> std::convertible_to<bool>;
{ std::is_constructible_v<typename T::private_key_t, Botan::RandomNumberGenerator&, decltype(impl.mode())> };
};

}

/**
* This is an abstraction over the Known Answer Tests used by the KEM candidates
* in the NIST PQC competition.
Expand All @@ -49,22 +30,54 @@ concept PQC_KEM_KAT_Test_Implementation =
*
* See also: https://csrc.nist.gov/projects/post-quantum-cryptography/post-quantum-cryptography-standardization/example-files
*/
template <detail::PQC_KEM_KAT_Test_Implementation Delegate>
class PK_PQC_KEM_KAT_Test : public PK_Test {
public:
PK_PQC_KEM_KAT_Test() : PK_Test(Delegate::algo_name, Delegate::input_file, "Seed,SS,PK,SK,CT") {}
protected:
/// Type of a KAT vector entry that can be recomputed using the seed
enum class VarType { SharedSecret, PublicKey, PrivateKey, Ciphertext };

private:
using Private_Key = typename Delegate::private_key_t;
using Public_Key = typename Delegate::public_key_t;
PK_PQC_KEM_KAT_Test(const std::string& algo_name, const std::string& input_file) :
PK_Test(algo_name, input_file, "Seed,SS,PK,SK,CT") {}

// --- Callbacks ---

/// Map a recomputed value to the expected value from the KAT vector (e.g. apply a hash function if Botan's KAT entry is hashed)
virtual std::vector<uint8_t> map_value(const std::string& params,
std::span<const uint8_t> value,
VarType value_type) const = 0;

/// Create an RNG that can be used to generate the keypair. @p rng is the DRBG that is used to expand the seed.
virtual Fixed_Output_RNG rng_for_keygen(const std::string& params, Botan::RandomNumberGenerator& rng) const = 0;

/// Create an RNG that can be used to generate the keypair. @p rng is the DRBG that is used to expand the seed.
virtual Fixed_Output_RNG rng_for_encapsulation(const std::string& params,
Botan::RandomNumberGenerator& rng) const = 0;

/// Return true if the algorithm with the specified params should be tested
virtual bool is_available(const std::string& params) const = 0;

/// Callback to test the RNG's state after key generation. If not overridden checks that the RNG is empty.
virtual void inspect_rng_after_keygen(const std::string& params,
const Fixed_Output_RNG& rng_keygen,
Test::Result& result) const {
BOTAN_UNUSED(params);
result.confirm("All prepared random bits used for key generation", rng_keygen.empty());
}

/// Callback to test the RNG's state after encapsulation. If not overridden checks that the RNG is empty.
virtual void inspect_rng_after_encaps(const std::string& params,
const Fixed_Output_RNG& rng_encaps,
Test::Result& result) const {
BOTAN_UNUSED(params);
result.confirm("All prepared random bits used for encapsulation", rng_encaps.empty());
}

private:
bool skip_this_test(const std::string& header, const VarMap&) override {
bool skip_this_test(const std::string& params, const VarMap&) final {
#if !defined(BOTAN_HAS_AES)
BOTAN_UNUSED(header);
BOTAN_UNUSED(params);
return true;
#else
return !Delegate(header).available();
return !is_available(params);
#endif
}

Expand All @@ -77,47 +90,54 @@ class PK_PQC_KEM_KAT_Test : public PK_Test {
#endif
}

Test::Result run_one_test(const std::string& header, const VarMap& vars) final {
Test::Result result(Botan::fmt("PQC KAT for {} with parameters {}", algo_name(), header));
auto d = Delegate(header);
Test::Result run_one_test(const std::string& params, const VarMap& vars) final {
Test::Result result(Botan::fmt("PQC KAT for {} with parameters {}", algo_name(), params));

// All PQC algorithms use this DRBG in their KAT tests to generate
// their private keys. The amount of data that needs to be pulled from
// the RNG for keygen and encapsulation is dependent on the algorithm
// and the implementation.
auto ctr_drbg = create_drbg(vars.get_req_bin("Seed"));
auto rng_keygen = d.rng_for_keygen(*ctr_drbg);
auto rng_encaps = d.rng_for_encapsulation(*ctr_drbg);
auto rng_keygen = rng_for_keygen(params, *ctr_drbg);
auto rng_encaps = rng_for_encapsulation(params, *ctr_drbg);

// Key Generation
auto sk = Private_Key(rng_keygen, d.mode());
result.test_is_eq("Generated private key", d.map_value(sk.raw_private_key_bits()), vars.get_req_bin("SK"));
result.confirm("All prepared random bits used for key generation", rng_keygen.empty());
auto sk = Botan::create_private_key(algo_name(), rng_keygen, params);
result.test_is_eq("Generated private key",
map_value(params, sk->raw_private_key_bits(), VarType::PrivateKey),
vars.get_req_bin("SK"));
inspect_rng_after_keygen(params, rng_keygen, result);

// Algorithm properties
result.test_eq("algorithm name", sk.algo_name(), algo_name());
FAlbertDev marked this conversation as resolved.
Show resolved Hide resolved
result.confirm("supported operation", sk.supports_operation(Botan::PublicKeyOperation::KeyEncapsulation));
result.test_gte("Key has reasonable estimated strength (lower)", sk.estimated_strength(), 64);
result.test_lt("Key has reasonable estimated strength (upper)", sk.estimated_strength(), 512);
result.test_eq("Algorithm name", sk->algo_name(), algo_name());
result.confirm("Supported operation KeyEncapsulation",
sk->supports_operation(Botan::PublicKeyOperation::KeyEncapsulation));
result.test_gte("Key has reasonable estimated strength (lower)", sk->estimated_strength(), 64);
result.test_lt("Key has reasonable estimated strength (upper)", sk->estimated_strength(), 512);

// Extract Public Key
auto pk = sk.public_key();
result.test_is_eq("Generated public key", d.map_value(pk->public_key_bits()), vars.get_req_bin("PK"));
auto pk = sk->public_key();
result.test_is_eq("Generated public key",
map_value(params, pk->public_key_bits(), VarType::PublicKey),
vars.get_req_bin("PK"));

// Serialize/Deserialize the Public Key
auto pk2 = Public_Key(pk->public_key_bits(), d.mode());
auto pk2 = Botan::load_public_key(pk->algorithm_identifier(), pk->public_key_bits());

// Encapsulation
auto enc = Botan::PK_KEM_Encryptor(pk2, "Raw");
auto enc = Botan::PK_KEM_Encryptor(*pk2, "Raw");
const auto encaped = enc.encrypt(rng_encaps, 0 /* no KDF */);
result.test_is_eq("Shared Secret", encaped.shared_key(), Botan::lock(vars.get_req_bin("SS")));
result.test_is_eq("Ciphertext", d.map_value(encaped.encapsulated_shared_key()), vars.get_req_bin("CT"));
result.confirm("All prepared random bits used for encapsulation", rng_encaps.empty());
result.test_is_eq(
"Shared Secret", map_value(params, encaped.shared_key(), VarType::SharedSecret), vars.get_req_bin("SS"));
result.test_is_eq("Ciphertext",
map_value(params, encaped.encapsulated_shared_key(), VarType::Ciphertext),
vars.get_req_bin("CT"));
inspect_rng_after_encaps(params, rng_keygen, result);

// Decapsulation
Private_Key sk2(sk.private_key_bits(), d.mode());
auto sk2 = Botan::load_private_key(sk->algorithm_identifier(), sk->private_key_bits());
Botan::Null_RNG null_rng;
auto dec = Botan::PK_KEM_Decryptor(sk2, null_rng, "Raw");
auto dec = Botan::PK_KEM_Decryptor(*sk2, null_rng, "Raw");
const auto shared_key = dec.decrypt(encaped.encapsulated_shared_key(), 0 /* no KDF */);
result.test_is_eq("Decaps. Shared Secret", shared_key, Botan::lock(vars.get_req_bin("SS")));

Expand Down