Skip to content

Commit

Permalink
Add example for hybrid key encapsulation
Browse files Browse the repository at this point in the history
This also acts as a regression test for the ability of
applications to implement custom public-key algorithms.
  • Loading branch information
reneme committed Jan 9, 2024
1 parent 596f5ab commit ad34def
Showing 1 changed file with 363 additions and 0 deletions.
363 changes: 363 additions & 0 deletions src/examples/hybrid_key_encapsulation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
#include <botan/auto_rng.h>
#include <botan/pk_algs.h>
#include <botan/pk_ops_impl.h>
#include <botan/pubkey.h>

#include <iostream>
#include <memory>

/**
* 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<Botan::Public_Key> kex, std::unique_ptr<Botan::Public_Key> 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<Botan::PK_Ops::KEM_Encryption> 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<uint8_t> public_key_bits() const override {
throw Botan::Not_Implemented("Key serialization is not supported");
}

std::unique_ptr<Botan::Private_Key> 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::min(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<Botan::Public_Key> m_kex_pk;
std::unique_ptr<Botan::Public_Key> 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<Botan::Private_Key> kex, std::unique_ptr<Botan::Private_Key> 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<Botan::PK_Ops::KEM_Decryption> 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<uint8_t> private_key_bits() const override {
throw Botan::Not_Implemented("Key serialization is not supported");
}

std::unique_ptr<Botan::Public_Key> public_key() const override {
return std::make_unique<Hybrid_PublicKey>(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<Botan::Private_Key> m_kex_sk;
std::unique_ptr<Botan::Private_Key> m_kem_sk;
};

namespace {

// Note: Typically we wouldn't want to use a hard-coded key derivation
// function here. This is mostly a workaround for some API
// shortcomings documented in GH #3706.
//
// TODO: Use "Raw" KDF and remove those hard-coded values.
constexpr size_t intermediate_shared_key_size = 32;
constexpr char intermediate_kdf[] = "HKDF(SHA-256)";

/**
* This implements the actual hybrid key encapsulation operation. It derives
* from Botan::PK_Ops::KEM_Encryption_with_KDF, allowing users to apply a
* Key Derivation Function (KDF) to the raw shared secret.
*/
class Hybrid_Encryption_Operation : public Botan::PK_Ops::KEM_Encryption_with_KDF {
public:
Hybrid_Encryption_Operation(const Hybrid_PublicKey& hybrid_pk, std::string_view kdf) :
Botan::PK_Ops::KEM_Encryption_with_KDF(kdf),
m_hybrid_pk(hybrid_pk),
m_kem_encryptor(hybrid_pk.kem_public_key(), 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();
}

protected:
/**
* This method performs the actual hybrid key encapsulation operation.
* It is called by the base class, which takes care of the KDF.
*/
void raw_kem_encrypt(std::span<uint8_t> out_encapsed,
std::span<uint8_t> out_shared,
Botan::RandomNumberGenerator& rng) 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 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, intermediate_kdf);

// 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.
const auto kex_shared_key =
kex.derive_key(intermediate_shared_key_size, 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 encapsed = m_kem_encryptor.encrypt(rng, intermediate_shared_key_size);
const auto& kem_shared_key = encapsed.shared_key();
const auto& kem_encapsed_key = encapsed.encapsulated_shared_key();

// 4. Hybrid: Concatenate the ephemeral public key and the KEM's
// encapsulation to form a combined "hybrid encapsulation".
BOTAN_ASSERT_NOMSG(out_encapsed.size() == kex_encapsed_key.size() + kem_encapsed_key.size());
std::copy(kex_encapsed_key.begin(), kex_encapsed_key.end(), out_encapsed.begin());
std::copy(kem_encapsed_key.begin(), kem_encapsed_key.end(), out_encapsed.begin() + kex_encapsed_key.size());

// 5. Hybrid: Concatenate the shared secrets of both algorithms to
// form a combined "hybrid shared secret".
BOTAN_ASSERT_NOMSG(out_shared.size() == kex_shared_key.size() + kem_shared_key.size());
std::copy(kex_shared_key.begin(), kex_shared_key.end(), out_shared.begin());
std::copy(kem_shared_key.begin(), kem_shared_key.end(), out_shared.begin() + kex_shared_key.size());
}

