From afc7bf7cefd26ac00fc0613754bb2a131fc38fa8 Mon Sep 17 00:00:00 2001 From: Rene Meusel Date: Tue, 9 Jan 2024 09:56:53 +0100 Subject: [PATCH] Add example for hybrid key encapsulation This also acts as a regression test for the ability of applications to implement custom public-key algorithms. --- src/examples/hybrid_key_encapsulation.cpp | 377 ++++++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 src/examples/hybrid_key_encapsulation.cpp diff --git a/src/examples/hybrid_key_encapsulation.cpp b/src/examples/hybrid_key_encapsulation.cpp new file mode 100644 index 00000000000..09fecae5ac5 --- /dev/null +++ b/src/examples/hybrid_key_encapsulation.cpp @@ -0,0 +1,377 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +/** + * This class is an example of a custom public-key algorithm in Botan. + * + * It combines a classic key exchange algorithm like Diffie-Hellman and a key + * encapsulation mechanism (KEM) to provide a "hybrid" key encapsulation + * mechanism (KEM). + * + * This approach is useful as an intermediate step towards post-quantum secure + * cryptography as it combines the historical confidence in a classic algorithm + * with the future-proofness of a post-quantum algorithm. + * + * Other use cases for such a custom public-key algorithm class include: + * - adding support for a new public-key algorithm that Botan doesn't support + * - writing a wrapper to offload public-key operations to a hardware device + */ +class Hybrid_PublicKey : public virtual Botan::Public_Key { + public: + explicit Hybrid_PublicKey(std::unique_ptr kex, std::unique_ptr kem) : + m_kex_pk(std::move(kex)), m_kem_pk(std::move(kem)) { + BOTAN_ASSERT_NONNULL(m_kex_pk); + BOTAN_ASSERT_NONNULL(m_kem_pk); + BOTAN_ASSERT_NOMSG(m_kex_pk->supports_operation(Botan::PublicKeyOperation::KeyAgreement)); + BOTAN_ASSERT_NOMSG(m_kem_pk->supports_operation(Botan::PublicKeyOperation::KeyEncapsulation)); + } + + std::string algo_name() const override { + return "Hybrid-KEM(" + m_kex_pk->algo_name() + "," + m_kem_pk->algo_name() + ")"; + } + + /** + * This returns an object of a custom sub-class of + * Botan::PK_Ops::KEM_Encryption. See below for the implementation of that + * class, where the actual hybrid operation is performed. + * + * Note, that applications typically don't call this directly, but they + * use the Botan::PK_KEM_Encryptor class, which in turn calls this method. + * See the main() function below for an example. + */ + std::unique_ptr create_kem_encryption_op(std::string_view params, + std::string_view provider) const override; + + /** + * In an actual implementation, when you want to use this key in a + * protocol like X.509, this may return an algorithm identifier that fits + * your needs. For instance, using a custom OID. + */ + Botan::AlgorithmIdentifier algorithm_identifier() const override { + throw Botan::Not_Implemented("Hybrid-KEM does not have an algorithm identifier"); + } + + /** + * In an actual implementation, this may return a serialized + * representation of the public keys. For instance, using some ASN.1 + * encoding to combine the two public keys. + */ + std::vector public_key_bits() const override { + throw Botan::Not_Implemented("Key serialization is not supported"); + } + + std::unique_ptr generate_another(Botan::RandomNumberGenerator& rng) const override; + + bool supports_operation(Botan::PublicKeyOperation op) const override { + return op == Botan::PublicKeyOperation::KeyEncapsulation; + } + + size_t estimated_strength() const override { + return std::max(m_kex_pk->estimated_strength(), m_kem_pk->estimated_strength()); + } + + size_t key_length() const override { return m_kex_pk->key_length() + m_kem_pk->key_length(); } + + bool check_key(Botan::RandomNumberGenerator& rng, bool strong) const override { + return m_kex_pk->check_key(rng, strong) && m_kem_pk->check_key(rng, strong); + } + + const Botan::Public_Key& kex_public_key() const { return *m_kex_pk; } + + const Botan::Public_Key& kem_public_key() const { return *m_kem_pk; } + + private: + std::unique_ptr m_kex_pk; + std::unique_ptr m_kem_pk; +}; + +/** + * This is the private key class for the custom public-key algorithm. + */ +class Hybrid_PrivateKey : public virtual Botan::Private_Key, + public virtual Hybrid_PublicKey { + public: + explicit Hybrid_PrivateKey(std::unique_ptr kex, std::unique_ptr kem) : + Hybrid_PublicKey(kex->public_key(), kem->public_key()), + m_kex_sk(std::move(kex)), + m_kem_sk(std::move(kem)) {} + + /** + * This returns an object of a custom sub-class of Botan::PK_Ops::KEM_Decryption. + * See below for the implementation of that class, where the actual hybrid operation + * is performed. + * + * Note, that applications typically don't call this directly, but they + * use the Botan::PK_KEM_Decryptor class, which in turn calls this method. + * See the main() function below for an example. + */ + std::unique_ptr create_kem_decryption_op(Botan::RandomNumberGenerator& rng, + std::string_view params, + std::string_view provider) const override; + + /** + * In an actual implementation, this should return a serialized + * representation of the private keys. For instance, using some ASN.1 + * encoding to combine the two private keys. + */ + Botan::secure_vector private_key_bits() const override { + throw Botan::Not_Implemented("Key serialization is not supported"); + } + + std::unique_ptr public_key() const override { + return std::make_unique(m_kex_sk->public_key(), m_kem_sk->public_key()); + } + + const Botan::Private_Key& kex_private_key() const { return *m_kex_sk; } + + const Botan::Private_Key& kem_private_key() const { return *m_kem_sk; } + + private: + std::unique_ptr m_kex_sk; + std::unique_ptr m_kem_sk; +}; + +namespace { + +/** + * This implements the actual hybrid key encapsulation operation. It derives + * shared secrets from the key exchange algorithm (KEX) and the key + * encapsulation mechanism (KEM), and combines them using a Key Derivation + * Function (KDF). + */ +class Hybrid_Encryption_Operation : public Botan::PK_Ops::KEM_Encryption { + public: + Hybrid_Encryption_Operation(const Hybrid_PublicKey& hybrid_pk, std::string_view kdf) : + m_hybrid_pk(hybrid_pk), + m_kem_encryptor(hybrid_pk.kem_public_key(), "Raw"), + m_kdf(Botan::KDF::create_or_throw(kdf)) { + BOTAN_ASSERT_NONNULL(m_kdf); + } + + /** + * This returns the length of the encapsulated key in bytes. For such a + * hybrid key encapsulation, this comprises the length of the KEX's public + * key (ephemeral key pair) and the length of the KEM's encapsulated key. + */ + size_t encapsulated_key_length() const override { + return m_hybrid_pk.kex_public_key().public_key_bits().size() + m_kem_encryptor.encapsulated_key_length(); + } + + /** + * This returns the length of the output shared secret in bytes. It is + * the output length of the KDF, which acts as the "combiner" of the + * shared secrets of both algorithms. + */ + size_t shared_key_length(size_t desired_shared_key_length) const override { return desired_shared_key_length; } + + /** + * This method performs the actual hybrid key encapsulation operation. + */ + void kem_encrypt(std::span out_encapsed_key, + std::span out_shared_key, + Botan::RandomNumberGenerator& rng, + size_t desired_shared_key_length, + std::span salt) override { + // The basic idea of the hybrid operation: + // 1. Generate an ephemeral key pair for the key exchange algorithm, + // 2. and agree on a shared secret using the KEX's public key of the + // other party and the ephemeral private key, + // 3. Encapsulate a shared secret using the KEM's public key of the + // other party, resulting in a shared secret and its encapsulation, + // 4. Concatenate the ephemeral public key and the encapsulation to + // form a "hybrid encapsulation" (to be sent to the other party), + // 5. Concatenate the shared secrets of both algorithms and pass the + // result through a user-defined key derivation function to form a + // "hybrid shared secret" (to be used by the application). + + // 1. KEX: Generate an ephemeral key pair with the same parameters as + // the provided key exchange public key. + auto ephemeral_keypair = m_hybrid_pk.kex_public_key().generate_another(rng); + + // Note: Currently, we cannot pre-create the PK_Key_Agreement object in + // the constructor, because it requires an RNG object. + // + // TODO: fix this upstream by harmonizing the constructors of the + // PK_Key_Agreement and PK_KEM_Encryptor classes. + Botan::PK_Key_Agreement kex(*ephemeral_keypair, rng, "Raw"); + + // 2. KEX: Agree on a shared secret using the public key of the other + // party and our ephemeral private key. The ephemeral public + // key acts as the "encapsulation" of the key agreement. + // + // Note: kex.derive_key() does not have a std::span<> based overload to + // write straight into the output buffer. + // + // TODO: kex.derive_key() should allow a std::span<>-based out param, + // which would save a copy in this case. (See GH #3318) + const auto kex_shared_key = + kex.derive_key(0 /* no KDF */, m_hybrid_pk.kex_public_key().public_key_bits()).bits_of(); + const auto kex_encapsed_key = ephemeral_keypair->public_key_bits(); + + // 3. KEX: Encapsulate a shared secret using the KEM's public key, + // yielding a shared secret and its encapsulation. + const auto [kem_encapsed_key, kem_shared_key] = + Botan::KEM_Encapsulation::destructure(m_kem_encryptor.encrypt(rng)); + + // 4. Hybrid: Concatenate the ephemeral public key and the KEM's + // encapsulation to form a combined "hybrid encapsulation". + BOTAN_ASSERT_NOMSG(out_encapsed_key.size() == kex_encapsed_key.size() + kem_encapsed_key.size()); + std::copy(kex_encapsed_key.begin(), kex_encapsed_key.end(), out_encapsed_key.begin()); + std::copy( + kem_encapsed_key.begin(), kem_encapsed_key.end(), out_encapsed_key.begin() + kex_encapsed_key.size()); + + // 5. Hybrid: Combine the shared secrets of both algorithms. + Botan::secure_vector concat_shared_key; + concat_shared_key.insert(concat_shared_key.end(), kex_shared_key.begin(), kex_shared_key.end()); + concat_shared_key.insert(concat_shared_key.end(), kem_shared_key.begin(), kem_shared_key.end()); + + BOTAN_ASSERT_NOMSG(out_shared_key.size() >= desired_shared_key_length); + m_kdf->derive_key(out_shared_key.first(desired_shared_key_length), concat_shared_key, salt, {}); + } + + private: + const Hybrid_PublicKey& m_hybrid_pk; + Botan::PK_KEM_Encryptor m_kem_encryptor; + const std::unique_ptr m_kdf; +}; + +/** + * This implements the actual hybrid key decapsulation operation. It derives + * the shared secrets from the key exchange algorithm (KEX) and the key + * encapsulation mechanism (KEM), and combines them using a Key Derivation + * Function (KDF). + */ +class Hybrid_Decryption_Operation : public Botan::PK_Ops::KEM_Decryption { + public: + Hybrid_Decryption_Operation(const Hybrid_PrivateKey& hybrid_sk, + Botan::RandomNumberGenerator& rng, + std::string_view kdf) : + m_hybrid_sk(hybrid_sk), + m_key_agreement(hybrid_sk.kex_private_key(), rng, "Raw"), + m_kem_decryptor(hybrid_sk.kem_private_key(), rng, "Raw"), + m_kdf(Botan::KDF::create_or_throw(kdf)) { + BOTAN_ASSERT_NONNULL(m_kdf); + } + + /** + * This returns the length of the encapsulated key in bytes. For such a + * hybrid key encapsulation, this comprises the length of the KEX's public + * key (ephemeral key pair) and the length of the KEM's encapsulated key. + */ + size_t encapsulated_key_length() const override { + return m_hybrid_sk.kex_public_key().public_key_bits().size() + m_kem_decryptor.encapsulated_key_length(); + } + + /** + * This returns the length of the output shared secret in bytes. It is + * the output length of the KDF, which acts as the "combiner" of the + * shared secrets of both algorithms. + */ + size_t shared_key_length(size_t desired_shared_key_length) const override { return desired_shared_key_length; } + + /** + * This method performs the actual hybrid key decapsulation operation. + */ + void kem_decrypt(std::span out_shared_key, + std::span encapsulated_key, + size_t desired_shared_key_length, + std::span salt) override { + BOTAN_ASSERT_NOMSG(encapsulated_key.size() == encapsulated_key_length()); + + // The basic idea of the hybrid operation: + // 1. Extract the ephemeral public key and the KEM's encapsulation + // from the hybrid encapsulation, + // 2. Agree on a shared secret using the KEX's private key and the + // ephemeral public key (from the other party), + // 3. Decapsulate a shared secret using the KEM's private key and + // the KEM's encapsulation (from the other party), + // 4. Concatenate the shared secrets of both algorithms and pass the + // result through a user-defined key derivation function to form a + // "hybrid shared secret" (to be used by the application). + + // 1. Hybrid: Extract the ephemeral public key and the encapsulation. + const auto kex_encapsed_key = + encapsulated_key.subspan(0, m_hybrid_sk.kex_public_key().public_key_bits().size()); + const auto kem_encapsed_key = encapsulated_key.subspan(kex_encapsed_key.size()); + + // 2. KEX: Agree on a shared secret using the KEX's private key and the + // ephemeral public key of the other party. + const auto kex_shared_key = m_key_agreement.derive_key(0 /* no KDF */, kex_encapsed_key).bits_of(); + + // 3. KEM: Decapsulate a shared secret using the KEM's private key and + // the encapsulation of the other party. + const auto kem_shared_key = m_kem_decryptor.decrypt(kem_encapsed_key); + + // 4. Hybrid: Combine the shared secrets of both algorithms. + Botan::secure_vector concat_shared_key; + concat_shared_key.insert(concat_shared_key.end(), kex_shared_key.begin(), kex_shared_key.end()); + concat_shared_key.insert(concat_shared_key.end(), kem_shared_key.begin(), kem_shared_key.end()); + + BOTAN_ASSERT_NOMSG(out_shared_key.size() >= desired_shared_key_length); + m_kdf->derive_key(out_shared_key.first(desired_shared_key_length), concat_shared_key, salt, {}); + } + + private: + const Hybrid_PrivateKey& m_hybrid_sk; + Botan::PK_Key_Agreement m_key_agreement; + Botan::PK_KEM_Decryptor m_kem_decryptor; + std::unique_ptr m_kdf; +}; + +} // namespace + +std::unique_ptr Hybrid_PublicKey::create_kem_encryption_op(std::string_view params, + std::string_view) const { + return std::make_unique(*this, params); +} + +std::unique_ptr Hybrid_PrivateKey::create_kem_decryption_op( + Botan::RandomNumberGenerator& rng, std::string_view params, std::string_view) const { + return std::make_unique(*this, rng, params); +} + +std::unique_ptr Hybrid_PublicKey::generate_another(Botan::RandomNumberGenerator& rng) const { + return std::make_unique(m_kex_pk->generate_another(rng), m_kem_pk->generate_another(rng)); +} + +int main() { + Botan::AutoSeeded_RNG rng; + + // Alice generates two key pairs suitable for: + // 1) key exchange (X25519), and + // 2) key encapsulation (Kyber). + // + // She then combines them into a custom "hybrid" key pair that acts + // like a key encapsulation mechanism (KEM). + const auto private_key_of_alice = std::make_unique( + Botan::create_private_key("Curve25519", rng), Botan::create_private_key("Kyber", rng, "Kyber-768-r3")); + const auto public_key_of_alice = private_key_of_alice->public_key(); + + // Bob uses Alice's public key to encapsulate a shared secret, and + // derives a shared key from it using HKDF. + Botan::PK_KEM_Encryptor kem_enc(*public_key_of_alice, "HKDF(SHA-256)"); + const auto encapsulation_by_bob = kem_enc.encrypt(rng); + + // Alice decapsulates the shared secret from Bob's encapsulation using her + // private key, and derives a matching shared key using HKDF. + Botan::PK_KEM_Decryptor kem_dec(*private_key_of_alice, rng, "HKDF(SHA-256)"); + const auto shared_key = kem_dec.decrypt(encapsulation_by_bob.encapsulated_shared_key()); + + // Check that Alice and Bob now share the same secret + std::cout << "Alice's shared key: " << Botan::hex_encode(shared_key) << "\n" + << "Bob's shared key: " << Botan::hex_encode(encapsulation_by_bob.shared_key()) << "\n"; + + if(shared_key == encapsulation_by_bob.shared_key()) { + std::cout << '\n' << "Alice and Bob share the same secret!\n"; + return 0; + } else { + return 1; + } +}