Skip to content

Commit

Permalink
Merge pull request #3892 from Rohde-Schwarz/refactor/pqc-key-kat-test
Browse files Browse the repository at this point in the history
Prepare PQC KEM KAT Tests for Classic McEliece
  • Loading branch information
reneme committed Jan 18, 2024
2 parents b8afed8 + 0e30809 commit 7f15ce7
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 91 deletions.
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());
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

0 comments on commit 7f15ce7

Please sign in to comment.