Skip to content

caching_sha2_password can now be used without TLS #480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 69 commits into from
Jun 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
080ada8
Initial impl
anarthal May 9, 2025
a7a0b74
Fix integration tests
anarthal May 9, 2025
0a3ee27
simplify integration tests
anarthal May 9, 2025
8804521
Fixed scramble size
anarthal May 9, 2025
063d46a
update challenge => scramble terminology
anarthal May 9, 2025
a1eb3ff
remove assertion
anarthal May 9, 2025
9bfd88b
Move encryption function to a separate file
anarthal May 9, 2025
b26efd6
Prototype to test openssl edge cases
anarthal May 9, 2025
17cc582
Fixed regression in csha2p hashing
anarthal May 10, 2025
4ae5fb9
unit tests now build again
anarthal May 10, 2025
6514182
test cases for encrypt
anarthal May 10, 2025
b206dcf
unit test strategy
anarthal May 11, 2025
36d53d9
encrypt success cases 1
anarthal May 30, 2025
fc3b258
8192 key
anarthal May 30, 2025
07eaee8
all characters test
anarthal May 30, 2025
dbefe00
Sanitize translate_openssl_error
anarthal May 30, 2025
b74b234
empty key buffer test
anarthal May 30, 2025
a5b60db
key errors
anarthal May 30, 2025
0b9a192
empty password
anarthal May 30, 2025
d9bcb58
proper handling of EVP_PKEY_CTX_set_rsa_padding failure
anarthal Jun 1, 2025
985b5a5
make success cases use parametric tests
anarthal Jun 1, 2025
6b21e68
integration test
anarthal Jun 1, 2025
9dc5abc
fix mocking tests
anarthal Jun 1, 2025
380dd60
use Boost.Test
anarthal Jun 1, 2025
e3cb4d1
track num calls
anarthal Jun 1, 2025
463d2d5
more edge cases
anarthal Jun 1, 2025
1e7f38a
refactor and fixes
anarthal Jun 1, 2025
ae32f4f
finished mocking tests
anarthal Jun 1, 2025
193664a
Added algo_test::expect_any_write
anarthal Jun 2, 2025
efef293
first handshake_csha2p test
anarthal Jun 2, 2025
88a79cd
fullauth key error
anarthal Jun 2, 2025
be7023d
fullauth error
anarthal Jun 2, 2025
c0638a2
fullauth_encrypterror
anarthal Jun 2, 2025
20d9227
Finished handshake tests
anarthal Jun 2, 2025
c2cc560
Docs
anarthal Jun 2, 2025
245c878
Properly add the Boost.Container dependency
anarthal Jun 2, 2025
1a197e1
proper cmake for boost_mysql_test_csha2p_encrypt_password_errors
anarthal Jun 2, 2025
ffd20d8
B2 prototype
anarthal Jun 2, 2025
fa4ff38
mock test cleanup
anarthal Jun 2, 2025
397255a
todo cleanup
anarthal Jun 2, 2025
5eb1f1c
split hash/encrypt password tests
anarthal Jun 2, 2025
16433dd
add source info checks
anarthal Jun 2, 2025
03a96a1
remove spurius includes
anarthal Jun 2, 2025
2859abe
Remove problematic openssl/types.h include
anarthal Jun 3, 2025
cc247cb
Fixed a memory corruption
anarthal Jun 3, 2025
3d7b42a
Add client_errc::unknown_openssl_error
anarthal Jun 3, 2025
a70479e
make things fail even if openssl does not provide extra error info
anarthal Jun 3, 2025
dd2e4fa
Use EVP_PKEY_size for compatibility
anarthal Jun 3, 2025
af8b222
test system errors
anarthal Jun 3, 2025
f9c1e29
remove unnecessary static_buffer::resize
anarthal Jun 3, 2025
47fd214
Remove TODO
anarthal Jun 3, 2025
426e34f
Disable problematic PFR check
anarthal Jun 3, 2025
153c226
remove Jamfile comment
anarthal Jun 3, 2025
aa32bd3
rephrase docs
anarthal Jun 3, 2025
a2faaad
fix edge case with scramble sizes
anarthal Jun 3, 2025
2370f8a
Recover build_unix_local structure
anarthal Jun 3, 2025
cb0ac8c
restrict the errors test to openssl 3+
anarthal Jun 3, 2025
969ef52
fix span conversion problems
anarthal Jun 3, 2025
170144b
introduce size check and simplify
anarthal Jun 3, 2025
209aeee
relax check in key_not_rsa
anarthal Jun 3, 2025
1ce0d52
correct the check and add tests
anarthal Jun 3, 2025
54fa0d8
fix unreliable test across openssl versions
anarthal Jun 3, 2025
0e840e9
fix narrowing conversion
anarthal Jun 4, 2025
09de831
Disable PFR problematic tests
anarthal Jun 4, 2025
dbfd8ab
Fix narrowing conversion warning
anarthal Jun 4, 2025
11ad921
Fixed PFR problem
anarthal Jun 4, 2025
a045836
Workaround PFR MSVC problem
anarthal Jun 5, 2025
e774a8c
Add missing test
anarthal Jun 6, 2025
dfb962a
Merge branch 'feature/313-csha2p-plaintext' of github.com:boostorg/my…
anarthal Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ target_link_libraries(
Boost::charconv
Boost::compat
Boost::config
Boost::container
Boost::core
Boost::describe
Boost::endian
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions build.jam
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions doc/qbk/05_connection_establishment.qbk
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion include/boost/mysql/client_errc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,

/**
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions include/boost/mysql/impl/error_categories.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<unknown MySQL client error>";
}
}
Expand Down
29 changes: 29 additions & 0 deletions include/boost/mysql/impl/internal/sansio/auth_plugin_common.hpp
Original file line number Diff line number Diff line change
@@ -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 <boost/config.hpp>

#include <cstddef>

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
96 changes: 65 additions & 31 deletions include/boost/mysql/impl/internal/sansio/caching_sha2_password.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
#include <boost/mysql/impl/internal/protocol/impl/protocol_types.hpp>
#include <boost/mysql/impl/internal/protocol/impl/serialization_context.hpp>
#include <boost/mysql/impl/internal/protocol/static_buffer.hpp>
#include <boost/mysql/impl/internal/sansio/auth_plugin_common.hpp>
#include <boost/mysql/impl/internal/sansio/connection_state_data.hpp>
#include <boost/mysql/impl/internal/sansio/csha2p_encrypt_password.hpp>

#include <boost/asio/ssl/error.hpp>
#include <boost/container/small_vector.hpp>
#include <boost/core/span.hpp>
#include <boost/system/result.hpp>
#include <boost/system/system_category.hpp>

#include <array>
#include <cstddef>
#include <cstdint>
#include <openssl/sha.h>

Expand All @@ -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<const std::uint8_t, csha2p_challenge_length> challenge,
span<std::uint8_t, csha2p_response_length> output
span<const std::uint8_t, scramble_size> scramble,
span<std::uint8_t, csha2p_hash_size> 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<std::uint8_t, csha2p_response_length> password_sha;
std::array<std::uint8_t, csha2p_hash_size> password_sha;
SHA256(reinterpret_cast<const unsigned char*>(password.data()), password.size(), password_sha.data());

// SHA(password_sha) concat challenge = buffer
std::array<std::uint8_t, csha2p_response_length + csha2p_challenge_length> buffer;
// SHA(password_sha) concat scramble = buffer
std::array<std::uint8_t, csha2p_hash_size + scramble_size> 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<std::uint8_t, csha2p_response_length> salted_password;
// SHA(SHA(password_sha) concat scramble) = SHA(buffer) = salted_password
std::array<std::uint8_t, csha2p_hash_size> 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<static_buffer<32>> csha2p_hash_password(
inline static_buffer<max_hash_size> csha2p_hash_password(
string_view password,
span<const std::uint8_t> challenge
span<const std::uint8_t, scramble_size> 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<max_hash_size> res(csha2p_hash_size);
csha2p_hash_password_impl(
password,
span<const std::uint8_t, csha2p_challenge_length>(challenge),
span<std::uint8_t, csha2p_response_length>(res.data(), csha2p_response_length)
scramble,
span<std::uint8_t, csha2p_hash_size>(res.data(), csha2p_hash_size)
);
return res;
}
Expand All @@ -106,13 +106,32 @@ 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<const std::uint8_t, scramble_size> scramble,
span<const std::uint8_t> server_key
)
{
container::small_vector<std::uint8_t, 512> 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<const char*>(buff.data()), buff.size())},
seqnum
);
}

public:
csha2p_algo() = default;

next_action resume(
connection_state_data& st,
span<const std::uint8_t> server_data,
string_view password,
span<const std::uint8_t, scramble_size> scramble,
bool secure_channel,
std::uint8_t& seqnum
)
Expand All @@ -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
{
Expand Down
Loading