Skip to content

Commit

Permalink
Introduce support for TLS 1.3 with hybrid key exchange
Browse files Browse the repository at this point in the history
(draft-ietf-tls-hybrid-design)

Co-Authored-By: Fabian Albert <fabian.albert@rohde-schwarz.com>
  • Loading branch information
reneme and FAlbertDev committed Jul 4, 2023
1 parent 3c5fd12 commit 01eb20c
Show file tree
Hide file tree
Showing 11 changed files with 1,163 additions and 2 deletions.
363 changes: 363 additions & 0 deletions src/lib/tls/tls13_pqc/hybrid_public_key.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
/**
* Composite key pair that exposes the Public/Private key API but combines
* multiple key agreement schemes into a hybrid algorithm.
*
* (C) 2023 Jack Lloyd
* 2023 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#include <botan/internal/hybrid_public_key.h>

#include <botan/pk_algs.h>

#include <botan/internal/fmt.h>
#include <botan/internal/kex_to_kem_adapter.h>
#include <botan/internal/pk_ops_impl.h>
#include <botan/internal/stl_util.h>

namespace Botan::TLS {

namespace {

template <typename RetT, typename KeyT, typename ReducerT>
RetT reduce(const std::vector<KeyT>& keys, RetT acc, ReducerT reducer) {
for(const KeyT& key : keys) {
acc = reducer(std::move(acc), key);
}
return acc;
}

std::vector<std::pair<std::string, std::string>> algorithm_specs_for_group(Group_Params group) {
BOTAN_ASSERT_NOMSG(is_hybrid(group));

switch(group) {
case Group_Params::HYBRID_X25519_KYBER_512_R3_CLOUDFLARE:
return {{"Curve25519", "Curve25519"}, {"Kyber", "Kyber-512-r3"}};
case Group_Params::HYBRID_X25519_KYBER_768_R3_CLOUDFLARE:
return {{"Curve25519", "Curve25519"}, {"Kyber", "Kyber-768-r3"}};

default:
return {};
}
}

std::vector<AlgorithmIdentifier> algorithm_identifiers_for_group(Group_Params group) {
BOTAN_ASSERT_NOMSG(is_hybrid(group));

const auto specs = algorithm_specs_for_group(group);
std::vector<AlgorithmIdentifier> result;

// This maps the string-based algorithm specs hard-coded above to OID-based
// AlgorithmIdentifier objects. The mapping is needed because
// load_public_key() depends on those while create_private_key() requires the
// strong-based spec.
//
// TODO: This is inconvenient, confusing and error-prone. Find a better way
// to load arbitrary public keys.
std::transform(specs.begin(), specs.end(), std::back_inserter(result), [](const auto& spec) {
return AlgorithmIdentifier(spec.second, AlgorithmIdentifier::USE_EMPTY_PARAM);
});

return result;
}

std::vector<size_t> public_value_lengths_for_group(Group_Params group) {
BOTAN_ASSERT_NOMSG(is_hybrid(group));

// This duplicates information of the algorithm internals.
//
// TODO: Find a way to expose important algorithm constants globally
// in the library, to avoid violating the DRY principle.
switch(group) {
case Group_Params::HYBRID_X25519_KYBER_512_R3_CLOUDFLARE:
return {32, 768};
case Group_Params::HYBRID_X25519_KYBER_768_R3_CLOUDFLARE:
return {32, 1088};

default:
return {};
}
}

} // namespace

std::unique_ptr<Hybrid_KEM_PublicKey> Hybrid_KEM_PublicKey::load_for_group(
Group_Params group, std::span<const uint8_t> concatenated_public_values) {
auto public_value_lengths = public_value_lengths_for_group(group);
auto alg_ids = algorithm_identifiers_for_group(group);
BOTAN_ASSERT_NOMSG(public_value_lengths.size() == alg_ids.size());

BufferSlicer public_value_slicer(concatenated_public_values);
std::vector<std::unique_ptr<Public_Key>> pks;
for(size_t idx = 0; idx < alg_ids.size(); ++idx) {
pks.emplace_back(load_public_key(alg_ids[idx], public_value_slicer.take(public_value_lengths[idx])));
}
return std::make_unique<Hybrid_KEM_PublicKey>(std::move(pks));
}

Hybrid_KEM_PublicKey::Hybrid_KEM_PublicKey(std::vector<std::unique_ptr<Public_Key>> pks) {
BOTAN_ARG_CHECK(!pks.empty(), "List of public keys must not be empty");
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<Public_Key> {
if(key->supports_operation(PublicKeyOperation::KeyAgreement)) {
return std::make_unique<KEX_to_KEM_Adapter_PublicKey>(std::move(key));
} else {
return std::move(key);
}
});
}

std::string Hybrid_KEM_PublicKey::algo_name() const {
std::string algo_name = "Hybrid(";
for(size_t i = 0; i < m_public_keys.size(); ++i) {
algo_name += m_public_keys[i]->algo_name();
if(i < m_public_keys.size() - 1) {
algo_name += ",";
}
}
algo_name += ")";
return algo_name;
}

size_t Hybrid_KEM_PublicKey::estimated_strength() const {
return reduce(
m_public_keys, size_t(0), [](size_t es, const auto& key) { return std::max(es, key->estimated_strength()); });
}

size_t Hybrid_KEM_PublicKey::key_length() const {
return reduce(m_public_keys, size_t(0), [](size_t kl, const auto& key) { return kl + key->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");
}

std::vector<uint8_t> Hybrid_KEM_PublicKey::public_key_bits() const {
// Technically, that's not really correct. The docstring for public_key_bits()
// states that it should return a BER-encoding of the public key.
//
// TODO: Perhaps add something like Public_Key::raw_public_key_bits() to
// better reflect what we need here.
return public_value();
}

std::vector<uint8_t> Hybrid_KEM_PublicKey::public_value() const {
// draft-ietf-tls-hybrid-design-06 3.2
// The values are directly concatenated, without any additional encoding
// or length fields; this assumes that the representation and length of
// elements is fixed once the algorithm is fixed. If concatenation were
// 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<uint8_t>(), [](auto pkb, const auto& key) {
// Technically, this is not correct! `public_key_bits()` is meant to
// return a BER-encoded public key. For (e.g.) Kyber, that contract is
// broken: It returns the raw encoding as used in the reference
// implementation.
//
// TODO: Provide something like Public_Key::raw_public_key_bits() to
// reflect that difference. Also: Key agreement keys could return
// their raw public value there.
return concat(pkb, key->public_key_bits());
});
}

bool Hybrid_KEM_PublicKey::supports_operation(PublicKeyOperation op) const {
return PublicKeyOperation::KeyEncapsulation == op;
}

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) {
std::transform(
key.public_keys().begin(),
key.public_keys().end(),
std::back_inserter(m_kem_encryptors),
[&](const auto& pubkey) { return std::make_unique<PK_KEM_Encryptor>(*pubkey, "Raw", provider); });
}

size_t raw_kem_shared_key_length() const override {
return reduce(m_kem_encryptors, size_t(0), [](auto acc, const auto& kem_enc) {
return acc + kem_enc->shared_key_length(0 /* no KDF */);
});
}

