diff --git a/CMakeLists.txt b/CMakeLists.txt index df6ace8ac..4c092cb19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ target_link_libraries( Boost::charconv Boost::compat Boost::config + Boost::container Boost::core Boost::describe Boost::endian diff --git a/README.md b/README.md index cf76f4c80..0d77d98fc 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ And with the following databases: of executing a prepared statement is sent in binary format rather than in text. - Stored procedures. - Authentication methods (authentication plugins): mysql_native_password and - caching_sha2_password. These are the default methods in MySQL 5 and MySQL 8, + caching_sha2_password. These are the default methods in MySQL 5/MariaDB and MySQL 8, respectively. - Encrypted connections (TLS). - TCP and UNIX socket transports. diff --git a/build.jam b/build.jam index a7af9a2bf..a560ff210 100644 --- a/build.jam +++ b/build.jam @@ -11,6 +11,7 @@ constant boost_dependencies : /boost/charconv//boost_charconv /boost/compat//boost_compat /boost/config//boost_config + /boost/container//boost_container /boost/core//boost_core /boost/describe//boost_describe /boost/endian//boost_endian diff --git a/doc/qbk/05_connection_establishment.qbk b/doc/qbk/05_connection_establishment.qbk index 1c3ac2afc..cd812cfa5 100644 --- a/doc/qbk/05_connection_establishment.qbk +++ b/doc/qbk/05_connection_establishment.qbk @@ -41,8 +41,9 @@ This library implements the two most common authentication plugins: connections. It sends the password hashed, salted by a nonce. * [mysqllink caching-sha2-pluggable-authentication.html `caching_sha2_password`]. This is the default plugin for - MySQL 8.0+. It can only be used over secure transports, - like TCP with TLS or UNIX sockets. + MySQL 8.0+. This plugin used to require using TLS. + Since Boost 1.89, this is no longer the case, and it can be used + with plaintext connections, too. Multi-factor authentication is not yet supported. diff --git a/include/boost/mysql/client_errc.hpp b/include/boost/mysql/client_errc.hpp index dc453b257..91b59bdbe 100644 --- a/include/boost/mysql/client_errc.hpp +++ b/include/boost/mysql/client_errc.hpp @@ -50,7 +50,10 @@ enum class client_errc : int /// The user employs an authentication plugin not known to this library. unknown_auth_plugin, - /// The authentication plugin requires the connection to use SSL. + /** + * \brief (Legacy) The authentication plugin requires the connection to use SSL. + * This code is no longer used, since all supported plugins support plaintext connections. + */ auth_plugin_requires_ssl, /** @@ -169,6 +172,9 @@ enum class client_errc : int * (protocol violation). */ bad_handshake_packet_type, + + /// An OpenSSL function failed and did not provide any extra diagnostics. + unknown_openssl_error, }; BOOST_MYSQL_DECL diff --git a/include/boost/mysql/impl/error_categories.ipp b/include/boost/mysql/impl/error_categories.ipp index ebef09fc4..9e5ea884b 100644 --- a/include/boost/mysql/impl/error_categories.ipp +++ b/include/boost/mysql/impl/error_categories.ipp @@ -103,6 +103,8 @@ inline const char* error_to_string(client_errc error) case client_errc::bad_handshake_packet_type: return "During handshake, the server sent a packet type that is not allowed in the current state " "(protocol violation)."; + case client_errc::unknown_openssl_error: + return "An OpenSSL function failed and did not provide any extra diagnostics."; default: return ""; } } diff --git a/include/boost/mysql/impl/internal/sansio/auth_plugin_common.hpp b/include/boost/mysql/impl/internal/sansio/auth_plugin_common.hpp new file mode 100644 index 000000000..9a06df2bc --- /dev/null +++ b/include/boost/mysql/impl/internal/sansio/auth_plugin_common.hpp @@ -0,0 +1,29 @@ +// +// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_AUTH_PLUGIN_COMMON_HPP +#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_AUTH_PLUGIN_COMMON_HPP + +#include + +#include + +namespace boost { +namespace mysql { +namespace detail { + +// All scrambles in all the plugins we know have this size +BOOST_INLINE_CONSTEXPR std::size_t scramble_size = 20u; + +// Hashed passwords vary in size, but they all fit in a buffer like this +BOOST_INLINE_CONSTEXPR std::size_t max_hash_size = 32u; + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/impl/internal/sansio/caching_sha2_password.hpp b/include/boost/mysql/impl/internal/sansio/caching_sha2_password.hpp index d7416b6b4..8d9a55539 100644 --- a/include/boost/mysql/impl/internal/sansio/caching_sha2_password.hpp +++ b/include/boost/mysql/impl/internal/sansio/caching_sha2_password.hpp @@ -18,12 +18,18 @@ #include #include #include +#include #include +#include +#include +#include #include #include +#include #include +#include #include #include @@ -35,59 +41,53 @@ namespace mysql { namespace detail { // Constants -BOOST_INLINE_CONSTEXPR std::size_t csha2p_challenge_length = 20; -BOOST_INLINE_CONSTEXPR std::size_t csha2p_response_length = 32; +BOOST_INLINE_CONSTEXPR std::size_t csha2p_hash_size = 32; BOOST_INLINE_CONSTEXPR const char* csha2p_plugin_name = "caching_sha2_password"; +static_assert(csha2p_hash_size <= max_hash_size, ""); +static_assert(csha2p_hash_size == SHA256_DIGEST_LENGTH, "Buffer size mismatch"); inline void csha2p_hash_password_impl( string_view password, - span challenge, - span output + span scramble, + span output ) { - static_assert(csha2p_response_length == SHA256_DIGEST_LENGTH, "Buffer size mismatch"); - - // SHA(SHA(password_sha) concat challenge) XOR password_sha + // SHA(SHA(password_sha) concat scramble) XOR password_sha // hash1 = SHA(pass) - std::array password_sha; + std::array password_sha; SHA256(reinterpret_cast(password.data()), password.size(), password_sha.data()); - // SHA(password_sha) concat challenge = buffer - std::array buffer; + // SHA(password_sha) concat scramble = buffer + std::array buffer; SHA256(password_sha.data(), password_sha.size(), buffer.data()); - std::memcpy(buffer.data() + csha2p_response_length, challenge.data(), csha2p_challenge_length); + std::memcpy(buffer.data() + csha2p_hash_size, scramble.data(), scramble.size()); - // SHA(SHA(password_sha) concat challenge) = SHA(buffer) = salted_password - std::array salted_password; + // SHA(SHA(password_sha) concat scramble) = SHA(buffer) = salted_password + std::array salted_password; SHA256(buffer.data(), buffer.size(), salted_password.data()); // salted_password XOR password_sha - for (unsigned i = 0; i < csha2p_response_length; ++i) + for (unsigned i = 0; i < csha2p_hash_size; ++i) { output[i] = salted_password[i] ^ password_sha[i]; } } -inline system::result> csha2p_hash_password( +inline static_buffer csha2p_hash_password( string_view password, - span challenge + span scramble ) { - // If the challenge doesn't match the expected size, - // something wrong is going on and we should fail - if (challenge.size() != csha2p_challenge_length) - return client_errc::protocol_value_error; - // Empty passwords are not hashed if (password.empty()) return {}; // Run the algorithm - static_buffer<32> res(csha2p_response_length); + static_buffer res(csha2p_hash_size); csha2p_hash_password_impl( password, - span(challenge), - span(res.data(), csha2p_response_length) + scramble, + span(res.data(), csha2p_hash_size) ); return res; } @@ -106,6 +106,24 @@ class csha2p_algo return server_data.size() == 1u && server_data[0] == 3; } + static next_action encrypt_password( + connection_state_data& st, + std::uint8_t& seqnum, + string_view password, + span scramble, + span server_key + ) + { + container::small_vector buff; + auto ec = csha2p_encrypt_password(password, scramble, server_key, buff, asio::error::ssl_category); + if (ec) + return ec; + return st.write( + string_eof{string_view(reinterpret_cast(buff.data()), buff.size())}, + seqnum + ); + } + public: csha2p_algo() = default; @@ -113,6 +131,7 @@ class csha2p_algo connection_state_data& st, span server_data, string_view password, + span scramble, bool secure_channel, std::uint8_t& seqnum ) @@ -124,19 +143,34 @@ class csha2p_algo // or told us to read again because an OK packet or error packet is coming. if (is_perform_full_auth(server_data)) { - // At this point, we don't support full auth over insecure channels - if (!secure_channel) + if (secure_channel) { - return make_error_code(client_errc::auth_plugin_requires_ssl); - } + // We should send a packet with just the password, as a NULL-terminated string + BOOST_MYSQL_YIELD(resume_point_, 1, st.write(string_null{password}, seqnum)) - // We should send a packet with just the password, as a NULL-terminated string - BOOST_MYSQL_YIELD(resume_point_, 1, st.write(string_null{password}, seqnum)) + // The server shouldn't send us any more packets + return error_code(client_errc::bad_handshake_packet_type); + } + else + { + // Request the server's public key + BOOST_MYSQL_YIELD(resume_point_, 2, st.write(int1{2}, seqnum)) + + // Encrypt the password with the key we were given + BOOST_MYSQL_YIELD( + resume_point_, + 3, + encrypt_password(st, seqnum, password, scramble, server_data) + ) + + // The server shouldn't send us any more packets + return error_code(client_errc::bad_handshake_packet_type); + } } else if (is_fast_auth_ok(server_data)) { // We should wait for the server to send an OK or an error - BOOST_MYSQL_YIELD(resume_point_, 2, st.read(seqnum)) + BOOST_MYSQL_YIELD(resume_point_, 4, st.read(seqnum)) } else { diff --git a/include/boost/mysql/impl/internal/sansio/csha2p_encrypt_password.hpp b/include/boost/mysql/impl/internal/sansio/csha2p_encrypt_password.hpp new file mode 100644 index 000000000..2a96859ec --- /dev/null +++ b/include/boost/mysql/impl/internal/sansio/csha2p_encrypt_password.hpp @@ -0,0 +1,199 @@ +// +// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CSHA2P_ENCRYPT_PASSWORD_HPP +#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CSHA2P_ENCRYPT_PASSWORD_HPP + +// Having this in a separate file allows us to mock the OpenSSL API in the tests + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace mysql { +namespace detail { + +// The OpenSSL category is passed as parameter to avoid including asio/ssl headers here. +// Doing so would make mocking OpenSSL more difficult (more functions used) +inline error_code translate_openssl_error( + unsigned long code, + const source_location* loc, + const system::error_category& openssl_category +) +{ + // If ERR_SYSTEM_ERROR is true, the error code is a system error. + // This function only exists since OpenSSL 3 +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + if (ERR_SYSTEM_ERROR(code)) + { + return error_code(ERR_GET_REASON(code), system::system_category(), loc); + } +#endif + + // In OpenSSL < 3, error codes > 0x80000000 are reserved for the user, + // so it's unlikely that we will encounter these here. Overflow here + // is implementation-defined behavior (and not UB), so we're fine. + // This is what Asio does, anyway. + int int_code = static_cast(code); + + // An error code of zero would mean success, while this function is always + // called because an OpenSSL primitive failed. It might indicate that OpenSSL + // did not provide any extra error information. But it should still be an error + if (int_code == 0) + return error_code(client_errc::unknown_openssl_error, loc); + else + return error_code(int_code, openssl_category, loc); +} + +inline container::small_vector csha2p_salt_password( + string_view password, + span scramble +) +{ + // Salt the password, as a NULL-terminated string + container::small_vector res(password.size() + 1u, 0); + for (std::size_t i = 0; i < password.size(); ++i) + res[i] = password[i] ^ scramble[i % scramble.size()]; + + // Add the NULL terminator. It should be salted, too. Since 0 ^ U = U, + // the byte should be the scramble at the position we're in + res[password.size()] = scramble[password.size() % scramble.size()]; + + return res; +} + +inline error_code csha2p_encrypt_password( + string_view password, + span scramble, + span server_key, + container::small_vector& output, + const system::error_category& openssl_category +) +{ + // RAII helpers + struct bio_deleter + { + void operator()(BIO* bio) const noexcept { BIO_free(bio); } + }; + using unique_bio = std::unique_ptr; + + struct evp_pkey_deleter + { + void operator()(EVP_PKEY* pkey) const noexcept { EVP_PKEY_free(pkey); } + }; + using unique_evp_pkey = std::unique_ptr; + + struct evp_pkey_ctx_deleter + { + void operator()(EVP_PKEY_CTX* ctx) const noexcept { EVP_PKEY_CTX_free(ctx); } + }; + using unique_evp_pkey_ctx = std::unique_ptr; + + // Apply a sanity check to the key buffer size + constexpr std::size_t max_key_buffer_size = 1024u * 1024u; // 1MB + if (server_key.size() > max_key_buffer_size) + { + static constexpr auto loc = BOOST_CURRENT_LOCATION; + return error_code(client_errc::protocol_value_error, &loc); + } + + // Try to parse the private key + unique_bio bio{BIO_new_mem_buf(server_key.data(), static_cast(server_key.size()))}; + if (!bio) + { + static constexpr auto loc = BOOST_CURRENT_LOCATION; + return translate_openssl_error(ERR_get_error(), &loc, openssl_category); + } + unique_evp_pkey key(PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr)); + if (!key) + { + static constexpr auto loc = BOOST_CURRENT_LOCATION; + return translate_openssl_error(ERR_get_error(), &loc, openssl_category); + } + + // Salt the password + auto salted_password = csha2p_salt_password(password, scramble); + + // Set up the encryption context + unique_evp_pkey_ctx ctx(EVP_PKEY_CTX_new(key.get(), nullptr)); + if (!ctx) + { + static constexpr auto loc = BOOST_CURRENT_LOCATION; + return translate_openssl_error(ERR_get_error(), &loc, openssl_category); + } + if (EVP_PKEY_encrypt_init(ctx.get()) <= 0) + { + static constexpr auto loc = BOOST_CURRENT_LOCATION; + return translate_openssl_error(ERR_get_error(), &loc, openssl_category); + } + int rsa_pad_res = EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING); + if (rsa_pad_res <= 0) + { + // If the server passed us a key type that does not support encryption, + // OpenSSL returns -2 and does not add an error to the stack (ERR_get_error returns 0). + // This shouldn't happen with real servers, so we re-use an existing error code and set + // the source location to allow diagnosis + static constexpr auto loc = BOOST_CURRENT_LOCATION; + if (rsa_pad_res == -2) + return error_code(client_errc::protocol_value_error, &loc); + else + return translate_openssl_error(ERR_get_error(), &loc, openssl_category); + } + + // Allocate a buffer for encryption + int max_size = EVP_PKEY_size(key.get()); + if (max_size <= 0) + { + static constexpr auto loc = BOOST_CURRENT_LOCATION; + return translate_openssl_error(ERR_get_error(), &loc, openssl_category); + } + output.resize(max_size); + + // Encrypt + std::size_t actual_size = static_cast(max_size); + if (EVP_PKEY_encrypt( + ctx.get(), + output.data(), + &actual_size, + salted_password.data(), + salted_password.size() + ) <= 0) + { + static constexpr auto loc = BOOST_CURRENT_LOCATION; + return translate_openssl_error(ERR_get_error(), &loc, openssl_category); + } + + // Adjust size + BOOST_ASSERT(actual_size <= output.size()); + output.resize(actual_size); + + // Done + return error_code(); +} + +} // namespace detail +} // namespace mysql +} // namespace boost + +#endif diff --git a/include/boost/mysql/impl/internal/sansio/handshake.hpp b/include/boost/mysql/impl/internal/sansio/handshake.hpp index 54feb5e19..a51347196 100644 --- a/include/boost/mysql/impl/internal/sansio/handshake.hpp +++ b/include/boost/mysql/impl/internal/sansio/handshake.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -34,12 +35,15 @@ #include #include +#include #include +#include namespace boost { namespace mysql { namespace detail { +// Stores which authentication plugin we're using, plus any required state. Variant-like class any_authentication_plugin { enum class type_t @@ -58,23 +62,19 @@ class any_authentication_plugin public: any_authentication_plugin() = default; - // Emplaces the plugin and computes the first authentication response by hashing the password - system::result> bootstrap_plugin( - string_view plugin_name, - string_view password, - span challenge - ) + // Emplaces a plugin of the type given by plugin_name. Errors on unknown plugin + error_code emplace_by_name(string_view plugin_name) { if (plugin_name == mnp_plugin_name) { type_ = type_t::mnp; - return mnp_hash_password(password, challenge); + return error_code(); } else if (plugin_name == csha2p_plugin_name) { type_ = type_t::csha2p; csha2p_ = csha2p_algo(); // Reset any leftover state, just in case - return csha2p_hash_password(password, challenge); + return error_code(); } else { @@ -82,10 +82,26 @@ class any_authentication_plugin } } + // Hashes the password with the selected plugin + static_buffer hash_password( + string_view password, + span scramble + ) const + { + switch (type_) + { + case type_t::mnp: return mnp_hash_password(password, scramble); + case type_t::csha2p: return csha2p_hash_password(password, scramble); + default: BOOST_ASSERT(false); return {}; // LCOV_EXCL_LINE + } + } + + // Invokes the plugin action. Use when a more_data packet is received. next_action resume( connection_state_data& st, - boost::span server_data, + span server_data, string_view password, + span scramble, bool secure_channel, std::uint8_t& seqnum ) @@ -95,7 +111,8 @@ class any_authentication_plugin case type_t::mnp: // This algorithm doesn't allow more data frames return error_code(client_errc::bad_handshake_packet_type); - case type_t::csha2p: return csha2p_.resume(st, server_data, password, secure_channel, seqnum); + case type_t::csha2p: + return csha2p_.resume(st, server_data, password, scramble, secure_channel, seqnum); default: BOOST_ASSERT(false); return next_action(client_errc::bad_handshake_packet_type); // LCOV_EXCL_LINE @@ -118,7 +135,7 @@ class handshake_algo int resume_point_{0}; handshake_params hparams_; any_authentication_plugin plugin_; - static_buffer<32> hashed_password_; + std::array scramble_; std::uint8_t sequence_number_{0}; bool secure_channel_{false}; @@ -198,6 +215,20 @@ class handshake_algo } } + // Saves the scramble, checking that it has the right size + error_code save_scramble(span value) + { + // All scrambles must have exactly this size. Otherwise, it's a protocol violation error + if (value.size() != scramble_size) + return client_errc::protocol_value_error; + + // Store the scramble + std::memcpy(scramble_.data(), value.data(), scramble_size); + + // Done + return error_code(); + } + error_code process_hello(connection_state_data& st, diagnostics& diag, span buffer) { // Deserialize server hello @@ -219,18 +250,14 @@ class handshake_algo // If we're using SSL, mark the channel as secure secure_channel_ = secure_channel_ || has_capabilities(*negotiated_caps, capabilities::ssl); - // Emplace the authentication plugin and compute the first response - auto hashed_password = plugin_.bootstrap_plugin( - hello.auth_plugin_name, - hparams_.password(), - hello.auth_plugin_data - ); - if (hashed_password.has_error()) - return hashed_password.error(); + // Save which authentication plugin we're using. Do this before saving the scramble, + // as an unknown plugin might have a scramble size different to what we know + err = plugin_.emplace_by_name(hello.auth_plugin_name); + if (err) + return err; - // Save it for later - hashed_password_ = *hashed_password; - return error_code(); + // Save the scramble for later + return save_scramble(hello.auth_plugin_data); } // Response to that initial greeting @@ -243,29 +270,41 @@ class handshake_algo }; } - login_request compose_login_request(const connection_state_data& st) + next_action serialize_login_request(connection_state_data& st) { - return login_request{ - st.current_capabilities, - static_cast(max_packet_size), - hparams_.connection_collation(), - hparams_.username(), - hashed_password_, - hparams_.database(), - plugin_.name(), - }; + auto hashed_password = plugin_.hash_password(hparams_.password(), scramble_); + return st.write( + login_request{ + st.current_capabilities, + static_cast(max_packet_size), + hparams_.connection_collation(), + hparams_.username(), + hashed_password, + hparams_.database(), + plugin_.name(), + }, + sequence_number_ + ); } // Processes auth_switch and auth_more_data messages, and leaves the result in auth_resp_ next_action process_auth_switch(connection_state_data& st, auth_switch msg) { - // Emplace the authentication plugin and compute the first response - auto hashed_password = plugin_.bootstrap_plugin(msg.plugin_name, hparams_.password(), msg.auth_data); - if (hashed_password.has_error()) - return hashed_password.error(); + // Emplace the new authentication plugin + auto ec = plugin_.emplace_by_name(msg.plugin_name); + if (ec) + return ec; + + // Store the scramble for later (required by caching_sha2_password, for instance) + ec = save_scramble(msg.auth_data); + if (ec) + return ec; + + // Hash the password + auto hashed_password = plugin_.hash_password(hparams_.password(), scramble_); // Serialize the response - return st.write(auth_switch_response{*hashed_password}, sequence_number_); + return st.write(auth_switch_response{hashed_password}, sequence_number_); } void on_success(connection_state_data& st, const ok_view& ok) @@ -312,7 +351,7 @@ class handshake_algo } // Compose and send handshake response - BOOST_MYSQL_YIELD(resume_point_, 4, st.write(compose_login_request(st), sequence_number_)) + BOOST_MYSQL_YIELD(resume_point_, 4, serialize_login_request(st)) // Receive the response BOOST_MYSQL_YIELD(resume_point_, 5, st.read(sequence_number_)) @@ -342,6 +381,7 @@ class handshake_algo st, resp.data.more_data, hparams_.password(), + scramble_, secure_channel_, sequence_number_ ); diff --git a/include/boost/mysql/impl/internal/sansio/mysql_native_password.hpp b/include/boost/mysql/impl/internal/sansio/mysql_native_password.hpp index 04f615647..0b2b13a42 100644 --- a/include/boost/mysql/impl/internal/sansio/mysql_native_password.hpp +++ b/include/boost/mysql/impl/internal/sansio/mysql_native_password.hpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -30,59 +31,49 @@ namespace mysql { namespace detail { // Constants -BOOST_INLINE_CONSTEXPR std::size_t mnp_challenge_length = 20; -BOOST_INLINE_CONSTEXPR std::size_t mnp_response_length = 20; +BOOST_INLINE_CONSTEXPR std::size_t mnp_hash_size = 20; BOOST_INLINE_CONSTEXPR const char* mnp_plugin_name = "mysql_native_password"; +static_assert(mnp_hash_size <= max_hash_size, ""); +static_assert(mnp_hash_size == SHA_DIGEST_LENGTH, "Buffer size mismatch"); // SHA1( password ) XOR SHA1( "20-bytes random data from server" SHA1( SHA1( password ) ) ) inline void mnp_hash_password_impl( string_view password, - span challenge, - span output + span scramble, + span output ) { - static_assert(mnp_response_length == SHA_DIGEST_LENGTH, "Buffer size mismatch"); - // SHA1 (password) - std::array password_sha1; + std::array password_sha1; SHA1(reinterpret_cast(password.data()), password.size(), password_sha1.data()); - // Add server challenge (salt) - std::array salted_buffer; - std::memcpy(salted_buffer.data(), challenge.data(), challenge.size()); - SHA1(password_sha1.data(), password_sha1.size(), salted_buffer.data() + mnp_challenge_length); - std::array salted_sha1; + // Add server scramble (salt) + std::array salted_buffer; + std::memcpy(salted_buffer.data(), scramble.data(), scramble.size()); + SHA1(password_sha1.data(), password_sha1.size(), salted_buffer.data() + mnp_hash_size); + std::array salted_sha1; SHA1(salted_buffer.data(), salted_buffer.size(), salted_sha1.data()); // XOR - for (std::size_t i = 0; i < SHA_DIGEST_LENGTH; ++i) + for (std::size_t i = 0; i < mnp_hash_size; ++i) { output[i] = password_sha1[i] ^ salted_sha1[i]; } } // The static buffer size is chosen so that every plugin uses the same size -inline system::result> mnp_hash_password( +inline static_buffer mnp_hash_password( string_view password, - span challenge + span scramble ) { - // If the challenge doesn't match the expected size, - // something wrong is going on and we should fail - if (challenge.size() != mnp_challenge_length) - return client_errc::protocol_value_error; - // Empty passwords are not hashed if (password.empty()) return {}; // Run the algorithm - static_buffer<32> res(mnp_response_length); - mnp_hash_password_impl( - password, - span(challenge), - span(res.data(), mnp_response_length) - ); + static_buffer res(mnp_hash_size); + mnp_hash_password_impl(password, scramble, span(res.data(), mnp_hash_size)); return res; } diff --git a/include/boost/mysql/impl/is_fatal_error.ipp b/include/boost/mysql/impl/is_fatal_error.ipp index 4256a0be6..d384e59ea 100644 --- a/include/boost/mysql/impl/is_fatal_error.ipp +++ b/include/boost/mysql/impl/is_fatal_error.ipp @@ -84,7 +84,8 @@ bool boost::mysql::is_fatal_error(error_code ec) noexcept case client_errc::unknown_auth_plugin: case client_errc::server_unsupported: case client_errc::auth_plugin_requires_ssl: - case client_errc::bad_handshake_packet_type: return true; + case client_errc::bad_handshake_packet_type: + case client_errc::unknown_openssl_error: return true; default: return false; } diff --git a/include/boost/mysql/impl/pfr.hpp b/include/boost/mysql/impl/pfr.hpp index bdacdae92..061086712 100644 --- a/include/boost/mysql/impl/pfr.hpp +++ b/include/boost/mysql/impl/pfr.hpp @@ -62,20 +62,32 @@ using pfr_fields_t = decltype(pfr::structure_to_tuple(std::declval())) template constexpr std::array to_name_table_storage(std::array input) noexcept { - std::array res; + std::array res{}; for (std::size_t i = 0; i < N; ++i) res[i] = input[i]; return res; } -// Workaround for https://github.com/boostorg/pfr/issues/165 -constexpr std::array to_name_table_storage(std::array) noexcept +template +constexpr inline std::size_t pfr_row_size_v = mp11::mp_size>::value; + +template +constexpr std::array> create_pfr_name_table() noexcept { - return {}; + // Some MSVC compilers have trouble with pfr::names_as_array() when + // the row type is empty + if constexpr (pfr_row_size_v == 0u) + { + return {}; + } + else + { + return to_name_table_storage(pfr::names_as_array()); + } } template -constexpr inline auto pfr_names_storage = to_name_table_storage(pfr::names_as_array()); +constexpr inline auto pfr_names_storage = create_pfr_name_table(); template class row_traits, false> diff --git a/test/integration/db_setup_sha256.sql b/test/integration/db_setup_sha256.sql index b6be0e504..2b61d5c7f 100644 --- a/test/integration/db_setup_sha256.sql +++ b/test/integration/db_setup_sha256.sql @@ -14,17 +14,25 @@ CREATE USER 'sha2p_user'@'%' IDENTIFIED WITH 'sha256_password'; ALTER USER 'sha2p_user'@'%' IDENTIFIED BY 'sha2p_password'; GRANT ALL PRIVILEGES ON boost_mysql_integtests.* TO 'sha2p_user'@'%'; --- User that uses caching_sha2_password plugin +-- User that uses the caching_sha2_password plugin DROP USER IF EXISTS 'csha2p_user'@'%'; CREATE USER 'csha2p_user'@'%' IDENTIFIED WITH 'caching_sha2_password'; ALTER USER 'csha2p_user'@'%' IDENTIFIED BY 'csha2p_password'; GRANT ALL PRIVILEGES ON boost_mysql_integtests.* TO 'csha2p_user'@'%'; --- User that uses caching_sha2_password plugin with an empty password +-- User that uses the caching_sha2_password plugin with an empty password DROP USER IF EXISTS 'csha2p_empty_password_user'@'%'; CREATE USER 'csha2p_empty_password_user'@'%' IDENTIFIED WITH 'caching_sha2_password'; ALTER USER 'csha2p_empty_password_user'@'%' IDENTIFIED BY ''; GRANT ALL PRIVILEGES ON boost_mysql_integtests.* TO 'csha2p_empty_password_user'@'%'; +-- User that uses the caching_sha2_password plugin with a password longer than the scramble +-- (mora than 20 characters). Relevant for the slow path without TLS +DROP USER IF EXISTS 'csha2p_long_password_user'@'%'; +CREATE USER 'csha2p_long_password_user'@'%' IDENTIFIED WITH 'caching_sha2_password'; +ALTER USER 'csha2p_long_password_user'@'%' IDENTIFIED BY '1234567890abcdefghijklmnopqrstuvwxyz'; +GRANT ALL PRIVILEGES ON boost_mysql_integtests.* TO 'csha2p_long_password_user'@'%'; + + FLUSH PRIVILEGES; diff --git a/test/integration/test/handshake.cpp b/test/integration/test/handshake.cpp index 7700b9685..186e63f23 100644 --- a/test/integration/test/handshake.cpp +++ b/test/integration/test/handshake.cpp @@ -65,10 +65,11 @@ struct transport_test_case }; std::ostream& operator<<(std::ostream& os, const transport_test_case& tc) { return os << tc.name; } -std::vector make_secure_transports() +std::vector make_all_transports() { std::vector res{ - {"tcp_ssl", connect_params_builder().ssl(ssl_mode::require).build(), true}, + {"tcp", connect_params_builder().ssl(ssl_mode::disable).build(), false}, + {"tcp_ssl", connect_params_builder().ssl(ssl_mode::require).build(), true }, }; #ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS @@ -81,14 +82,6 @@ std::vector make_secure_transports() return res; } -std::vector make_all_transports() -{ - auto res = make_secure_transports(); - res.push_back({"tcp", connect_params_builder().ssl(ssl_mode::disable).build(), false}); - return res; -} - -auto secure_transports = make_secure_transports(); auto all_transports = make_all_transports(); // Check whether the connection is using SSL or not @@ -165,10 +158,11 @@ BOOST_FIXTURE_TEST_CASE(tcp_connection_, tcp_connection_fixture) BOOST_AUTO_TEST_SUITE_END() // mysql_native_password -// caching_sha2_password. We acquire a named lock -// to avoid race conditions with other test runs +// caching_sha2_password +// https://dev.mysql.com/doc/refman/8.4/en/caching-sha2-pluggable-authentication.html +// The plugin has a server-wide cache that influences the message exchange. +// We acquire a named lock to avoid race conditions with other test runs // (which happens in b2 builds). -// The sha256 cache is shared between all clients. struct caching_sha2_lock : any_connection_fixture { caching_sha2_lock() @@ -216,7 +210,6 @@ static void clear_sha256_cache() fix.conn.async_execute("FLUSH PRIVILEGES", result, as_netresult).validate_no_error(); }; -// Cache hit means that we are sending the password hashed, so it is OK to not have SSL for this BOOST_DATA_TEST_CASE_F(any_connection_fixture, cache_hit, all_transports) { // Setup @@ -230,8 +223,7 @@ BOOST_DATA_TEST_CASE_F(any_connection_fixture, cache_hit, all_transports) check_ssl(conn, sample.expect_ssl); } -// Cache miss succeeds only if the underlying transport is secure -BOOST_DATA_TEST_CASE_F(any_connection_fixture, cache_miss_success, secure_transports) +BOOST_DATA_TEST_CASE_F(any_connection_fixture, cache_miss, all_transports) { // Setup connect_params params = sample.params; @@ -244,21 +236,7 @@ BOOST_DATA_TEST_CASE_F(any_connection_fixture, cache_miss_success, secure_transp check_ssl(conn, sample.expect_ssl); } -// A cache miss would force us send a plaintext password over a non-TLS connection, so we fail -BOOST_FIXTURE_TEST_CASE(cache_miss_error, any_connection_fixture) -{ - // Setup - connect_params params = connect_params_builder() - .ssl(ssl_mode::disable) - .credentials(regular_user, regular_passwd) - .build(); - clear_sha256_cache(); - - // Handshake fails - conn.async_connect(params, as_netresult).validate_error(client_errc::auth_plugin_requires_ssl); -} - -// Empty password users can log in regardless of the SSL usage or cache state +// The protocol behaves differently with empty passwords BOOST_DATA_TEST_CASE_F(any_connection_fixture, empty_password_cache_hit, all_transports) { // Setup @@ -285,11 +263,22 @@ BOOST_DATA_TEST_CASE_F(any_connection_fixture, empty_password_cache_miss, all_tr check_ssl(conn, sample.expect_ssl); } +// Passwords longer than the scramble work correctly. +// This is only relevant for cache misses over insecure channels. +BOOST_FIXTURE_TEST_CASE(long_password_cache_miss, any_connection_fixture) +{ + auto params = connect_params_builder() + .ssl(ssl_mode::disable) + .credentials("csha2p_long_password_user", "1234567890abcdefghijklmnopqrstuvwxyz") + .build(); + clear_sha256_cache(); + conn.async_connect(params, as_netresult).validate_no_error(); +} + BOOST_FIXTURE_TEST_CASE(bad_password_cache_hit, any_connection_fixture) { - // Note: test over non-TLS would return "ssl required" auto params = connect_params_builder() - .ssl(ssl_mode::require) + .ssl(ssl_mode::disable) .credentials(regular_user, "bad_password") .build(); load_sha256_cache(regular_user, regular_passwd); @@ -299,9 +288,8 @@ BOOST_FIXTURE_TEST_CASE(bad_password_cache_hit, any_connection_fixture) BOOST_FIXTURE_TEST_CASE(bad_password_cache_miss, any_connection_fixture) { - // Note: test over non-TLS would return "ssl required" auto params = connect_params_builder() - .ssl(ssl_mode::require) + .ssl(ssl_mode::disable) .credentials(regular_user, "bad_password") .build(); clear_sha256_cache(); @@ -314,7 +302,7 @@ BOOST_FIXTURE_TEST_CASE(bad_db_cache_miss, any_connection_fixture) { // Setup auto params = connect_params_builder() - .ssl(ssl_mode::require) + .ssl(ssl_mode::disable) .credentials(regular_user, regular_passwd) .database("bad_db") .build(); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index d65841353..85bcf8d6a 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable( test/sansio/handshake/handshake_mnp_hash_password.cpp test/sansio/handshake/handshake_csha2p.cpp test/sansio/handshake/handshake_csha2p_hash_password.cpp + test/sansio/handshake/handshake_csha2p_encrypt_password.cpp test/sansio/handshake/handshake_capabilities.cpp test/sansio/handshake/handshake_connection_state_data.cpp test/sansio/read_resultset_head.cpp @@ -154,3 +155,36 @@ add_test( NAME boost_mysql_unittests COMMAND boost_mysql_unittests ) + +# Tests edge cases when encrypting passwords that require mocking OpenSSL. +# This executable should not link to any OpenSSL libraries. +add_executable( + boost_mysql_test_csha2p_encrypt_password_errors + test_csha2p_encrypt_password_errors.cpp +) +target_include_directories( + boost_mysql_test_csha2p_encrypt_password_errors + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../include # our own includes + ${OPENSSL_INCLUDE_DIR} # we only need the includes +) +target_link_libraries( + boost_mysql_test_csha2p_encrypt_password_errors + PRIVATE + Boost::unit_test_framework + Boost::assert + Boost::config + Boost::container + Boost::core + Boost::system +) +target_compile_features( + boost_mysql_test_csha2p_encrypt_password_errors + INTERFACE + cxx_std_11 +) +boost_mysql_test_target_settings(boost_mysql_test_csha2p_encrypt_password_errors) +add_test( + NAME boost_mysql_test_csha2p_encrypt_password_errors + COMMAND boost_mysql_test_csha2p_encrypt_password_errors +) diff --git a/test/unit/Jamfile b/test/unit/Jamfile index dfd463fc6..cc989d47f 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -43,6 +43,7 @@ run test/sansio/handshake/handshake_mnp_hash_password.cpp test/sansio/handshake/handshake_csha2p.cpp test/sansio/handshake/handshake_csha2p_hash_password.cpp + test/sansio/handshake/handshake_csha2p_encrypt_password.cpp test/sansio/handshake/handshake_capabilities.cpp test/sansio/handshake/handshake_connection_state_data.cpp test/sansio/read_resultset_head.cpp @@ -144,3 +145,11 @@ run include : target-name boost_mysql_unittests ; + + +# Tests edge cases when encrypting passwords that require mocking OpenSSL. +# It looks like this works even if boost_mysql links to the SSL libraries. +run test_csha2p_encrypt_password_errors.cpp + /boost/mysql/test//boost_mysql + /boost/test//boost_unit_test_framework/off +; diff --git a/test/unit/include/test_unit/algo_test.hpp b/test/unit/include/test_unit/algo_test.hpp index cd6badae2..7952cd091 100644 --- a/test/unit/include/test_unit/algo_test.hpp +++ b/test/unit/include/test_unit/algo_test.hpp @@ -70,6 +70,7 @@ class BOOST_ATTRIBUTE_NODISCARD algo_test detail::next_action_type type; std::vector bytes; error_code result; + bool check; }; std::vector steps_; @@ -98,7 +99,12 @@ class BOOST_ATTRIBUTE_NODISCARD algo_test std::size_t num_steps_to_run ) const; - algo_test& add_step(detail::next_action_type act_type, std::vector bytes, error_code ec); + algo_test& add_step( + detail::next_action_type act_type, + std::vector bytes, + error_code ec, + bool check = true + ); void check_impl( any_algo_ref algo, @@ -126,6 +132,12 @@ class BOOST_ATTRIBUTE_NODISCARD algo_test return add_step(detail::next_action_type::write, std::move(bytes), result); } + BOOST_ATTRIBUTE_NODISCARD + algo_test& expect_any_write(error_code result = {}) + { + return add_step(detail::next_action_type::write, {}, result, false); + } + BOOST_ATTRIBUTE_NODISCARD algo_test& expect_read(std::vector result_bytes) { @@ -226,7 +238,7 @@ class BOOST_ATTRIBUTE_NODISCARD algo_test struct algo_fixture_base { - static constexpr std::size_t default_max_buffsize = 1024u; + static constexpr std::size_t default_max_buffsize = 4u * 1024u * 1024u; detail::connection_state_data st; diff --git a/test/unit/src/utils.cpp b/test/unit/src/utils.cpp index c483d7416..5bb25ae97 100644 --- a/test/unit/src/utils.cpp +++ b/test/unit/src/utils.cpp @@ -113,7 +113,10 @@ detail::next_action boost::mysql::test::algo_test::run_algo_until_step( if (step.type == detail::next_action_type::read) handle_read(st, step); else if (step.type == detail::next_action_type::write) - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, step.bytes); + { + if (step.check) + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, step.bytes); + } // Other actions don't need any handling act = algo.resume(st, diag, step.result); @@ -126,10 +129,11 @@ detail::next_action boost::mysql::test::algo_test::run_algo_until_step( boost::mysql::test::algo_test& boost::mysql::test::algo_test::add_step( detail::next_action_type act_type, std::vector bytes, - error_code ec + error_code ec, + bool check ) { - steps_.push_back(step_t{act_type, std::move(bytes), ec}); + steps_.push_back(step_t{act_type, std::move(bytes), ec, check}); return *this; } diff --git a/test/unit/test/client_errc.cpp b/test/unit/test/client_errc.cpp index cc62cf4d3..7310bc2e6 100644 --- a/test/unit/test/client_errc.cpp +++ b/test/unit/test/client_errc.cpp @@ -33,7 +33,7 @@ BOOST_AUTO_TEST_CASE(coverage) { // Check that no value causes problems. // Ensure that all branches of the switch/case are covered - for (int i = 1; i <= 30; ++i) + for (int i = 1; i <= 31; ++i) { BOOST_TEST_CONTEXT(i) { diff --git a/test/unit/test/pfr.cpp b/test/unit/test/pfr.cpp index a8e7a156e..05cf263eb 100644 --- a/test/unit/test/pfr.cpp +++ b/test/unit/test/pfr.cpp @@ -19,6 +19,7 @@ using namespace boost::mysql; using namespace boost::mysql::test; +using boost::test_tools::per_element; using detail::get_row_name_table; using detail::get_row_size; using detail::is_pfr_reflectable; @@ -90,21 +91,14 @@ static_assert(get_row_size>() == 3u, ""); static_assert(get_row_size>() == 1u, ""); // name table -static void compare_name_tables(name_table_t lhs, name_table_t rhs) -{ - std::vector lhsvec(lhs.begin(), lhs.end()); - std::vector rhsvec(rhs.begin(), rhs.end()); - BOOST_TEST(lhsvec == rhsvec); -} - BOOST_AUTO_TEST_CASE(get_row_name_table_) { const string_view expected_s1[] = {"i", "f", "double_field"}; const string_view expected_s2[] = {"s"}; - compare_name_tables(get_row_name_table>(), name_table_t()); - compare_name_tables(get_row_name_table>(), expected_s1); - compare_name_tables(get_row_name_table>(), expected_s2); + BOOST_TEST(get_row_name_table>() == name_table_t(), per_element()); + BOOST_TEST(get_row_name_table>() == expected_s1, per_element()); + BOOST_TEST(get_row_name_table>() == expected_s2, per_element()); } // meta check diff --git a/test/unit/test/sansio/handshake/handshake.cpp b/test/unit/test/sansio/handshake/handshake.cpp index b3455f99e..9a7b23b5e 100644 --- a/test/unit/test/sansio/handshake/handshake.cpp +++ b/test/unit/test/sansio/handshake/handshake.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include "handshake_common.hpp" #include "test_common/create_diagnostics.hpp" #include "test_unit/create_err.hpp" @@ -64,7 +66,22 @@ BOOST_AUTO_TEST_CASE(hello_unknown_plugin) // Run the test algo_test() - .expect_read(server_hello_builder().auth_plugin("unknown").auth_data(csha2p_challenge).build()) + .expect_read(server_hello_builder().auth_plugin("unknown").auth_data(csha2p_scramble).build()) + .check(fix, client_errc::unknown_auth_plugin); +} + +// If the plugin is unknown, the scramble size might be different +BOOST_AUTO_TEST_CASE(hello_unknown_plugin_different_scramble_size) +{ + // Setup + handshake_fixture fix; + + // Run the test + algo_test() + .expect_read(server_hello_builder() + .auth_plugin("unknown") + .auth_data(std::vector(30, 0xab)) + .build()) .check(fix, client_errc::unknown_auth_plugin); } @@ -80,9 +97,8 @@ BOOST_AUTO_TEST_CASE(initial_response_error_flavor) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).version("11.4.2-MariaDB-ubu2404").build() - ) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).version("11.4.2-MariaDB-ubu2404").build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read( err_builder().seqnum(2).code(mariadb_server_errc::er_bad_data).message("bad data").build_frame() ) @@ -107,12 +123,11 @@ BOOST_AUTO_TEST_CASE(authswitch_hash_password_error) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_auth_switch_frame(2, "mysql_native_password", std::vector(21, 0x0a)) ) .check(fix, client_errc::protocol_value_error); @@ -127,14 +142,13 @@ BOOST_AUTO_TEST_CASE(authswitch_error) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_challenge)) - .expect_write(create_frame(3, mnp_response)) + .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_scramble)) + .expect_write(create_frame(3, mnp_hash)) .expect_read(err_builder() .seqnum(4) .code(common_server_errc::er_access_denied_error) @@ -154,14 +168,13 @@ BOOST_AUTO_TEST_CASE(authswitch_error_flavor) .expect_read(server_hello_builder() .version("11.4.2-MariaDB-ubu2404") .auth_plugin("caching_sha2_password") - .auth_data(csha2p_challenge) + .auth_data(csha2p_scramble) .build()) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_challenge)) - .expect_write(create_frame(3, mnp_response)) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_scramble)) + .expect_write(create_frame(3, mnp_hash)) .expect_read( err_builder().seqnum(4).code(mariadb_server_errc::er_bad_data).message("Denied").build_frame() ) @@ -181,15 +194,14 @@ BOOST_AUTO_TEST_CASE(authswitch_authswitch) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_challenge)) - .expect_write(create_frame(3, mnp_response)) - .expect_read(create_auth_switch_frame(4, "mysql_native_password", mnp_challenge)) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_scramble)) + .expect_write(create_frame(3, mnp_hash)) + .expect_read(create_auth_switch_frame(4, "mysql_native_password", mnp_scramble)) .check(fix, client_errc::bad_handshake_packet_type); } @@ -202,13 +214,30 @@ BOOST_AUTO_TEST_CASE(authswitch_unknown_plugin) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_auth_switch_frame(2, "unknown", mnp_scramble)) + .check(fix, client_errc::unknown_auth_plugin); +} + +// If the plugin is unknown, the scramble may have a different size +BOOST_AUTO_TEST_CASE(authswitch_unknown_plugin_different_scramble_size) +{ + // Setup + handshake_fixture fix; + + // Run the test + algo_test() + .expect_read( + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_auth_switch_frame(2, "unknown", mnp_challenge)) + .expect_read(create_auth_switch_frame(2, "unknown", std::vector(30, 0xab))) .check(fix, client_errc::unknown_auth_plugin); } @@ -221,14 +250,13 @@ BOOST_AUTO_TEST_CASE(authswitch_to_itself) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_auth_switch_frame(2, "caching_sha2_password", csha2p_challenge)) - .expect_write(create_frame(3, csha2p_response)) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_auth_switch_frame(2, "caching_sha2_password", csha2p_scramble)) + .expect_write(create_frame(3, csha2p_hash)) .expect_read(create_more_data_frame(4, csha2p_fast_auth_ok)) .expect_read(create_ok_frame(5, ok_builder().build())) .will_set_status(connection_status::ready) @@ -251,14 +279,13 @@ BOOST_AUTO_TEST_CASE(moredata_authswitch) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_fast_auth_ok)) - .expect_read(create_auth_switch_frame(3, "mysql_native_password", mnp_challenge)) + .expect_read(create_auth_switch_frame(3, "mysql_native_password", mnp_scramble)) .check(fix, client_errc::bad_handshake_packet_type); } @@ -270,14 +297,14 @@ BOOST_AUTO_TEST_CASE(authswitch_moredata_authswitch) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("mysql_native_password").auth_data(mnp_challenge).build() + server_hello_builder().auth_plugin("mysql_native_password").auth_data(mnp_scramble).build() ) .expect_write( - login_request_builder().auth_plugin("mysql_native_password").auth_response(mnp_response).build() + login_request_builder().auth_plugin("mysql_native_password").auth_response(mnp_hash).build() ) - .expect_read(create_auth_switch_frame(2, "caching_sha2_password", csha2p_challenge)) - .expect_write(create_frame(3, csha2p_response)) - .expect_read(create_auth_switch_frame(4, "caching_sha2_password", csha2p_challenge)) + .expect_read(create_auth_switch_frame(2, "caching_sha2_password", csha2p_scramble)) + .expect_write(create_frame(3, csha2p_hash)) + .expect_read(create_auth_switch_frame(4, "caching_sha2_password", csha2p_scramble)) .check(fix, client_errc::bad_handshake_packet_type); } @@ -292,12 +319,11 @@ BOOST_AUTO_TEST_CASE(moredata_error_flavor) .expect_read(server_hello_builder() .version("11.4.2-MariaDB-ubu2404") .auth_plugin("caching_sha2_password") - .auth_data(csha2p_challenge) + .auth_data(csha2p_scramble) .build()) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) .expect_read(create_more_data_frame(2, csha2p_fast_auth_ok)) .expect_read( err_builder().seqnum(3).code(mariadb_server_errc::er_bad_data).message("Denied").build_frame() @@ -324,12 +350,12 @@ BOOST_AUTO_TEST_CASE(network_errors) // Run the test algo_test() - .expect_read(server_hello_builder().caps(tls_caps).auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().caps(tls_caps).auth_data(mnp_scramble).build()) .expect_write(create_ssl_request()) .expect_ssl_handshake() - .expect_write(login_request_builder().seqnum(2).caps(tls_caps).auth_response(mnp_response).build()) - .expect_read(create_auth_switch_frame(3, "caching_sha2_password", csha2p_challenge)) - .expect_write(create_frame(4, csha2p_response)) + .expect_write(login_request_builder().seqnum(2).caps(tls_caps).auth_response(mnp_hash).build()) + .expect_read(create_auth_switch_frame(3, "caching_sha2_password", csha2p_scramble)) + .expect_write(create_frame(4, csha2p_hash)) .expect_read(create_more_data_frame(5, csha2p_perform_full_auth)) .expect_write(create_frame(6, null_terminated_password())) .expect_read(create_ok_frame(7, ok_builder().build())) @@ -344,12 +370,11 @@ BOOST_AUTO_TEST_CASE(network_errors_read_moredata) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(client_errc::wrong_num_params) .check(fix, client_errc::wrong_num_params); } diff --git a/test/unit/test/sansio/handshake/handshake_capabilities.cpp b/test/unit/test/sansio/handshake/handshake_capabilities.cpp index 825813e13..973aa4d11 100644 --- a/test/unit/test/sansio/handshake/handshake_capabilities.cpp +++ b/test/unit/test/sansio/handshake/handshake_capabilities.cpp @@ -32,8 +32,8 @@ BOOST_AUTO_TEST_CASE(db_nonempty_supported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(db_caps).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().caps(db_caps).auth_response(mnp_response).db("mydb").build()) + .expect_read(server_hello_builder().caps(db_caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(db_caps).auth_response(mnp_hash).db("mydb").build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(db_caps) @@ -49,7 +49,7 @@ BOOST_AUTO_TEST_CASE(db_nonempty_unsupported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_scramble).build()) .check(fix, client_errc::server_unsupported); } @@ -61,8 +61,8 @@ BOOST_AUTO_TEST_CASE(db_empty_supported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(db_caps).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_response).build()) + .expect_read(server_hello_builder().caps(db_caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -79,8 +79,8 @@ BOOST_AUTO_TEST_CASE(db_empty_unsupported) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -105,8 +105,8 @@ BOOST_AUTO_TEST_CASE(multiq_true_supported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(multiq_caps).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().caps(multiq_caps).auth_response(mnp_response).build()) + .expect_read(server_hello_builder().caps(multiq_caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(multiq_caps).auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(multiq_caps) @@ -125,7 +125,7 @@ BOOST_AUTO_TEST_CASE(multiq_true_unsupported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_scramble).build()) .check(fix, client_errc::server_unsupported); } @@ -137,8 +137,8 @@ BOOST_AUTO_TEST_CASE(multiq_false_supported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(multiq_caps).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_response).build()) + .expect_read(server_hello_builder().caps(multiq_caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -155,8 +155,8 @@ BOOST_AUTO_TEST_CASE(multiq_false_unsupported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_response).build()) + .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -184,11 +184,10 @@ BOOST_AUTO_TEST_CASE(tls_on) // Run the test algo_test() - .expect_read(server_hello_builder().caps(tls_caps).auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().caps(tls_caps).auth_data(mnp_scramble).build()) .expect_write(create_ssl_request()) .expect_ssl_handshake() - .expect_write( - login_request_builder().seqnum(2).caps(tls_caps).auth_response(mnp_response).build() + .expect_write(login_request_builder().seqnum(2).caps(tls_caps).auth_response(mnp_hash).build() ) .expect_read(create_ok_frame(3, ok_builder().build())) .will_set_status(connection_status::ready) @@ -271,8 +270,8 @@ BOOST_AUTO_TEST_CASE(tls_off) // Run the test algo_test() - .expect_read(server_hello_builder().caps(tc.server_caps).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_response).build()) + .expect_read(server_hello_builder().caps(tc.server_caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -294,7 +293,7 @@ BOOST_AUTO_TEST_CASE(tls_error_unsupported) // Run the test algo_test() - .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().caps(min_caps).auth_data(mnp_scramble).build()) .check(fix, client_errc::server_doesnt_support_ssl); } @@ -338,7 +337,7 @@ BOOST_AUTO_TEST_CASE(caps_mandatory) // Run the test algo_test() - .expect_read(server_hello_builder().caps(tc.caps).auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().caps(tc.caps).auth_data(mnp_scramble).build()) .check(fix, client_errc::server_unsupported); } } @@ -365,9 +364,8 @@ BOOST_AUTO_TEST_CASE(caps_optional) // Run the test algo_test() - .expect_read(server_hello_builder().caps(min_caps | tc.caps).auth_data(mnp_challenge).build()) - .expect_write( - login_request_builder().caps(min_caps | tc.caps).auth_response(mnp_response).build() + .expect_read(server_hello_builder().caps(min_caps | tc.caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(min_caps | tc.caps).auth_response(mnp_hash).build() ) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) @@ -416,8 +414,8 @@ BOOST_AUTO_TEST_CASE(caps_ignored) // Run the test algo_test() - .expect_read(server_hello_builder().caps(min_caps | tc.caps).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_response).build()) + .expect_read(server_hello_builder().caps(min_caps | tc.caps).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().caps(min_caps).auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) diff --git a/test/unit/test/sansio/handshake/handshake_common.hpp b/test/unit/test/sansio/handshake/handshake_common.hpp index 6e943f0a1..9fcdb95f5 100644 --- a/test/unit/test/sansio/handshake/handshake_common.hpp +++ b/test/unit/test/sansio/handshake/handshake_common.hpp @@ -223,29 +223,30 @@ inline std::vector create_more_data_frame( })); } -// These challenge/responses have been captured with Wireshark. +// These scrambles/hashes have been captured with Wireshark. constexpr const char* password = "example_password"; -constexpr std::uint8_t mnp_challenge[] = { +constexpr std::uint8_t mnp_scramble[] = { 0x1b, 0x0f, 0x6e, 0x59, 0x1b, 0x70, 0x33, 0x01, 0x0c, 0x01, 0x7e, 0x2e, 0x30, 0x7a, 0x79, 0x5c, 0x02, 0x50, 0x51, 0x35, }; -constexpr std::uint8_t mnp_response[] = { +constexpr std::uint8_t mnp_hash[] = { 0xbe, 0xa5, 0xb5, 0xe7, 0x9c, 0x05, 0x23, 0x34, 0xda, 0x06, 0x1d, 0xaf, 0xd9, 0x8b, 0x4b, 0x09, 0x86, 0xe5, 0xd1, 0x4a, }; -constexpr std::uint8_t csha2p_challenge[] = { +constexpr std::uint8_t csha2p_scramble[] = { 0x6f, 0x1b, 0x3b, 0x64, 0x39, 0x01, 0x46, 0x44, 0x53, 0x3b, 0x74, 0x3c, 0x3e, 0x3c, 0x3c, 0x0b, 0x30, 0x77, 0x1a, 0x49, }; -constexpr std::uint8_t csha2p_response[] = { +constexpr std::uint8_t csha2p_hash[] = { 0xa7, 0xc3, 0x7f, 0x88, 0x25, 0xec, 0x92, 0x2c, 0x88, 0xba, 0x47, 0x04, 0x14, 0xd2, 0xa3, 0xa3, 0x5e, 0xa9, 0x41, 0x8e, 0xdc, 0x89, 0xeb, 0xe2, 0xa1, 0xec, 0xd8, 0x4f, 0x73, 0xa1, 0x49, 0x60, }; +constexpr std::uint8_t csha2p_request_key[] = {0x02}; constexpr std::uint8_t csha2p_fast_auth_ok[] = {0x03}; constexpr std::uint8_t csha2p_perform_full_auth[] = {0x04}; diff --git a/test/unit/test/sansio/handshake/handshake_connection_state_data.cpp b/test/unit/test/sansio/handshake/handshake_connection_state_data.cpp index a71759a5c..a35a9da92 100644 --- a/test/unit/test/sansio/handshake/handshake_connection_state_data.cpp +++ b/test/unit/test/sansio/handshake/handshake_connection_state_data.cpp @@ -31,8 +31,8 @@ BOOST_AUTO_TEST_CASE(hello_connection_id) // Run the test algo_test() - .expect_read(server_hello_builder().connection_id(value).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().connection_id(value).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -65,8 +65,8 @@ BOOST_AUTO_TEST_CASE(flavor) // Run the test algo_test() - .expect_read(server_hello_builder().version(tc.version).auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().version(tc.version).auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -91,10 +91,10 @@ BOOST_AUTO_TEST_CASE(unknown_collation) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) .expect_write(login_request_builder() .collation(mysql_collations::utf8mb4_0900_as_ci) - .auth_response(mnp_response) + .auth_response(mnp_hash) .build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) @@ -113,8 +113,8 @@ BOOST_AUTO_TEST_CASE(backslash_escapes) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().no_backslash_escapes(true).build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -133,8 +133,8 @@ BOOST_AUTO_TEST_CASE(meta_mode) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -166,8 +166,8 @@ BOOST_AUTO_TEST_CASE(connection_status_success) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) diff --git a/test/unit/test/sansio/handshake/handshake_csha2p.cpp b/test/unit/test/sansio/handshake/handshake_csha2p.cpp index fd8f0c46f..6cd4b4d1a 100644 --- a/test/unit/test/sansio/handshake/handshake_csha2p.cpp +++ b/test/unit/test/sansio/handshake/handshake_csha2p.cpp @@ -13,8 +13,10 @@ #include #include "handshake_common.hpp" +#include "handshake_csha2p_keys.hpp" #include "test_common/create_diagnostics.hpp" #include "test_unit/create_err.hpp" +#include "test_unit/create_frame.hpp" #include "test_unit/create_ok.hpp" #include "test_unit/create_ok_frame.hpp" @@ -36,12 +38,11 @@ BOOST_AUTO_TEST_CASE(ok) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -60,12 +61,11 @@ BOOST_AUTO_TEST_CASE(err) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(err_builder() .seqnum(2) .code(common_server_errc::er_access_denied_error) @@ -74,27 +74,6 @@ BOOST_AUTO_TEST_CASE(err) .check(fix, common_server_errc::er_access_denied_error, create_server_diag("Denied")); } -// At the moment, this plugin requires TLS, so this is an error -BOOST_AUTO_TEST_CASE(fullauth) -{ - // Setup - handshake_fixture fix; - - // Run the test - algo_test() - .expect_read(server_hello_builder() - .caps(tls_caps) - .auth_plugin("caching_sha2_password") - .auth_data(csha2p_challenge) - .build()) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) - .check(fix, client_errc::auth_plugin_requires_ssl); -} - // Receiving an unknown more data frame (something != fullauth or fastok) is illegal BOOST_AUTO_TEST_CASE(moredata) { @@ -104,12 +83,11 @@ BOOST_AUTO_TEST_CASE(moredata) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, std::vector{3, 4})) .check(fix, client_errc::bad_handshake_packet_type); } @@ -123,12 +101,11 @@ BOOST_AUTO_TEST_CASE(fastok_ok) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_fast_auth_ok)) .expect_read(create_ok_frame(3, ok_builder().build())) .will_set_status(connection_status::ready) @@ -148,12 +125,11 @@ BOOST_AUTO_TEST_CASE(fastok_err) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_fast_auth_ok)) .expect_read(err_builder() .seqnum(3) @@ -172,12 +148,11 @@ BOOST_AUTO_TEST_CASE(fastok_fastok) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_fast_auth_ok)) .expect_read(create_more_data_frame(3, csha2p_fast_auth_ok)) .check(fix, client_errc::bad_handshake_packet_type); @@ -192,12 +167,11 @@ BOOST_AUTO_TEST_CASE(fastok_fullauth) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_fast_auth_ok)) .expect_read(create_more_data_frame(3, csha2p_perform_full_auth)) .check(fix, client_errc::bad_handshake_packet_type); @@ -212,12 +186,11 @@ BOOST_AUTO_TEST_CASE(fastok_moredata) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_fast_auth_ok)) .expect_read(create_more_data_frame(3, std::vector{10, 20, 30})) .check(fix, client_errc::bad_handshake_packet_type); @@ -232,13 +205,13 @@ BOOST_AUTO_TEST_CASE(authswitch_fastok_ok) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("mysql_native_password").auth_data(mnp_challenge).build() + server_hello_builder().auth_plugin("mysql_native_password").auth_data(mnp_scramble).build() ) .expect_write( - login_request_builder().auth_plugin("mysql_native_password").auth_response(mnp_response).build() + login_request_builder().auth_plugin("mysql_native_password").auth_response(mnp_hash).build() ) - .expect_read(create_auth_switch_frame(2, "caching_sha2_password", csha2p_challenge)) - .expect_write(create_frame(3, csha2p_response)) + .expect_read(create_auth_switch_frame(2, "caching_sha2_password", csha2p_scramble)) + .expect_write(create_frame(3, csha2p_hash)) .expect_read(create_more_data_frame(4, csha2p_fast_auth_ok)) .expect_read(create_ok_frame(5, ok_builder().build())) .will_set_status(connection_status::ready) @@ -248,6 +221,137 @@ BOOST_AUTO_TEST_CASE(authswitch_fastok_ok) .check(fix); } +// If the server requests us to perform full auth and we're using plaintext, +// we request the server's public key +BOOST_AUTO_TEST_CASE(fullauth_key_ok) +{ + // Setup + handshake_fixture fix; + + // Run the test + algo_test() + .expect_read(server_hello_builder() + .caps(tls_caps) + .auth_plugin("caching_sha2_password") + .auth_data(csha2p_scramble) + .build()) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) + .expect_write(create_frame(3, csha2p_request_key)) + .expect_read(create_more_data_frame(4, public_key_2048)) + .expect_any_write() // the exact encryption result is not deterministic + .expect_read(create_ok_frame(6, ok_builder().build())) + .will_set_status(connection_status::ready) + .will_set_capabilities(min_caps) + .will_set_current_charset(utf8mb4_charset) + .will_set_connection_id(42) + .check(fix); +} + +// The server might send us an error after we sent the password (e.g. unauthorized) +BOOST_AUTO_TEST_CASE(fullauth_key_error) +{ + // Setup + handshake_fixture fix; + + // Run the test + algo_test() + .expect_read(server_hello_builder() + .caps(tls_caps) + .auth_plugin("caching_sha2_password") + .auth_data(csha2p_scramble) + .build()) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) + .expect_write(create_frame(3, csha2p_request_key)) + .expect_read(create_more_data_frame(4, public_key_2048)) + .expect_any_write() // the exact encryption result is not deterministic + .expect_read(err_builder() + .seqnum(6) + .code(common_server_errc::er_access_denied_error) + .message("Denied") + .build_frame()) + .check(fix, common_server_errc::er_access_denied_error, create_server_diag("Denied")); +} + +// The server might send us an error instead of the key, +// if the key pair for caching_sha2_password was misconfigured +BOOST_AUTO_TEST_CASE(fullauth_error) +{ + // Setup + handshake_fixture fix; + + // Run the test + algo_test() + .expect_read(server_hello_builder() + .caps(tls_caps) + .auth_plugin("caching_sha2_password") + .auth_data(csha2p_scramble) + .build()) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) + .expect_write(create_frame(3, csha2p_request_key)) + .expect_read(err_builder() + .seqnum(4) + .code(common_server_errc::er_access_denied_error) + .message("Bad key") + .build_frame()) + .check(fix, common_server_errc::er_access_denied_error, create_server_diag("Bad key")); +} + +// If encryption fails (e.g. because the server sent us an invalid key), we fail appropriately. +// Size checks are the only ones that yield a predictable error code +BOOST_AUTO_TEST_CASE(fullauth_encrypterror) +{ + // Setup + handshake_fixture fix; + + // Run the test + algo_test() + .expect_read(server_hello_builder() + .caps(tls_caps) + .auth_plugin("caching_sha2_password") + .auth_data(csha2p_scramble) + .build()) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) + .expect_write(create_frame(3, csha2p_request_key)) + .expect_read(create_more_data_frame(4, std::vector(2u * 1024u * 1024u, 0xab))) + .check(fix, client_errc::protocol_value_error); +} + +// After encrypting the password, no more messages are expected +BOOST_AUTO_TEST_CASE(fullauth_key_moredata) +{ + // Setup + handshake_fixture fix; + + // Run the test + algo_test() + .expect_read(server_hello_builder() + .caps(tls_caps) + .auth_plugin("caching_sha2_password") + .auth_data(csha2p_scramble) + .build()) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) + .expect_write(create_frame(3, csha2p_request_key)) + .expect_read(create_more_data_frame(4, public_key_2048)) + .expect_any_write() // the exact encryption result is not deterministic + .expect_read(create_more_data_frame(6, csha2p_perform_full_auth)) + .check(fix, client_errc::bad_handshake_packet_type); +} + // If we're using a secure transport (e.g. UNIX socket), caching_sha2_password // just sends the raw password BOOST_AUTO_TEST_CASE(securetransport_fullauth_ok) @@ -258,12 +362,11 @@ BOOST_AUTO_TEST_CASE(securetransport_fullauth_ok) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) .expect_write(create_frame(3, null_terminated_password())) .expect_read(create_ok_frame(4, ok_builder().build())) @@ -285,12 +388,11 @@ BOOST_AUTO_TEST_CASE(securetransport_fullauth_err) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) .expect_write(create_frame(3, null_terminated_password())) .expect_read(err_builder() @@ -310,12 +412,11 @@ BOOST_AUTO_TEST_CASE(securetransport_fullauth_fastok) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) .expect_write(create_frame(3, null_terminated_password())) .expect_read(create_more_data_frame(4, csha2p_fast_auth_ok)) @@ -331,12 +432,11 @@ BOOST_AUTO_TEST_CASE(securetransport_fullauth_fullauth) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) .expect_write(create_frame(3, null_terminated_password())) .expect_read(create_more_data_frame(4, csha2p_perform_full_auth)) @@ -352,12 +452,11 @@ BOOST_AUTO_TEST_CASE(securetransport_fullauth_moredata) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) .expect_read(create_more_data_frame(2, csha2p_perform_full_auth)) .expect_write(create_frame(3, null_terminated_password())) .expect_read(create_more_data_frame(4, std::vector{4, 3, 2})) @@ -376,7 +475,7 @@ BOOST_AUTO_TEST_CASE(tls) .expect_read(server_hello_builder() .caps(tls_caps) .auth_plugin("caching_sha2_password") - .auth_data(csha2p_challenge) + .auth_data(csha2p_scramble) .build()) .expect_write(create_ssl_request()) .expect_ssl_handshake() @@ -384,7 +483,7 @@ BOOST_AUTO_TEST_CASE(tls) .seqnum(2) .caps(tls_caps) .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) + .auth_response(csha2p_hash) .build()) .expect_read(create_more_data_frame(3, csha2p_perform_full_auth)) .expect_write(create_frame(4, null_terminated_password())) diff --git a/test/unit/test/sansio/handshake/handshake_csha2p_encrypt_password.cpp b/test/unit/test/sansio/handshake/handshake_csha2p_encrypt_password.cpp new file mode 100644 index 000000000..cafcf2c6e --- /dev/null +++ b/test/unit/test/sansio/handshake/handshake_csha2p_encrypt_password.cpp @@ -0,0 +1,385 @@ +// +// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "handshake_csha2p_keys.hpp" +#include "test_common/assert_buffer_equals.hpp" +#include "test_common/printing.hpp" + +using namespace boost::mysql; +using namespace boost::mysql::test; +using boost::span; +using boost::asio::error::ssl_category; +using detail::csha2p_encrypt_password; + +using buffer_type = boost::container::small_vector; + +namespace { + +// Encrypting with RSA and OAEP padding involves random numbers for padding. +// There isn't a reliable way to seed OpenSSL's random number generators so that +// the output is deterministic. So we do the following: +// 1. We know the server's public and private keys and the password (the constants below). +// 2. We capture a scramble and a corresponding ciphertext using Wireshark. +// 3. We decrypt the ciphertext with OpenSSL to obtain the expected plaintext (the salted password). +// 4. Tests run csha2p_encrypt_password, decrypt its output, and verify that it matches the expected +// plaintext. +BOOST_AUTO_TEST_SUITE(test_handshake_csha2p_encrypt_password) + +constexpr std::uint8_t scramble[] = { + 0x0f, 0x64, 0x4f, 0x2f, 0x2b, 0x3b, 0x27, 0x6b, 0x45, 0x5c, + 0x53, 0x01, 0x13, 0x7e, 0x4f, 0x10, 0x26, 0x23, 0x5d, 0x27, +}; + +// Decrypts the output of +std::vector decrypt(span private_key, span ciphertext) +{ + // RAII helpers + struct bio_deleter + { + void operator()(BIO* bio) const noexcept { BIO_free(bio); } + }; + using unique_bio = std::unique_ptr; + + struct evp_pkey_deleter + { + void operator()(EVP_PKEY* pkey) const noexcept { EVP_PKEY_free(pkey); } + }; + using unique_evp_pkey = std::unique_ptr; + + struct evp_pkey_ctx_deleter + { + void operator()(EVP_PKEY_CTX* ctx) const noexcept { EVP_PKEY_CTX_free(ctx); } + }; + using unique_evp_pkey_ctx = std::unique_ptr; + + // Create a BIO with the key + unique_bio bio(BIO_new_mem_buf(private_key.data(), static_cast(private_key.size()))); + BOOST_TEST_REQUIRE((bio != nullptr), "Creating a BIO failed: " << ERR_get_error()); + + // Load the key + unique_evp_pkey pkey(PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr)); + BOOST_TEST_REQUIRE((bio != nullptr), "Loading the key failed: " << ERR_get_error()); + + // Create a decryption context + unique_evp_pkey_ctx ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + BOOST_TEST_REQUIRE((ctx != nullptr), "Creating a context failed: " << ERR_get_error()); + + // Initialize it + BOOST_TEST_REQUIRE( + EVP_PKEY_decrypt_init(ctx.get()) > 0, + "Initializing decryption failed: " << ERR_get_error() + ); + + // Set the padding scheme + BOOST_TEST_REQUIRE( + EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) > 0, + "Setting the padding scheme failed: " << ERR_get_error() + ); + + // Determine the size of the decrypted buffer + std::size_t out_size = 0; + BOOST_TEST_REQUIRE( + EVP_PKEY_decrypt(ctx.get(), nullptr, &out_size, ciphertext.data(), ciphertext.size()) > 0, + "Determining decryption size failed: " << ERR_get_error() + ); + std::vector res(out_size, 0); + + // Actually decrypt + BOOST_TEST_REQUIRE( + EVP_PKEY_decrypt(ctx.get(), res.data(), &out_size, ciphertext.data(), ciphertext.size()) > 0, + "Decrypting failed: " << ERR_get_error() + ); + res.resize(out_size); + + // Done + return res; +} + +BOOST_AUTO_TEST_CASE(success) +{ + // A password with all the possible ASCII values + std::string all_chars_password; + for (int i = 0; i < 256; ++i) + all_chars_password.push_back(static_cast(i)); + + struct + { + string_view name; + std::string password; + span public_key; + span private_key; + std::vector expected_decrypted; + } test_cases[] = { + // clang-format off + // An empty password does not cause trouble. This is an edge case, since + // an empty password should employ a different workflow + { + "password_empty", + {}, + span(public_key_2048), + span(private_key_2048), + {0x0f} + }, + + // Password is < length of the scramble + { + "password_shorter_scramble", + "csha2p_password", + span(public_key_2048), + span(private_key_2048), + { + 0x6c, 0x17, 0x27, 0x4e, 0x19, 0x4b, 0x78, 0x1b, 0x24, 0x2f, 0x20, 0x76, 0x7c, 0x0c, 0x2b, 0x10 + } + }, + + // Password (with NULL byte) is equal to the length of the scramble + { + "password_same_size_scramble", + "hjbjd923KKLiosoi90J", + span(public_key_2048), + span(private_key_2048), + { + 0x67, 0x0e, 0x2d, 0x45, 0x4f, 0x02, 0x15, 0x58, 0x0e, 0x17, + 0x1f, 0x68, 0x7c, 0x0d, 0x20, 0x79, 0x1f, 0x13, 0x17, 0x27 + } + }, + + // Passwords longer than the scramble use it cyclically + { + "password_longer_scramble", + "kjaski829380jvnnM,ka;::_kshf93IJCLIJO)jcjsnaklO?a", + span(public_key_2048), + span(private_key_2048), + { + 0x64, 0x0e, 0x2e, 0x5c, 0x40, 0x52, 0x1f, 0x59, 0x7c, 0x6f, 0x6b, 0x31, 0x79, 0x08, 0x21, 0x7e, 0x6b, + 0x0f, 0x36, 0x46, 0x34, 0x5e, 0x75, 0x70, 0x40, 0x48, 0x4f, 0x0d, 0x7c, 0x6f, 0x1a, 0x4b, 0x50, 0x32, + 0x06, 0x5a, 0x69, 0x0a, 0x37, 0x44, 0x65, 0x17, 0x21, 0x4e, 0x40, 0x57, 0x68, 0x54, 0x24, 0x5c, + } + }, + + // The longest password that a 2048 RSA key can encrypt with the OAEP scheme + { + "password_max_size_2048", + "hjbjd923KKLkjbdkcjwhekiy8393ou2weusidhiahJBKJKHCIHCKJIu9KHO09IJIpojaf0w39jalsjMMKjkjhiue93I=))" + "UXIOJKXNKNhkai8923oiawiakssaknhakhIIHICHIO)CU)" + "IHCKHCKJhisiweioHHJHUCHIIIJIOPkjgwijiosoi9jsu84HHUHCHI9839hdjsbsdjuUHJjbJ", + span(public_key_2048), + span(private_key_2048), + { + 0x67, 0x0e, 0x2d, 0x45, 0x4f, 0x02, 0x15, 0x58, 0x0e, 0x17, 0x1f, 0x6a, 0x79, 0x1c, 0x2b, 0x7b, 0x45, + 0x49, 0x2a, 0x4f, 0x6a, 0x0f, 0x26, 0x56, 0x13, 0x08, 0x1e, 0x58, 0x2a, 0x29, 0x61, 0x76, 0x76, 0x0b, + 0x3c, 0x79, 0x42, 0x4b, 0x34, 0x46, 0x67, 0x2e, 0x0d, 0x64, 0x61, 0x70, 0x6f, 0x28, 0x0c, 0x14, 0x10, + 0x4a, 0x59, 0x37, 0x3a, 0x29, 0x6d, 0x6b, 0x12, 0x17, 0x36, 0x2d, 0x05, 0x66, 0x5b, 0x54, 0x4d, 0x0a, + 0x23, 0x6c, 0x24, 0x32, 0x2a, 0x14, 0x2e, 0x7c, 0x55, 0x49, 0x10, 0x6a, 0x44, 0x0e, 0x24, 0x45, 0x43, + 0x52, 0x52, 0x0e, 0x7c, 0x6f, 0x1a, 0x3c, 0x3a, 0x57, 0x1a, 0x48, 0x6f, 0x6c, 0x17, 0x6c, 0x57, 0x2a, + 0x04, 0x61, 0x43, 0x50, 0x46, 0x02, 0x7d, 0x65, 0x61, 0x32, 0x7c, 0x17, 0x2e, 0x67, 0x4f, 0x42, 0x36, + 0x54, 0x7c, 0x05, 0x24, 0x41, 0x43, 0x5a, 0x4c, 0x03, 0x0c, 0x15, 0x1b, 0x48, 0x50, 0x36, 0x06, 0x5f, + 0x0f, 0x60, 0x08, 0x0e, 0x46, 0x2c, 0x0c, 0x64, 0x63, 0x78, 0x6c, 0x21, 0x2d, 0x35, 0x20, 0x68, 0x64, + 0x1b, 0x26, 0x7f, 0x6e, 0x6b, 0x17, 0x6f, 0x5a, 0x27, 0x07, 0x66, 0x62, 0x72, 0x6d, 0x22, 0x0a, 0x0c, + 0x38, 0x6b, 0x74, 0x09, 0x26, 0x7a, 0x4f, 0x4c, 0x2e, 0x48, 0x66, 0x5d, 0x25, 0x5c, 0x5e, 0x03, 0x13, + 0x23, 0x0d, 0x09, 0x1b, 0x42, 0x5b, 0x37, 0x76, 0x28, 0x15, 0x1a, 0x35, 0x43, 0x65, 0x17, 0x2d, 0x5c, + 0x4f, 0x51, 0x52, 0x3e, 0x0d, 0x16, 0x39, 0x63, 0x59, 0x7e, + } + }, + + // Longer passwords are accepted if the key supports them. + // Concretely, passwords longer than 512 (size of our SBO) are supported + { + "password_longer_sbo", + std::string(600, '5'), + span(public_key_8192), + span(private_key_8192), + { + 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, + 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, + 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, + 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, + 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, + 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, + 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, + 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, + 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, + 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, + 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, + 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, + 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, + 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, + 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, + 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, + 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, + 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, + 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, + 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, + 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, + 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, + 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, + 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, + 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, + 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, + 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, + 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, + 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, + 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, + 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, + 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, + 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, 0x68, 0x12, 0x3a, + 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, 0x25, 0x13, 0x16, + 0x68, 0x12, 0x3a, 0x51, 0x7a, 0x1a, 0x1e, 0x0e, 0x12, 0x5e, 0x70, 0x69, 0x66, 0x34, 0x26, 0x4b, 0x7a, + 0x25, 0x13, 0x16, 0x68, 0x12, 0x0f + } + }, + + // No character causes trouble + { + "password_all_characters", + all_chars_password, + span(public_key_8192), + span(private_key_8192), + { + 0x0f, 0x65, 0x4d, 0x2c, 0x2f, 0x3e, 0x21, 0x6c, 0x4d, 0x55, 0x59, 0x0a, 0x1f, 0x73, 0x41, 0x1f, 0x36, + 0x32, 0x4f, 0x34, 0x1b, 0x71, 0x59, 0x38, 0x33, 0x22, 0x3d, 0x70, 0x59, 0x41, 0x4d, 0x1e, 0x33, 0x5f, + 0x6d, 0x33, 0x02, 0x06, 0x7b, 0x00, 0x27, 0x4d, 0x65, 0x04, 0x07, 0x16, 0x09, 0x44, 0x75, 0x6d, 0x61, + 0x32, 0x27, 0x4b, 0x79, 0x27, 0x1e, 0x1a, 0x67, 0x1c, 0x33, 0x59, 0x71, 0x10, 0x6b, 0x7a, 0x65, 0x28, + 0x01, 0x19, 0x15, 0x46, 0x5b, 0x37, 0x05, 0x5b, 0x6a, 0x6e, 0x13, 0x68, 0x5f, 0x35, 0x1d, 0x7c, 0x7f, + 0x6e, 0x71, 0x3c, 0x1d, 0x05, 0x09, 0x5a, 0x4f, 0x23, 0x11, 0x4f, 0x46, 0x42, 0x3f, 0x44, 0x6b, 0x01, + 0x29, 0x48, 0x43, 0x52, 0x4d, 0x00, 0x29, 0x31, 0x3d, 0x6e, 0x63, 0x0f, 0x3d, 0x63, 0x52, 0x56, 0x2b, + 0x50, 0x77, 0x1d, 0x35, 0x54, 0x57, 0x46, 0x59, 0x14, 0xc5, 0xdd, 0xd1, 0x82, 0x97, 0xfb, 0xc9, 0x97, + 0xae, 0xaa, 0xd7, 0xac, 0x83, 0xe9, 0xc1, 0xa0, 0xbb, 0xaa, 0xb5, 0xf8, 0xd1, 0xc9, 0xc5, 0x96, 0x8b, + 0xe7, 0xd5, 0x8b, 0xba, 0xbe, 0xc3, 0xb8, 0xaf, 0xc5, 0xed, 0x8c, 0x8f, 0x9e, 0x81, 0xcc, 0xed, 0xf5, + 0xf9, 0xaa, 0xbf, 0xd3, 0xe1, 0xbf, 0x96, 0x92, 0xef, 0x94, 0xbb, 0xd1, 0xf9, 0x98, 0x93, 0x82, 0x9d, + 0xd0, 0xf9, 0xe1, 0xed, 0xbe, 0xd3, 0xbf, 0x8d, 0xd3, 0xe2, 0xe6, 0x9b, 0xe0, 0xc7, 0xad, 0x85, 0xe4, + 0xe7, 0xf6, 0xe9, 0xa4, 0x95, 0x8d, 0x81, 0xd2, 0xc7, 0xab, 0x99, 0xc7, 0xfe, 0xfa, 0x87, 0xfc, 0xd3, + 0xb9, 0x91, 0xf0, 0xcb, 0xda, 0xc5, 0x88, 0xa1, 0xb9, 0xb5, 0xe6, 0xfb, 0x97, 0xa5, 0xfb, 0xca, 0xce, + 0xb3, 0xc8, 0xff, 0x95, 0xbd, 0xdc, 0xdf, 0xce, 0xd1, 0x9c, 0xbd, 0xa5, 0xa9, 0xfa, 0xef, 0x83, 0xb1, + 0xef, 0x26 + } + } + + // clang-format on + }; + + for (const auto& tc : test_cases) + { + BOOST_TEST_CONTEXT(tc.name) + { + // Setup + buffer_type buff; + + // Call the function + auto ec = csha2p_encrypt_password(tc.password, scramble, tc.public_key, buff, ssl_category); + + // Verify + BOOST_TEST_REQUIRE(ec == error_code()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(decrypt(tc.private_key, buff), tc.expected_decrypted); + } + } +} + +// +// Errors. For OpenSSL errors, it's not defined what exact code will each function return. +// + +// We passed an empty buffer to the key parser +BOOST_AUTO_TEST_CASE(error_key_buffer_empty) +{ + buffer_type buff; + auto ec = csha2p_encrypt_password("csha2p_password", scramble, {}, buff, ssl_category); + BOOST_TEST((ec.category() == ssl_category)); + BOOST_TEST(ec.value() > 0); // is an error + BOOST_TEST(ec.message() != ""); // produces some output + BOOST_TEST(ec.has_location()); +} + +BOOST_AUTO_TEST_CASE(error_key_malformed) +{ + constexpr std::uint8_t key_buffer[] = "-----BEGIN PUBLIC KEY-----zwIDAQAB__kaj0?))="; + buffer_type buff; + auto ec = csha2p_encrypt_password("csha2p_password", scramble, key_buffer, buff, ssl_category); + BOOST_TEST((ec.category() == ssl_category)); + BOOST_TEST(ec.value() > 0); // is an error + BOOST_TEST(ec.message() != ""); // produces some output + BOOST_TEST(ec.has_location()); +} + +// Passing in a public key type that does not support encryption operations +// (like ECDSA) fails +BOOST_AUTO_TEST_CASE(error_key_doesnt_support_encryption) +{ + constexpr unsigned char key_buffer[] = R"%(-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERDkCI/degPJXEIYYncyvGsTdj9YI +4xQ6KPTzoF+DY2jM09w1TCncxk9XV1eOo44UMDUuK9K01halQy70mohFSQ== +-----END PUBLIC KEY----- +)%"; + + buffer_type buff; + auto ec = csha2p_encrypt_password("csha2p_password", scramble, key_buffer, buff, ssl_category); + BOOST_TEST((ec.category() == ssl_category)); + BOOST_TEST(ec.value() > 0); // is an error + BOOST_TEST(ec.message() != ""); // produces some output + BOOST_TEST(ec.has_location()); +} + +// Passing in a public key type that allows encryption but is not RSA fails as expected. +// This is a SM2 key, generated by: +// openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2 -out public.pem +// openssl pkey -in private.pem -pubout -out public.pem +BOOST_AUTO_TEST_CASE(error_key_not_rsa) +{ + // ECDSA public key set up for SM2 encryption + constexpr unsigned char public_key_sm2[] = R"%(-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEWGAXSpPHb2bWQjROuegjWPcuVwNW +mVpTh++3j7pnpWUjnFuarvWmWh/H6t96/pTx566FKGxZpLn3H9TLHZJsog== +-----END PUBLIC KEY----- +)%"; + + buffer_type buff; + auto ec = csha2p_encrypt_password("csha2p_password", scramble, public_key_sm2, buff, ssl_category); + BOOST_TEST(ec.failed()); // OpenSSL might or might not provide an error code here + BOOST_TEST(ec.has_location()); +} + +BOOST_AUTO_TEST_CASE(error_key_too_big) +{ + buffer_type buff; + std::vector key_buffer(2u * 1024u * 1024u, 0x0a); + auto ec = csha2p_encrypt_password("csha2p_password", scramble, key_buffer, buff, ssl_category); + BOOST_TEST(ec == client_errc::protocol_value_error); + BOOST_TEST(ec.has_location()); +} + +// If a password longer than the longest allowed plaintext is provided, we fail +BOOST_AUTO_TEST_CASE(error_password_too_big) +{ + buffer_type buff; + std::string passwd(214u, 'a'); // 213 is the max RSA/OAEP plaintext size + auto ec = csha2p_encrypt_password(passwd, scramble, public_key_2048, buff, ssl_category); + BOOST_TEST((ec.category() == ssl_category)); + BOOST_TEST(ec.value() > 0); // is an error + BOOST_TEST(ec.message() != ""); // produces some output + BOOST_TEST(ec.has_location()); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace \ No newline at end of file diff --git a/test/unit/test/sansio/handshake/handshake_csha2p_hash_password.cpp b/test/unit/test/sansio/handshake/handshake_csha2p_hash_password.cpp index ffada8bdc..2fbcaefa9 100644 --- a/test/unit/test/sansio/handshake/handshake_csha2p_hash_password.cpp +++ b/test/unit/test/sansio/handshake/handshake_csha2p_hash_password.cpp @@ -5,62 +5,38 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include - #include +#include + #include "test_common/assert_buffer_equals.hpp" -#include "test_common/printing.hpp" using namespace boost::mysql; -using namespace boost::mysql::test; using detail::csha2p_hash_password; namespace { BOOST_AUTO_TEST_SUITE(test_handshake_csha2p_hash_password) -// Values snooped using the MySQL Python connector -constexpr std::uint8_t challenge[20] = { +// Values snooped using Wireshark +constexpr std::uint8_t scramble[20] = { 0x3e, 0x3b, 0x4, 0x55, 0x4, 0x70, 0x16, 0x3a, 0x4c, 0x15, 0x35, 0x3, 0x15, 0x76, 0x73, 0x22, 0x46, 0x8, 0x18, 0x1, }; -constexpr std::uint8_t expected[32] = { - 0xa1, 0xc1, 0xe1, 0xe9, 0x1b, 0xb6, 0x54, 0x4b, 0xa7, 0x37, 0x4b, 0x9c, 0x56, 0x6d, 0x69, 0x3e, - 0x6, 0xca, 0x7, 0x2, 0x98, 0xac, 0xd1, 0x6, 0x18, 0xc6, 0x90, 0x38, 0x9d, 0x88, 0xe1, 0x20, -}; - BOOST_AUTO_TEST_CASE(nonempty_password) { - auto res = csha2p_hash_password("hola", challenge); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(res.value(), expected); -} - -BOOST_AUTO_TEST_CASE(empty_password) -{ - auto res = csha2p_hash_password("", challenge); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(res.value(), std::vector()); -} + constexpr std::uint8_t expected_hash[32] = { + 0xa1, 0xc1, 0xe1, 0xe9, 0x1b, 0xb6, 0x54, 0x4b, 0xa7, 0x37, 0x4b, 0x9c, 0x56, 0x6d, 0x69, 0x3e, + 0x6, 0xca, 0x7, 0x2, 0x98, 0xac, 0xd1, 0x6, 0x18, 0xc6, 0x90, 0x38, 0x9d, 0x88, 0xe1, 0x20, + }; -BOOST_AUTO_TEST_CASE(bad_challenge_length_nonempty_password) -{ - constexpr std::uint8_t bad_challenge[] = {0x00, 0x01, 0x02}; - auto res = csha2p_hash_password("hola", bad_challenge); - BOOST_TEST(res.error() == client_errc::protocol_value_error); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(csha2p_hash_password("hola", scramble), expected_hash); } -BOOST_AUTO_TEST_CASE(bad_challenge_length_nempty_password) -{ - constexpr std::uint8_t bad_challenge[] = {0x00, 0x01, 0x02}; - auto res = csha2p_hash_password("", bad_challenge); - BOOST_TEST(res.error() == client_errc::protocol_value_error); -} - -BOOST_AUTO_TEST_CASE(empty_challenge) +BOOST_AUTO_TEST_CASE(empty_password) { - auto res = csha2p_hash_password("", {}); - BOOST_TEST(res.error() == client_errc::protocol_value_error); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(csha2p_hash_password("", scramble), std::vector()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/sansio/handshake/handshake_csha2p_keys.hpp b/test/unit/test/sansio/handshake/handshake_csha2p_keys.hpp new file mode 100644 index 000000000..5977c2108 --- /dev/null +++ b/test/unit/test/sansio/handshake/handshake_csha2p_keys.hpp @@ -0,0 +1,189 @@ +// +// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_MYSQL_TEST_UNIT_TEST_SANSIO_HANDSHAKE_HANDSHAKE_CSHA2P_KEYS_HPP +#define BOOST_MYSQL_TEST_UNIT_TEST_SANSIO_HANDSHAKE_HANDSHAKE_CSHA2P_KEYS_HPP + +namespace boost { +namespace mysql { +namespace test { + +constexpr unsigned char public_key_2048[] = R"%(-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA36OYSpdiy1lFDrdO1Vux +GwjPTo35R2+mXqW2SZV7kH5C6BSCeoTk6STVRBJbgOCabtp5bpUZ+x2bYWOZp4fs +JakC75CN2YTJoYg5z5U6XUBEWn6WNBpvEoSJaUtrzfU69J07uWqB6v0MdJf3JTgd +ILfGKvk2T+maxqUiYObs0BJd5eKJZDlUaf2r4a9KC8zGUZzHdgtZEXlkHVNLEbbD +Ju4KjtCtJCG1NEBAh3oSnNp/Q1FKFywqU7YnEBWI0B9C5UcKNFbg7M35daimXfGp +V7WJKhO9w7iBJYL1SW+PwyUCh3DNsuSm3nLmuwKhTvGQHZJS/5OVdSHgZhjDnk2V +WwIDAQAB +-----END PUBLIC KEY----- +)%"; + +constexpr unsigned char private_key_2048[] = R"%(-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDfo5hKl2LLWUUO +t07VW7EbCM9OjflHb6ZepbZJlXuQfkLoFIJ6hOTpJNVEEluA4Jpu2nlulRn7HZth +Y5mnh+wlqQLvkI3ZhMmhiDnPlTpdQERafpY0Gm8ShIlpS2vN9Tr0nTu5aoHq/Qx0 +l/clOB0gt8Yq+TZP6ZrGpSJg5uzQEl3l4olkOVRp/avhr0oLzMZRnMd2C1kReWQd +U0sRtsMm7gqO0K0kIbU0QECHehKc2n9DUUoXLCpTticQFYjQH0LlRwo0VuDszfl1 +qKZd8alXtYkqE73DuIElgvVJb4/DJQKHcM2y5Kbecua7AqFO8ZAdklL/k5V1IeBm +GMOeTZVbAgMBAAECggEANI0UtDJunKoVeCfK9ofdTiT70dG6yfaKeaMm+pONvZ5t +ymtHXdLsl3x4QM6vgdFFeNcNwdZ3jHKgmHn3GU7vRso4TmMBciOp3bNNImJGnLMF +XN5yHTw47XkHcR6v7m25tNFdv2wvqzBbROqQwMY20gFdJ6v3/z89h4A2W97nttyp +ixcNdSTHOfu6iUceEGi2PjHrvw4STPQeihXFNTnYG7hvlWzAerQ5STx5K2n4JoXz +xI5MuHS6PGj5EBPUoq0+EQhmhORWBdNCMcijpHqobVDjifPRY2JbI3zZHtVjYpGH +otmc72hIDjNW7RX1ePKW/gsq6p2by3U8+4dOdya4AQKBgQDz4/6uo7atJSTgVo3d +Zr/7UJ4Qi2mlc992jLAqTQle6JchhNERoPvb19Qy8BaOz6LcxmuMXnRij05mxdyb +LmoH8TPe6RFLCOJrRapkUjUtRD8dIg6UNFLk98LD5t3o5PoHwNpBFfokRlchwoHL +uBvbEHQkrPX2xPbgla5e7zJLgQKBgQDqvjCUMvmap9+AlqxGFhv51V9dIlKvT0xW +p5KEMVfs3JXCHAM7o0ClZ8NitHXw18E8+iMG4pWw3+FX4K6tKGR+rCeGCUNGuiQT +FzXjrEej+Pnuk8zacjXkbS+PNnEqhpSq6STZVFn2UW+ZWAoq2iAMR7qw0eAjylln +h//Wad3+2wKBgAjwWEtKUM2zyNA4G+b7dxnc8I4mre6UeqI7sdE7FZbW64Mc/RSq +U9DQ7kQXrJv7XDq/Qv3YEGf0XKlDozxEzToRSxdmb23Sm4nW+dHHeY95Kt8EeohQ +CqG9uvO3KHb6vXc/SECOb6aYtWTVXjB7RPoYdklJ1ZH/0hSVJ9ju52cBAoGBAK63 +A90p25F6ZOWGP46iogve/e2JwFTvBnhwnKJ7P1/yBhzFULqwlUsG4euzOR0a2J6T +5kIXnyZYW5ZWimwi5jlJ1Nj0R/h6TqNO4TMlZOTsSMmDhDMKUoZDpeRHtw7ZwAk9 +IcoH+DVXA2L0ngyq8LNzJ8a3TsYUs1pVZNunTC2FAoGARe4x29tdri8akxxwF3BV +bjJ9qRUIfIDK8rGWRdw94vVCB7XVmSWCEchmLqA1DqGYvAhYMYjkXTg9akfBTUQS +s+8JasUuQam8Y88JAfC0QqGbLgUsh0TpRUOXj+YQuoNiMVu14NNgYgFkx71WtvAq +kUmkxr/moPcZ+O1ahVjv/Us= +-----END PRIVATE KEY----- +)%"; + +constexpr unsigned char public_key_8192[] = R"%(-----BEGIN PUBLIC KEY----- +MIIEIjANBgkqhkiG9w0BAQEFAAOCBA8AMIIECgKCBAEA43dhEnCSwC/hAAm9XrZc +t0QaC4bkoSifeiL00+U8xBbAuAYnZSQ4PBbEERnxIRLpgCf0b2SDvCXPJmXB6lKz +1W4vVcw2fVuG4Kmp5C9skhXeGXrXoEOgSxBp8VkWWWB1tbpKmKwdGnh6O0JOwWRq +wWxefR3J9EWBNTz0vvbjGxYWt0XfcCjULPuXuPVlrbZkTsdCl8ncWG8G/OjRNHr3 +f9eiWHb2gA+xsUJfP/iWlUmy+9MiEGBT+JMW2wZAOu7JgVbyZWXV/pkwodP1IVFy +c9RTexnC7Th3dDlT+HcAWXDCTuWwq2KhD/WUnfXO6uZEjyp3aJiJOa7Mceag0B9t +T9d1AZkXQs3PZYn+Sl1cF3hq6e8OoKHZsA95PAtNrXfDe7vkyICB4sp1Dkqg75Ws +arOTC45CWcr/woReqCDYRiSr0aQodlJmSvSIlOz5IAyB40nFzA3VgEgTgHJ4mU7O +QcsOSStmZNvk6Hl9xfUfJi9xTB/vwf7FKWe+IcDGdkU9WV3yJ6UTXPW4TQu25uTc +6WoK4Us8Lk2h7f+Tk2dsyt+VTDOma5qbwTT8U/NKSd8Se5ewS4++ERqaRikLmp0o +AciYxH00Wk3ZLPFUn9/svcA4BAwOCTkYjIQvW/gLDrYyu4qyrgkGXiNs0L2QYB7l +AUTOl5M0Bmez5Nl8SIdaVk4bj5D+4BbHrDZGLMkkP1KWRgNFOJOfLT3JgMPxDwme +n4qFs/HV/d4qh8kzyjITsBRQj1EloqqU+40WJ9X3mIEo7xQ1DPloQMWADDYrO/bX +fFtYnTtnJLV6ndTSdQz0yZ7ubROJFatPy1VQ6AiBeN24sq9iKm6NXuWjszS0paZ/ +JfO+QOTB9D3lVnFzV2yHRIboHNpm/X1eVeXDlzzVTobqVr4eN78bL4+VQiGQav1I +8ebv3Gx9+NO1qVfsWHC89I+hw9HSN69gEyCmeWwIuY8hMGfTBgERVKPkWhSPM0cU +JlkM+ER1Pngz93TR7tWPsp9tYaKMG4DVth0fINRSVsFFkeHu1CLeKboUW8QMoH+A +vPDVnSE2M3ecAeFz0yNXTwVtHbQT2YpYZ88gnYvYcRNdqeOvRKkrGUON0fDGvBnF +xxcuNzZVP2wBEUTkYljnLrJ5zR+uabPgrQiIhTVdDplRRZd6DVRh9s357L5b94tX +wtNAFfHT3AsU/C5p8O6taxckrxYMqNsOJ/6zYp7gUTTdb3e/V1c3xgi/x8+gUrUn +ivGZZdOaWUbSDZf1cz2kfQPsaiDDPVKL7goo0ZA5gnbjkhHUNpGmE5KauHek9rxO +2KX3dpjl3YJyHSmOKv7Lf46Uja0cNWbTh/0nHlG5xtbzBNGvxF90iGmHDmr+u6BT +zwIDAQAB +-----END PUBLIC KEY----- +)%"; + +constexpr unsigned char private_key_8192[] = R"%(-----BEGIN PRIVATE KEY----- +MIISQgIBADANBgkqhkiG9w0BAQEFAASCEiwwghIoAgEAAoIEAQDjd2EScJLAL+EA +Cb1etly3RBoLhuShKJ96IvTT5TzEFsC4BidlJDg8FsQRGfEhEumAJ/RvZIO8Jc8m +ZcHqUrPVbi9VzDZ9W4bgqankL2ySFd4ZetegQ6BLEGnxWRZZYHW1ukqYrB0aeHo7 +Qk7BZGrBbF59Hcn0RYE1PPS+9uMbFha3Rd9wKNQs+5e49WWttmROx0KXydxYbwb8 +6NE0evd/16JYdvaAD7GxQl8/+JaVSbL70yIQYFP4kxbbBkA67smBVvJlZdX+mTCh +0/UhUXJz1FN7GcLtOHd0OVP4dwBZcMJO5bCrYqEP9ZSd9c7q5kSPKndomIk5rsxx +5qDQH21P13UBmRdCzc9lif5KXVwXeGrp7w6godmwD3k8C02td8N7u+TIgIHiynUO +SqDvlaxqs5MLjkJZyv/ChF6oINhGJKvRpCh2UmZK9IiU7PkgDIHjScXMDdWASBOA +cniZTs5Byw5JK2Zk2+ToeX3F9R8mL3FMH+/B/sUpZ74hwMZ2RT1ZXfInpRNc9bhN +C7bm5NzpagrhSzwuTaHt/5OTZ2zK35VMM6ZrmpvBNPxT80pJ3xJ7l7BLj74RGppG +KQuanSgByJjEfTRaTdks8VSf3+y9wDgEDA4JORiMhC9b+AsOtjK7irKuCQZeI2zQ +vZBgHuUBRM6XkzQGZ7Pk2XxIh1pWThuPkP7gFsesNkYsySQ/UpZGA0U4k58tPcmA +w/EPCZ6fioWz8dX93iqHyTPKMhOwFFCPUSWiqpT7jRYn1feYgSjvFDUM+WhAxYAM +Nis79td8W1idO2cktXqd1NJ1DPTJnu5tE4kVq0/LVVDoCIF43biyr2Iqbo1e5aOz +NLSlpn8l875A5MH0PeVWcXNXbIdEhugc2mb9fV5V5cOXPNVOhupWvh43vxsvj5VC +IZBq/Ujx5u/cbH3407WpV+xYcLz0j6HD0dI3r2ATIKZ5bAi5jyEwZ9MGARFUo+Ra +FI8zRxQmWQz4RHU+eDP3dNHu1Y+yn21hoowbgNW2HR8g1FJWwUWR4e7UIt4puhRb +xAygf4C88NWdITYzd5wB4XPTI1dPBW0dtBPZilhnzyCdi9hxE12p469EqSsZQ43R +8Ma8GcXHFy43NlU/bAERRORiWOcusnnNH65ps+CtCIiFNV0OmVFFl3oNVGH2zfns +vlv3i1fC00AV8dPcCxT8Lmnw7q1rFySvFgyo2w4n/rNinuBRNN1vd79XVzfGCL/H +z6BStSeK8Zll05pZRtINl/VzPaR9A+xqIMM9UovuCijRkDmCduOSEdQ2kaYTkpq4 +d6T2vE7Ypfd2mOXdgnIdKY4q/st/jpSNrRw1ZtOH/SceUbnG1vME0a/EX3SIaYcO +av67oFPPAgMBAAECggQAfigr0opVGfp0FA1S1kDWU16WA2ahTzC0oozYtN0jQq5L +3MSs/M+F0O3feIymy+0tTELcsxtQZP2jUmyFjGyqCOm/nxpP7l7hA6GV9FTJJoyy +TfdvuBdJw9gqqgz69D8nic70qJBs482GHW+9Nk13WCe+kC4BYFVcQCa6p19OvisW +FjfOoOpEI16224JfDmVmZLrnGECA0RtjCMonna/FrUXvaJkyRfxuVR22rkg1XD8v +4bNL5UFH0UnjFz70SLs/T1jlv48njLlx248vGXeOvuc4FcJH9kGnHvLcu6VksDZ1 +zkReI+/j3HIcJy+5v1ZPGAg5ie1vzmpAQbvj3QpRGkMpReWenRKAwJQ0URJOjUXg +JjbMKhMaJSev2bl7L4aJCQtA7GM5posbOP3zHG4q3lMSbwpLinmoOD4qMZ1l1iFo +mjEtr9Iroc7WIaL82OWW9HRqG65gh3FyP389m+m1Q5BXMAW+GJpM7xLSywQUbp1J +fSsJUtL2juxW62l7qQTl7bbJI2vOvXQa78BbhNvSGjMSLboIerXb5aAmPU7TbAFt +UIIk/vEVCadVe0ooHah3G80Zng7vH5VdkyQYp3waQEL9V50JeDxNAzwl7zXGm8cM +SlJVRpBAKU725U9A8rvij1lxmEyxF20WYP+CH42C/Z0n57Fg3VyOzZJB+Af59nr3 +43o0nwpFVB8BkqxHXmB9sYD+G+hTeGfzytmR6JGMJPT5RBcNfjYjlfe05pzg22Iq +mOAy8b0ceHgAWUjfvU/xl9J7RqPCT8RM4ZbQloO5hmmPV6Zt6KMZKEzN/f3nRFro +NUEr6NRqIFYL0DTzlK9dQqOR5Ep5VKo1MoqOJ+AlfyXpcUcTkbAGXJgE2pDYg7jp +FpP33SgAUT/hhbDTgBY6Xd80gGn9xzPZDd3pzW4fSIkDxz4o/GX73lbRrjeCkQ6i +Txv1/dI9GjLUk0+iD+ZRFA48PghBb8YZvuVaMnNbPh2wH0nsGN4P4rosrLogETuM +z2SY22EknApNU3X1XjivqhJYgekpdZAnSJFgSB0eHnc4fU/e5esJvGnTKhrwkwst +t26ZU4rWqNLp5wIvFiUuwwvcWhKDOgYvJpA5I/AoOt7qT6zfj1gha0Y4Gt50eWBE +L5ramkxKMDnFTycxVu9B6R9r96EBT1yVny3bDJw8tFcgTmVQJSXO5B6L5vuLZwiC +MLZi+CsV+d2w0DTKUh651++OBPY63ir7OfBwHdqNXYxEkC1v7b9QKvMxmSdt1jeG +9C7vcT6YxeREbgLr7fZAXw/09rptLj6c8cMLWYmxVcv+Nq8kW7M9zDZ0i7+eGT81 +985boD7Sp5UWaCaGJhaVE/73XoZ9u59zWoF8x3EJIQKCAgEA9HVjuYVhsRyUGyr2 +GxvaJNN3kvY0ax5nReAkekGWypZlnuFbIj8mtDtu5gcXw0pI2gAPlKHdoX05zCRz +eIJh+Yq1oDAYqG4n7kph8Kn1kK1SYaJXGnFAZUQ203n/ltiYPYwI9wdXuYhO7wsS +R81Hdl9CIjiA0vNz7TmNQlfHFEtIckP+8SloSiXOQSMo1BZs+aRXSJ5qNdpBWgx+ +5FlHkroxtbYuA1c10+RSgVxWIH6qA1WP8k+8JeQiXuJpu2Ct3Il4dZMs1VJeKzwZ +Q5XoA5W6tpvhHx5RJ4dU7lmiPZQNaAHXrU8FXrB/cFUiFJH+2Xzrs5gBKudfjcSE +wFI1yJBRgW4gadhhjGvGxlsDdkSh/B8WlBhhy2hofpWL+jH205bMlyYv2wRUEYK7 +APsW4TEBURNV/Xo+h0upwdYBRrRHVjPU+ovYrPWJV9tYqvoyeru5K4v9Bwp99FsX +Rh3olGT3x/2kC4eccaSugBcyvkna+RgJlJDe/rqIIoSaI/oembATl3m56S2DlEoB +hOdepj5zKDDEknnEEdO96C1Un5PZ7cYPcqwRct51diJN5+o0h2BO7EwHb77cwvPQ +oBDQE52abwMvu7pHOw3F1+W8B6y7JuLSvCa1LO12ol/unFfwHGer24iAXgt8aqZd +BEDwEgihhJ5TmGTT+9dO3XZZGW0CggIBAO40nZ7gZFDqtiiEIrEqoI9Lu9hequ8z +PYnHAwwSThY4+Pc2FyCtelgY1Lg1TM1eBrH0AhFXdSa6N0fAGePCFi2BlVq8X8VK +nD2fNSdY+4BDyWVn/WsMh84inIPSFzfkq+Nj+b5bSlqWhp7lb60/uS3GOOb6fYkR +dziuc0uFsZJbQDXg5BdUdUHaE3Ooby0MdCk9y0dE9dIR/tNCxhpO6Dd+nl2FjLYF +6/BKAtdsBBCKnmWxeOYwCzExvMdRu7mJnmpVGLowjUh9NBR7Ezt3Jvti9OQ1lI3k +HscK7hrHzM1u7tRlxl9lq/X6vQqOaZurtza1HyMdFDgGq3jBAMRjYstjwgxma6AT ++cEJiWy+eVIWHmahgfgEpYXyf9z066n7MiXHDMgaLjdFROSbj84/DHtJkJaqBbIc +YCdsnfXakGyD0PZdT7lUfy4bvDOGI0H7RvcKMfRkPnBokDsFfYFNmQoCj9O0mf2T +sPKpOzM4gGz03zpNvE8b6fBEUNUz4JBMEOc5GLAS8WCrH1iVAu2Mw0jAi4VmCvhR +fsnr/HaLkRPBKNcsz1asZZbHqb9u9fEL6ZYk9txEDHgiduHyxPdytPAz6F2E3Eu0 +98rxjCe/k4qluV9kGOlDsYKa+f50r7W6E51QfHQLAYpCqtbp2WTj4P8q3nlQ06Bm +M+9E0uMzbrirAoICAF1O3WS3w6Utyl5gVJXeWLKLwO1oanOkpDioqGO920eyhlFR +pU56GlTbBqZoeKqDFTGYqlnKOuVj/gastyJ9adYtGsxs70yC11z+KUoKJYA2l+ZK +Z8LhDXpZwi+QNn2maN29MMLRm6tmmvJlIHIlqaxGCeEz/gAHCu22dPOou4VEgv+S +cqIscvEyYvq7596kPK5BC0vdo56wkxdDA8A3T7lytnysb/24cQRS9ycHTpySnGQv +aYVM5/zyiif7de4epd4y3rbKGWfHS8hm5SHF+0w6/4yqDRCqqsFSx5k+v02P0Fot +sdwl+F+/MLV42UxOuZ7cLr9bOr7cl71uEFm0R3EpnOKxXU/pVrqZfMLDhJvE8Kti +VmTqtZFFZfVDMa2rGpKC0c6ztbp8eXZBlw11ybLk2KLQpZbd7TYJLF+fRtdtAnml +yRpk/KxwAB93yu1gGJp+QtybT1Y7q/30MvsBeYAC1g0RBGeeOJmsCSs9L5IwcJN5 +mFaLwYIrQsEiKg+nbbyt15yOyuZ1B+83HENVaOw9lAj4LF/YeH1xe+A+RTmv3pQC +cG0Nvo9A2EbiKyhlXe16VkWdc400peEH3U7re/CwzHypE7QtEvk4dZbFyrKHPNxH +4bYNdEQU056AzXwBmNXOwGtIO+8ppTC0FXcFLl1DzBrpr/DQM5XCBglEHhg1AoIC +AFWj4Q9fyXE2EWubpgVgN/2M0upFjtsU5wkD3dqXMi/XJ9tpPQNom1XVB5V6xDQJ +nAqamau2b84OoRVQwX4bJ3IQ5quKkjwSSP32oVuWKEXDGUM2EexMwv6ffvn9rI9R +zWKhbQa9N4w+FgRGpNH62Q7V91tDr6J5/w0H2zfJxz/BQuKcCiVBHi8gwmGQqvfd +RF4Xc2AaMO7nvWAi36pRuDdLdJBXFXHTyzHGyiK9GPEBhVU2aysHFt8G7MIUZpOc +ILJGCe/WyNTI/tJmNVHp0sAKodTyVoh0/YO+MEC8mKs7OO5v8NQXb62uCg0jimCH +agVnNNyg9cX2z+tIKIhy2vAY24ktwX/57o8yaJAKIwAaJ6/qXRnYQdJYjxPXkmq4 +fx0J5VSD5R3F77DpJNiX3lrs5ejlE8snXIKQEHJ1s/rvoU8R2TneYSMooY88qKxu +NONYbQFakQBE96XgoXC9f0oUBbWtdreuQ63anggaRkHl/+OsUwl2FbNmPFGKpy/5 +yRH4eyHCjbmdjFWCrVzOgN9FKmQ5fbQtSJI8H7ZXEz+w8If7+kdFD/kXq7XBpPaW +u9JZU895P6ppaahuadY1DUxWvTHyNGmblIMIOMWJoPf2ASGEkVg8GDPGmB6dwRZq +4eZrK3NlCZa1xUojJR+atifHN9kR8CP42q8pZVB+C06lAoICAQDkowYg/d2KXdgs +mYJTOoE4XA/2lGrF9k4dKcqgljThVAP8NiUW4u+874Y8Xq0hPD/KaatfomcaQZUX +u62kz7Jd3OkRxYieG95c0vRlad9xxyrlre7slCsG9jNungMF3P/TyC767kOBgkEJ +dWtQB1veFGE0fcU34Y7OrPijfmECBuxmtGwO2l3LAW86PBFWVCUCnYhQVwjBYzGE +IpZazNG+xoeoLWrSg1ehOTTZZqgQrVVrIMHLKV1bM9NFv2qBx8wSPfQKgtpA1LE3 +0Ssbepqv/N5WRXzSJ/maWG4BF0qpXaYti0TGDv8iqN1FXcYIBL4BXCgLzJlMmg2I +K+V38fXkhHULlGcvu/9rA3m1OWhs1pPrUTb7v5afHdyd9zSteof5QIsk6CaaiWxg +TW0M6vXmSoiUWIbKl71TCCsSVSJ82ZA9Wn0xtITqaFz9oqiaQbV+Cj9VzX332zGM +Y2dEck/00yPVI30T+ycwX3xN3jnpusWv/omDKQ/UMrBXPzzJpIzAaRnfm13sZcvh +FsKX4nSNK6rJ1uONSJu+dIiOA1YCabhCab7NRMCn+Bj+fHHkpvir24QbmqX1R8Au +415sPjVLQr2FNiHaXDzwiCS0jdNhTLGV8xaykv8bKAzH19lNfqZcZStwSP2DoQ8O +2vrUYCC8gWblftYH+yTWu2bQ4Y1pDw== +-----END PRIVATE KEY----- +)%"; + +} // namespace test +} // namespace mysql +} // namespace boost + +#endif diff --git a/test/unit/test/sansio/handshake/handshake_mnp.cpp b/test/unit/test/sansio/handshake/handshake_mnp.cpp index d1acd077e..a40da24ad 100644 --- a/test/unit/test/sansio/handshake/handshake_mnp.cpp +++ b/test/unit/test/sansio/handshake/handshake_mnp.cpp @@ -29,8 +29,8 @@ BOOST_AUTO_TEST_CASE(ok) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(create_ok_frame(2, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -46,8 +46,8 @@ BOOST_AUTO_TEST_CASE(err) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) .expect_read(err_builder() .seqnum(2) .code(common_server_errc::er_access_denied_error) @@ -65,14 +65,13 @@ BOOST_AUTO_TEST_CASE(authswitch_ok) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_challenge)) - .expect_write(create_frame(3, mnp_response)) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() + ) + .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_scramble)) + .expect_write(create_frame(3, mnp_hash)) .expect_read(create_ok_frame(4, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_capabilities(min_caps) @@ -90,10 +89,10 @@ BOOST_AUTO_TEST_CASE(mnp_tls) // Run the test algo_test() - .expect_read(server_hello_builder().caps(tls_caps).auth_data(mnp_challenge).build()) + .expect_read(server_hello_builder().caps(tls_caps).auth_data(mnp_scramble).build()) .expect_write(create_ssl_request()) .expect_ssl_handshake() - .expect_write(login_request_builder().seqnum(2).caps(tls_caps).auth_response(mnp_response).build()) + .expect_write(login_request_builder().seqnum(2).caps(tls_caps).auth_response(mnp_hash).build()) .expect_read(create_ok_frame(3, ok_builder().build())) .will_set_status(connection_status::ready) .will_set_tls_active(true) @@ -111,9 +110,9 @@ BOOST_AUTO_TEST_CASE(moredata) // Run the test algo_test() - .expect_read(server_hello_builder().auth_data(mnp_challenge).build()) - .expect_write(login_request_builder().auth_response(mnp_response).build()) - .expect_read(create_more_data_frame(2, mnp_challenge)) + .expect_read(server_hello_builder().auth_data(mnp_scramble).build()) + .expect_write(login_request_builder().auth_response(mnp_hash).build()) + .expect_read(create_more_data_frame(2, mnp_scramble)) .check(fix, client_errc::bad_handshake_packet_type); } @@ -125,15 +124,14 @@ BOOST_AUTO_TEST_CASE(authswitch_moredata) // Run the test algo_test() .expect_read( - server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_challenge).build() + server_hello_builder().auth_plugin("caching_sha2_password").auth_data(csha2p_scramble).build() + ) + .expect_write( + login_request_builder().auth_plugin("caching_sha2_password").auth_response(csha2p_hash).build() ) - .expect_write(login_request_builder() - .auth_plugin("caching_sha2_password") - .auth_response(csha2p_response) - .build()) - .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_challenge)) - .expect_write(create_frame(3, mnp_response)) - .expect_read(create_more_data_frame(4, mnp_challenge)) + .expect_read(create_auth_switch_frame(2, "mysql_native_password", mnp_scramble)) + .expect_write(create_frame(3, mnp_hash)) + .expect_read(create_more_data_frame(4, mnp_scramble)) .check(fix, client_errc::bad_handshake_packet_type); } diff --git a/test/unit/test/sansio/handshake/handshake_mnp_hash_password.cpp b/test/unit/test/sansio/handshake/handshake_mnp_hash_password.cpp index ed1dbbcec..214813846 100644 --- a/test/unit/test/sansio/handshake/handshake_mnp_hash_password.cpp +++ b/test/unit/test/sansio/handshake/handshake_mnp_hash_password.cpp @@ -10,7 +10,6 @@ #include #include "test_common/assert_buffer_equals.hpp" -#include "test_common/printing.hpp" using namespace boost::mysql; using namespace boost::mysql::test; @@ -21,45 +20,24 @@ namespace { BOOST_AUTO_TEST_SUITE(test_handshake_mnp_hash_password) // Values snooped using Wireshark -constexpr std::uint8_t challenge[20] = { +constexpr std::uint8_t scramble[20] = { 0x79, 0x64, 0x3d, 0x12, 0x1d, 0x71, 0x74, 0x47, 0x5f, 0x48, 0x3e, 0x3e, 0x0b, 0x62, 0x0a, 0x03, 0x3d, 0x27, 0x3a, 0x4c, }; -constexpr std::uint8_t expected[20] = { - 0xf1, 0xb2, 0xfb, 0x1c, 0x8d, 0xe7, 0x5d, 0xb8, 0xeb, 0xa8, - 0x12, 0x6a, 0xd1, 0x0f, 0xe9, 0xb1, 0x10, 0x50, 0xd4, 0x28, -}; BOOST_AUTO_TEST_CASE(nonempty_password) { - auto res = mnp_hash_password("root", challenge); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(res.value(), expected); -} + constexpr std::uint8_t expected_hash[20] = { + 0xf1, 0xb2, 0xfb, 0x1c, 0x8d, 0xe7, 0x5d, 0xb8, 0xeb, 0xa8, + 0x12, 0x6a, 0xd1, 0x0f, 0xe9, 0xb1, 0x10, 0x50, 0xd4, 0x28, + }; -BOOST_AUTO_TEST_CASE(empty_password) -{ - auto res = mnp_hash_password("", challenge); - BOOST_MYSQL_ASSERT_BUFFER_EQUALS(res.value(), std::vector()); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(mnp_hash_password("root", scramble), expected_hash); } -BOOST_AUTO_TEST_CASE(bad_challenge_length_nonempty_password) -{ - constexpr std::uint8_t bad_challenge[] = {0x01, 0x02, 0x03}; - auto res = mnp_hash_password("root", bad_challenge); - BOOST_TEST(res.error() == client_errc::protocol_value_error); -} - -BOOST_AUTO_TEST_CASE(bad_challenge_length_empty_password) -{ - constexpr std::uint8_t bad_challenge[] = {0x01, 0x02, 0x03}; - auto res = mnp_hash_password("", bad_challenge); - BOOST_TEST(res.error() == client_errc::protocol_value_error); -} - -BOOST_AUTO_TEST_CASE(empty_challenge) +BOOST_AUTO_TEST_CASE(empty_password) { - auto res = mnp_hash_password("root", {}); - BOOST_TEST(res.error() == client_errc::protocol_value_error); + BOOST_MYSQL_ASSERT_BUFFER_EQUALS(mnp_hash_password("", scramble), std::vector()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test_csha2p_encrypt_password_errors.cpp b/test/unit/test_csha2p_encrypt_password_errors.cpp new file mode 100644 index 000000000..cf4425267 --- /dev/null +++ b/test/unit/test_csha2p_encrypt_password_errors.cpp @@ -0,0 +1,259 @@ +// +// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#define BOOST_TEST_MODULE test_csha2p_encrypt_password_errors + +#include + +#include +#include +#include +#include + +#include +#include + +using namespace boost::mysql; +using detail::csha2p_encrypt_password; + +// Contains tests that need mocking OpenSSL functions. +// We do this at link time, by defining the functions declared in OpenSSL headers here +// and not linking to libssl/libcrypto. +// These tests cover cases that can't be covered directly by the unit tests using the real OpenSSL. +// Try to put as few tests here as possible. +// Some of the mocked functions are macros in OpenSSL versions before 3, so this setup can't work + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + +namespace { + +// If we use asio::error::ssl_category, many more other OpenSSL functions +// become used, and mocking becomes problematic. +class mock_ssl_category final : public boost::system::error_category +{ +public: + const char* name() const noexcept override { return "mock_ssl"; } + std::string message(int) const override { return {}; } +} ssl_category; + +constexpr std::uint8_t scramble[20]{}; + +using vector_type = boost::container::small_vector; + +struct +{ + // Number of times that each function has been called. + // Tracking this helps us to check that we're actually covering the case we want + std::size_t BIO_new_mem_buf_calls{}; + std::size_t PEM_read_bio_PUBKEY_calls{}; + std::size_t EVP_PKEY_CTX_new_calls{}; + std::size_t EVP_PKEY_encrypt_init_calls{}; + std::size_t EVP_PKEY_CTX_set_rsa_padding_calls{}; + std::size_t EVP_PKEY_get_size_calls{}; + std::size_t EVP_PKEY_encrypt_calls{}; + + BIO* bio{reinterpret_cast(static_cast(100))}; + EVP_PKEY* key{reinterpret_cast(static_cast(200))}; + EVP_PKEY_CTX* ctx{reinterpret_cast(static_cast(300))}; + int set_rsa_padding_result{1}; + int get_size_result{256}; + std::size_t actual_ciphertext_size{256u}; + unsigned long last_error{0u}; + +} openssl_mock; + +BOOST_AUTO_TEST_CASE(error_creating_bio) +{ + // Setup + openssl_mock = {}; + openssl_mock.bio = nullptr; + openssl_mock.last_error = 42u; + vector_type out; + + // Call the function + auto ec = csha2p_encrypt_password("passwd", scramble, {}, out, ssl_category); + + // Check + BOOST_TEST(ec == error_code(42, ssl_category)); + BOOST_TEST(ec.has_location()); + BOOST_TEST(openssl_mock.BIO_new_mem_buf_calls == 1u); + BOOST_TEST(openssl_mock.PEM_read_bio_PUBKEY_calls == 0u); +} + +BOOST_AUTO_TEST_CASE(error_creating_pkey_ctx) +{ + // Setup + openssl_mock = {}; + openssl_mock.ctx = nullptr; + openssl_mock.last_error = 42u; + vector_type out; + + // Call the function + auto ec = csha2p_encrypt_password("passwd", scramble, {}, out, ssl_category); + + // Check + BOOST_TEST(ec == error_code(42, ssl_category)); + BOOST_TEST(ec.has_location()); + BOOST_TEST(openssl_mock.EVP_PKEY_CTX_new_calls == 1u); + BOOST_TEST(openssl_mock.EVP_PKEY_encrypt_init_calls == 0u); +} + +BOOST_AUTO_TEST_CASE(error_setting_rsa_padding) +{ + // Setup. The return value should be != -2, which indicates + // operation not supported and is handled separately + openssl_mock = {}; + openssl_mock.set_rsa_padding_result = -1; + openssl_mock.last_error = 42u; + vector_type out; + + // Call the function + auto ec = csha2p_encrypt_password("passwd", scramble, {}, out, ssl_category); + + // Check + BOOST_TEST(ec == error_code(42, ssl_category)); + BOOST_TEST(ec.has_location()); + BOOST_TEST(openssl_mock.EVP_PKEY_CTX_set_rsa_padding_calls == 1u); + BOOST_TEST(openssl_mock.EVP_PKEY_encrypt_calls == 0u); +} + +// Getting a zero size as max buffer size might happen in theory (although it shouldn't for RSA) +BOOST_AUTO_TEST_CASE(get_size_zero) +{ + // Setup + openssl_mock = {}; + openssl_mock.get_size_result = 0; + openssl_mock.last_error = 42u; + vector_type out; + + // Call the function + auto ec = csha2p_encrypt_password("passwd", scramble, {}, out, ssl_category); + + // Check + BOOST_TEST(ec == error_code(42, ssl_category)); + BOOST_TEST(ec.has_location()); + BOOST_TEST(openssl_mock.EVP_PKEY_get_size_calls == 1u); + BOOST_TEST(openssl_mock.EVP_PKEY_encrypt_calls == 0u); +} + +// In theory, the encryption function may communicate that it didn't use all the bytes +// in the buffer. This shouldn't happen in RSA, but we handle the case anyway +BOOST_AUTO_TEST_CASE(encrypt_actual_size_lt_max_size) +{ + // Setup + openssl_mock = {}; + openssl_mock.get_size_result = 256; + openssl_mock.actual_ciphertext_size = 200u; + vector_type out; + + // Call the function + auto ec = csha2p_encrypt_password("passwd", scramble, {}, out, ssl_category); + + // Check + BOOST_TEST(ec == error_code()); + BOOST_TEST(out.size() == 200u); +} + +// OpenSSL functions might fail without adding an error to the stack. +// If that's the case, the operation must still fail +BOOST_AUTO_TEST_CASE(error_code_zero) +{ + // Setup. The return value should be != -2, which indicates + // operation not supported and is handled separately + openssl_mock = {}; + openssl_mock.set_rsa_padding_result = -1; + vector_type out; + + // Call the function + auto ec = csha2p_encrypt_password("passwd", scramble, {}, out, ssl_category); + + // Check + BOOST_TEST(ec == error_code(client_errc::unknown_openssl_error)); + BOOST_TEST(ec.has_location()); + BOOST_TEST(openssl_mock.EVP_PKEY_CTX_set_rsa_padding_calls == 1u); + BOOST_TEST(openssl_mock.EVP_PKEY_encrypt_calls == 0u); +} + +// OpenSSL 3+ might report system errors represented as codes > 0x80000000 +BOOST_AUTO_TEST_CASE(error_code_system) +{ + // Setup. The return value should be != -2, which indicates + // operation not supported and is handled separately + openssl_mock = {}; + openssl_mock.set_rsa_padding_result = -1; + openssl_mock.last_error = 0x800000ab; + vector_type out; + + // Call the function + auto ec = csha2p_encrypt_password("passwd", scramble, {}, out, ssl_category); + + // Check + BOOST_TEST(ec.failed()); + BOOST_TEST(ec.has_location()); + BOOST_TEST((ec.category() == boost::system::system_category())); + BOOST_TEST(openssl_mock.EVP_PKEY_CTX_set_rsa_padding_calls == 1u); + BOOST_TEST(openssl_mock.EVP_PKEY_encrypt_calls == 0u); +} + +} // namespace + +// Implement the OpenSSL functions +BIO* BIO_new_mem_buf(const void*, int) +{ + ++openssl_mock.BIO_new_mem_buf_calls; + return openssl_mock.bio; +} +int BIO_free(BIO*) { return 0; } + +EVP_PKEY* PEM_read_bio_PUBKEY(BIO* bio, EVP_PKEY**, pem_password_cb*, void*) +{ + ++openssl_mock.PEM_read_bio_PUBKEY_calls; + BOOST_TEST(bio == openssl_mock.bio); + return openssl_mock.key; +} +void EVP_PKEY_free(EVP_PKEY*) {} + +EVP_PKEY_CTX* EVP_PKEY_CTX_new(EVP_PKEY* pkey, ENGINE*) +{ + ++openssl_mock.EVP_PKEY_CTX_new_calls; + BOOST_TEST(pkey == openssl_mock.key); + return openssl_mock.ctx; +} +void EVP_PKEY_CTX_free(EVP_PKEY_CTX*) {} +int EVP_PKEY_encrypt_init(EVP_PKEY_CTX* ctx) +{ + ++openssl_mock.EVP_PKEY_encrypt_init_calls; + BOOST_TEST(ctx == openssl_mock.ctx); + return 1; +} +int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX* ctx, int) +{ + ++openssl_mock.EVP_PKEY_CTX_set_rsa_padding_calls; + BOOST_TEST(ctx == openssl_mock.ctx); + return openssl_mock.set_rsa_padding_result; +} + +int EVP_PKEY_get_size(const EVP_PKEY* pkey) +{ + ++openssl_mock.EVP_PKEY_get_size_calls; + BOOST_TEST(pkey == openssl_mock.key); + return openssl_mock.get_size_result; +} +int EVP_PKEY_encrypt(EVP_PKEY_CTX* ctx, unsigned char*, size_t* actual_size, const unsigned char*, size_t) +{ + ++openssl_mock.EVP_PKEY_encrypt_calls; + BOOST_TEST(ctx == openssl_mock.ctx); + if (actual_size) + *actual_size = openssl_mock.actual_ciphertext_size; + return 1; +} + +unsigned long ERR_get_error() { return openssl_mock.last_error; } + +#else +BOOST_AUTO_TEST_CASE(dummy) {} +#endif diff --git a/tools/scripts/build_unix_local.sh b/tools/scripts/build_unix_local.sh index 7a6e5a286..46e110a5d 100755 --- a/tools/scripts/build_unix_local.sh +++ b/tools/scripts/build_unix_local.sh @@ -10,11 +10,11 @@ set -e repo_base=$(realpath $(dirname $(realpath $0))/../..) -BK=b2 -IMAGE=build-clang11 +BK=cmake +IMAGE=build-cmake3_8 SHA=e9696175806589fc91e160399eedeac8fdc88c68 CONTAINER=builder-$IMAGE -FULL_IMAGE=bench +FULL_IMAGE=ghcr.io/anarthal-containers/$IMAGE:$SHA DB=mysql-8.4.1 docker start $DB || docker run -d \ @@ -50,8 +50,8 @@ case $BK in cmake) cmd="$db_args --cmake-build-type=Debug --build-shared-libs=1 - --cxxstd=20 - --install-test=1 + --cxxstd=11 + --install-test=0 " ;; diff --git a/tools/scripts/file_headers.py b/tools/scripts/file_headers.py index 8bcd06bcf..df7f42c95 100755 --- a/tools/scripts/file_headers.py +++ b/tools/scripts/file_headers.py @@ -277,7 +277,7 @@ def verify_test_consistency(): for test_type in ('unit', 'integration'): for ftocheck in ('Jamfile', 'CMakeLists.txt'): base_path = path.join(REPO_BASE, 'test', test_type) - tests = glob.glob(base_path + '/**/*.cpp', recursive=True) + tests = glob.glob(base_path + '/test/**/*.cpp', recursive=True) tests = [elm.replace(base_path + '/', '') for elm in tests] with open(path.join(REPO_BASE, 'test', test_type, ftocheck), 'rt') as f: