From 3235a24f3f3cc4e1c62361999c2a51ea5f440a2e Mon Sep 17 00:00:00 2001 From: Fabian Albert Date: Tue, 14 May 2024 14:11:25 +0200 Subject: [PATCH] Abstraction layer for hybrid KEMs --- src/lib/pubkey/hybrid_kem/hybrid_kem.cpp | 93 ++++++ src/lib/pubkey/hybrid_kem/hybrid_kem.h | 129 ++++++++ src/lib/pubkey/hybrid_kem/hybrid_kem_ops.cpp | 101 ++++++ src/lib/pubkey/hybrid_kem/hybrid_kem_ops.h | 141 ++++++++ src/lib/pubkey/hybrid_kem/info.txt | 20 ++ src/lib/pubkey/kex_to_kem_adapter/info.txt | 17 + .../kex_to_kem_adapter.cpp | 8 +- .../kex_to_kem_adapter}/kex_to_kem_adapter.h | 6 +- src/lib/tls/tls13_pqc/hybrid_public_key.cpp | 304 +++++++----------- src/lib/tls/tls13_pqc/hybrid_public_key.h | 49 +-- src/lib/tls/tls13_pqc/info.txt | 3 +- src/tests/test_tls_hybrid_kem_key.cpp | 20 +- 12 files changed, 651 insertions(+), 240 deletions(-) create mode 100644 src/lib/pubkey/hybrid_kem/hybrid_kem.cpp create mode 100644 src/lib/pubkey/hybrid_kem/hybrid_kem.h create mode 100644 src/lib/pubkey/hybrid_kem/hybrid_kem_ops.cpp create mode 100644 src/lib/pubkey/hybrid_kem/hybrid_kem_ops.h create mode 100644 src/lib/pubkey/hybrid_kem/info.txt create mode 100644 src/lib/pubkey/kex_to_kem_adapter/info.txt rename src/lib/{tls/tls13_pqc => pubkey/kex_to_kem_adapter}/kex_to_kem_adapter.cpp (98%) rename src/lib/{tls/tls13_pqc => pubkey/kex_to_kem_adapter}/kex_to_kem_adapter.h (96%) diff --git a/src/lib/pubkey/hybrid_kem/hybrid_kem.cpp b/src/lib/pubkey/hybrid_kem/hybrid_kem.cpp new file mode 100644 index 00000000000..7b5d19e1127 --- /dev/null +++ b/src/lib/pubkey/hybrid_kem/hybrid_kem.cpp @@ -0,0 +1,93 @@ +/** +* Abstraction for a combined KEM public and private key. +* +* (C) 2024 Jack Lloyd +* 2024 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ +#include + +#include +#include +#include +#include +#include + +namespace Botan { + +Hybrid_PublicKey::Hybrid_PublicKey(std::vector> pks) : m_pks(std::move(pks)) { + BOTAN_ARG_CHECK(m_pks.size() >= 2, "List of public keys must include at least two keys"); + BOTAN_ARG_CHECK(std::all_of(m_pks.begin(), m_pks.end(), [](const auto& pk) { return pk != nullptr; }), + "List of public keys contains a nullptr"); + BOTAN_ARG_CHECK( + std::all_of(m_pks.begin(), + m_pks.end(), + [](const auto& pk) { return pk->supports_operation(PublicKeyOperation::KeyEncapsulation); }), + "Some provided public key is not compatible with this hybrid wrapper"); + m_key_length = reduce(m_pks, size_t(0), [](size_t kl, const auto& key) { return std::max(kl, key->key_length()); }); + m_estimated_strength = + reduce(m_pks, size_t(0), [](size_t es, const auto& key) { return std::max(es, key->estimated_strength()); }); +} + +bool Hybrid_PublicKey::check_key(RandomNumberGenerator& rng, bool strong) const { + return reduce(public_keys(), true, [&](bool ckr, const auto& key) { return ckr && key->check_key(rng, strong); }); +} + +std::vector Hybrid_PublicKey::public_key_bits() const { + return reduce(public_keys(), std::vector(), [](auto pkb, const auto& key) { + // Technically, this is not correct! `public_key_bits()` is meant to + // return a BER-encoded public key. + // + // TODO: Provide something like Public_Key::raw_public_key_bits() to + // reflect that difference. + return concat(pkb, key->public_key_bits()); + }); +} + +bool Hybrid_PublicKey::supports_operation(PublicKeyOperation op) const { + return PublicKeyOperation::KeyEncapsulation == op; +} + +std::vector> Hybrid_PublicKey::generate_other_sks_from_pks( + RandomNumberGenerator& rng) const { + std::vector> new_private_keys; + std::transform( + public_keys().begin(), public_keys().end(), std::back_inserter(new_private_keys), [&](const auto& public_key) { + return public_key->generate_another(rng); + }); + return new_private_keys; +} + +Hybrid_PrivateKey::Hybrid_PrivateKey(std::vector> private_keys) : + m_sks(std::move(private_keys)) { + BOTAN_ARG_CHECK(m_sks.size() >= 2, "List of secret keys must include at least two keys"); + BOTAN_ARG_CHECK(std::all_of(m_sks.begin(), m_sks.end(), [](const auto& sk) { return sk != nullptr; }), + "List of secret keys contains a nullptr"); + BOTAN_ARG_CHECK( + std::all_of(m_sks.begin(), + m_sks.end(), + [](const auto& sk) { return sk->supports_operation(PublicKeyOperation::KeyEncapsulation); }), + "Some provided secret key is not compatible with this hybrid wrapper"); +} + +secure_vector Hybrid_PrivateKey::private_key_bits() const { + throw Not_Implemented("Hybrid private keys cannot be serialized"); +} + +bool Hybrid_PrivateKey::check_key(RandomNumberGenerator& rng, bool strong) const { + return reduce(private_keys(), true, [&](bool ckr, const auto& key) { return ckr && key->check_key(rng, strong); }); +} + +std::vector> Hybrid_PrivateKey::extract_public_keys( + const std::vector>& private_keys) { + std::vector> public_keys; + public_keys.reserve(private_keys.size()); + for(const auto& private_key : private_keys) { + BOTAN_ARG_CHECK(private_key != nullptr, "List of private keys contains a nullptr"); + public_keys.push_back(private_key->public_key()); + } + return public_keys; +} + +} // namespace Botan diff --git a/src/lib/pubkey/hybrid_kem/hybrid_kem.h b/src/lib/pubkey/hybrid_kem/hybrid_kem.h new file mode 100644 index 00000000000..e0d5eadf1dd --- /dev/null +++ b/src/lib/pubkey/hybrid_kem/hybrid_kem.h @@ -0,0 +1,129 @@ +/** +* Abstraction for a combined KEM public and private key. +* +* (C) 2024 Jack Lloyd +* 2024 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_HYBRID_KEM_H_ +#define BOTAN_HYBRID_KEM_H_ + +#include +#include +#include + +#include +#include + +namespace Botan { + +/** + * @brief Abstraction for a combined KEM public key. + * + * Two or more KEM public keys are combined into a single KEM public key. Derived classes + * must implement the abstract methods to provide the encryption operation, e.g. by + * specifying how encryption results are combined to the ciphertext and how a KEM combiner + * is applied to derive the shared secret using the individual shared secrets, ciphertexts, + * and other context information. + */ +class BOTAN_TEST_API Hybrid_PublicKey : public virtual Public_Key { + public: + /** + * @brief Constructor for a list of multiple KEM public keys. + * + * To use KEX algorithms use the KEX_to_KEM_Adapter_PublicKey. + * @param public_keys List of public keys to combine + */ + explicit Hybrid_PublicKey(std::vector> public_keys); + + Hybrid_PublicKey(Hybrid_PublicKey&&) = default; + Hybrid_PublicKey(const Hybrid_PublicKey&) = delete; + Hybrid_PublicKey& operator=(Hybrid_PublicKey&&) = default; + Hybrid_PublicKey& operator=(const Hybrid_PublicKey&) = delete; + ~Hybrid_PublicKey() override = default; + + size_t estimated_strength() const override { return m_estimated_strength; } + + size_t key_length() const override { return m_key_length; } + + bool check_key(RandomNumberGenerator& rng, bool strong) const override; + + std::vector public_key_bits() const override; + + bool supports_operation(PublicKeyOperation op) const override; + + /// @returns the public keys combined in this hybrid key + const std::vector>& public_keys() const { return m_pks; } + + protected: + // Default constructor used for virtual inheritance to prevent, that the derived class + // calls the constructor twice. + Hybrid_PublicKey() = default; + + std::vector> copy_public_keys() const; + + /** + * @brief Helper function for generate_another. Generate a new private key for each + * public key in this hybrid key. + */ + std::vector> generate_other_sks_from_pks(RandomNumberGenerator& rng) const; + + private: + std::vector> m_pks; + + size_t m_key_length; + size_t m_estimated_strength; +}; + +BOTAN_DIAGNOSTIC_PUSH +BOTAN_DIAGNOSTIC_IGNORE_INHERITED_VIA_DOMINANCE + +/** + * @brief Abstraction for a combined KEM private key. + * + * Two or more KEM private keys are combined into a single KEM private key. Derived classes + * must implement the abstract methods to provide the decryption operation, e.g. by + * specifying how a KEM combiner is applied to derive the shared secret using the + * individual shared secrets, ciphertexts, and other context information. + */ +class BOTAN_TEST_API Hybrid_PrivateKey : virtual public Private_Key { + public: + Hybrid_PrivateKey(const Hybrid_PrivateKey&) = delete; + Hybrid_PrivateKey& operator=(const Hybrid_PrivateKey&) = delete; + + Hybrid_PrivateKey(Hybrid_PrivateKey&&) = default; + Hybrid_PrivateKey& operator=(Hybrid_PrivateKey&&) = default; + + ~Hybrid_PrivateKey() override = default; + + /** + * @brief Constructor for a list of multiple KEM private keys. + * + * To use KEX algorithms use the KEX_to_KEM_Adapter_PrivateKey. + * @param private_keys List of private keys to combine + */ + Hybrid_PrivateKey(std::vector> private_keys); + + /// Disabled by default + secure_vector private_key_bits() const override; + + /// @returns the private keys combined in this hybrid key + const std::vector>& private_keys() const { return m_sks; } + + bool check_key(RandomNumberGenerator& rng, bool strong) const override; + + protected: + static std::vector> extract_public_keys( + const std::vector>& private_keys); + + private: + std::vector> m_sks; +}; + +BOTAN_DIAGNOSTIC_POP + +} // namespace Botan + +#endif diff --git a/src/lib/pubkey/hybrid_kem/hybrid_kem_ops.cpp b/src/lib/pubkey/hybrid_kem/hybrid_kem_ops.cpp new file mode 100644 index 00000000000..f16d34cc274 --- /dev/null +++ b/src/lib/pubkey/hybrid_kem/hybrid_kem_ops.cpp @@ -0,0 +1,101 @@ +#include + +#include + +namespace Botan { + +KEM_Encryption_with_Combiner::KEM_Encryption_with_Combiner(const std::vector>& public_keys, + std::string_view provider) : + m_encapsulated_key_length(0) { + m_encryptors.reserve(public_keys.size()); + for(const auto& pk : public_keys) { + const auto& newenc = m_encryptors.emplace_back(*pk, "Raw", provider); + m_encapsulated_key_length += newenc.encapsulated_key_length(); + } +} + +void KEM_Encryption_with_Combiner::kem_encrypt(std::span out_encapsulated_key, + std::span out_shared_key, + RandomNumberGenerator& rng, + size_t desired_shared_key_len, + std::span salt) { + BOTAN_ARG_CHECK(out_encapsulated_key.size() == encapsulated_key_length(), + "Encapsulated key output buffer has wrong size"); + BOTAN_ARG_CHECK(out_shared_key.size() == shared_key_length(desired_shared_key_len), + "Shared key output buffer has wrong size"); + + std::vector> shared_secrets; + shared_secrets.reserve(m_encryptors.size()); + + std::vector> ciphertexts; + ciphertexts.reserve(m_encryptors.size()); + + for(auto& encryptor : m_encryptors) { + auto [ct, ss] = KEM_Encapsulation::destructure(encryptor.encrypt(rng, 0 /* no KDF */)); + shared_secrets.push_back(std::move(ss)); + ciphertexts.push_back(std::move(ct)); + } + combine_ciphertexts(out_encapsulated_key, ciphertexts, salt); + combine_shared_secrets(out_shared_key, shared_secrets, ciphertexts, desired_shared_key_len, salt); +} + +void KEM_Encryption_with_Combiner::combine_ciphertexts(std::span out_ciphertext, + const std::vector>& ciphertexts, + std::span salt) { + BOTAN_ARG_CHECK(salt.empty(), "Salt not supported by this KEM"); + BOTAN_ARG_CHECK(ciphertexts.size() == m_encryptors.size(), "Invalid number of ciphertexts"); + BOTAN_ARG_CHECK(out_ciphertext.size() == encapsulated_key_length(), "Invalid output buffer size"); + BufferStuffer ct_stuffer(out_ciphertext); + for(size_t idx = 0; idx < ciphertexts.size(); idx++) { + BOTAN_ARG_CHECK(ciphertexts.at(idx).size() == m_encryptors.at(idx).encapsulated_key_length(), + "Invalid ciphertext length"); + ct_stuffer.append(ciphertexts.at(idx)); + } + BOTAN_ASSERT_NOMSG(ct_stuffer.full()); +} + +KEM_Decryption_with_Combiner::KEM_Decryption_with_Combiner( + const std::vector>& private_keys, + RandomNumberGenerator& rng, + std::string_view provider) : + m_encapsulated_key_length(0) { + m_decryptors.reserve(private_keys.size()); + for(const auto& sk : private_keys) { + const auto& newenc = m_decryptors.emplace_back(*sk, rng, "Raw", provider); + m_encapsulated_key_length += newenc.encapsulated_key_length(); + } +} + +void KEM_Decryption_with_Combiner::kem_decrypt(std::span out_shared_key, + std::span encapsulated_key, + size_t desired_shared_key_len, + std::span salt) { + BOTAN_ARG_CHECK(encapsulated_key.size() == encapsulated_key_length(), "Invalid encapsulated key length"); + BOTAN_ARG_CHECK(out_shared_key.size() == shared_key_length(desired_shared_key_len), "Invalid output buffer size"); + + std::vector> shared_secrets; + shared_secrets.reserve(m_decryptors.size()); + auto ciphertexts = split_ciphertexts(encapsulated_key); + BOTAN_ASSERT(ciphertexts.size() == m_decryptors.size(), "Correct number of ciphertexts"); + + for(size_t idx = 0; idx < m_decryptors.size(); idx++) { + shared_secrets.push_back(m_decryptors.at(idx).decrypt(ciphertexts.at(idx), 0 /* no KDF */)); + } + + combine_shared_secrets(out_shared_key, shared_secrets, ciphertexts, desired_shared_key_len, salt); +} + +std::vector> KEM_Decryption_with_Combiner::split_ciphertexts( + std::span concat_ciphertext) { + BOTAN_ARG_CHECK(concat_ciphertext.size() == encapsulated_key_length(), "Wrong ciphertext length"); + std::vector> ciphertexts; + ciphertexts.reserve(m_decryptors.size()); + BufferSlicer ct_slicer(concat_ciphertext); + for(const auto& decryptor : m_decryptors) { + ciphertexts.push_back(ct_slicer.copy_as_vector(decryptor.encapsulated_key_length())); + } + BOTAN_ASSERT_NOMSG(ct_slicer.empty()); + return ciphertexts; +} + +} // namespace Botan diff --git a/src/lib/pubkey/hybrid_kem/hybrid_kem_ops.h b/src/lib/pubkey/hybrid_kem/hybrid_kem_ops.h new file mode 100644 index 00000000000..0bf66273437 --- /dev/null +++ b/src/lib/pubkey/hybrid_kem/hybrid_kem_ops.h @@ -0,0 +1,141 @@ +/** +* Abstraction for a combined KEM encryptors and decryptors. +* +* (C) 2024 Jack Lloyd +* 2024 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_HYBRID_KEM_OPS_H_ +#define BOTAN_HYBRID_KEM_OPS_H_ + +#include +#include +#include +#include + +#include +#include + +namespace Botan { + +/** + * @brief Abstract interface for a KEM encryption operation for KEM combiners. + * + * Multiple public keys are used to encapsulate shared secrets. These shared + * secrets (and maybe the ciphertexts and public keys) are combined using the + * KEM combiner to derive the final shared secret. + * + */ +class KEM_Encryption_with_Combiner : public PK_Ops::KEM_Encryption { + public: + KEM_Encryption_with_Combiner(const std::vector>& public_keys, + std::string_view provider); + + void kem_encrypt(std::span out_encapsulated_key, + std::span out_shared_key, + RandomNumberGenerator& rng, + size_t desired_shared_key_len, + std::span salt) final; + + /// The default implementation returns the sum of the encapsulated key lengths of the underlying KEMs. + size_t encapsulated_key_length() const override { return m_encapsulated_key_length; } + + protected: + /** + * @brief Defines how multiple ciphertexts are combined into a single ciphertext. + * + * The default implementation concatenates the ciphertexts. + * + * @param out_ciphertext The output buffer for the combined ciphertext + * @param ciphertexts The ciphertexts to combine + * @param salt The salt. In this default implementation the salt must be empty. + */ + virtual void combine_ciphertexts(std::span out_ciphertext, + const std::vector>& ciphertexts, + std::span salt); + + /** + * @brief Describes how the shared secrets are combined to derive the final shared secret. + * + * @param out_shared_secret the output buffer for the shared secret + * @param shared_secrets a list of shared secrets coreesponding to the public keys + * @param ciphertexts a list of encapsulated shared secrets + * @param desired_shared_key_len the desired shared key length + * @param salt the salt (input of kem_encrypt) + */ + virtual void combine_shared_secrets(std::span out_shared_secret, + const std::vector>& shared_secrets, + const std::vector>& ciphertexts, + size_t desired_shared_key_len, + std::span salt) = 0; + + std::vector& encryptors() { return m_encryptors; } + + const std::vector& encryptors() const { return m_encryptors; } + + private: + std::vector m_encryptors; + size_t m_encapsulated_key_length; +}; + +/** + * @brief Abstract interface for a KEM decryption operation for KEM combiners. + * + * Multiple private keys are used to decapsulate shared secrets from a combined + * ciphertext (concatenated in most cases). These shared + * secrets (and maybe the ciphertexts and public keys) are combined using the + * KEM combiner to derive the final shared secret. + */ +class KEM_Decryption_with_Combiner : public PK_Ops::KEM_Decryption { + public: + KEM_Decryption_with_Combiner(const std::vector>& private_keys, + RandomNumberGenerator& rng, + std::string_view provider); + + void kem_decrypt(std::span out_shared_key, + std::span encapsulated_key, + size_t desired_shared_key_len, + std::span salt) final; + + /// The default implementation returns the sum of the encapsulated key lengths of the underlying KEMs. + size_t encapsulated_key_length() const override { return m_encapsulated_key_length; } + + protected: + /** + * @brief Defines how the individual ciphertexts are extracted from the combined ciphertext. + * + * The default implementation splits concatenated ciphertexts. + * @param concat_ciphertext The combined ciphertext + * @returns The individual ciphertexts + */ + virtual std::vector> split_ciphertexts(std::span concat_ciphertext); + + /** + * @brief Describes how the shared secrets are combined to derive the final shared secret. + * + * @param out_shared_secret the output buffer for the shared secret + * @param shared_secrets a list of shared secrets coreesponding to the public keys + * @param ciphertexts the list of encapsulated shared secrets + * @param desired_shared_key_len the desired shared key length + * @param salt the salt (input of kem_decrypt) + */ + virtual void combine_shared_secrets(std::span out_shared_secret, + const std::vector>& shared_secrets, + const std::vector>& ciphertexts, + size_t desired_shared_key_len, + std::span salt) = 0; + + std::vector& decryptors() { return m_decryptors; } + + const std::vector& decryptors() const { return m_decryptors; } + + private: + std::vector m_decryptors; + size_t m_encapsulated_key_length; +}; + +} // namespace Botan + +#endif // BOTAN_HYBRID_KEM_OPS_H_ diff --git a/src/lib/pubkey/hybrid_kem/info.txt b/src/lib/pubkey/hybrid_kem/info.txt new file mode 100644 index 00000000000..f51fb9e8cd7 --- /dev/null +++ b/src/lib/pubkey/hybrid_kem/info.txt @@ -0,0 +1,20 @@ + +HYBRID_KEM -> 20240425 + + + +name -> "Hybrid KEM" +type -> "Internal" + + + +hybrid_kem.h + + + +hybrid_kem_ops.h + + + + + diff --git a/src/lib/pubkey/kex_to_kem_adapter/info.txt b/src/lib/pubkey/kex_to_kem_adapter/info.txt new file mode 100644 index 00000000000..f251f46b071 --- /dev/null +++ b/src/lib/pubkey/kex_to_kem_adapter/info.txt @@ -0,0 +1,17 @@ + +KEX_TO_KEM_ADAPTER -> 20240504 + + + +name -> "KEX to KEM adapter" +type -> "Internal" + + + + +kex_to_kem_adapter.h + + + + + diff --git a/src/lib/tls/tls13_pqc/kex_to_kem_adapter.cpp b/src/lib/pubkey/kex_to_kem_adapter/kex_to_kem_adapter.cpp similarity index 98% rename from src/lib/tls/tls13_pqc/kex_to_kem_adapter.cpp rename to src/lib/pubkey/kex_to_kem_adapter/kex_to_kem_adapter.cpp index fd51dfcc9f9..c90c8027249 100644 --- a/src/lib/tls/tls13_pqc/kex_to_kem_adapter.cpp +++ b/src/lib/pubkey/kex_to_kem_adapter/kex_to_kem_adapter.cpp @@ -31,7 +31,7 @@ #include #endif -namespace Botan::TLS { +namespace Botan { namespace { @@ -230,6 +230,10 @@ secure_vector KEX_to_KEM_Adapter_PrivateKey::private_key_bits() const { return m_private_key->private_key_bits(); } +secure_vector KEX_to_KEM_Adapter_PrivateKey::raw_private_key_bits() const { + return m_private_key->raw_private_key_bits(); +} + std::unique_ptr KEX_to_KEM_Adapter_PrivateKey::public_key() const { return std::make_unique(m_private_key->public_key()); } @@ -248,4 +252,4 @@ std::unique_ptr KEX_to_KEM_Adapter_PrivateKey::create_ke return std::make_unique(*m_private_key, rng, kdf, provider); } -} // namespace Botan::TLS +} // namespace Botan diff --git a/src/lib/tls/tls13_pqc/kex_to_kem_adapter.h b/src/lib/pubkey/kex_to_kem_adapter/kex_to_kem_adapter.h similarity index 96% rename from src/lib/tls/tls13_pqc/kex_to_kem_adapter.h rename to src/lib/pubkey/kex_to_kem_adapter/kex_to_kem_adapter.h index 9c57f24d343..67eb822a0ef 100644 --- a/src/lib/tls/tls13_pqc/kex_to_kem_adapter.h +++ b/src/lib/pubkey/kex_to_kem_adapter/kex_to_kem_adapter.h @@ -15,7 +15,7 @@ #include -namespace Botan::TLS { +namespace Botan { /** * Adapter to use a key agreement key pair (e.g. ECDH) as a key encapsulation @@ -71,6 +71,8 @@ class BOTAN_TEST_API KEX_to_KEM_Adapter_PrivateKey final : public KEX_to_KEM_Ada secure_vector private_key_bits() const override; + secure_vector raw_private_key_bits() const override; + std::unique_ptr public_key() const override; bool check_key(RandomNumberGenerator& rng, bool strong) const override; @@ -84,6 +86,6 @@ class BOTAN_TEST_API KEX_to_KEM_Adapter_PrivateKey final : public KEX_to_KEM_Ada BOTAN_DIAGNOSTIC_POP -} // namespace Botan::TLS +} // namespace Botan #endif diff --git a/src/lib/tls/tls13_pqc/hybrid_public_key.cpp b/src/lib/tls/tls13_pqc/hybrid_public_key.cpp index a49dd9c66c9..c326456b0a1 100644 --- a/src/lib/tls/tls13_pqc/hybrid_public_key.cpp +++ b/src/lib/tls/tls13_pqc/hybrid_public_key.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -142,6 +143,98 @@ std::vector public_value_lengths_for_group(Group_Params group) { } } +std::vector> convert_kex_to_kem_pks(std::vector> pks) { + std::vector> result; + std::transform(pks.begin(), pks.end(), std::back_inserter(result), [](auto& key) -> std::unique_ptr { + BOTAN_ARG_CHECK(key != nullptr, "Public key list contains a nullptr"); + if(key->supports_operation(PublicKeyOperation::KeyAgreement) && + !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) { + return std::make_unique(std::move(key)); + } else { + return std::move(key); + } + }); + return result; +} + +std::vector> convert_kex_to_kem_sks(std::vector> sks) { + std::vector> result; + std::transform(sks.begin(), sks.end(), std::back_inserter(result), [](auto& key) -> std::unique_ptr { + BOTAN_ARG_CHECK(key != nullptr, "Private key list contains a nullptr"); + if(key->supports_operation(PublicKeyOperation::KeyAgreement) && + !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) { + auto ka_key = dynamic_cast(key.get()); + BOTAN_ASSERT_NONNULL(ka_key); + (void)key.release(); + return std::make_unique(std::unique_ptr(ka_key)); + } else { + return std::move(key); + } + }); + return result; +} + +template +void concat_secret_combiner(KEM_Operation& op, + std::span out_shared_secret, + const std::vector>& shared_secrets, + size_t desired_shared_key_len) { + BOTAN_ARG_CHECK(out_shared_secret.size() == op.shared_key_length(desired_shared_key_len), + "Invalid output buffer size"); + + BufferStuffer shared_secret_stuffer(out_shared_secret); + for(size_t idx = 0; idx < shared_secrets.size(); idx++) { + shared_secret_stuffer.append(shared_secrets.at(idx)); + } + BOTAN_ASSERT_NOMSG(shared_secret_stuffer.full()); +} + +template +size_t concat_shared_key_length(const std::vector& operation) { + return reduce( + operation, size_t(0), [](size_t acc, const auto& op) { return acc + op.shared_key_length(0 /*no KDF*/); }); +} + +/// Encryptor that simply concatenates the multiple shared secrets +class Hybrid_TLS_KEM_Encryptor final : public KEM_Encryption_with_Combiner { + public: + Hybrid_TLS_KEM_Encryptor(const std::vector>& public_keys, std::string_view provider) : + KEM_Encryption_with_Combiner(public_keys, provider) {} + + void combine_shared_secrets(std::span out_shared_secret, + const std::vector>& shared_secrets, + const std::vector>& /*ciphertexts*/, + size_t desired_shared_key_len, + std::span /*salt*/) override { + concat_secret_combiner(*this, out_shared_secret, shared_secrets, desired_shared_key_len); + } + + size_t shared_key_length(size_t /*desired_shared_key_len*/) const override { + return concat_shared_key_length(encryptors()); + } +}; + +/// Decryptor that simply concatenates the multiple shared secrets +class Hybrid_TLS_KEM_Decryptor final : public KEM_Decryption_with_Combiner { + public: + Hybrid_TLS_KEM_Decryptor(const std::vector>& private_keys, + RandomNumberGenerator& rng, + const std::string_view provider) : + KEM_Decryption_with_Combiner(private_keys, rng, provider) {} + + void combine_shared_secrets(std::span out_shared_secret, + const std::vector>& shared_secrets, + const std::vector>& /*ciphertexts*/, + size_t desired_shared_key_len, + std::span /*salt*/) override { + concat_secret_combiner(*this, out_shared_secret, shared_secrets, desired_shared_key_len); + } + + size_t shared_key_length(size_t /*desired_shared_key_len*/) const override { + return concat_shared_key_length(decryptors()); + } +}; + } // namespace std::unique_ptr Hybrid_KEM_PublicKey::load_for_group( @@ -164,58 +257,25 @@ std::unique_ptr Hybrid_KEM_PublicKey::load_for_group( return std::make_unique(std::move(pks)); } -Hybrid_KEM_PublicKey::Hybrid_KEM_PublicKey(std::vector> pks) { - BOTAN_ARG_CHECK(pks.size() >= 2, "List of public keys must include at least two keys"); - BOTAN_ARG_CHECK(std::all_of(pks.begin(), pks.end(), [](const auto& pk) { return pk != nullptr; }), - "List of public keys contains a nullptr"); - BOTAN_ARG_CHECK(std::all_of(pks.begin(), - pks.end(), - [](const auto& pk) { - return pk->supports_operation(PublicKeyOperation::KeyEncapsulation) || - pk->supports_operation(PublicKeyOperation::KeyAgreement); - }), - "Some provided public key is not compatible with this hybrid wrapper"); - - std::transform( - pks.begin(), pks.end(), std::back_inserter(m_public_keys), [](auto& key) -> std::unique_ptr { - if(key->supports_operation(PublicKeyOperation::KeyAgreement) && - !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) { - return std::make_unique(std::move(key)); - } else { - return std::move(key); - } - }); - - m_key_length = - reduce(m_public_keys, size_t(0), [](size_t kl, const auto& key) { return std::max(kl, key->key_length()); }); - m_estimated_strength = reduce( - m_public_keys, size_t(0), [](size_t es, const auto& key) { return std::max(es, key->estimated_strength()); }); -} +Hybrid_KEM_PublicKey::Hybrid_KEM_PublicKey(std::vector> pks) : + Hybrid_PublicKey(convert_kex_to_kem_pks(std::move(pks))) {} + +Hybrid_KEM_PrivateKey::Hybrid_KEM_PrivateKey(std::vector> sks) : + Hybrid_PublicKey(convert_kex_to_kem_pks(extract_public_keys(sks))), + Hybrid_PrivateKey(convert_kex_to_kem_sks(std::move(sks))) {} std::string Hybrid_KEM_PublicKey::algo_name() const { std::ostringstream algo_name("Hybrid("); - for(size_t i = 0; i < m_public_keys.size(); ++i) { + for(size_t i = 0; i < public_keys().size(); ++i) { if(i > 0) { algo_name << ","; } - algo_name << m_public_keys[i]->algo_name(); + algo_name << public_keys().at(i)->algo_name(); } algo_name << ")"; return algo_name.str(); } -size_t Hybrid_KEM_PublicKey::estimated_strength() const { - return m_estimated_strength; -} - -size_t Hybrid_KEM_PublicKey::key_length() const { - return m_key_length; -} - -bool Hybrid_KEM_PublicKey::check_key(RandomNumberGenerator& rng, bool strong) const { - return reduce(m_public_keys, true, [&](bool ckr, const auto& key) { return ckr && key->check_key(rng, strong); }); -} - AlgorithmIdentifier Hybrid_KEM_PublicKey::algorithm_identifier() const { throw Botan::Not_Implemented("Hybrid keys don't have an algorithm identifier"); } @@ -232,87 +292,23 @@ std::vector Hybrid_KEM_PublicKey::raw_public_key_bits() const { // to be used with values that are not fixed-length, a length prefix or // other unambiguous encoding must be used to ensure that the composition // of the two values is injective. - return reduce(m_public_keys, std::vector(), [](auto pkb, const auto& key) { + return reduce(public_keys(), std::vector(), [](auto pkb, const auto& key) { return concat(pkb, key->raw_public_key_bits()); }); } std::unique_ptr Hybrid_KEM_PublicKey::generate_another(RandomNumberGenerator& rng) const { - std::vector> new_private_keys; - std::transform( - m_public_keys.begin(), m_public_keys.end(), std::back_inserter(new_private_keys), [&](const auto& public_key) { - return public_key->generate_another(rng); - }); - return std::make_unique(std::move(new_private_keys)); -} - -bool Hybrid_KEM_PublicKey::supports_operation(PublicKeyOperation op) const { - return PublicKeyOperation::KeyEncapsulation == op; + return std::make_unique(generate_other_sks_from_pks(rng)); } -namespace { - -class Hybrid_KEM_Encryption_Operation final : public PK_Ops::KEM_Encryption_with_KDF { - public: - Hybrid_KEM_Encryption_Operation(const Hybrid_KEM_PublicKey& key, - std::string_view kdf, - std::string_view provider) : - PK_Ops::KEM_Encryption_with_KDF(kdf), m_raw_kem_shared_key_length(0), m_encapsulated_key_length(0) { - m_kem_encryptors.reserve(key.public_keys().size()); - for(const auto& k : key.public_keys()) { - const auto& newenc = m_kem_encryptors.emplace_back(*k, "Raw", provider); - m_raw_kem_shared_key_length += newenc.shared_key_length(0 /* no KDF */); - m_encapsulated_key_length += newenc.encapsulated_key_length(); - } - } - - size_t raw_kem_shared_key_length() const override { return m_raw_kem_shared_key_length; } - - size_t encapsulated_key_length() const override { return m_encapsulated_key_length; } - - void raw_kem_encrypt(std::span out_encapsulated_key, - std::span raw_shared_key, - Botan::RandomNumberGenerator& rng) override { - BOTAN_ASSERT_NOMSG(out_encapsulated_key.size() == encapsulated_key_length()); - BOTAN_ASSERT_NOMSG(raw_shared_key.size() == raw_kem_shared_key_length()); - - BufferStuffer encaps_key_stuffer(out_encapsulated_key); - BufferStuffer shared_key_stuffer(raw_shared_key); - - for(auto& kem_enc : m_kem_encryptors) { - kem_enc.encrypt(encaps_key_stuffer.next(kem_enc.encapsulated_key_length()), - shared_key_stuffer.next(kem_enc.shared_key_length(0 /* no KDF */)), - rng); - } - } - - private: - std::vector m_kem_encryptors; - size_t m_raw_kem_shared_key_length; - size_t m_encapsulated_key_length; -}; - -} // namespace - std::unique_ptr Hybrid_KEM_PublicKey::create_kem_encryption_op( - std::string_view kdf, std::string_view provider) const { - return std::make_unique(*this, kdf, provider); -} - -namespace { - -auto extract_public_keys(const std::vector>& private_keys) { - std::vector> public_keys; - public_keys.reserve(private_keys.size()); - for(const auto& private_key : private_keys) { - BOTAN_ARG_CHECK(private_key != nullptr, "List of private keys contains a nullptr"); - public_keys.push_back(private_key->public_key()); + std::string_view params, std::string_view provider) const { + if(params != "Raw" && !params.empty()) { + throw Botan::Invalid_Argument("Hybrid KEM encryption does not support KDFs"); } - return public_keys; + return std::make_unique(public_keys(), provider); } -} // namespace - std::unique_ptr Hybrid_KEM_PrivateKey::generate_from_group(Group_Params group, RandomNumberGenerator& rng) { const auto algo_spec = algorithm_specs_for_group(group); @@ -324,88 +320,12 @@ std::unique_ptr Hybrid_KEM_PrivateKey::generate_from_grou return std::make_unique(std::move(private_keys)); } -Hybrid_KEM_PrivateKey::Hybrid_KEM_PrivateKey(std::vector> sks) : - Hybrid_KEM_PublicKey(extract_public_keys(sks)) { - BOTAN_ARG_CHECK(sks.size() >= 2, "List of private keys must include at least two keys"); - BOTAN_ARG_CHECK(std::all_of(sks.begin(), - sks.end(), - [](const auto& sk) { - return sk->supports_operation(PublicKeyOperation::KeyEncapsulation) || - sk->supports_operation(PublicKeyOperation::KeyAgreement); - }), - "Some provided private key is not compatible with this hybrid wrapper"); - - std::transform( - sks.begin(), sks.end(), std::back_inserter(m_private_keys), [](auto& key) -> std::unique_ptr { - if(key->supports_operation(PublicKeyOperation::KeyAgreement) && - !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) { - auto ka_key = dynamic_cast(key.get()); - BOTAN_ASSERT_NONNULL(ka_key); - (void)key.release(); - return std::make_unique(std::unique_ptr(ka_key)); - } else { - return std::move(key); - } - }); -} - -secure_vector Hybrid_KEM_PrivateKey::private_key_bits() const { - throw Not_Implemented("Hybrid private keys cannot be serialized"); -} - -std::unique_ptr Hybrid_KEM_PrivateKey::public_key() const { - return std::make_unique(extract_public_keys(m_private_keys)); -} - -bool Hybrid_KEM_PrivateKey::check_key(RandomNumberGenerator& rng, bool strong) const { - return reduce(m_public_keys, true, [&](bool ckr, const auto& key) { return ckr && key->check_key(rng, strong); }); -} - -namespace { - -class Hybrid_KEM_Decryption final : public PK_Ops::KEM_Decryption_with_KDF { - public: - Hybrid_KEM_Decryption(const Hybrid_KEM_PrivateKey& key, - RandomNumberGenerator& rng, - const std::string_view kdf, - const std::string_view provider) : - PK_Ops::KEM_Decryption_with_KDF(kdf), m_encapsulated_key_length(0), m_raw_kem_shared_key_length(0) { - m_decryptors.reserve(key.private_keys().size()); - for(const auto& private_key : key.private_keys()) { - const auto& newdec = m_decryptors.emplace_back(*private_key, rng, "Raw", provider); - m_encapsulated_key_length += newdec.encapsulated_key_length(); - m_raw_kem_shared_key_length += newdec.shared_key_length(0 /* no KDF */); - } - } - - void raw_kem_decrypt(std::span out_shared_key, std::span encap_key) override { - BOTAN_ASSERT_NOMSG(out_shared_key.size() == raw_kem_shared_key_length()); - BOTAN_ASSERT_NOMSG(encap_key.size() == encapsulated_key_length()); - - BufferSlicer encap_key_slicer(encap_key); - BufferStuffer shared_secret_stuffer(out_shared_key); - - for(auto& decryptor : m_decryptors) { - decryptor.decrypt(shared_secret_stuffer.next(decryptor.shared_key_length(0 /* no KDF */)), - encap_key_slicer.take(decryptor.encapsulated_key_length())); - } - } - - size_t encapsulated_key_length() const override { return m_encapsulated_key_length; } - - size_t raw_kem_shared_key_length() const override { return m_raw_kem_shared_key_length; } - - private: - std::vector m_decryptors; - size_t m_encapsulated_key_length; - size_t m_raw_kem_shared_key_length; -}; - -} // namespace - std::unique_ptr Hybrid_KEM_PrivateKey::create_kem_decryption_op( - RandomNumberGenerator& rng, std::string_view kdf, std::string_view provider) const { - return std::make_unique(*this, rng, kdf, provider); + RandomNumberGenerator& rng, std::string_view params, std::string_view provider) const { + if(params != "Raw" && !params.empty()) { + throw Botan::Invalid_Argument("Hybrid KEM decryption does not support KDFs"); + } + return std::make_unique(private_keys(), rng, provider); } } // namespace Botan::TLS diff --git a/src/lib/tls/tls13_pqc/hybrid_public_key.h b/src/lib/tls/tls13_pqc/hybrid_public_key.h index 3d2def00520..7288ea71463 100644 --- a/src/lib/tls/tls13_pqc/hybrid_public_key.h +++ b/src/lib/tls/tls13_pqc/hybrid_public_key.h @@ -13,6 +13,7 @@ #include +#include #include #include @@ -37,7 +38,7 @@ namespace Botan::TLS { * serializes and parses keys and ciphertexts as described in the * above-mentioned IETF draft for a post-quantum TLS 1.3. */ -class BOTAN_TEST_API Hybrid_KEM_PublicKey : public virtual Public_Key { +class BOTAN_TEST_API Hybrid_KEM_PublicKey : public virtual Hybrid_PublicKey { public: static std::unique_ptr load_for_group(Group_Params group, std::span concatenated_public_values); @@ -45,34 +46,18 @@ class BOTAN_TEST_API Hybrid_KEM_PublicKey : public virtual Public_Key { public: explicit Hybrid_KEM_PublicKey(std::vector> pks); - Hybrid_KEM_PublicKey(Hybrid_KEM_PublicKey&&) = default; - Hybrid_KEM_PublicKey(const Hybrid_KEM_PublicKey&) = delete; - Hybrid_KEM_PublicKey& operator=(Hybrid_KEM_PublicKey&&) = default; - Hybrid_KEM_PublicKey& operator=(const Hybrid_KEM_PublicKey&) = delete; - ~Hybrid_KEM_PublicKey() = default; - std::string algo_name() const override; - size_t estimated_strength() const override; - size_t key_length() const override; - bool check_key(RandomNumberGenerator& rng, bool strong) const override; AlgorithmIdentifier algorithm_identifier() const override; std::vector raw_public_key_bits() const override; std::vector public_key_bits() const override; std::unique_ptr generate_another(RandomNumberGenerator& rng) const final; - bool supports_operation(PublicKeyOperation op) const override; - + // no KDF support std::unique_ptr create_kem_encryption_op( - std::string_view kdf, std::string_view provider = "base") const override; - - const auto& public_keys() const { return m_public_keys; } + std::string_view params, std::string_view provider = "base") const override; protected: - std::vector> m_public_keys; - - private: - size_t m_key_length; - size_t m_estimated_strength; + Hybrid_KEM_PublicKey() = default; }; BOTAN_DIAGNOSTIC_PUSH @@ -82,8 +67,8 @@ BOTAN_DIAGNOSTIC_IGNORE_INHERITED_VIA_DOMINANCE * Composes a number of private keys for hybrid key agreement as defined in this * IETF draft: https://datatracker.ietf.org/doc/html/draft-ietf-tls-hybrid-design-04 */ -class BOTAN_TEST_API Hybrid_KEM_PrivateKey final : public Private_Key, - public Hybrid_KEM_PublicKey { +class BOTAN_TEST_API Hybrid_KEM_PrivateKey final : public Hybrid_KEM_PublicKey, + public Hybrid_PrivateKey { public: /** * Generate a hybrid private key for the given TLS code point. @@ -93,23 +78,19 @@ class BOTAN_TEST_API Hybrid_KEM_PrivateKey final : public Private_Key, public: Hybrid_KEM_PrivateKey(std::vector> private_keys); - secure_vector private_key_bits() const override; + std::unique_ptr public_key() const override { + return std::make_unique(extract_public_keys(private_keys())); + } - std::unique_ptr public_key() const override; - - bool check_key(RandomNumberGenerator& rng, bool strong) const override; + bool check_key(RandomNumberGenerator& rng, bool strong) const override { + return Hybrid_PrivateKey::check_key(rng, strong); + } + // no KDF support std::unique_ptr create_kem_decryption_op( - RandomNumberGenerator& rng, std::string_view kdf, std::string_view provider = "base") const override; - - const auto& private_keys() const { return m_private_keys; } - - private: - std::vector> m_private_keys; + RandomNumberGenerator& rng, std::string_view params, std::string_view provider = "base") const override; }; -BOTAN_DIAGNOSTIC_POP - } // namespace Botan::TLS #endif diff --git a/src/lib/tls/tls13_pqc/info.txt b/src/lib/tls/tls13_pqc/info.txt index cd2a4ab76e3..1abc87e75df 100644 --- a/src/lib/tls/tls13_pqc/info.txt +++ b/src/lib/tls/tls13_pqc/info.txt @@ -12,9 +12,10 @@ brief -> "Hybrid Key Exchange for TLS 1.3 with Post-Quantum Algorithms" hybrid_public_key.h -kex_to_kem_adapter.h tls13 +hybrid_kem +kex_to_kem_adapter diff --git a/src/tests/test_tls_hybrid_kem_key.cpp b/src/tests/test_tls_hybrid_kem_key.cpp index 658511b9539..adcf7a2fb55 100644 --- a/src/tests/test_tls_hybrid_kem_key.cpp +++ b/src/tests/test_tls_hybrid_kem_key.cpp @@ -161,14 +161,16 @@ std::vector hybrid_kem_keypair() { return { Botan_Tests::CHECK("public handles empty list", [](auto& result) { - result.test_throws("hybrid KEM key does not accept an empty list of keys", - [] { Botan::TLS::Hybrid_KEM_PublicKey({}); }); + result.test_throws("hybrid KEM key does not accept an empty list of keys", [] { + Botan::TLS::Hybrid_KEM_PublicKey(std::vector>(0)); + }); }), Botan_Tests::CHECK("private handles empty list", [](auto& result) { - result.test_throws("hybrid KEM key does not accept an empty list of keys", - [] { Botan::TLS::Hybrid_KEM_PrivateKey({}); }); + result.test_throws("hybrid KEM key does not accept an empty list of keys", [] { + Botan::TLS::Hybrid_KEM_PrivateKey(std::vector>(0)); + }); }), Botan_Tests::CHECK("public key handles nullptr", @@ -216,8 +218,8 @@ std::vector hybrid_kem_keypair() { void kex_to_kem_roundtrip(Test::Result& result, const std::function()>& kex_fn) { - Botan::TLS::KEX_to_KEM_Adapter_PrivateKey kexkem_key(kex_fn()); - Botan::TLS::KEX_to_KEM_Adapter_PublicKey kexkem_public_key(kex_fn()); + Botan::KEX_to_KEM_Adapter_PrivateKey kexkem_key(kex_fn()); + Botan::KEX_to_KEM_Adapter_PublicKey kexkem_public_key(kex_fn()); auto& rng = global_test_rng(); @@ -248,15 +250,15 @@ std::vector kex_to_kem_adapter() { Botan_Tests::CHECK("handles nullptr", [](auto& result) { result.test_throws("private KEM adapter handles nullptr", - [] { Botan::TLS::KEX_to_KEM_Adapter_PrivateKey(nullptr); }); + [] { Botan::KEX_to_KEM_Adapter_PrivateKey(nullptr); }); result.test_throws("public KEM adapter handles nullptr", - [] { Botan::TLS::KEX_to_KEM_Adapter_PublicKey(nullptr); }); + [] { Botan::KEX_to_KEM_Adapter_PublicKey(nullptr); }); }), Botan_Tests::CHECK("handles non-KEX keys", [](auto& result) { result.test_throws("public KEM adapter does not work with KEM keys", - [] { Botan::TLS::KEX_to_KEM_Adapter_PublicKey{kem()}; }); + [] { Botan::KEX_to_KEM_Adapter_PublicKey{kem()}; }); }), Botan_Tests::CHECK("Diffie-Hellman roundtrip", [](auto& result) { kex_to_kem_roundtrip(result, kex_dh); }),