size_t encapsulated_key_length() const override {
return reduce(m_kem_encryptors, size_t(0), [](auto acc, const auto& kem_enc) {
return acc + kem_enc->encapsulated_key_length();
});
}

void raw_kem_encrypt(secure_vector<uint8_t>& out_encapsulated_key,
secure_vector<uint8_t>& raw_shared_key,
Botan::RandomNumberGenerator& rng) override {
out_encapsulated_key.resize(encapsulated_key_length());
raw_shared_key.resize(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) {
// TODO: Once PK_KEM_Encryptor uses std::span for its out-params, we
// probably want to pre-allocate the upstream out-params and
// use the BufferStuffer helper to place the downstream KEM
// outputs in the right location. Avoiding lots of copy and alloc.
// See also Hybrid_KEM_Decryption_Operation
secure_vector<uint8_t> out_encaps_buffer;
secure_vector<uint8_t> out_shared_key_buffer;
kem_enc->encrypt(out_encaps_buffer, out_shared_key_buffer, 0 /* no KDF */, rng);
encaps_key_stuffer.append(out_encaps_buffer);
shared_key_stuffer.append(out_shared_key_buffer);
}
}

private:
// Note: PK_KEM_Encryptor can neither be moved nor copied. Hence, we wrap
// it into a std::unique_ptr<> before storing it in a variant/vector.
std::vector<std::unique_ptr<PK_KEM_Encryptor>> m_kem_encryptors;
};

} // namespace

std::unique_ptr<Botan::PK_Ops::KEM_Encryption> Hybrid_KEM_PublicKey::create_kem_encryption_op(
std::string_view kdf, std::string_view provider) const {
return std::make_unique<Hybrid_KEM_Encryption_Operation>(*this, kdf, provider);
}