/**
* This returns the length of the raw shared secret in bytes. As we
* derived from Botan::PK_Ops::KEM_Encryption_with_KDF, this will be
* used by the base class to determine the length of the output buffer.
*/
size_t raw_kem_shared_key_length() const override { return 2 * intermediate_shared_key_size; }

private:
const Hybrid_PublicKey& m_hybrid_pk;
Botan::PK_KEM_Encryptor m_kem_encryptor;
};

/**
* This implements the actual hybrid key decapsulation operation. It derives
* from Botan::PK_Ops::KEM_Decryption_with_KDF, allowing users to apply a
* Key Derivation Function (KDF) to the raw shared secret.
*/
class Hybrid_Decryption_Operation : public Botan::PK_Ops::KEM_Decryption_with_KDF {
public:
Hybrid_Decryption_Operation(const Hybrid_PrivateKey& hybrid_sk,
Botan::RandomNumberGenerator& rng,
std::string_view kdf) :
Botan::PK_Ops::KEM_Decryption_with_KDF(kdf),
m_hybrid_sk(hybrid_sk),
m_key_agreement(hybrid_sk.kex_private_key(), rng, intermediate_kdf),
m_kem_decryptor(hybrid_sk.kem_private_key(), rng, intermediate_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();
}

protected:
/**
* This method performs the actual hybrid key decapsulation operation.
* It is called by the base class, which takes care of the KDF.
*/
void raw_kem_decrypt(std::span<uint8_t> out_raw_shared_key, std::span<const uint8_t> encapsulated_key) override {
// 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 to form a
// "hybrid shared secret" (that is equal to the other party's).

// 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(intermediate_shared_key_size, 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, intermediate_shared_key_size);

// 4. Hybrid: Concatenate the shared secrets of both algorithms.
BOTAN_ASSERT_NOMSG(out_raw_shared_key.size() == kex_shared_key.size() + kem_shared_key.size());
std::copy(kex_shared_key.begin(), kex_shared_key.end(), out_raw_shared_key.begin());
std::copy(kem_shared_key.begin(), kem_shared_key.end(), out_raw_shared_key.begin() + kex_shared_key.size());
}

/**
* This returns the length of the raw shared secret in bytes. As we
* derived from Botan::PK_Ops::KEM_Encryption_with_KDF, this will be
* used by the base class to determine the length of the output buffer.
*/
size_t raw_kem_shared_key_length() const override { return 2 * intermediate_shared_key_size; }

private:
const Hybrid_PrivateKey& m_hybrid_sk;
Botan::PK_Key_Agreement m_key_agreement;
Botan::PK_KEM_Decryptor m_kem_decryptor;
};

} // namespace

std::unique_ptr<Botan::PK_Ops::KEM_Encryption> Hybrid_PublicKey::create_kem_encryption_op(std::string_view params,
std::string_view) const {
return std::make_unique<Hybrid_Encryption_Operation>(*this, params);
}

std::unique_ptr<Botan::PK_Ops::KEM_Decryption> Hybrid_PrivateKey::create_kem_decryption_op(
Botan::RandomNumberGenerator& rng, std::string_view params, std::string_view) const {
return std::make_unique<Hybrid_Decryption_Operation>(*this, rng, params);
}

std::unique_ptr<Botan::Private_Key> Hybrid_PublicKey::generate_another(Botan::RandomNumberGenerator& rng) const {
return std::make_unique<Hybrid_PrivateKey>(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<Hybrid_PrivateKey>(
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
if(shared_key == encapsulation_by_bob.shared_key()) {
std::cout << "Alice and Bob share the same secret!\n";
return 0;
} else {
return 1;
}
}

0 comments on commit ad34def

Please sign in to comment.