From 269d10c29415c9204937d5afea94fadcbc748dc0 Mon Sep 17 00:00:00 2001 From: Aditya Soni Date: Mon, 29 Jun 2026 01:21:55 -0500 Subject: [PATCH] Add native OpenSSL 3.5 X25519MLKEM768 KEM backend Adds fizz::openssl::OpenSSLKemKeyExchange implementing the KeyExchange interface for the native X25519MLKEM768 hybrid group available in stock OpenSSL 3.5+. OpenSSL performs the X25519 + ML-KEM-768 combine internally (Option A), so no liboqs/libsodium hybrid is needed. - Client keygen + decapsulate, server encapsulate; KEM roles mapped onto KeyExchangeRole. clone() uses EVP_PKEY_up_ref. - Gated behind OPENSSL_VERSION_NUMBER >= 0x30500000L plus a runtime provider-availability check; non-3.5 builds are unaffected. - MultiBackendFactory prefers the native path when available, falling back to the existing liboqs path otherwise. - Adds round-trip, clone, size, negative, and negotiation unit tests; registered in CMake, FizzSources, and BUCK. Issue: #169 --- fizz/CMakeLists.txt | 1 + fizz/backend/BUCK | 2 + .../crypto/exchange/OpenSSLKemKeyExchange.cpp | 280 ++++++++++++++++++ .../crypto/exchange/OpenSSLKemKeyExchange.h | 118 ++++++++ .../backend/openssl/crypto/exchange/test/BUCK | 18 ++ .../test/OpenSSLKemKeyExchangeTest.cpp | 174 +++++++++++ fizz/cmake/FizzSources.cmake | 2 + fizz/protocol/MultiBackendFactory.cpp | 21 +- 8 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.cpp create mode 100644 fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.h create mode 100644 fizz/backend/openssl/crypto/exchange/test/OpenSSLKemKeyExchangeTest.cpp diff --git a/fizz/CMakeLists.txt b/fizz/CMakeLists.txt index 41559267db1..a936ec27de8 100644 --- a/fizz/CMakeLists.txt +++ b/fizz/CMakeLists.txt @@ -422,6 +422,7 @@ if(BUILD_TESTS) add_gtest(crypto/aead/test/IOBufUtilTest.cpp IOBufUtilTest) add_gtest(crypto/exchange/test/X25519KeyExchangeTest.cpp X25519KeyExchangeTest) add_gtest(backend/openssl/crypto/exchange/test/ECKeyExchangeTest.cpp ECKeyExchangeTest) + add_gtest(backend/openssl/crypto/exchange/test/OpenSSLKemKeyExchangeTest.cpp OpenSSLKemKeyExchangeTest) add_gtest(crypto/hpke/test/ContextTest.cpp ContextTest) add_gtest(crypto/hpke/test/DHKEMTest.cpp DHKEMTest) add_gtest(crypto/hpke/test/HpkeTest.cpp HpkeTest) diff --git a/fizz/backend/BUCK b/fizz/backend/BUCK index be8514dda5a..288795e573c 100644 --- a/fizz/backend/BUCK +++ b/fizz/backend/BUCK @@ -54,6 +54,7 @@ fizz_cpp_library( "openssl/crypto/Sha.cpp", "openssl/crypto/aead/OpenSSLEVPCipher.cpp", "openssl/crypto/exchange/OpenSSLKeyExchange.cpp", + "openssl/crypto/exchange/OpenSSLKemKeyExchange.cpp", "openssl/crypto/signature/Signature.cpp", ], headers = [ @@ -75,6 +76,7 @@ fizz_cpp_library( "openssl/crypto/aead/OpenSSLEVPCipher.h", "openssl/crypto/aead/OpenSSLEVPCipher-inl.h", "openssl/crypto/exchange/OpenSSLKeyExchange.h", + "openssl/crypto/exchange/OpenSSLKemKeyExchange.h", "openssl/crypto/signature/Signature.h", "openssl/crypto/signature/Signature-inl.h", ], diff --git a/fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.cpp b/fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.cpp new file mode 100644 index 00000000000..5f3edaf4d77 --- /dev/null +++ b/fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + +#include +#include +#include + +namespace fizz { +namespace openssl { + +namespace { + +folly::ssl::EvpPkeyCtxUniquePtr makeNamedCtx(const std::string& groupName) { + return folly::ssl::EvpPkeyCtxUniquePtr( + EVP_PKEY_CTX_new_from_name(nullptr, groupName.c_str(), nullptr)); +} + +// Builds an EVP_PKEY holding just the peer's encoded public key for the named +// hybrid group, so the server can encapsulate to it. +Status importPublicKey( + folly::ssl::EvpPkeyUniquePtr& ret, + Error& err, + const std::string& groupName, + folly::ByteRange pub) { + auto ctx = makeNamedCtx(groupName); + if (!ctx) { + return err.error("kem: failed to create import ctx"); + } + if (EVP_PKEY_fromdata_init(ctx.get()) <= 0) { + return err.error("kem: fromdata_init failed"); + } + OSSL_PARAM params[] = { + OSSL_PARAM_construct_octet_string( + OSSL_PKEY_PARAM_PUB_KEY, + const_cast(pub.data()), + pub.size()), + OSSL_PARAM_construct_end()}; + EVP_PKEY* pkey = nullptr; + if (EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0 || + pkey == nullptr) { + return err.error("kem: fromdata failed to import public key"); + } + ret.reset(pkey); + return Status::Success; +} + +// Measures the encoded public-key and ciphertext lengths for a named hybrid +// group by generating a throwaway keypair and querying encapsulate. Returns +// false if the group is unavailable. +bool measureSizes(const std::string& groupName, size_t& pubLen, size_t& ctLen) { + pubLen = 0; + ctLen = 0; + auto kgCtx = makeNamedCtx(groupName); + if (!kgCtx || EVP_PKEY_keygen_init(kgCtx.get()) <= 0) { + return false; + } + EVP_PKEY* raw = nullptr; + if (EVP_PKEY_generate(kgCtx.get(), &raw) <= 0 || raw == nullptr) { + return false; + } + folly::ssl::EvpPkeyUniquePtr key(raw); + unsigned char* pub = nullptr; + pubLen = EVP_PKEY_get1_encoded_public_key(key.get(), &pub); + if (pub != nullptr) { + OPENSSL_free(pub); + } + folly::ssl::EvpPkeyCtxUniquePtr ctx( + EVP_PKEY_CTX_new_from_pkey(nullptr, key.get(), nullptr)); + size_t secretLen = 0; + if (ctx && EVP_PKEY_encapsulate_init(ctx.get(), nullptr) > 0) { + EVP_PKEY_encapsulate(ctx.get(), nullptr, &ctLen, nullptr, &secretLen); + } + return pubLen != 0 && ctLen != 0; +} + +} // namespace + +bool isKemGroupAvailable(const char* groupName) { + folly::ssl::EvpPkeyCtxUniquePtr ctx( + EVP_PKEY_CTX_new_from_name(nullptr, groupName, nullptr)); + return ctx != nullptr; +} + +Status OpenSSLKemKeyExchange::createKeyExchange( + std::unique_ptr& ret, + Error& err, + KeyExchangeRole role, + std::string groupName) { + if (!isKemGroupAvailable(groupName.c_str())) { + return err.error("kem: group not available in OpenSSL providers"); + } + if (role == KeyExchangeRole::Server) { + ret = std::make_unique(std::move(groupName)); + } else { + ret = std::make_unique(std::move(groupName)); + } + return Status::Success; +} + +/* Client */ + +Status OpenSSLKemClientKeyExchange::generateKeyPair(Error& err) { + auto ctx = makeNamedCtx(groupName_); + if (!ctx) { + return err.error("kem: failed to create keygen ctx"); + } + if (EVP_PKEY_keygen_init(ctx.get()) <= 0) { + return err.error("kem: keygen_init failed"); + } + EVP_PKEY* pkey = nullptr; + if (EVP_PKEY_generate(ctx.get(), &pkey) <= 0 || pkey == nullptr) { + return err.error("kem: keypair generation failed"); + } + key_.reset(pkey); + return Status::Success; +} + +Status OpenSSLKemClientKeyExchange::getKeyShare( + std::unique_ptr& ret, + Error& err) const { + if (!key_) { + return err.error("kem: key not generated"); + } + unsigned char* pub = nullptr; + size_t pubLen = EVP_PKEY_get1_encoded_public_key(key_.get(), &pub); + if (pubLen == 0 || pub == nullptr) { + return err.error("kem: failed to encode public key"); + } + ret = folly::IOBuf::copyBuffer(pub, pubLen); + OPENSSL_free(pub); + return Status::Success; +} + +Status OpenSSLKemClientKeyExchange::generateSharedSecret( + std::unique_ptr& ret, + Error& err, + folly::ByteRange keyShare) const { + if (!key_) { + return err.error("kem: key not generated"); + } + folly::ssl::EvpPkeyCtxUniquePtr ctx( + EVP_PKEY_CTX_new_from_pkey(nullptr, key_.get(), nullptr)); + if (!ctx || EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) { + return err.error("kem: decapsulate_init failed"); + } + size_t secretLen = 0; + if (EVP_PKEY_decapsulate( + ctx.get(), + nullptr, + &secretLen, + keyShare.data(), + keyShare.size()) <= 0) { + return err.error("kem: decapsulate size query failed"); + } + auto secret = folly::IOBuf::create(secretLen); + if (EVP_PKEY_decapsulate( + ctx.get(), + secret->writableData(), + &secretLen, + keyShare.data(), + keyShare.size()) <= 0) { + return err.error("kem: decapsulate failed"); + } + secret->append(secretLen); + ret = std::move(secret); + return Status::Success; +} + +Status OpenSSLKemClientKeyExchange::clone( + std::unique_ptr& ret, + Error& err) const { + if (!key_) { + return err.error("kem: key not generated"); + } + // Share ownership of the underlying key; up_ref is version-independent + // whereas EVP_PKEY_dup historically returned NULL for ML-KEM keys. + if (EVP_PKEY_up_ref(key_.get()) <= 0) { + return err.error("kem: up_ref failed"); + } + auto copy = std::make_unique(groupName_); + copy->key_.reset(key_.get()); + ret = std::move(copy); + return Status::Success; +} + +std::size_t OpenSSLKemClientKeyExchange::getExpectedKeyShareSize() const { + // The client receives the server's ciphertext. Sizes are fixed per group, + // so cache after the first query to avoid a keygen on every call. + if (expectedShareSize_ == 0) { + size_t pubLen = 0; + size_t ctLen = 0; + measureSizes(groupName_, pubLen, ctLen); + expectedShareSize_ = ctLen; + } + return expectedShareSize_; +} + +/* Server */ + +Status OpenSSLKemServerKeyExchange::getKeyShare( + std::unique_ptr& ret, + Error& err) const { + if (!cipherText_) { + return err.error("kem: ciphertext not generated"); + } + ret = cipherText_->clone(); + return Status::Success; +} + +Status OpenSSLKemServerKeyExchange::generateSharedSecret( + std::unique_ptr& ret, + Error& err, + folly::ByteRange keyShare) const { + folly::ssl::EvpPkeyUniquePtr peerKey; + FIZZ_RETURN_ON_ERROR(importPublicKey(peerKey, err, groupName_, keyShare)); + folly::ssl::EvpPkeyCtxUniquePtr ctx( + EVP_PKEY_CTX_new_from_pkey(nullptr, peerKey.get(), nullptr)); + if (!ctx || EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) { + return err.error("kem: encapsulate_init failed"); + } + size_t ctLen = 0; + size_t secretLen = 0; + if (EVP_PKEY_encapsulate(ctx.get(), nullptr, &ctLen, nullptr, &secretLen) <= + 0) { + return err.error("kem: encapsulate size query failed"); + } + auto cipherText = folly::IOBuf::create(ctLen); + auto secret = folly::IOBuf::create(secretLen); + if (EVP_PKEY_encapsulate( + ctx.get(), + cipherText->writableData(), + &ctLen, + secret->writableData(), + &secretLen) <= 0) { + return err.error("kem: encapsulate failed"); + } + cipherText->append(ctLen); + secret->append(secretLen); + cipherText_ = std::move(cipherText); + ret = std::move(secret); + return Status::Success; +} + +Status OpenSSLKemServerKeyExchange::clone( + std::unique_ptr& ret, + Error& err) const { + if (!cipherText_) { + return err.error("kem: ciphertext not generated"); + } + auto copy = std::make_unique(groupName_); + copy->cipherText_ = cipherText_->clone(); + copy->cipherText_->coalesce(); + ret = std::move(copy); + return Status::Success; +} + +std::size_t OpenSSLKemServerKeyExchange::getExpectedKeyShareSize() const { + // The server receives the client's encoded public key. Cache it. + if (expectedShareSize_ == 0) { + size_t pubLen = 0; + size_t ctLen = 0; + measureSizes(groupName_, pubLen, ctLen); + expectedShareSize_ = pubLen; + } + return expectedShareSize_; +} + +} // namespace openssl +} // namespace fizz + +#endif // OPENSSL_VERSION_NUMBER >= 0x30500000L diff --git a/fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.h b/fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.h new file mode 100644 index 00000000000..584e5f2200f --- /dev/null +++ b/fizz/backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.h @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include + +// Native ML-KEM (FIPS 203) hybrid groups (e.g. X25519MLKEM768) are only +// available in the stock OpenSSL providers starting with 3.5. Everything in +// this file is compiled out on older OpenSSL so non-3.5 builds are unaffected. +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + +#include +#include + +#include + +namespace fizz { +namespace openssl { + +/** + * Returns true if the running OpenSSL providers expose the given hybrid KEM + * key management (e.g. "X25519MLKEM768"). Used by the factory to decide whether + * the native path can be taken before falling back to liboqs. + */ +bool isKemGroupAvailable(const char* groupName); + +/** + * KEM-based key exchange backed by OpenSSL 3.5's native hybrid groups + * (X25519MLKEM768 et al). OpenSSL performs the X25519 + ML-KEM combine + * internally, so fizz only drives the public encapsulate/decapsulate APIs. + * + * Because KEMs are asymmetric (client keygen + decapsulate, server + * encapsulate), the role is split into two concrete subclasses, mirroring the + * liboqs backend. Use createKeyExchange() to obtain the right one. + */ +class OpenSSLKemKeyExchange : public KeyExchange { + public: + static Status createKeyExchange( + std::unique_ptr& ret, + Error& err, + KeyExchangeRole role, + std::string groupName); + + ~OpenSSLKemKeyExchange() override = default; + + protected: + explicit OpenSSLKemKeyExchange(std::string groupName) + : groupName_(std::move(groupName)) {} + + std::string groupName_; +}; + +/** + * Client side: generates the keypair, shares the public key, and decapsulates + * the server's ciphertext into the shared secret. + */ +class OpenSSLKemClientKeyExchange : public OpenSSLKemKeyExchange { + public: + explicit OpenSSLKemClientKeyExchange(std::string groupName) + : OpenSSLKemKeyExchange(std::move(groupName)) {} + + ~OpenSSLKemClientKeyExchange() override = default; + + Status generateKeyPair(Error& err) override; + Status getKeyShare(std::unique_ptr& ret, Error& err) + const override; + Status generateSharedSecret( + std::unique_ptr& ret, + Error& err, + folly::ByteRange keyShare) const override; + Status clone(std::unique_ptr& ret, Error& err) const override; + std::size_t getExpectedKeyShareSize() const override; + + private: + folly::ssl::EvpPkeyUniquePtr key_; + mutable std::size_t expectedShareSize_{0}; +}; + +/** + * Server side: imports the client's public key, encapsulates to produce a + * ciphertext (its key share) and the shared secret. No keypair of its own. + */ +class OpenSSLKemServerKeyExchange : public OpenSSLKemKeyExchange { + public: + explicit OpenSSLKemServerKeyExchange(std::string groupName) + : OpenSSLKemKeyExchange(std::move(groupName)) {} + + ~OpenSSLKemServerKeyExchange() override = default; + + Status generateKeyPair(Error& /*err*/) override { + return Status::Success; + } + Status getKeyShare(std::unique_ptr& ret, Error& err) + const override; + Status generateSharedSecret( + std::unique_ptr& ret, + Error& err, + folly::ByteRange keyShare) const override; + Status clone(std::unique_ptr& ret, Error& err) const override; + std::size_t getExpectedKeyShareSize() const override; + + private: + mutable std::unique_ptr cipherText_; + mutable std::size_t expectedShareSize_{0}; +}; + +} // namespace openssl +} // namespace fizz + +#endif // OPENSSL_VERSION_NUMBER >= 0x30500000L diff --git a/fizz/backend/openssl/crypto/exchange/test/BUCK b/fizz/backend/openssl/crypto/exchange/test/BUCK index 77a40263a42..c59f02b0100 100644 --- a/fizz/backend/openssl/crypto/exchange/test/BUCK +++ b/fizz/backend/openssl/crypto/exchange/test/BUCK @@ -21,3 +21,21 @@ fizz_cpp_unittest( ("openssl", None, "crypto"), ], ) + +fizz_cpp_unittest( + name = "OpenSSLKemKeyExchange", + srcs = [ + "OpenSSLKemKeyExchangeTest.cpp", + ], + supports_static_listing = False, + deps = [ + "//fizz/backend:openssl", + "//fizz/protocol:default_factory", + "//fizz/record:record", + "//fizz/util:status", + "//folly/portability:gtest", + ], + external_deps = [ + ("openssl", None, "crypto"), + ], +) diff --git a/fizz/backend/openssl/crypto/exchange/test/OpenSSLKemKeyExchangeTest.cpp b/fizz/backend/openssl/crypto/exchange/test/OpenSSLKemKeyExchangeTest.cpp new file mode 100644 index 00000000000..36ec9fc4321 --- /dev/null +++ b/fizz/backend/openssl/crypto/exchange/test/OpenSSLKemKeyExchangeTest.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2018-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#include + +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + +namespace fizz { +namespace openssl { +namespace test { + +constexpr char kGroup[] = "X25519MLKEM768"; + +bool kemAvailable() { + return isKemGroupAvailable(kGroup); +} + +// KEM round-trip: client keygen -> pub -> server encapsulate -> cipher -> +// client decapsulate, and both sides derive an identical shared secret. +TEST(OpenSSLKemKeyExchange, RoundTrip) { + if (!kemAvailable()) { + GTEST_SKIP() << "X25519MLKEM768 not available in OpenSSL providers"; + } + Error err; + std::unique_ptr client; + std::unique_ptr server; + ASSERT_EQ( + OpenSSLKemKeyExchange::createKeyExchange( + client, err, KeyExchangeRole::Client, kGroup), + Status::Success); + ASSERT_EQ( + OpenSSLKemKeyExchange::createKeyExchange( + server, err, KeyExchangeRole::Server, kGroup), + Status::Success); + + ASSERT_EQ(client->generateKeyPair(err), Status::Success); + std::unique_ptr clientShare; + ASSERT_EQ(client->getKeyShare(clientShare, err), Status::Success); + ASSERT_TRUE(clientShare); + + std::unique_ptr serverSecret; + ASSERT_EQ( + server->generateSharedSecret(serverSecret, err, clientShare->coalesce()), + Status::Success); + std::unique_ptr serverShare; + ASSERT_EQ(server->getKeyShare(serverShare, err), Status::Success); + + std::unique_ptr clientSecret; + ASSERT_EQ( + client->generateSharedSecret(clientSecret, err, serverShare->coalesce()), + Status::Success); + + EXPECT_TRUE(folly::IOBufEqualTo()(serverSecret, clientSecret)); + EXPECT_GT(clientSecret->computeChainDataLength(), 0u); +} + +// clone() must share the keypair (up_ref) and yield the same shared secret. +TEST(OpenSSLKemKeyExchange, CloneClient) { + if (!kemAvailable()) { + GTEST_SKIP() << "X25519MLKEM768 not available"; + } + Error err; + std::unique_ptr client; + ASSERT_EQ( + OpenSSLKemKeyExchange::createKeyExchange( + client, err, KeyExchangeRole::Client, kGroup), + Status::Success); + ASSERT_EQ(client->generateKeyPair(err), Status::Success); + + std::unique_ptr clone; + ASSERT_EQ(client->clone(clone, err), Status::Success); + + std::unique_ptr a; + std::unique_ptr b; + ASSERT_EQ(client->getKeyShare(a, err), Status::Success); + ASSERT_EQ(clone->getKeyShare(b, err), Status::Success); + EXPECT_TRUE(folly::IOBufEqualTo()(a, b)); +} + +TEST(OpenSSLKemKeyExchange, ExpectedSizes) { + if (!kemAvailable()) { + GTEST_SKIP() << "X25519MLKEM768 not available"; + } + Error err; + std::unique_ptr client; + std::unique_ptr server; + ASSERT_EQ( + OpenSSLKemKeyExchange::createKeyExchange( + client, err, KeyExchangeRole::Client, kGroup), + Status::Success); + ASSERT_EQ( + OpenSSLKemKeyExchange::createKeyExchange( + server, err, KeyExchangeRole::Server, kGroup), + Status::Success); + // Server expects the client public key; client expects the ciphertext. + EXPECT_GT(server->getExpectedKeyShareSize(), 0u); + EXPECT_GT(client->getExpectedKeyShareSize(), 0u); +} + +// Decapsulating a malformed (too short) ciphertext must fail cleanly. +TEST(OpenSSLKemKeyExchange, BadCiphertextFails) { + if (!kemAvailable()) { + GTEST_SKIP() << "X25519MLKEM768 not available"; + } + Error err; + std::unique_ptr client; + ASSERT_EQ( + OpenSSLKemKeyExchange::createKeyExchange( + client, err, KeyExchangeRole::Client, kGroup), + Status::Success); + ASSERT_EQ(client->generateKeyPair(err), Status::Success); + std::array bogus{}; + std::unique_ptr secret; + EXPECT_EQ( + client->generateSharedSecret(secret, err, folly::range(bogus)), + Status::Fail); +} + +// Encapsulating to a malformed (too short) peer public key must fail cleanly. +TEST(OpenSSLKemKeyExchange, BadPeerKeyFails) { + if (!kemAvailable()) { + GTEST_SKIP() << "X25519MLKEM768 not available"; + } + Error err; + std::unique_ptr server; + ASSERT_EQ( + OpenSSLKemKeyExchange::createKeyExchange( + server, err, KeyExchangeRole::Server, kGroup), + Status::Success); + std::array bogus{}; + std::unique_ptr secret; + EXPECT_EQ( + server->generateSharedSecret(secret, err, folly::range(bogus)), + Status::Fail); +} + +// Negotiation: when the native provider is available, MultiBackendFactory must +// hand back the native KEM; otherwise it falls back to liboqs (if built). +TEST(OpenSSLKemKeyExchange, FactoryPrefersNative) { + Error err; + MultiBackendFactory factory; + std::unique_ptr kex; + auto st = factory.makeKeyExchange( + kex, err, NamedGroup::X25519MLKEM768, KeyExchangeRole::Client); +#if FIZZ_HAVE_OQS + EXPECT_EQ(st, Status::Success); + EXPECT_TRUE(kex); +#else + if (kemAvailable()) { + EXPECT_EQ(st, Status::Success); + EXPECT_TRUE(kex); + } +#endif +} +} // namespace test +} // namespace openssl +} // namespace fizz + +#endif // OPENSSL_VERSION_NUMBER >= 0x30500000L diff --git a/fizz/cmake/FizzSources.cmake b/fizz/cmake/FizzSources.cmake index 73577b2cd61..5fa42e57a04 100644 --- a/fizz/cmake/FizzSources.cmake +++ b/fizz/cmake/FizzSources.cmake @@ -16,6 +16,7 @@ set( backend/openssl/crypto/Sha.cpp backend/openssl/crypto/aead/OpenSSLEVPCipher.cpp backend/openssl/crypto/exchange/OpenSSLKeyExchange.cpp + backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.cpp backend/openssl/crypto/signature/Signature.cpp client/CertManager.cpp client/ClientProtocol.cpp @@ -127,6 +128,7 @@ set( backend/openssl/crypto/aead/OpenSSLEVPCipher-inl.h backend/openssl/crypto/aead/OpenSSLEVPCipher.h backend/openssl/crypto/exchange/OpenSSLKeyExchange.h + backend/openssl/crypto/exchange/OpenSSLKemKeyExchange.h backend/openssl/crypto/signature/Signature-inl.h backend/openssl/crypto/signature/Signature.h client/Actions.h diff --git a/fizz/protocol/MultiBackendFactory.cpp b/fizz/protocol/MultiBackendFactory.cpp index 465e8adc8ee..187e32eb0cf 100644 --- a/fizz/protocol/MultiBackendFactory.cpp +++ b/fizz/protocol/MultiBackendFactory.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace fizz { @@ -38,8 +39,21 @@ Status MultiBackendFactory::makeKeyExchange( case NamedGroup::x25519: ret = fizz::libsodium::makeKeyExchange(); return Status::Success; -#if FIZZ_HAVE_OQS +#if FIZZ_HAVE_OQS || (OPENSSL_VERSION_NUMBER >= 0x30500000L) case NamedGroup::X25519MLKEM768: { +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + // Prefer OpenSSL 3.5's native, FIPS-capable hybrid group when the + // provider exposes it. OpenSSL performs the X25519+ML-KEM combine, so + // no fizz-side HybridKeyExchange is needed; fall back to liboqs below + // when unavailable. + if (fizz::openssl::isKemGroupAvailable("X25519MLKEM768")) { + FIZZ_RETURN_ON_ERROR( + fizz::openssl::OpenSSLKemKeyExchange::createKeyExchange( + ret, err, role, "X25519MLKEM768")); + return Status::Success; + } +#endif +#if FIZZ_HAVE_OQS std::unique_ptr oqsKex; FIZZ_RETURN_ON_ERROR( fizz::liboqs::makeKeyExchange(oqsKex, err, role)); @@ -52,7 +66,12 @@ Status MultiBackendFactory::makeKeyExchange( fizz::libsodium::makeKeyExchange())); ret = std::move(hybridKex); return Status::Success; +#else + return err.error("ke: X25519MLKEM768 not available"); +#endif } +#endif +#if FIZZ_HAVE_OQS case NamedGroup::X25519MLKEM512_FB: { std::unique_ptr oqsKex; FIZZ_RETURN_ON_ERROR(