namespace {

auto extract_public_keys(const std::vector<std::unique_ptr<Private_Key>>& private_keys) {
std::vector<std::unique_ptr<Public_Key>> public_keys;
std::transform(
private_keys.begin(), private_keys.end(), std::back_inserter(public_keys), [](const auto& private_key) {
BOTAN_ARG_CHECK(private_key != nullptr, "List of private keys contains a nullptr");
return private_key->public_key();
});
return public_keys;
}

} // namespace

std::unique_ptr<Hybrid_KEM_PrivateKey> Hybrid_KEM_PrivateKey::generate_from_group(Group_Params group,
RandomNumberGenerator& rng) {
const auto algo_spec = algorithm_specs_for_group(group);
std::vector<std::unique_ptr<Private_Key>> private_keys;
std::transform(algo_spec.begin(), algo_spec.end(), std::back_inserter(private_keys), [&](const auto& spec) {
return create_private_key(spec.first, rng, spec.second);
});
return std::make_unique<Hybrid_KEM_PrivateKey>(std::move(private_keys));
}

Hybrid_KEM_PrivateKey::Hybrid_KEM_PrivateKey(std::vector<std::unique_ptr<Private_Key>> sks) :
Hybrid_KEM_PublicKey(extract_public_keys(sks)) {
BOTAN_ARG_CHECK(!sks.empty(), "List of private keys must not be empty");
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<Private_Key> {
if(key->supports_operation(PublicKeyOperation::KeyAgreement)) {
auto ka_key = dynamic_cast<PK_Key_Agreement_Key*>(key.get());
BOTAN_ASSERT_NONNULL(ka_key);
(void)key.release();
return std::make_unique<KEX_to_KEM_Adapter_PrivateKey>(std::unique_ptr<PK_Key_Agreement_Key>(ka_key));
} else {
return std::move(key);
}
});
}

secure_vector<uint8_t> Hybrid_KEM_PrivateKey::private_key_bits() const {
throw Not_Implemented("Hybrid private keys cannot be serialized");
}

std::unique_ptr<Public_Key> Hybrid_KEM_PrivateKey::public_key() const {
std::vector<std::unique_ptr<Public_Key>> pks;
std::transform(m_private_keys.cbegin(), m_private_keys.cend(), std::back_inserter(pks), [](const auto& sk) {
return sk->public_key();
});
return std::make_unique<Hybrid_KEM_PublicKey>(std::move(pks));
}

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) {
std::transform(key.private_keys().begin(),
key.private_keys().end(),
std::back_inserter(m_decryptors_with_encapsulation_lengths),
[&](const auto& private_key) {
PK_KEM_Encryptor enc(*private_key, "Raw");
return std::pair{std::make_unique<PK_KEM_Decryptor>(*private_key, rng, "Raw", provider),
enc.encapsulated_key_length()};
});
}

secure_vector<uint8_t> raw_kem_decrypt(const uint8_t encap_key[], size_t len) override {
secure_vector<uint8_t> shared_secret(raw_kem_shared_key_length());

BufferSlicer encap_key_slicer({encap_key, len});
BufferStuffer shared_secret_stuffer(shared_secret);

for(auto& [decryptor, encapsulation_length] : m_decryptors_with_encapsulation_lengths) {
const auto public_value_part = encap_key_slicer.take(encapsulation_length);
shared_secret_stuffer.append(decryptor->decrypt(public_value_part, 0 /* no KDF */, {}));
}

return shared_secret;
}

size_t raw_kem_shared_key_length() const override {
return reduce(
m_decryptors_with_encapsulation_lengths, size_t(0), [](auto acc, const auto& decryptor_with_length) {
return acc + decryptor_with_length.first->shared_key_length(0 /* no KDF */);
});
}

private:
std::vector<std::pair<std::unique_ptr<PK_KEM_Decryptor>, size_t>> m_decryptors_with_encapsulation_lengths;
};

} // namespace

std::unique_ptr<Botan::PK_Ops::KEM_Decryption> Hybrid_KEM_PrivateKey::create_kem_decryption_op(
RandomNumberGenerator& rng, std::string_view kdf, std::string_view provider) const {
return std::make_unique<Hybrid_KEM_Decryption>(*this, rng, kdf, provider);
}

} // namespace Botan::TLS
Loading

0 comments on commit 01eb20c

Please sign in to comment.