Skip to content

Commit

Permalink
Enable post-quantum ML-KEM-768+x25519 TLS 1.3 ciphersuite by default
Browse files Browse the repository at this point in the history
This adjusts the default logic for both which groups to offer and which group to
select during negotiation.

For offering, we send the first pure ECC group in the preference list. This
avoids sending large PQ shares to servers that don't support them. If the client
for whatever reason does not have any pure ECC groups, then we send a share of
whatever their top preference is.

On the server side, if the client indicated support for any mutually supported
PQ algorithm, we always select it, even if the client sent some other type of
keyshare. Previously we would always prefer to select a group that the client
sent a share of, to reduce round trips.

This also rearranges the default list, and removes some of the most expensive
FFDHE groups.

Expose which group was used for key exchange in the Session_Summary, since
otherwise there is no way to know if PQ exchange occurred or not.

Co-authored-by: René Meusel <[email protected]>
  • Loading branch information
randombit and reneme committed Dec 27, 2024
1 parent 960fdcf commit 13dd167
Show file tree
Hide file tree
Showing 18 changed files with 281 additions and 92 deletions.
8 changes: 0 additions & 8 deletions src/bogo_shim/bogo_shim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1004,14 +1004,6 @@ class Shim_Policy final : public Botan::TLS::Policy {
if(group == Botan::TLS::Group_Params::HYBRID_X25519_KYBER_768_R3_OQS) {
groups.push_back(group);
}

// TODO: once `TLS::Policy::key_exchange_groups()` contains it by
// default, remove this explicit check.
//
// See: https://github.com/randombit/botan/pull/4305
if(group == Botan::TLS::Group_Params::HYBRID_X25519_ML_KEM_768) {
groups.push_back(group);
}
}

return groups;
Expand Down
2 changes: 2 additions & 0 deletions src/bogo_shim/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
"TooManyChangeCipherSpec-Server-TLS13": "Limits on the number of CCS are not implemented",
"TooManyKeyUpdates": "Limits on the number of KeyUpdates are not implemented",

"PostQuantumNotEnabledByDefaultInClients": "Oh yes it is",

"TLS12SessionID-TLS13": "We don't offer TLS 1.3 when a TLS 1.2 session was found",
"Ticket-Forbidden-TLS13": "We don't offer TLS 1.3 when a TLS 1.2 session was found",
"Resume-Client-NoResume-TLS12-TLS13-TLS": "We don't offer TLS 1.3 when a TLS 1.2 session was found",
Expand Down
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
2 changes: 1 addition & 1 deletion src/lib/tls/tls13/info.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<defines>
TLS_13 -> 20210721
TLS_13 -> 20241208
</defines>

<module_info>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tls/tls13_pqc/info.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<defines>
TLS_13_PQC -> 20230919
TLS_13_PQC -> 20241208
</defines>

<module_info>
Expand Down
84 changes: 84 additions & 0 deletions src/lib/tls/tls_algos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,90 @@ 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_ML_KEM)
if(is_pure_ml_kem() || is_pqc_hybrid_ml_kem()) {
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
38 changes: 28 additions & 10 deletions src/lib/tls/tls_algos.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,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 Down Expand Up @@ -242,32 +247,42 @@ class BOTAN_PUBLIC_API(3, 2) Group_Params final {
BOTAN_DIAGNOSTIC_POP
}

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() 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
constexpr bool is_pqc_hybrid() const {
return is_pqc_hybrid_ml_kem() || is_pqc_hybrid_kyber() || is_pqc_hybrid_frodokem();
}

constexpr bool is_kem() const {
Expand All @@ -279,6 +294,9 @@ class BOTAN_PUBLIC_API(3, 2) Group_Params final {
BOTAN_DIAGNOSTIC_POP
}

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

// Returns std::nullopt if the param has no known name
std::optional<std::string> to_string() const;

Expand Down
66 changes: 56 additions & 10 deletions src/lib/tls/tls_policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,21 @@ Group_Params Policy::choose_key_exchange_group(const std::vector<Group_Params>&
return Group_Params::NONE;
}

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

// First check if the peer sent a PQ share of a group we also support
for(auto share : offered_by_peer) {
if(share.is_post_quantum() && value_exists(our_groups, share)) {
return share;
}
}

// Then check if the peer offered a PQ algo we also support
for(auto share : supported_by_peer) {
if(share.is_post_quantum() && value_exists(our_groups, share)) {
return share;
}
}

// 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.
Expand Down Expand Up @@ -161,30 +175,62 @@ 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_X25519)
Group_Params::X25519,
#endif

Group_Params::SECP256R1,

#if defined(BOTAN_HAS_X25519) && defined(BOTAN_HAS_ML_KEM) && defined(BOTAN_HAS_TLS_13_PQC)
Group_Params_Code::HYBRID_X25519_ML_KEM_768,
#endif

#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::BRAINPOOL256R1,
Group_Params::BRAINPOOL384R1,
Group_Params::BRAINPOOL512R1,

Group_Params::FFDHE_2048,
Group_Params::FFDHE_3072,

Group_Params::FFDHE_2048, Group_Params::FFDHE_3072, Group_Params::FFDHE_4096, Group_Params::FFDHE_6144,
Group_Params::FFDHE_8192,
// 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()) {
BOTAN_ASSERT(!supported_groups.empty(), "Policy allows at least one key exchange group");

/*
* Initially prefer sending a key share only of the first pure-ECC
* group, since these shares are small and PQ support is still not
* that widespread.
*/
for(auto group : key_exchange_groups()) {
if(group.is_pure_ecc_group()) {
groups_to_offer.push_back(group);
break;
}
}

/*
* If for some reason no pure ECC groups are enabled then simply
* send a share of whatever the policys top preference is.
*/
if(groups_to_offer.empty()) {
groups_to_offer.push_back(supported_groups.front());
}

return groups_to_offer;
}

Expand Down Expand Up @@ -651,7 +697,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 13dd167

Please sign in to comment.