From 4743845d9697e57870d3244b6580e48d45d4ae70 Mon Sep 17 00:00:00 2001 From: 0xG0nz0 <8682922+0xg0nz0@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:17:57 +0000 Subject: [PATCH] Add unit test for default TLS 1.2 / 1.3 cipher configuration --- CMakeLists.txt | 8 +- sdk/net/crypto/ssl.cc | 209 ++++++++++++++++++++++++++---------------- sdk/net/crypto/ssl.h | 13 ++- tests/CMakeLists.txt | 10 ++ tests/client_test.cc | 1 - tests/model_test.cc | 1 - tests/ssl_test.cc | 16 ++++ 7 files changed, 171 insertions(+), 87 deletions(-) create mode 100644 tests/ssl_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c66e33..3ace2f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,8 +29,9 @@ externalproject_add(wolfssl GIT_TAG v5.7.0-stable PREFIX ${CMAKE_BINARY_DIR}/wolfssl BUILD_IN_SOURCE 1 - CONFIGURE_COMMAND autoreconf -i COMMAND /configure --prefix= --enable-all --enable-harden --enable-keylog-export --disable-ech + CONFIGURE_COMMAND autoreconf -i COMMAND /configure --prefix= --enable-all --enable-harden --enable-keylog-export --enable-static --disable-ech BUILD_COMMAND make -j ${NPROC} + BUILD_BYPRODUCTS ${CMAKE_BINARY_DIR}/wolfssl/lib/libwolfssl.a INSTALL_COMMAND make install UPDATE_COMMAND "" ) @@ -69,6 +70,7 @@ externalproject_add(curl ) set(WOLFSSL_INCLUDE_DIR ${CMAKE_BINARY_DIR}/wolfssl/include) +set(WOLFSSL_LIB_DIR ${CMAKE_BINARY_DIR}/wolfssl/lib) set(NGHTTP3_INCLUDE_DIR ${CMAKE_BINARY_DIR}/nghttp3/include) set(NGTCP2_INCLUDE_DIR ${CMAKE_BINARY_DIR}/ngtcp2/include) set(CURL_INCLUDE_DIR ${CMAKE_BINARY_DIR}/curl/include) @@ -88,14 +90,16 @@ target_include_directories(iggy PRIVATE ${NGTCP2_INCLUDE_DIR} ${CURL_INCLUDE_DIR} ) -add_dependencies(iggy curl) +add_dependencies(iggy curl wolfssl) target_link_libraries( iggy PRIVATE ada::ada + fmt::fmt libuv::uv_a unofficial-sodium::sodium ${CURL_LIB_DIR}/libcurl.a + ${WOLFSSL_LIB_DIR}/libwolfssl.a ) # even though this is related to unit tests, to get a full report we need to ensure that diff --git a/sdk/net/crypto/ssl.cc b/sdk/net/crypto/ssl.cc index f21461e..d57c80f 100644 --- a/sdk/net/crypto/ssl.cc +++ b/sdk/net/crypto/ssl.cc @@ -2,101 +2,152 @@ #include #include "fmt/format.h" -const std::vector iggy::ssl::SSLOptions::getDefaultCipherList() { +const std::vector iggy::ssl::SSLOptions::getDefaultCipherList(iggy::ssl::ProtocolVersion protocolVersion) { auto ciphers = std::vector(); -#if defined(HAVE_AESGCM) && !defined(NO_DH) -#ifdef WOLFSSL_TLS13 - ciphers.push_back("TLS13-AES128-GCM-SHA256"); -#ifndef WOLFSSL_NO_TLS12 - ciphers.push_back("DHE-PSK-AES128-GCM-SHA256"); -#endif + + // References: + // - https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html + // - https://ssl-config.mozilla.org + + if (protocolVersion == iggy::ssl::ProtocolVersion::TLSV1_3) { +// sanity check to make sure TLS 1.3 is compiled in +#ifdef WOLFSSL_NO_TLS13 + throw std::runtime_error("TLS 1.3 is not supported by this build of WolfSSL"); #else - ciphers.push_back("DHE-PSK-AES128-GCM-SHA256"); -#endif -#elif defined(HAVE_AESGCM) && defined(WOLFSSL_TLS13) - ciphers.push_back("TLS13-AES128-GCM-SHA256"); -#ifndef WOLFSSL_NO_TLS12 - ciphers.push_back("PSK-AES128-GCM-SHA256"); -#endif -#elif defined(HAVE_NULL_CIPHER) - ciphers.push_back("PSK-NULL-SHA256"); -#elif !defined(NO_AES_CBC) - ciphers.push_back("PSK-AES128-CBC-SHA256"); + // recommended TLS 1.3 ciphers +#if defined(HAVE_AESGCM) && !defined(NO_AES) + ciphers.push_back("TLS_AES_128_GCM_SHA256"); +#if defined(WOLFSSL_SHA384) + ciphers.push_back("TLS_AES_256_GCM_SHA384"); +#endif // WOLFSSL_SHA384 +#endif // HAVE_AESGCM && !NO_AES +#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305) + ciphers.push_back("TLS_CHACHA20_POLY1305_SHA256"); +#endif // HAVE_CHACHA && HAVE_POLY1305 +#endif // WOLFSSL_NO_TLS13 + } else if (protocolVersion == iggy::ssl::ProtocolVersion::TLSV1_2) { +#ifdef WOLFSSL_NO_TLS12 + throw std::runtime_error("TLS 1.2 is not supported by this build of WolfSSL"); #else - ciphers.push_back("PSK-AES128-GCM-SHA256"); +#if !defined(HAVE_ECC) || !defined(HAVE_SUPPORTED_CURVES) + throw std::runtime_error("ECC + Supported Curves must be enabled to support ECDHE"); +#endif // HAVE_ECC && HAVE_SUPPORTED_CURVES + + // recommended TLS 1.2 ciphers +#if defined(HAVE_AESGCM) && !defined(NO_AES) + ciphers.push_back("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); +#ifndef NO_RSA + ciphers.push_back("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); +#endif // NO_RSA +#if defined(WOLFSSL_SHA384) + ciphers.push_back("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"); +#endif // WOLFSSL_SHA384 +#ifndef NO_RSA + ciphers.push_back("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); +#endif // NO_RSA +#endif // HAVE_AESGCM && !NO_AES +#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305) + ciphers.push_back("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"); +#ifndef NO_RSA + ciphers.push_back("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"); +#endif #endif + } else { + auto protocolVersionName = iggy::ssl::getProtocolVersionName(protocolVersion); + throw std::runtime_error(fmt::format("Unsupported protocol version: {}", protocolVersionName)); + } + + if (ciphers.empty()) { + auto protocolVersionName = iggy::ssl::getProtocolVersionName(protocolVersion); + throw std::runtime_error(fmt::format("No ciphers available for the specified protocol version: {}", protocolVersionName)); + } return ciphers; -} - -iggy::ssl::SSLContext::SSLContext(const SSLOptions& options, const PKIEnvironment& pkiEnv) - : options(options) - , pkiEnv(pkiEnv) { - // before we make any other wolfSSL calls, make sure library is initialized once and only once - std::call_once(sslInitDone, []() { wolfSSL_Init(); }); - - // for now we only support a TLS 1.3 client context; if we generalize this code we can expand - this->ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); - if (!this->ctx) { - char* errMsg = wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr); - throw std::runtime_error(fmt::format("Failed to allocate WolfSSL TLS context: {}", errMsg)); +#endif } - this->cm = wolfSSL_CTX_GetCertManager(ctx); - // set up the supported ciphers - std::string delimiter = ":"; - std::string joinedCiphers; + iggy::ssl::SSLContext::SSLContext(const SSLOptions& options, const PKIEnvironment& pkiEnv) + : options(options) + , pkiEnv(pkiEnv) { + // before we make any other wolfSSL calls, make sure library is initialized once and only once + std::call_once(sslInitDone, []() { wolfSSL_Init(); }); + + // for now we only support a TLS 1.3 client context; if we generalize this code we can expand + this->ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + if (!this->ctx) { + char* errMsg = wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr); + throw std::runtime_error(fmt::format("Failed to allocate WolfSSL TLS context: {}", errMsg)); + } + this->cm = wolfSSL_CTX_GetCertManager(ctx); + + // set up the supported ciphers + std::string delimiter = ":"; + std::string joinedCiphers; - auto supportedCiphers = options.getCiphers(); - if (!supportedCiphers.empty()) { - joinedCiphers = std::accumulate(std::next(supportedCiphers.begin()), supportedCiphers.end(), supportedCiphers[0], - [delimiter](std::string a, std::string b) { return a + delimiter + b; }); + auto supportedCiphers = options.getCiphers(); + if (!supportedCiphers.empty()) { + joinedCiphers = std::accumulate(std::next(supportedCiphers.begin()), supportedCiphers.end(), supportedCiphers[0], + [delimiter](std::string a, std::string b) { return a + delimiter + b; }); + } + int ret = wolfSSL_CTX_set_cipher_list(this->ctx, joinedCiphers.c_str()); + if (ret != SSL_SUCCESS) { + char* errMsg = wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr); + throw std::runtime_error(fmt::format("Failed to set cipher list: {}", errMsg)); + } } - int ret = wolfSSL_CTX_set_cipher_list(this->ctx, joinedCiphers.c_str()); - if (ret != SSL_SUCCESS) { - char* errMsg = wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr); - throw std::runtime_error(fmt::format("Failed to set cipher list: {}", errMsg)); + + iggy::ssl::SSLContext::SSLContext(const SSLContext& other) + : options(other.options) + , pkiEnv(other.pkiEnv) { + this->ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + this->cm = wolfSSL_CTX_GetCertManager(ctx); } -} - -iggy::ssl::SSLContext::SSLContext(const SSLContext& other) - : options(other.options) - , pkiEnv(other.pkiEnv) { - this->ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); - this->cm = wolfSSL_CTX_GetCertManager(ctx); -} - -iggy::ssl::SSLContext::SSLContext(SSLContext&& other) - : options(other.options) - , pkiEnv(other.pkiEnv) { - this->ctx = other.ctx; - this->cm = other.cm; - other.ctx = nullptr; - other.cm = nullptr; -} - -iggy::ssl::SSLContext::~SSLContext() { - if (this->ctx) { - wolfSSL_CTX_free(this->ctx); + + iggy::ssl::SSLContext::SSLContext(SSLContext && other) + : options(other.options) + , pkiEnv(other.pkiEnv) { + this->ctx = other.ctx; + this->cm = other.cm; + other.ctx = nullptr; + other.cm = nullptr; } -} -iggy::ssl::SSLContext& iggy::ssl::SSLContext::operator=(const iggy::ssl::SSLContext& other) { - if (this != &other) { + iggy::ssl::SSLContext::~SSLContext() { if (this->ctx) { wolfSSL_CTX_free(this->ctx); } - this->ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); } - return *this; -} -iggy::ssl::SSLContext& iggy::ssl::SSLContext::operator=(SSLContext&& other) { - if (this != &other) { - if (this->ctx) { - wolfSSL_CTX_free(this->ctx); + iggy::ssl::SSLContext& iggy::ssl::SSLContext::operator=(const iggy::ssl::SSLContext& other) { + if (this != &other) { + if (this->ctx) { + wolfSSL_CTX_free(this->ctx); + } + this->ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); } - this->ctx = other.ctx; - other.ctx = nullptr; + return *this; + } + + iggy::ssl::SSLContext& iggy::ssl::SSLContext::operator=(SSLContext&& other) { + if (this != &other) { + if (this->ctx) { + wolfSSL_CTX_free(this->ctx); + } + this->ctx = other.ctx; + other.ctx = nullptr; + } + return *this; } - return *this; -} + + std::string iggy::ssl::getProtocolVersionName(iggy::ssl::ProtocolVersion protocolVersion) { + switch (protocolVersion) { + case iggy::ssl::ProtocolVersion::TLSV1_3: + return "TLSV1_3"; + case iggy::ssl::ProtocolVersion::TLSV1_2: + return "TLSV1_2"; + default: + int protocolVersionInt = static_cast(protocolVersion); + throw std::runtime_error(fmt::format("Unsupported protocol version code: {}", protocolVersionInt)); + } + } + + std::once_flag iggy::ssl::SSLContext::sslInitDone = std::once_flag(); diff --git a/sdk/net/crypto/ssl.h b/sdk/net/crypto/ssl.h index 4b7e2da..97085f8 100644 --- a/sdk/net/crypto/ssl.h +++ b/sdk/net/crypto/ssl.h @@ -22,7 +22,12 @@ enum PeerType { CLIENT, SERVER }; * We do not support the older, less-secure variations since the expectation is the library will be used in a controlled client-server * environment where the developer can ensure the server endpoint is adequately hardened. */ -enum ProtocolVersion { SSLV3 = 0, TLSV1_2 = 1, TLSV1_3 = 2 }; +enum ProtocolVersion { TLSV1_2 = 0, TLSV1_3 = 1 }; + +/** + * @brief Helper function to get protocol version name given the enum. + */ +std::string getProtocolVersionName(iggy::ssl::ProtocolVersion protocolVersion); /** * @brief All options related to SSL/TLS are in -- what ciphers to use, client vs. server, etc.. @@ -34,8 +39,8 @@ class SSLOptions { private: std::optional peerCertPath = std::nullopt; PeerType peerType = PeerType::CLIENT; - ProtocolVersion minimumSupportedProtocolVersion = TLSV1_3; - std::vector ciphers = getDefaultCipherList(); + ProtocolVersion minimumSupportedProtocolVersion = ProtocolVersion::TLSV1_3; + std::vector ciphers = getDefaultCipherList(ProtocolVersion::TLSV1_3); public: /** @@ -47,7 +52,7 @@ class SSLOptions { * @brief Gets the default cipher list for use in SSL/TLS contexts. * @return A vector of cipher strings, all uppercase. */ - static const std::vector getDefaultCipherList(); + static const std::vector getDefaultCipherList(ProtocolVersion protocolVersion); /** * @brief Gets the list of requested supported ciphers; will be validated by the context during init. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fe76e09..022dd82 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,16 @@ if(BUILD_TESTS) client_test.cc iggy_protocol_provider_test.cc model_test.cc + ssl_test.cc + ) + target_compile_features(iggy_cpp_test PRIVATE cxx_std_17) + target_include_directories(iggy_cpp_test PRIVATE + ${SODIUM_INCLUDE_DIR} + ${ADA_INCLUDE_DIR} + ${WOLFSSL_INCLUDE_DIR} + ${NGHTTP3_INCLUDE_DIR} + ${NGTCP2_INCLUDE_DIR} + ${CURL_INCLUDE_DIR} ) target_link_libraries( iggy_cpp_test diff --git a/tests/client_test.cc b/tests/client_test.cc index 652eccf..137512f 100644 --- a/tests/client_test.cc +++ b/tests/client_test.cc @@ -1,4 +1,3 @@ -#define CATCH_CONFIG_MAIN #include "../sdk/client.h" #include "unit_testutils.h" diff --git a/tests/model_test.cc b/tests/model_test.cc index 52abed9..7825143 100644 --- a/tests/model_test.cc +++ b/tests/model_test.cc @@ -1,4 +1,3 @@ -#define CATCH_CONFIG_MAIN #include "../sdk/model.h" #include "unit_testutils.h" diff --git a/tests/ssl_test.cc b/tests/ssl_test.cc new file mode 100644 index 0000000..5c5561f --- /dev/null +++ b/tests/ssl_test.cc @@ -0,0 +1,16 @@ +#include "../sdk/net/crypto/ssl.h" +#include "unit_testutils.h" + +TEST_CASE("SSL configuration", UT_TAG) { + iggy::ssl::SSLOptions options; + auto cipherListTLSV1_2 = options.getDefaultCipherList(iggy::ssl::ProtocolVersion::TLSV1_2); + auto cipherListTLSV1_3 = options.getDefaultCipherList(iggy::ssl::ProtocolVersion::TLSV1_3); + + REQUIRE(cipherListTLSV1_2.size() == 6); + REQUIRE(cipherListTLSV1_3.size() == 3); + + std::string tls12Cipher = "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; + std::string tls13Cipher = "TLS_CHACHA20_POLY1305_SHA256"; + CHECK(std::find(cipherListTLSV1_2.begin(), cipherListTLSV1_2.end(), tls12Cipher) != cipherListTLSV1_2.end()); + CHECK(std::find(cipherListTLSV1_3.begin(), cipherListTLSV1_3.end(), tls13Cipher) != cipherListTLSV1_3.end()); +}