-
Notifications
You must be signed in to change notification settings - Fork 562
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce support for TLS 1.3 with hybrid key exchange
(draft-ietf-tls-hybrid-design) Co-Authored-By: Fabian Albert <fabian.albert@rohde-schwarz.com>
- Loading branch information
1 parent
3c5fd12
commit 01eb20c
Showing
11 changed files
with
1,163 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.