Skip to content

Commit

Permalink
Enable the post-quantum x25519+ML-KEM-768 TLS ciphersuite by default
Browse files Browse the repository at this point in the history
This change also rearranges the default kex list, and removes some of
the most expensive FFDHE groups.

Group selection logic is changed such that if the client supports a
post quantum algorithm, it is always preferred even if the client
sent other (non-PQC) key shares.

Expose which group was used for key exchange in the Session_Summary.

Co-authored-by: René Meusel <[email protected]>
  • Loading branch information
randombit and reneme committed Oct 24, 2024
1 parent b09c72b commit ec0d301
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 104 deletions.
11 changes: 7 additions & 4 deletions src/cli/tls_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,17 @@ class Callbacks : public Botan::TLS::Callbacks {
void tls_session_activated() override { output() << "Handshake complete\n"; }

void tls_session_established(const Botan::TLS::Session_Summary& session) override {
output() << "Handshake complete, " << session.version().to_string() << " using "
<< session.ciphersuite().to_string();
output() << "Handshake complete, " << session.version().to_string() << "\n";

if(const auto& psk = session.external_psk_identity()) {
output() << " (utilized PSK identity: " << maybe_hex_encode(psk.value()) << ")";
output() << "Utilized PSK identity: " << maybe_hex_encode(psk.value()) << "\n";
}

output() << std::endl;
output() << "Negotiated ciphersuite " << session.ciphersuite().to_string() << "\n";

if(auto kex_params = session.kex_parameters()) {
output() << "Key exchange using " << *kex_params << "\n";
}

if(const auto& session_id = session.session_id(); !session_id.empty()) {
output() << "Session ID " << Botan::hex_encode(session_id.get()) << "\n";
Expand Down
77 changes: 77 additions & 0 deletions src/lib/tls/tls_algos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,83 @@ Auth_Method auth_method_from_string(std::string_view str) {
throw Invalid_Argument(fmt("Unknown TLS signature method '{}'", str));
}

bool Group_Params::is_available() const {
#if !defined(BOTAN_HAS_X25519)
if(is_x25519()) {
return false;
}
if(is_pqc_hybrid() && pqc_hybrid_ecc() == Group_Params_Code::X25519) {
return false;
}
#endif

#if !defined(BOTAN_HAS_X448)
if(is_x448()) {
return false;
}
if(is_pqc_hybrid() && pqc_hybrid_ecc() == Group_Params_Code::X448) {
return false;
}
#endif

#if !defined(BOTAN_HAS_DIFFIE_HELLMAN)
if(is_in_ffdhe_range()) {
return false;
}
#endif

#if !defined(BOTAN_HAS_KYBER_ROUND3)
if(is_pure_kyber() || is_pqc_hybrid_kyber()) {
return false;
}
#endif

#if !defined(BOTAN_HAS_FRODOKEM)
if(is_pure_frodokem() || is_pqc_hybrid_frodokem()) {
return false;
}
#endif

return true;
}

std::optional<Group_Params_Code> Group_Params::pqc_hybrid_ecc() const {
switch(m_code) {
case Group_Params_Code::HYBRID_X25519_ML_KEM_768:
case Group_Params_Code::HYBRID_X25519_KYBER_512_R3_CLOUDFLARE:
case Group_Params_Code::HYBRID_X25519_KYBER_512_R3_OQS:
case Group_Params_Code::HYBRID_X25519_KYBER_768_R3_OQS:
case Group_Params_Code::HYBRID_X25519_eFRODOKEM_640_SHAKE_OQS:
case Group_Params_Code::HYBRID_X25519_eFRODOKEM_640_AES_OQS:
return Group_Params_Code::X25519;

case Group_Params_Code::HYBRID_X448_KYBER_768_R3_OQS:
case Group_Params_Code::HYBRID_X448_eFRODOKEM_976_SHAKE_OQS:
case Group_Params_Code::HYBRID_X448_eFRODOKEM_976_AES_OQS:
return Group_Params_Code::X448;

case Group_Params_Code::HYBRID_SECP256R1_ML_KEM_768:
case Group_Params_Code::HYBRID_SECP256R1_KYBER_512_R3_OQS:
case Group_Params_Code::HYBRID_SECP256R1_KYBER_768_R3_OQS:
case Group_Params_Code::HYBRID_SECP256R1_eFRODOKEM_640_SHAKE_OQS:
case Group_Params_Code::HYBRID_SECP256R1_eFRODOKEM_640_AES_OQS:
return Group_Params_Code::SECP256R1;

case Group_Params_Code::HYBRID_SECP384R1_KYBER_768_R3_OQS:
case Group_Params_Code::HYBRID_SECP384R1_eFRODOKEM_976_SHAKE_OQS:
case Group_Params_Code::HYBRID_SECP384R1_eFRODOKEM_976_AES_OQS:
return Group_Params_Code::SECP384R1;

case Group_Params_Code::HYBRID_SECP521R1_KYBER_1024_R3_OQS:
case Group_Params_Code::HYBRID_SECP521R1_eFRODOKEM_1344_SHAKE_OQS:
case Group_Params_Code::HYBRID_SECP521R1_eFRODOKEM_1344_AES_OQS:
return Group_Params_Code::SECP521R1;

default:
return {};
}
}

std::optional<Group_Params> Group_Params::from_string(std::string_view group_name) {
if(group_name == "secp256r1") {
return Group_Params::SECP256R1;
Expand Down
44 changes: 31 additions & 13 deletions src/lib/tls/tls_algos.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ class BOTAN_PUBLIC_API(3, 2) Group_Params final {

constexpr uint16_t wire_code() const { return static_cast<uint16_t>(m_code); }

/**
* Returns false if this group/KEX is not available in the build configuration
*/
bool is_available() const;

constexpr bool is_x25519() const { return m_code == Group_Params_Code::X25519; }

constexpr bool is_x448() const { return m_code == Group_Params_Code::X448; }
Expand All @@ -198,7 +203,7 @@ class BOTAN_PUBLIC_API(3, 2) Group_Params final {
m_code == Group_Params_Code::FFDHE_8192;
}

constexpr bool is_pure_kyber() const {
constexpr bool is_pure_kyber_r3() const {
return m_code == Group_Params_Code::KYBER_512_R3_OQS || m_code == Group_Params_Code::KYBER_768_R3_OQS ||
m_code == Group_Params_Code::KYBER_1024_R3_OQS;
}
Expand All @@ -214,37 +219,50 @@ class BOTAN_PUBLIC_API(3, 2) Group_Params final {

constexpr bool is_pure_ecc_group() const { return is_x25519() || is_x448() || is_ecdh_named_curve(); }

constexpr bool is_post_quantum() const { return is_pure_kyber() || is_pure_frodokem() || is_pqc_hybrid(); }
constexpr bool is_post_quantum() const { return is_pure_kyber_r3() || is_pure_frodokem() || is_pqc_hybrid(); }

constexpr bool is_pqc_hybrid() const {
constexpr bool is_pqc_hybrid_ml_kem() const {
return m_code == Group_Params_Code::HYBRID_SECP256R1_ML_KEM_768 ||
m_code == Group_Params_Code::HYBRID_X25519_ML_KEM_768;
}

constexpr bool is_pqc_hybrid_kyber_r3() const {
BOTAN_DIAGNOSTIC_PUSH
BOTAN_DIAGNOSTIC_IGNORE_DEPRECATED_DECLARATIONS

return m_code == Group_Params_Code::HYBRID_SECP256R1_ML_KEM_768 ||
m_code == Group_Params_Code::HYBRID_X25519_ML_KEM_768 ||
m_code == Group_Params_Code::HYBRID_X25519_KYBER_512_R3_CLOUDFLARE ||
return m_code == Group_Params_Code::HYBRID_X25519_KYBER_512_R3_CLOUDFLARE ||
m_code == Group_Params_Code::HYBRID_X25519_KYBER_512_R3_OQS ||
m_code == Group_Params_Code::HYBRID_X25519_KYBER_768_R3_OQS ||
m_code == Group_Params_Code::HYBRID_X448_KYBER_768_R3_OQS ||
m_code == Group_Params_Code::HYBRID_X25519_eFRODOKEM_640_SHAKE_OQS ||
m_code == Group_Params_Code::HYBRID_SECP256R1_KYBER_512_R3_OQS ||
m_code == Group_Params_Code::HYBRID_SECP256R1_KYBER_768_R3_OQS ||
m_code == Group_Params_Code::HYBRID_SECP384R1_KYBER_768_R3_OQS ||
m_code == Group_Params_Code::HYBRID_SECP521R1_KYBER_1024_R3_OQS;

BOTAN_DIAGNOSTIC_POP
}

constexpr bool is_pqc_hybrid_frodokem() const {
return m_code == Group_Params_Code::HYBRID_X25519_eFRODOKEM_640_SHAKE_OQS ||
m_code == Group_Params_Code::HYBRID_X25519_eFRODOKEM_640_AES_OQS ||
m_code == Group_Params_Code::HYBRID_X448_eFRODOKEM_976_SHAKE_OQS ||
m_code == Group_Params_Code::HYBRID_X448_eFRODOKEM_976_AES_OQS ||
m_code == Group_Params_Code::HYBRID_SECP256R1_KYBER_512_R3_OQS ||
m_code == Group_Params_Code::HYBRID_SECP256R1_KYBER_768_R3_OQS ||
m_code == Group_Params_Code::HYBRID_SECP256R1_eFRODOKEM_640_SHAKE_OQS ||
m_code == Group_Params_Code::HYBRID_SECP256R1_eFRODOKEM_640_AES_OQS ||
m_code == Group_Params_Code::HYBRID_SECP384R1_KYBER_768_R3_OQS ||
m_code == Group_Params_Code::HYBRID_SECP384R1_eFRODOKEM_976_SHAKE_OQS ||
m_code == Group_Params_Code::HYBRID_SECP384R1_eFRODOKEM_976_AES_OQS ||
m_code == Group_Params_Code::HYBRID_SECP521R1_KYBER_1024_R3_OQS ||
m_code == Group_Params_Code::HYBRID_SECP521R1_eFRODOKEM_1344_SHAKE_OQS ||
m_code == Group_Params_Code::HYBRID_SECP521R1_eFRODOKEM_1344_AES_OQS;
}

BOTAN_DIAGNOSTIC_POP
// If this is a pqc hybrid group, returns the ECC ID
std::optional<Group_Params_Code> pqc_hybrid_ecc() const;

constexpr bool is_pqc_hybrid() const {
return is_pqc_hybrid_ml_kem() || is_pqc_hybrid_kyber_r3() || is_pqc_hybrid_frodokem();
}

constexpr bool is_kem() const { return is_pure_kyber() || is_pure_frodokem() || is_pqc_hybrid(); }
constexpr bool is_kem() const { return is_pure_kyber_r3() || is_pure_frodokem() || is_pqc_hybrid(); }

// Returns std::nullopt if the param has no known name
std::optional<std::string> to_string() const;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/tls/tls_callbacks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ std::unique_ptr<Public_Key> TLS::Callbacks::tls_deserialize_peer_public_key(
#endif

#if defined(BOTAN_HAS_KYBER)
if(group_params.is_pure_kyber()) {
if(group_params.is_pure_kyber_r3()) {
return std::make_unique<Kyber_PublicKey>(key_bits, KyberMode(group_params.to_string().value()));
}
#endif
Expand All @@ -245,7 +245,7 @@ std::unique_ptr<Public_Key> TLS::Callbacks::tls_deserialize_peer_public_key(

std::unique_ptr<Private_Key> TLS::Callbacks::tls_kem_generate_key(TLS::Group_Params group, RandomNumberGenerator& rng) {
#if defined(BOTAN_HAS_KYBER)
if(group.is_pure_kyber()) {
if(group.is_pure_kyber_r3()) {
return std::make_unique<Kyber_PrivateKey>(rng, KyberMode(group.to_string().value()));
}
#endif
Expand Down
84 changes: 69 additions & 15 deletions src/lib/tls/tls_policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,30 @@ Group_Params Policy::choose_key_exchange_group(const std::vector<Group_Params>&

const std::vector<Group_Params> our_groups = key_exchange_groups();

// Prefer groups that were offered by the peer for the sake of saving
// an additional round trip. For TLS 1.2, this won't be used.
const bool client_supports_pqc = std::any_of(
supported_by_peer.begin(), supported_by_peer.end(), [](const Group_Params& g) { return g.is_post_quantum(); });

if(client_supports_pqc) {
// If the client supports PQ and sent us a PQ key share we can use, take it
for(auto g : offered_by_peer) {
if(g.is_post_quantum() && value_exists(our_groups, g)) {
return g;
}
}

// If the client supports PQ but not a PQ key share, still prefer PQ
for(auto g : supported_by_peer) {
if(g.is_post_quantum() && value_exists(our_groups, g)) {
return g;
}
}
}

// If we are here, the client did not offer any (mutually supported)
// post quantum algorithms

// Prefer groups that were offered by the peer, for the sake of saving an
// additional round trip. For TLS 1.2, this won't be used.
for(auto g : offered_by_peer) {
if(value_exists(our_groups, g)) {
return g;
Expand Down Expand Up @@ -161,31 +183,63 @@ Group_Params Policy::default_dh_group() const {
}

std::vector<Group_Params> Policy::key_exchange_groups() const {
// Default list is ordered by performance
return {
// clang-format off
#if defined(BOTAN_HAS_TLS_13_PQC) && defined(BOTAN_HAS_ML_KEM) && defined(BOTAN_HAS_X25519)
Group_Params::HYBRID_X25519_ML_KEM_768,
#endif

#if defined(BOTAN_HAS_X25519)
Group_Params::X25519,
#endif

Group_Params::SECP256R1,

#if defined(BOTAN_HAS_X448)
Group_Params::X448,
Group_Params::X448,
#endif

Group_Params::SECP256R1, Group_Params::BRAINPOOL256R1, Group_Params::SECP384R1, Group_Params::BRAINPOOL384R1,
Group_Params::SECP521R1, Group_Params::BRAINPOOL512R1,
Group_Params::SECP384R1,
Group_Params::SECP521R1,

Group_Params::FFDHE_2048, Group_Params::FFDHE_3072, Group_Params::FFDHE_4096, Group_Params::FFDHE_6144,
Group_Params::FFDHE_8192,
Group_Params::BRAINPOOL256R1,
Group_Params::BRAINPOOL384R1,
Group_Params::BRAINPOOL512R1,

Group_Params::FFDHE_2048,
Group_Params::FFDHE_3072,

// clang-format on
};
}

std::vector<Group_Params> Policy::key_exchange_groups_to_offer() const {
// by default, we offer a key share for the most-preferred group, only
std::vector<Group_Params> groups_to_offer;
const auto supported_groups = key_exchange_groups();
if(!supported_groups.empty()) {
groups_to_offer.push_back(supported_groups.front());
/*
By default, we offer a key share for the most-preferred pure ECC group
by default, if any pure ECC group is enabled in the policy.
We skip PQC (or hybrids) since the keys are much larger and they are not
yet widely supported; the most common case is we waste a lot of packet
space sending a key share that the peer will ignore.
Likewise we skip DH since the keys are large
However if no pure ECC is enabled then we offer the first enabled
key exchange group, no matter what kind it is.
*/
const auto kex_groups = key_exchange_groups();

for(auto group : kex_groups) {
if(group.is_pure_ecc_group()) {
return {group};
}
}

if(kex_groups.empty()) {
return {};
} else {
return {kex_groups[0]};
}
return groups_to_offer;
}

size_t Policy::minimum_dh_group_size() const {
Expand Down Expand Up @@ -651,7 +705,7 @@ void Policy::print(std::ostream& o) const {
}
o << "maximum_session_tickets_per_client_hello = " << maximum_session_tickets_per_client_hello() << '\n';
o << "session_ticket_lifetime = " << session_ticket_lifetime().count() << '\n';
o << "reuse_session_tickets = " << reuse_session_tickets() << '\n';
print_bool(o, "reuse_session_tickets", reuse_session_tickets());
o << "new_session_tickets_upon_handshake_success = " << new_session_tickets_upon_handshake_success() << '\n';
o << "minimum_dh_group_size = " << minimum_dh_group_size() << '\n';
o << "minimum_ecdh_group_size = " << minimum_ecdh_group_size() << '\n';
Expand Down
Loading

0 comments on commit ec0d301

Please sign in to comment.