From 172e54f824b24908f6018fa68a30fcc10d0984e8 Mon Sep 17 00:00:00 2001 From: Kamil Holubicki Date: Mon, 6 Jul 2026 11:27:38 +0200 Subject: [PATCH 1/2] PBS-32: handle auth method switch negotiation https://perconadev.atlassian.net/browse/PBS-32 Implement AuthSwitchRequest handling when the client replies with an authentication plugin that differs from the server account plugin. The server now sends the switch request, reads the client auth switch response, updates the connection context, and continues normal authentication. Format the auth switch plugin data through a helper so caching_sha2_password keeps the historical trailing NULL filler expected by MySQL clients, while leaving future plugins free to define their own payload shape. --- src/minimysql/connection_context.cpp | 49 ++++++++++++++++++++++++++++ src/minimysql/connection_context.hpp | 4 +++ src/minimysql/network_service.cpp | 24 ++++++++++++-- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/minimysql/connection_context.cpp b/src/minimysql/connection_context.cpp index f859bac..a459668 100644 --- a/src/minimysql/connection_context.cpp +++ b/src/minimysql/connection_context.cpp @@ -178,6 +178,43 @@ void connection_context::parse_client_greeting( client_attributes_ = client_greeting.attributes(); } +[[nodiscard]] network_buffer_type +connection_context::generate_encoded_auth_method_switch() { + std::string result_buffer{}; + + classic_protocol::message::server::AuthMethodSwitch auth_method_switch{ + get_server_auth_method(), generate_server_auth_method_switch_data()}; + using auth_method_switch_frame = classic_protocol::frame::Frame< + classic_protocol::message::server::AuthMethodSwitch>; + auto encode_result{classic_protocol::encode( + {generate_sequence_number(), auth_method_switch}, + get_shared_capabilities(), boost::asio::dynamic_buffer(result_buffer))}; + + if (!encode_result) { + throw boost::system::system_error{encode_result.error()}; + } + return result_buffer; +} + +void connection_context::parse_client_auth_method_data( + const network_buffer_type &payload) { + auto buffer{boost::asio::buffer(payload)}; + using auth_method_data_frame = classic_protocol::frame::Frame< + classic_protocol::message::client::AuthMethodData>; + auto decode_result{classic_protocol::decode( + buffer, get_shared_capabilities())}; + if (!decode_result) { + throw boost::system::system_error{decode_result.error()}; + } + + validate_and_update_sequence_number(decode_result.value().second.seq_id()); + + // after the auth method switch the client uses the server's auth method + client_auth_method_ = server_auth_method_; + client_auth_method_data_ = + decode_result.value().second.payload().auth_method_data(); +} + [[nodiscard]] network_buffer_type connection_context::generate_encoded_fast_auth() { std::string result_buffer{}; @@ -429,6 +466,18 @@ connection_context::generate_server_auth_method_data() { return server_auth_method_data_; } +[[nodiscard]] std::string +connection_context::generate_server_auth_method_switch_data() const { + if (get_server_auth_method() == + caching_sha2_password_authenticator::plugin_name) { + // caching_sha2_password follows the historical handshake seed shape: + // 20 bytes of challenge data plus a trailing NUL filler. + return get_server_auth_method_data() + '\0'; + } + + return get_server_auth_method_data(); +} + [[nodiscard]] std::uint8_t connection_context::generate_sequence_number() { return sequence_number_++; } diff --git a/src/minimysql/connection_context.hpp b/src/minimysql/connection_context.hpp index a2c69d7..40d5875 100644 --- a/src/minimysql/connection_context.hpp +++ b/src/minimysql/connection_context.hpp @@ -130,6 +130,9 @@ class connection_context { [[nodiscard]] network_buffer_type generate_encoded_server_greeting(); void parse_client_greeting(const network_buffer_type &payload); + [[nodiscard]] network_buffer_type generate_encoded_auth_method_switch(); + void parse_client_auth_method_data(const network_buffer_type &payload); + [[nodiscard]] network_buffer_type generate_encoded_fast_auth(); [[nodiscard]] network_buffer_type generate_encoded_ok(); [[nodiscard]] network_buffer_type generate_encoded_eof(); @@ -228,6 +231,7 @@ class connection_context { [[nodiscard]] static capability_bitset get_default_server_capabilities() noexcept; [[nodiscard]] const std::string &generate_server_auth_method_data(); + [[nodiscard]] std::string generate_server_auth_method_switch_data() const; [[nodiscard]] std::uint8_t generate_sequence_number(); void validate_and_update_sequence_number(std::uint8_t sequence_number); diff --git a/src/minimysql/network_service.cpp b/src/minimysql/network_service.cpp index 412472f..c977aba 100644 --- a/src/minimysql/network_service.cpp +++ b/src/minimysql/network_service.cpp @@ -291,8 +291,28 @@ void handle_exception(std::string_view context) { << " authentication that does not match the one associated " "with the user account (" << context.get_server_auth_method() << ")\n"; - // TODO: send SwitchAuthentication packet - co_return; + + const auto auth_method_switch{ + context.generate_encoded_auth_method_switch()}; + print_generic(remote_endpoint, context, "auth method switch"); + co_await minimysql::async_write_mysql_frame( + socket, auth_method_switch, + network_service::session_authentication_timeout); + std::cout << "sent server auth method switch (" + << std::size(auth_method_switch) << " bytes to " + << remote_endpoint << ")\n"; + + co_await minimysql::async_read_mysql_frame( + socket, data, network_service::session_authentication_timeout); + std::cout << "received client auth method switch response (" + << std::size(data) << " bytes from " << remote_endpoint + << ")\n"; + context.parse_client_auth_method_data(data); + std::cout << "client auth method after switch: " + << context.get_client_auth_method() << '\n' + << " auth_method_data: " + << std::size(context.get_client_auth_method_data()) + << " byte(s)\n"; } if (!context.check_client_authentication()) { std::cout << "client authentication failed for " From 38272065e62ad23b8b34db45cd65efdda5c0db77 Mon Sep 17 00:00:00 2001 From: Kamil Holubicki Date: Thu, 16 Jul 2026 12:43:30 +0200 Subject: [PATCH 2/2] PBS-33: support caching_sha2_password RSA full auth in minimysql https://perconadev.atlassian.net/browse/PBS-33 Added: Encapsulate caching_sha2_password in caching_sha2_password_authenticator (scramble verify, AuthSwitch data, 0x03/0x04, PEM, RSA decrypt, cleartext stub for future TLS). Auth loop in network_service stays auth-method agnostic: optional AuthSwitch on plugin mismatch, then begin_authentication / outbound frames / client frames until done. Fast auth when the handshake scramble matches the configured password; otherwise full auth (0x04) with RSA on plain TCP (0x02 + PEM or ciphertext via --server-public-key-path). Wire AuthMoreData (0x01 || plugin data) for 0x03, 0x04, and PEM, matching Percona Server/MySql mpvio wrapping. Thread server_rsa_public/private_key_path from minimysql_app through network_service -> connection_context -> authenticator (empty = embedded defaults; one-sided config fails hard). Random 20-byte greeting salt; unit tests for fast path, RSA path, and key path validation. How this differs from Percona Server/MySql: No SHA2 digest cache: every successful scramble is treated as a cache hit, including the first connection. PS does full auth (RSA on plain TCP / cleartext on SSL) on cache miss, then caches for later fast auth. No TLS yet: connection_is_secure() is always false; cleartext-after-0x04 is stubbed only. PS accepts cleartext password over SSL/socket after 0x04. Single configured user/password and optional key paths (or embedded MTR keys), not ACL / auto-generated server RSA keys from config. --- .../caching_sha2_password_authenticator.cpp | 528 +++++++++++++++++- .../caching_sha2_password_authenticator.hpp | 113 +++- src/minimysql/connection_context.cpp | 183 +++++- src/minimysql/connection_context.hpp | 46 +- src/minimysql/network_service.cpp | 91 ++- src/minimysql/network_service.hpp | 6 +- src/minimysql_app.cpp | 8 +- tests/CMakeLists.txt | 21 + ...ching_sha2_password_authenticator_test.cpp | 262 +++++++++ 9 files changed, 1186 insertions(+), 72 deletions(-) create mode 100644 tests/caching_sha2_password_authenticator_test.cpp diff --git a/src/minimysql/caching_sha2_password_authenticator.cpp b/src/minimysql/caching_sha2_password_authenticator.cpp index bc7ae81..bd06c92 100644 --- a/src/minimysql/caching_sha2_password_authenticator.cpp +++ b/src/minimysql/caching_sha2_password_authenticator.cpp @@ -19,15 +19,63 @@ #include #include #include +#include +#include #include +#include #include +#include #include #include #include +#include +#include +#include #include +#include +#include #include +#include "minimysql/network_io_operations_fwd.hpp" + +// clang-format off +// caching_sha2_password authentication flow (minimysql mock): +// +// Client Server (this authenticator) +// | | +// |--- Handshake (plugin, scramble) ------->| +// | | +// |<-- AuthSwitch (plugin mismatch only) ---| needs_auth_method_switch() +// |--- Auth response (scramble) ----------->| begin_authentication() +// | | +// | [fast path — always tried first; no SHA2 digest cache; behaves as +// | permanent cache hit via verify_greeting_scramble(), even first conn] +// | | +// |<-- AuthMoreData 0x01|0x03 --------------| scramble matches password +// |<-- OK ----------------------------------| +// | | +// | [full auth — when scramble does not match; real server: cache miss] +// | | +// |<-- AuthMoreData 0x01|0x04 --------------| perform full authentication +// | | +// | Client chooses password encoding (server accepts per transport): +// | | +// | (A) secure transport [TLS stub; connection_is_secure() false today] +// |--- cleartext password (0-terminated) ->| verify_cleartext_password() +// |<-- OK / Access denied ------------------| +// | | +// | (B) plain TCP — client opts in to RSA (server expects ciphertext) +// |--- 0x02 Request public key (optional) ->| --get-server-public-key +// |<-- AuthMoreData 0x01|PEM ---------------| enqueue_public_key() +// | | (skip 0x02 via +// | | --server-public-key-path) +// |--- RSA-OAEP encrypted password -------->| verify_encrypted_password() +// |<-- OK / Access denied ------------------| +// | | +// | (C) plain TCP, no RSA flags — client fails locally before sending +// | ("Authentication requires secure connection.") +// clang-format on namespace { enum class digest_code_type : std::uint8_t { @@ -36,7 +84,6 @@ enum class digest_code_type : std::uint8_t { class digest_context { public: - // no std::string_view for 'type' as we need it to be nul-terminated explicit digest_context(digest_code_type digest_code) : impl_{EVP_MD_CTX_new(), digest_context_deleter{}} { if (!impl_) { @@ -118,29 +165,358 @@ std::string calculate_digest(digest_code_type digest_code, return ctx.finalize(); } +void xor_with_pattern(std::span data, std::string_view pattern) { + if (std::empty(pattern)) { + return; + } + + for (std::size_t index{0U}; index < std::size(data); ++index) { + data[index] = static_cast( + static_cast(data[index]) ^ + static_cast(pattern[index % std::size(pattern)])); + } +} + +constexpr std::uint8_t request_public_key{0x02U}; +constexpr std::uint8_t perform_full_authentication{0x04U}; + +// Embedded server RSA keys used when no server_rsa_*_key_path is supplied. +// Temporary for standalone minimysql; binlog server integration should pass +// paths from config via network_service. +constexpr std::string_view default_rsa_public_key_pem{ + "-----BEGIN PUBLIC KEY-----\n" + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvV2VNbsQPG0Bh0KC8F4z\n" + "CGXvMNcSicCiLXxeLWrJsmKZl0ggf2ydymYUUewq+dVxDdh85sdSvxEmtIWvKSRK\n" + "+RRCAURztq2Succd+24SF5IZYjlIJE/U0AYUxHzUcOsannfzui60IaTHpcBFHTJK\n" + "6myxGx9MORZmhfv580mfvz4yvgLjS5yGOIS6rlxD9YV1Y04Rx3SXQQBnC7rDBL91\n" + "ktNWvbclsonfytY19N9p+Gprms30yRT+BmPFB7TqpReeZa3ivg15g/z3BLNyvj3Y\n" + "KiQM3cd7ENJC2x2LRxL5pG684cFNStSjT4FvA+oh45UnU45aOSEjrxNkBG8ci0e+\n" + "VKX539rK+nDzTE/MHpnvfHp4DB+kSYBPuKHY2Eaw31NwPpfLWwEJPiDrktJJmRZq\n" + "ENMHLXksdiqGhvYmI33wZaZAfjbDZFMfPF5yBMBGDZ3aeNz5Le7uqS6g6XMOoiz/\n" + "d2S5RzRrCol1yqCBPtODjfFPC4K8GGYVkWZgSCf/PRt/DgDnZOfZSSYIQNeyr21e\n" + "mqgqQ+yhXEGKVjcDTKcbSLiWAdA+GkAzLAXXhafM8mrhpnGKdO4Or6ySz7G1vk2J\n" + "t2ZSdP740oVSJi59P9NEgXcbd3c4FzjXSOOsxfhPQfobUk3ikt55lN3fBX3mBvUd\n" + "uxNhAcQ02ZD5zXrX6+loiV8CAwEAAQ==\n" + "-----END PUBLIC KEY-----\n"}; + +constexpr std::string_view default_rsa_private_key_pem{ + "-----BEGIN RSA PRIVATE KEY-----\n" + "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\n" + "-----END RSA PRIVATE KEY-----\n"}; + +struct evp_pkey_deleter { + void operator()(EVP_PKEY *key) const noexcept { EVP_PKEY_free(key); } +}; + +using evp_pkey_ptr = std::unique_ptr; + +[[nodiscard]] evp_pkey_ptr load_private_key(std::string_view pem) { + BIO *bio{BIO_new_mem_buf(std::data(pem), static_cast(std::size(pem)))}; + if (bio == nullptr) { + throw std::runtime_error{"failed to allocate OpenSSL BIO for private key"}; + } + + evp_pkey_ptr key{PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr), + evp_pkey_deleter{}}; + BIO_free(bio); + + if (!key) { + throw std::runtime_error{"failed to parse RSA private key"}; + } + + return key; +} + +[[nodiscard]] std::string read_pem_file(std::string_view path) { + // Avoid istreambuf_iterator: GCC 14 -Wnull-dereference false positive under + // -O2. + std::ifstream file{std::string{path}, std::ios::binary}; + if (!file) { + throw std::runtime_error{"failed to open PEM file: " + std::string{path}}; + } + if (!file.seekg(0, std::ios_base::end)) { + throw std::runtime_error{"failed to seek PEM file: " + std::string{path}}; + } + const auto end_offset{static_cast(file.tellg())}; + if (end_offset < 0) { + throw std::runtime_error{"failed to size PEM file: " + std::string{path}}; + } + if (!file.seekg(0, std::ios_base::beg)) { + throw std::runtime_error{"failed to rewind PEM file: " + std::string{path}}; + } + + std::string contents(static_cast(end_offset), '\0'); + if (end_offset != 0 && !file.read(std::data(contents), end_offset)) { + throw std::runtime_error{"failed to read PEM file: " + std::string{path}}; + } + return contents; +} + } // anonymous namespace namespace minimysql { +struct caching_sha2_password_authenticator::rsa_key_pair { + rsa_key_pair(std::string public_key_pem, std::string_view private_key_pem) + : public_key_pem_{std::move(public_key_pem)}, + private_key_{load_private_key(private_key_pem)}, + cipher_length_{ + static_cast(EVP_PKEY_get_size(private_key_.get()))} {} + + static std::unique_ptr + from_paths(std::string_view server_rsa_public_key_path, + std::string_view server_rsa_private_key_path) { + return std::make_unique( + read_pem_file(server_rsa_public_key_path), + read_pem_file(server_rsa_private_key_path)); + } + + static std::unique_ptr embedded_default() { + return std::make_unique( + std::string{default_rsa_public_key_pem}, + std::string{default_rsa_private_key_pem}); + } + + [[nodiscard]] std::string_view public_key_pem() const noexcept { + return public_key_pem_; + } + + [[nodiscard]] std::size_t cipher_length() const noexcept { + return cipher_length_; + } + + [[nodiscard]] std::string + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + decrypt_password(std::string_view encrypted_password, + std::string_view salt) const { + if (std::size(encrypted_password) != cipher_length_) { + throw std::runtime_error{"encrypted password has unexpected length"}; + } + + std::string plain_text(cipher_length_ + 1U, '\0'); + std::size_t plain_text_length{cipher_length_}; + + EVP_PKEY_CTX *key_ctx{EVP_PKEY_CTX_new(private_key_.get(), nullptr)}; + if (key_ctx == nullptr) { + throw std::runtime_error{"failed to create RSA decrypt context"}; + } + + if (EVP_PKEY_decrypt_init(key_ctx) <= 0 || + EVP_PKEY_CTX_set_rsa_padding(key_ctx, RSA_PKCS1_OAEP_PADDING) <= 0 || + EVP_PKEY_decrypt( + key_ctx, + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(std::data(plain_text)), + &plain_text_length, + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast( + std::data(encrypted_password)), + std::size(encrypted_password)) <= 0) { + EVP_PKEY_CTX_free(key_ctx); + throw std::runtime_error{"failed to decrypt RSA password"}; + } + EVP_PKEY_CTX_free(key_ctx); + + xor_with_pattern(std::span{std::data(plain_text), cipher_length_ + 1U}, + salt); + + const auto password_end{plain_text.find('\0')}; + if (password_end == std::string::npos) { + throw std::runtime_error{"decrypted password is missing a terminator"}; + } + + return plain_text.substr(0, password_end); + } + +private: + std::string public_key_pem_; + evp_pkey_ptr private_key_; + std::size_t cipher_length_; +}; + +caching_sha2_password_authenticator::caching_sha2_password_authenticator( + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + std::string_view password, std::string_view server_rsa_public_key_path, + std::string_view server_rsa_private_key_path) + : password_{password} { + const bool has_public{!std::empty(server_rsa_public_key_path)}; + const bool has_private{!std::empty(server_rsa_private_key_path)}; + if (has_public != has_private) { + throw std::runtime_error{ + "server_rsa_public_key_path and server_rsa_private_key_path must both " + "be set or both be empty"}; + } + if (has_public) { + rsa_keys_ = rsa_key_pair::from_paths(server_rsa_public_key_path, + server_rsa_private_key_path); + } else { + rsa_keys_ = rsa_key_pair::embedded_default(); + } +} + +caching_sha2_password_authenticator::~caching_sha2_password_authenticator() = + default; + +// AuthSwitch only when the client plugin does not match. A cache miss on an +// already-matching plugin is handled by sending 0x04, not by restarting auth. +bool caching_sha2_password_authenticator::needs_auth_method_switch( + std::string_view client_plugin) noexcept { + return client_plugin != plugin_name; +} + +std::string +caching_sha2_password_authenticator::generate_auth_switch_plugin_data( + std::string_view salt) { + return std::string{salt} + '\0'; +} + +void caching_sha2_password_authenticator::begin_authentication( + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + std::string_view expected_username, std::string_view client_username, + std::string_view client_auth_data, std::string_view salt, + bool secure_transport, auth_packet_encoder &encoder) { + expected_username_ = expected_username; + client_username_ = client_username; + salt_ = salt; + secure_transport_ = secure_transport; + outbound_frames_.clear(); + phase_ = phase::idle; + // Minimysql has no SHA2 digest cache. We always behave as if fast auth had a + // cache hit: verify the handshake scramble against the configured password + // directly, even on the first connection. A real server would send 0x04 here + // on cache miss and require RSA (plain TCP) or cleartext over SSL for full + // auth (see verify_cleartext_password(); not reached until TLS exists). + // Alternative: skip verify_greeting_scramble() and always enqueue full + // authentication (0x04) to mirror first-login / cache-miss behavior. + + // NOTE: Disable the below greeting scramble verification to enable full + // authentication with public key exchange. + if (/* false && */ verify_greeting_scramble( + expected_username_, client_username_, client_auth_data, salt_)) { + enqueue_fast_auth_success(encoder); + phase_ = phase::succeeded; + return; + } + + enqueue_perform_full_authentication(encoder); + phase_ = phase::awaiting_full_auth_response; +} + +authentication_state +caching_sha2_password_authenticator::state() const noexcept { + switch (phase_) { + case phase::succeeded: + return authentication_state::succeeded; + case phase::failed: + return authentication_state::failed; + default: + return authentication_state::in_progress; + } +} + +// True while the server must read another client AuthMoreData frame: +// - awaiting_full_auth_response: client replies to 0x04 with either 0x02 +// (request PEM) or RSA ciphertext when it already has the public key, or with +// a cleartext password when secure_transport_ is true (SSL/TLS stub). +// - awaiting_encrypted_password: client sends ciphertext after receiving PEM. +bool caching_sha2_password_authenticator::expects_client_input() + const noexcept { + return phase_ == phase::awaiting_full_auth_response || + phase_ == phase::awaiting_encrypted_password; +} + +std::vector +caching_sha2_password_authenticator::take_outbound_frames() { + return std::exchange(outbound_frames_, {}); +} + +authentication_state caching_sha2_password_authenticator::submit_client_frame( + const network_buffer_type &frame, auth_packet_encoder &encoder) { + encoder.validate_incoming_sequence(frame); + const std::string_view payload{encoder.frame_payload(frame)}; + + if (phase_ == phase::awaiting_full_auth_response) { + if (secure_transport_) { + return verify_cleartext_password(payload); + } + + // After 0x04 the client may send 0x02 to fetch PEM + // (--get-server-public-key) or send RSA ciphertext immediately when it + // already loaded the key from disk + // (--server-public-key-path). + if (check_public_key_request(payload)) { + enqueue_public_key(encoder); + phase_ = phase::awaiting_encrypted_password; + return authentication_state::in_progress; + } + + return verify_encrypted_password(payload); + } + + if (phase_ == phase::awaiting_encrypted_password) { + return verify_encrypted_password(payload); + } + + phase_ = phase::failed; + return authentication_state::failed; +} + std::string caching_sha2_password_authenticator::scramble( // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) std::string_view password, std::string_view salt) { - // this is how client calculates client_auth_data for caching_sha2_password - // plugin: SHA256(password) XOR SHA256(SHA256(SHA256(password)), - // server_auth_data) - - // server, provided that it knows original password and server_auth_data - // (salt), can verify client_auth_data by calculating the same way and - // comparing the result with client_auth_data const auto digest_code{digest_code_type::sha256}; - // calculating hashed password auto result{calculate_digest(digest_code, password)}; - - // calculating double-hashed password const auto double_hashed_password{calculate_digest(digest_code, result)}; - // calculating salted triple-hashed password digest_context ctx(digest_code); ctx.update(double_hashed_password); ctx.update(salt); @@ -152,4 +528,132 @@ std::string caching_sha2_password_authenticator::scramble( return result; } +std::string_view +caching_sha2_password_authenticator::get_rsa_public_key_pem() const noexcept { + return rsa_keys_->public_key_pem(); +} + +std::size_t +caching_sha2_password_authenticator::get_rsa_cipher_length() const noexcept { + return rsa_keys_->cipher_length(); +} + +bool caching_sha2_password_authenticator::check_public_key_request( + std::string_view payload) noexcept { + return std::size(payload) == 1U && + static_cast(payload.front()) == request_public_key; +} + +std::string caching_sha2_password_authenticator::decrypt_rsa_password( + std::string_view encrypted_password, std::string_view salt) const { + return rsa_keys_->decrypt_password(encrypted_password, salt); +} + +// Outbound caching_sha2_password plugin packets after the handshake must be +// framed as AuthMoreData on the wire: +// +// MySQL frame payload = 0x01 || +// +// where is one of: +// - 0x03 fast auth success +// - 0x04 perform full authentication +// - PEM server RSA public key after a client 0x02 request +// +// On a real MySQL / Percona Server, the auth plugin does NOT prepend 0x01 +// itself. It calls MYSQL_PLUGIN_VIO::write_packet() with the raw plugin bytes +// (e.g. a single 0x04, or the PEM string). The server mpvio layer then wraps +// that payload via wrap_plguin_data_into_proper_command() / +// net_write_command(..., command=1, ...), which is AuthMoreData. See +// sql/auth/sql_authentication.cc (server_mpvio_write_packet) and +// sql/auth/sha2_password.cc (write_packet of perform_full_authentication / +// public key PEM). +// +// Minimysql has no mpvio / plugin VIO. We write classic-protocol frames +// directly, so encode_auth_method_data() must supply the AuthMoreData 0x01 +// status byte that the real server would have added for us. +// +// On the client side, client_mpvio_read_packet() (sql-common/client.cc) strips +// a leading 0x01 when present before handing data to the auth plugin. That is +// why the plugin logic checks for a 1-byte 0x03 / 0x04, and why PEM_read sees +// a clean "-----BEGIN PUBLIC KEY-----" buffer rather than a 0x01-prefixed PEM. +// +// Sending raw 0x04 or raw PEM without the 0x01 prefix can still interoperate +// with some clients (the strip is conditional), but it diverges from the +// server protocol and from the fast-auth path, which already used AuthMoreData. +// Always use encode_auth_method_data() for these continuations. +void caching_sha2_password_authenticator::enqueue_perform_full_authentication( + auth_packet_encoder &encoder) { + const char full_auth_code{static_cast(perform_full_authentication)}; + outbound_frames_.emplace_back( + encoder.encode_auth_method_data(std::string_view{&full_auth_code, 1U})); +} + +void caching_sha2_password_authenticator::enqueue_public_key( + auth_packet_encoder &encoder) { + outbound_frames_.emplace_back( + encoder.encode_auth_method_data(get_rsa_public_key_pem())); +} + +void caching_sha2_password_authenticator::enqueue_fast_auth_success( + auth_packet_encoder &encoder) { + static constexpr std::string_view fast_auth_code{"\x03"}; + outbound_frames_.emplace_back( + encoder.encode_auth_method_data(fast_auth_code)); +} + +authentication_state +caching_sha2_password_authenticator::verify_encrypted_password( + std::string_view encrypted_password) { + if (std::size(encrypted_password) != get_rsa_cipher_length()) { + phase_ = phase::failed; + return authentication_state::failed; + } + + try { + const auto decrypted_password{ + decrypt_rsa_password(encrypted_password, salt_)}; + if (expected_username_ == client_username_ && + decrypted_password == password_) { + phase_ = phase::succeeded; + return authentication_state::succeeded; + } + } catch (const std::exception &) { + phase_ = phase::failed; + return authentication_state::failed; + } + + phase_ = phase::failed; + return authentication_state::failed; +} + +authentication_state +caching_sha2_password_authenticator::verify_cleartext_password( + std::string_view password_payload) { + // Full auth over a secure transport: client sends a 0-terminated password + // without RSA. Minimysql has no TLS yet; connection_is_secure() is always + // false, so this remains a placeholder until SSL is wired up. + if (std::empty(password_payload) || password_payload.back() != '\0') { + phase_ = phase::failed; + return authentication_state::failed; + } + + const std::string_view password{ + password_payload.substr(0, std::size(password_payload) - 1U)}; + if (expected_username_ == client_username_ && password == password_) { + phase_ = phase::succeeded; + return authentication_state::succeeded; + } + + phase_ = phase::failed; + return authentication_state::failed; +} + +bool caching_sha2_password_authenticator::verify_greeting_scramble( + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + std::string_view expected_username, std::string_view client_username, + std::string_view client_auth_data, std::string_view salt) const { + return expected_username == client_username && + client_auth_data == scramble(password_, salt); +} + } // namespace minimysql diff --git a/src/minimysql/caching_sha2_password_authenticator.hpp b/src/minimysql/caching_sha2_password_authenticator.hpp index b9f69bb..aaff957 100644 --- a/src/minimysql/caching_sha2_password_authenticator.hpp +++ b/src/minimysql/caching_sha2_password_authenticator.hpp @@ -16,16 +16,127 @@ #ifndef MINIMYSQL_CACHING_SHA2_PASSWORD_AUTHENTICATOR_HPP #define MINIMYSQL_CACHING_SHA2_PASSWORD_AUTHENTICATOR_HPP +#include +#include +#include #include #include +#include + +#include "minimysql/network_io_operations_fwd.hpp" namespace minimysql { +class auth_packet_encoder { +public: + auth_packet_encoder() = default; + virtual ~auth_packet_encoder() = default; + + auth_packet_encoder(const auth_packet_encoder &) = delete; + auth_packet_encoder(auth_packet_encoder &&) = delete; + auth_packet_encoder &operator=(const auth_packet_encoder &) = delete; + auth_packet_encoder &operator=(auth_packet_encoder &&) = delete; + + [[nodiscard]] virtual network_buffer_type + encode_single_byte(std::uint8_t payload_byte) = 0; + [[nodiscard]] virtual network_buffer_type + encode_raw(std::string_view payload) = 0; + [[nodiscard]] virtual network_buffer_type + encode_auth_method_data(std::string_view payload) = 0; + virtual void + validate_incoming_sequence(const network_buffer_type &payload) = 0; + [[nodiscard]] virtual std::string_view + frame_payload(const network_buffer_type &payload) const = 0; +}; + +enum class authentication_state : std::uint8_t { + in_progress, + succeeded, + failed, +}; + class caching_sha2_password_authenticator { public: static constexpr std::string_view plugin_name{"caching_sha2_password"}; - static std::string scramble(std::string_view password, std::string_view salt); + explicit caching_sha2_password_authenticator( + std::string_view password, + std::string_view server_rsa_public_key_path = {}, + std::string_view server_rsa_private_key_path = {}); + ~caching_sha2_password_authenticator(); + + caching_sha2_password_authenticator( + const caching_sha2_password_authenticator &) = delete; + caching_sha2_password_authenticator & + operator=(const caching_sha2_password_authenticator &) = delete; + caching_sha2_password_authenticator( + caching_sha2_password_authenticator &&) noexcept = default; + caching_sha2_password_authenticator & + operator=(caching_sha2_password_authenticator &&) noexcept = default; + + [[nodiscard]] static bool + needs_auth_method_switch(std::string_view client_plugin) noexcept; + + [[nodiscard]] static std::string + generate_auth_switch_plugin_data(std::string_view salt); + + void begin_authentication(std::string_view expected_username, + std::string_view client_username, + std::string_view client_auth_data, + std::string_view salt, bool secure_transport, + auth_packet_encoder &encoder); + + [[nodiscard]] authentication_state state() const noexcept; + + [[nodiscard]] bool expects_client_input() const noexcept; + + [[nodiscard]] std::vector take_outbound_frames(); + + authentication_state submit_client_frame(const network_buffer_type &frame, + auth_packet_encoder &encoder); + + [[nodiscard]] static std::string scramble(std::string_view password, + std::string_view salt); + +private: + struct rsa_key_pair; + + [[nodiscard]] std::string_view get_rsa_public_key_pem() const noexcept; + [[nodiscard]] std::size_t get_rsa_cipher_length() const noexcept; + [[nodiscard]] static bool + check_public_key_request(std::string_view payload) noexcept; + [[nodiscard]] std::string + decrypt_rsa_password(std::string_view encrypted_password, + std::string_view salt) const; + + void enqueue_perform_full_authentication(auth_packet_encoder &encoder); + void enqueue_public_key(auth_packet_encoder &encoder); + void enqueue_fast_auth_success(auth_packet_encoder &encoder); + [[nodiscard]] authentication_state + verify_encrypted_password(std::string_view encrypted_password); + [[nodiscard]] authentication_state + verify_cleartext_password(std::string_view password_payload); + + [[nodiscard]] bool verify_greeting_scramble( + std::string_view expected_username, std::string_view client_username, + std::string_view client_auth_data, std::string_view salt) const; + + std::string password_; + std::string expected_username_; + std::string client_username_; + std::string salt_; + bool secure_transport_{false}; + std::unique_ptr rsa_keys_; + + enum class phase : std::uint8_t { + idle, + awaiting_full_auth_response, + awaiting_encrypted_password, + succeeded, + failed, + }; + phase phase_{phase::idle}; + std::vector outbound_frames_; }; } // namespace minimysql diff --git a/src/minimysql/connection_context.cpp b/src/minimysql/connection_context.cpp index a459668..f0b9516 100644 --- a/src/minimysql/connection_context.cpp +++ b/src/minimysql/connection_context.cpp @@ -19,14 +19,18 @@ #include #include #include +#include #include +#include #include #include #include #include #include +#include #include +#include #include @@ -58,6 +62,8 @@ namespace minimysql { namespace { +constexpr std::size_t server_auth_method_data_length{20U}; + template classic_protocol::frame::Frame decode_client_command_frame(const network_buffer_type &payload, @@ -78,20 +84,100 @@ decode_client_command_frame(const network_buffer_type &payload, connection_context::connection_context( // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - std::string_view server_username, std::string_view server_password) + std::string_view server_username, std::string_view server_password, + std::string_view server_rsa_public_key_path, + std::string_view server_rsa_private_key_path) : server_username_(server_username), server_password_(server_password), - connection_id_(next_connection_id_++) { + connection_id_(next_connection_id_++), + authenticator_{server_password, server_rsa_public_key_path, + server_rsa_private_key_path} { static_assert(std::is_same_v, "capability_bitset MUST be the same type as " "classic_protocol::capabilities::value_type"); } -[[nodiscard]] bool connection_context::check_client_authentication() const { - return get_client_username() == get_server_username() && - get_client_auth_method_data() == - caching_sha2_password_authenticator::scramble( - get_server_password(), get_server_auth_method_data()); +class connection_context::auth_packet_encoder_impl + : public auth_packet_encoder { +public: + explicit auth_packet_encoder_impl(connection_context &context) + : context_{context} {} + + [[nodiscard]] network_buffer_type + encode_single_byte(std::uint8_t payload_byte) override { + return context_.encode_single_byte_payload(payload_byte); + } + + [[nodiscard]] network_buffer_type + encode_raw(std::string_view payload) override { + return context_.encode_raw_payload(payload); + } + + [[nodiscard]] network_buffer_type + encode_auth_method_data(std::string_view payload) override { + return context_.encode_auth_method_data_payload(payload); + } + + void validate_incoming_sequence(const network_buffer_type &payload) override { + context_.validate_and_update_sequence_number_from_frame(payload); + } + + [[nodiscard]] std::string_view + frame_payload(const network_buffer_type &payload) const override { + return connection_context::get_frame_payload(payload); + } + +private: + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + connection_context &context_; +}; + +[[nodiscard]] auth_packet_encoder & +connection_context::get_auth_packet_encoder() { + if (!auth_packet_encoder_) { + auth_packet_encoder_ = std::make_unique(*this); + } + return *auth_packet_encoder_; +} + +[[nodiscard]] bool +connection_context::needs_auth_method_switch() const noexcept { + return caching_sha2_password_authenticator::needs_auth_method_switch( + get_client_auth_method()); +} + +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +[[nodiscard]] bool connection_context::connection_is_secure() const noexcept { + // Stub until minimysql gains TLS: Percona Server accepts cleartext password + // after 0x04 only when the transport is secure (SSL/TLS, socket, etc.). + return false; +} + +void connection_context::begin_authentication() { + authenticator_.begin_authentication( + get_server_username(), get_client_username(), + get_client_auth_method_data(), get_server_auth_method_data(), + connection_is_secure(), get_auth_packet_encoder()); +} + +[[nodiscard]] enum authentication_state +connection_context::authentication_state() const noexcept { + return authenticator_.state(); +} + +[[nodiscard]] bool +connection_context::expects_authentication_input() const noexcept { + return authenticator_.expects_client_input(); +} + +[[nodiscard]] std::vector +connection_context::take_authentication_outbound_frames() { + return authenticator_.take_outbound_frames(); +} + +enum authentication_state connection_context::submit_authentication_frame( + const network_buffer_type &payload) { + return authenticator_.submit_client_frame(payload, get_auth_packet_encoder()); } [[nodiscard]] bool @@ -182,8 +268,10 @@ void connection_context::parse_client_greeting( connection_context::generate_encoded_auth_method_switch() { std::string result_buffer{}; - classic_protocol::message::server::AuthMethodSwitch auth_method_switch{ - get_server_auth_method(), generate_server_auth_method_switch_data()}; + const classic_protocol::message::server::AuthMethodSwitch auth_method_switch{ + get_server_auth_method(), + caching_sha2_password_authenticator::generate_auth_switch_plugin_data( + get_server_auth_method_data())}; using auth_method_switch_frame = classic_protocol::frame::Frame< classic_protocol::message::server::AuthMethodSwitch>; auto encode_result{classic_protocol::encode( @@ -215,17 +303,58 @@ void connection_context::parse_client_auth_method_data( decode_result.value().second.payload().auth_method_data(); } +void connection_context::validate_and_update_sequence_number_from_frame( + const network_buffer_type &payload) { + auto buffer{boost::asio::buffer(payload)}; + auto decode_result{ + classic_protocol::decode(buffer, {})}; + if (!decode_result) { + throw boost::system::system_error{decode_result.error()}; + } + + validate_and_update_sequence_number(decode_result.value().second.seq_id()); +} + [[nodiscard]] network_buffer_type -connection_context::generate_encoded_fast_auth() { +connection_context::encode_single_byte_payload(std::uint8_t payload_byte) { + std::string result_buffer{}; + result_buffer.reserve(get_frame_header_length() + 1U); + + auto encode_result{classic_protocol::encode( + {1U, generate_sequence_number()}, get_shared_capabilities(), + boost::asio::dynamic_buffer(result_buffer))}; + if (!encode_result) { + throw boost::system::system_error{encode_result.error()}; + } + + result_buffer.push_back(static_cast(payload_byte)); + return result_buffer; +} + +[[nodiscard]] network_buffer_type +connection_context::encode_raw_payload(std::string_view payload) { + std::string result_buffer{}; + result_buffer.reserve(get_frame_header_length() + std::size(payload)); + + auto encode_result{classic_protocol::encode( + {std::size(payload), generate_sequence_number()}, + get_shared_capabilities(), boost::asio::dynamic_buffer(result_buffer))}; + if (!encode_result) { + throw boost::system::system_error{encode_result.error()}; + } + + result_buffer.append(payload); + return result_buffer; +} + +[[nodiscard]] network_buffer_type +connection_context::encode_auth_method_data_payload(std::string_view payload) { std::string result_buffer{}; - static constexpr std::string_view fast_auth_code{ - "\x03"}; // 0x03 means "fast auth success" in caching_sha2_password - // protocol using auth_method_data_frame = classic_protocol::frame::Frame< classic_protocol::message::server::AuthMethodData>; auto encode_res = classic_protocol::encode( - {generate_sequence_number(), {std::string{fast_auth_code}}}, + {generate_sequence_number(), {std::string{payload}}}, get_shared_capabilities(), boost::asio::dynamic_buffer(result_buffer)); if (!encode_res) { @@ -460,22 +589,26 @@ connection_context::get_default_server_capabilities() noexcept { [[nodiscard]] const std::string & connection_context::generate_server_auth_method_data() { - // TODO: generate random auth method data (for caching_sha2_password, it must - // be 20 random bytes) - server_auth_method_data_ = "01234567890123456789"; + server_auth_method_data_.assign(server_auth_method_data_length, '\0'); + if (RAND_bytes( + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast( + std::data(server_auth_method_data_)), + static_cast(std::size(server_auth_method_data_))) != 1) { + throw std::runtime_error{"failed to generate server auth method data"}; + } return server_auth_method_data_; } -[[nodiscard]] std::string -connection_context::generate_server_auth_method_switch_data() const { - if (get_server_auth_method() == - caching_sha2_password_authenticator::plugin_name) { - // caching_sha2_password follows the historical handshake seed shape: - // 20 bytes of challenge data plus a trailing NUL filler. - return get_server_auth_method_data() + '\0'; +[[nodiscard]] std::string_view connection_context::get_frame_payload( + const network_buffer_type &payload) noexcept { + if (std::size(payload) <= get_frame_header_length()) { + return {}; } - return get_server_auth_method_data(); + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + return {std::data(payload) + get_frame_header_length(), + std::size(payload) - get_frame_header_length()}; } [[nodiscard]] std::uint8_t connection_context::generate_sequence_number() { diff --git a/src/minimysql/connection_context.hpp b/src/minimysql/connection_context.hpp index 40d5875..45221a7 100644 --- a/src/minimysql/connection_context.hpp +++ b/src/minimysql/connection_context.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,8 @@ #include "minimysql/network_io_operations_fwd.hpp" +#include "minimysql/caching_sha2_password_authenticator.hpp" + namespace minimysql { class connection_context { @@ -39,10 +42,12 @@ class connection_context { static constexpr std::uint16_t default_server_status_flags{0U}; static constexpr std::uint8_t default_server_collation{0U}; static constexpr std::string_view default_server_auth_method{ - "caching_sha2_password"}; + caching_sha2_password_authenticator::plugin_name}; connection_context(std::string_view server_username, - std::string_view server_password); + std::string_view server_password, + std::string_view server_rsa_public_key_path = {}, + std::string_view server_rsa_private_key_path = {}); [[nodiscard]] const std::string &get_server_username() const noexcept { return server_username_; @@ -50,7 +55,19 @@ class connection_context { [[nodiscard]] const std::string &get_server_password() const noexcept { return server_password_; } - [[nodiscard]] bool check_client_authentication() const; + [[nodiscard]] bool check_shared_plugin_auth_supported() const; + [[nodiscard]] bool + check_shared_text_result_with_session_tracking_supported() const; + + [[nodiscard]] bool needs_auth_method_switch() const noexcept; + [[nodiscard]] bool connection_is_secure() const noexcept; + void begin_authentication(); + [[nodiscard]] enum authentication_state authentication_state() const noexcept; + [[nodiscard]] bool expects_authentication_input() const noexcept; + [[nodiscard]] std::vector + take_authentication_outbound_frames(); + enum authentication_state + submit_authentication_frame(const network_buffer_type &payload); [[nodiscard]] std::uint32_t get_connection_id() const noexcept { return connection_id_; @@ -70,9 +87,6 @@ class connection_context { [[nodiscard]] capability_bitset get_shared_capabilities() const noexcept { return client_capabilities_ & server_capabilities_; } - [[nodiscard]] bool check_shared_plugin_auth_supported() const; - [[nodiscard]] bool - check_shared_text_result_with_session_tracking_supported() const; [[nodiscard]] const std::string &get_server_auth_method() const noexcept { return server_auth_method_; @@ -133,7 +147,6 @@ class connection_context { [[nodiscard]] network_buffer_type generate_encoded_auth_method_switch(); void parse_client_auth_method_data(const network_buffer_type &payload); - [[nodiscard]] network_buffer_type generate_encoded_fast_auth(); [[nodiscard]] network_buffer_type generate_encoded_ok(); [[nodiscard]] network_buffer_type generate_encoded_eof(); [[nodiscard]] network_buffer_type @@ -228,13 +241,30 @@ class connection_context { std::string binlog_filename_{}; std::uint64_t binlog_position_{}; + caching_sha2_password_authenticator authenticator_; + [[nodiscard]] static capability_bitset get_default_server_capabilities() noexcept; [[nodiscard]] const std::string &generate_server_auth_method_data(); - [[nodiscard]] std::string generate_server_auth_method_switch_data() const; [[nodiscard]] std::uint8_t generate_sequence_number(); void validate_and_update_sequence_number(std::uint8_t sequence_number); + [[nodiscard]] network_buffer_type + encode_single_byte_payload(std::uint8_t payload_byte); + [[nodiscard]] network_buffer_type + encode_raw_payload(std::string_view payload); + [[nodiscard]] network_buffer_type + encode_auth_method_data_payload(std::string_view payload); + [[nodiscard]] static std::string_view + get_frame_payload(const network_buffer_type &payload) noexcept; + + class auth_packet_encoder_impl; + [[nodiscard]] auth_packet_encoder &get_auth_packet_encoder(); + + mutable std::unique_ptr auth_packet_encoder_; + void validate_and_update_sequence_number_from_frame( + const network_buffer_type &payload); + void encode_resultset_number_of_columns_internal( network_buffer_container &result_buffers, std::size_t number_of_columns); diff --git a/src/minimysql/network_service.cpp b/src/minimysql/network_service.cpp index c977aba..57dc1f4 100644 --- a/src/minimysql/network_service.cpp +++ b/src/minimysql/network_service.cpp @@ -60,6 +60,7 @@ #include +#include "minimysql/caching_sha2_password_authenticator.hpp" #include "minimysql/connection_context.hpp" #include "minimysql/network_io_operations.hpp" #include "minimysql/sample_event_collection.hpp" @@ -226,7 +227,11 @@ void handle_exception(std::string_view context) { // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) const std::string &username, // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) - const std::string &password) { + const std::string &password, + // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) + const std::string &server_rsa_public_key_path, + // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) + const std::string &server_rsa_private_key_path) { boost::system::error_code session_ec; const auto remote_endpoint{socket.remote_endpoint(session_ec)}; @@ -237,7 +242,9 @@ void handle_exception(std::string_view context) { minimysql::network_buffer_type data; data.reserve(network_service::expected_packet_size); - minimysql::connection_context context{username, password}; + minimysql::connection_context context{username, password, + server_rsa_public_key_path, + server_rsa_private_key_path}; // creating and sending server greeting packet: // protocol_version: 10 @@ -286,12 +293,7 @@ void handle_exception(std::string_view context) { co_return; } - if (context.get_client_auth_method() != context.get_server_auth_method()) { - std::cout << "client requested " << context.get_client_auth_method() - << " authentication that does not match the one associated " - "with the user account (" - << context.get_server_auth_method() << ")\n"; - + if (context.needs_auth_method_switch()) { const auto auth_method_switch{ context.generate_encoded_auth_method_switch()}; print_generic(remote_endpoint, context, "auth method switch"); @@ -314,7 +316,44 @@ void handle_exception(std::string_view context) { << std::size(context.get_client_auth_method_data()) << " byte(s)\n"; } - if (!context.check_client_authentication()) { + + context.begin_authentication(); + + for (;;) { + // An authenticator may produce several outbound AuthMoreData frames + // before it needs client input (for example fast-auth success plus a + // follow-up, or a multi-step RSA exchange). The inner loop sends every + // frame queued by begin_authentication() or submit_authentication_frame() + // in order; only then does the outer loop read the next client packet. + for (const auto &outbound_frame : + context.take_authentication_outbound_frames()) { + print_generic(remote_endpoint, context, "auth method data"); + co_await minimysql::async_write_mysql_frame( + socket, outbound_frame, + network_service::session_authentication_timeout); + std::cout << "sent server authentication packet (" + << std::size(outbound_frame) << " bytes to " + << remote_endpoint << ")\n"; + } + + if (context.authentication_state() != + minimysql::authentication_state::in_progress) { + break; + } + + if (!context.expects_authentication_input()) { + break; + } + + co_await minimysql::async_read_mysql_frame( + socket, data, network_service::session_authentication_timeout); + std::cout << "received client authentication packet (" << std::size(data) + << " bytes from " << remote_endpoint << ")\n"; + context.submit_authentication_frame(data); + } + + if (context.authentication_state() != + minimysql::authentication_state::succeeded) { std::cout << "client authentication failed for " << context.get_client_username() << '\n'; const auto access_denied{context.generate_encoded_access_denied()}; @@ -330,16 +369,6 @@ void handle_exception(std::string_view context) { std::cout << "client authentication succeeded for " << context.get_client_username() << '\n'; - // sending fast auth success - const auto fast_auth_success{context.generate_encoded_fast_auth()}; - print_generic(remote_endpoint, context, "auth method data (fast auth)"); - co_await minimysql::async_write_mysql_frame( - socket, fast_auth_success, - network_service::session_authentication_timeout); - std::cout << "sent server fast auth success (" - << std::size(fast_auth_success) << " bytes to " << remote_endpoint - << ")\n"; - // sending server ok after successful authentication const auto auth_ok{context.generate_encoded_ok()}; print_generic(remote_endpoint, context, "ok (auth)"); @@ -500,7 +529,11 @@ void handle_exception(std::string_view context) { // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) const std::string &username, // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) - const std::string &password) { + const std::string &password, + // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) + const std::string &server_rsa_public_key_path, + // NOLINTNEXTLINE(cppcoreguidelines-avoid-reference-coroutine-parameters) + const std::string &server_rsa_private_key_path) { const scope_tracer tracer("listener"); auto executor = acceptor.get_executor(); @@ -524,7 +557,9 @@ void handle_exception(std::string_view context) { // NOLINTNEXTLINE(misc-include-cleaner) boost::asio::co_spawn(executor, - session(std::move(socket), username, password), + session(std::move(socket), username, password, + server_rsa_public_key_path, + server_rsa_private_key_path), boost::asio::detached); } } catch (...) { @@ -537,13 +572,21 @@ void handle_exception(std::string_view context) { network_service::network_service( boost::asio::io_context &context, std::uint16_t listening_port, // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - std::string_view username, std::string_view password) - : username_(username), password_(password), context_{&context}, + std::string_view username, std::string_view password, + std::string_view server_rsa_public_key_path, + std::string_view server_rsa_private_key_path) + : username_(username), password_(password), + server_rsa_public_key_path_{server_rsa_public_key_path}, + server_rsa_private_key_path_{server_rsa_private_key_path}, + context_{&context}, acceptor_{std::make_unique( context, boost::asio::ip::tcp::endpoint{boost::asio::ip::tcp::v4(), listening_port})} { // NOLINTNEXTLINE(misc-include-cleaner) - boost::asio::co_spawn(*context_, listener(*acceptor_, username_, password_), + boost::asio::co_spawn(*context_, + listener(*acceptor_, username_, password_, + server_rsa_public_key_path_, + server_rsa_private_key_path_), boost::asio::detached); } diff --git a/src/minimysql/network_service.hpp b/src/minimysql/network_service.hpp index 7e341fe..f85a670 100644 --- a/src/minimysql/network_service.hpp +++ b/src/minimysql/network_service.hpp @@ -31,7 +31,9 @@ class network_service { network_service(boost::asio::io_context &context, std::uint16_t listening_port, std::string_view username, - std::string_view password); + std::string_view password, + std::string_view server_rsa_public_key_path = {}, + std::string_view server_rsa_private_key_path = {}); network_service(const network_service &) = delete; network_service &operator=(const network_service &) = delete; @@ -43,6 +45,8 @@ class network_service { private: std::string username_; std::string password_; + std::string server_rsa_public_key_path_; + std::string server_rsa_private_key_path_; boost::asio::io_context *context_; using acceptor_type = diff --git a/src/minimysql_app.cpp b/src/minimysql_app.cpp index c12f2df..0dd8977 100644 --- a/src/minimysql_app.cpp +++ b/src/minimysql_app.cpp @@ -36,13 +36,19 @@ int main(int /* argc */, char * /* argv */[]) { static constexpr std::string_view default_username{"rpl"}; static constexpr std::string_view default_password{"password"}; + // Optional server RSA key paths for caching_sha2_password full auth; empty + // uses embedded defaults until wired from binlog server config. + static constexpr std::string_view default_server_rsa_public_key_path{}; + static constexpr std::string_view default_server_rsa_private_key_path{}; int res{EXIT_FAILURE}; try { std::cout << "starting mini-mysql-server" << '\n'; boost::asio::io_context ctx; const minimysql::network_service service( - ctx, listening_port, default_username, default_password); + ctx, listening_port, default_username, default_password, + default_server_rsa_public_key_path, + default_server_rsa_private_key_path); boost::asio::signal_set signals(ctx, SIGINT, SIGTERM); signals.async_wait([&](auto, auto) { ctx.stop(); }); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4e014e7..041071f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -95,6 +95,25 @@ set_target_properties(event_test PROPERTIES CXX_EXTENSIONS NO ) +add_executable(caching_sha2_password_authenticator_test + caching_sha2_password_authenticator_test.cpp + "${PROJECT_SOURCE_DIR}/src/minimysql/caching_sha2_password_authenticator.cpp" +) +target_include_directories(caching_sha2_password_authenticator_test + PRIVATE + "${PROJECT_SOURCE_DIR}/src" +) +target_link_libraries(caching_sha2_password_authenticator_test + PRIVATE + binlog_server_compiler_flags + Boost::unit_test_framework + OpenSSL::Crypto +) +set_target_properties(caching_sha2_password_authenticator_test PROPERTIES + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) + set(test_run_options --no_color_output) add_test(NAME byte_span_encoding_test COMMAND byte_span_encoding_test ${test_run_options}) @@ -103,3 +122,5 @@ add_test(NAME tag_test COMMAND tag_test ${test_run_options}) add_test(NAME gtid_test COMMAND gtid_test ${test_run_options}) add_test(NAME gtid_set_test COMMAND gtid_set_test ${test_run_options}) add_test(NAME event_test COMMAND event_test ${test_run_options}) +add_test(NAME caching_sha2_password_authenticator_test + COMMAND caching_sha2_password_authenticator_test ${test_run_options}) diff --git a/tests/caching_sha2_password_authenticator_test.cpp b/tests/caching_sha2_password_authenticator_test.cpp new file mode 100644 index 0000000..2eb6594 --- /dev/null +++ b/tests/caching_sha2_password_authenticator_test.cpp @@ -0,0 +1,262 @@ +// Copyright (c) 2023-2026 Percona and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +#include +#include +#include +#include +#include +#include +#include +#include + +#define BOOST_TEST_MODULE CachingSha2PasswordAuthenticatorTests +// this include is needed as it provides the 'main()' function +// NOLINTNEXTLINE(misc-include-cleaner) +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "minimysql/caching_sha2_password_authenticator.hpp" +#include "minimysql/network_io_operations_fwd.hpp" + +namespace { + +class recording_encoder final : public minimysql::auth_packet_encoder { +public: + [[nodiscard]] minimysql::network_buffer_type + encode_single_byte(std::uint8_t payload_byte) override { + minimysql::network_buffer_type frame; + frame.push_back(static_cast(payload_byte)); + return frame; + } + + [[nodiscard]] minimysql::network_buffer_type + encode_raw(std::string_view payload) override { + return minimysql::network_buffer_type{payload}; + } + + [[nodiscard]] minimysql::network_buffer_type + encode_auth_method_data(std::string_view payload) override { + minimysql::network_buffer_type frame; + frame.push_back('\x01'); + frame.append(payload); + return frame; + } + + void validate_incoming_sequence( + const minimysql::network_buffer_type & /*payload*/) override {} + + [[nodiscard]] std::string_view + frame_payload(const minimysql::network_buffer_type &payload) const override { + return payload; + } +}; + +[[nodiscard]] std::string_view auth_more_data_payload(std::string_view frame) { + BOOST_REQUIRE_GE(std::size(frame), 1U); + BOOST_REQUIRE_EQUAL(static_cast(frame.front()), 0x01U); + return frame.substr(1U); +} + +[[nodiscard]] std::string rsa_encrypt_password(std::string_view public_key_pem, + std::string_view password, + std::string_view salt) { + BIO *bio{BIO_new_mem_buf(std::data(public_key_pem), + static_cast(std::size(public_key_pem)))}; + BOOST_REQUIRE(bio != nullptr); + + EVP_PKEY *key{PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr)}; + BIO_free(bio); + BOOST_REQUIRE(key != nullptr); + + const std::size_t cipher_length{ + static_cast(EVP_PKEY_get_size(key))}; + std::string plain(std::size(password) + 1U, '\0'); + plain.replace(0, std::size(password), password); + + for (std::size_t index{0U}; index < std::size(plain); ++index) { + plain[index] = static_cast( + static_cast(plain[index]) ^ + static_cast(salt[index % std::size(salt)])); + } + + std::string cipher(cipher_length, '\0'); + std::size_t out_length{cipher_length}; + + EVP_PKEY_CTX *ctx{EVP_PKEY_CTX_new(key, nullptr)}; + BOOST_REQUIRE(ctx != nullptr); + BOOST_REQUIRE(EVP_PKEY_encrypt_init(ctx) > 0); + BOOST_REQUIRE(EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) > 0); + BOOST_REQUIRE( + EVP_PKEY_encrypt( + ctx, + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(std::data(cipher)), &out_length, + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(std::data(plain)), + std::size(plain)) > 0); + + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(key); + cipher.resize(out_length); + return cipher; +} + +void write_temp_file(const std::string &path, std::string_view contents) { + std::ofstream out{path, std::ios::binary | std::ios::trunc}; + BOOST_REQUIRE(out); + out.write(std::data(contents), + static_cast(std::size(contents))); + BOOST_REQUIRE(out); +} + +} // namespace + +BOOST_AUTO_TEST_CASE(OneSidedServerRsaPathsThrow) { + BOOST_CHECK_THROW(minimysql::caching_sha2_password_authenticator( + "password", "/only/pub.pem", ""), + std::runtime_error); + BOOST_CHECK_THROW(minimysql::caching_sha2_password_authenticator( + "password", "", "/only/priv.pem"), + std::runtime_error); +} + +BOOST_AUTO_TEST_CASE(EmptyServerRsaPathsUseEmbeddedDefaults) { + BOOST_CHECK_NO_THROW( + minimysql::caching_sha2_password_authenticator("password")); +} + +BOOST_AUTO_TEST_CASE(FastAuthPathSucceedsWithMatchingScramble) { + static constexpr std::string_view password{"password"}; + static constexpr std::string_view username{"rpl"}; + static constexpr std::string_view salt{"01234567890123456789"}; + + minimysql::caching_sha2_password_authenticator authenticator{password}; + recording_encoder encoder; + + const auto scramble{ + minimysql::caching_sha2_password_authenticator::scramble(password, salt)}; + authenticator.begin_authentication(username, username, scramble, salt, false, + encoder); + + const auto outbound{authenticator.take_outbound_frames()}; + BOOST_REQUIRE_EQUAL(std::size(outbound), 1U); + BOOST_CHECK_EQUAL(auth_more_data_payload(outbound.front()), + std::string_view{"\x03"}); + BOOST_CHECK(authenticator.state() == + minimysql::authentication_state::succeeded); + BOOST_CHECK(!authenticator.expects_client_input()); +} + +BOOST_AUTO_TEST_CASE(FullAuthRsaPathSucceedsViaPublicKeyRequest) { + static constexpr std::string_view password{"password"}; + static constexpr std::string_view username{"rpl"}; + static constexpr std::string_view salt{"01234567890123456789"}; + + minimysql::caching_sha2_password_authenticator authenticator{password}; + recording_encoder encoder; + + authenticator.begin_authentication(username, username, "bad-scramble", salt, + false, encoder); + + auto outbound{authenticator.take_outbound_frames()}; + BOOST_REQUIRE_EQUAL(std::size(outbound), 1U); + BOOST_CHECK_EQUAL(auth_more_data_payload(outbound.front()), + std::string_view{"\x04"}); + BOOST_CHECK(authenticator.state() == + minimysql::authentication_state::in_progress); + + const minimysql::network_buffer_type public_key_request{"\x02"}; + BOOST_CHECK(authenticator.submit_client_frame(public_key_request, encoder) == + minimysql::authentication_state::in_progress); + + outbound = authenticator.take_outbound_frames(); + BOOST_REQUIRE_EQUAL(std::size(outbound), 1U); + const auto public_key_pem{auth_more_data_payload(outbound.front())}; + BOOST_CHECK(public_key_pem.starts_with("-----BEGIN PUBLIC KEY-----")); + + const auto ciphertext{rsa_encrypt_password(public_key_pem, password, salt)}; + BOOST_CHECK(authenticator.submit_client_frame(ciphertext, encoder) == + minimysql::authentication_state::succeeded); + BOOST_CHECK(authenticator.state() == + minimysql::authentication_state::succeeded); +} + +BOOST_AUTO_TEST_CASE(FullAuthCleartextPathOnSecureTransport) { + static constexpr std::string_view password{"password"}; + static constexpr std::string_view username{"rpl"}; + static constexpr std::string_view salt{"01234567890123456789"}; + + minimysql::caching_sha2_password_authenticator authenticator{password}; + recording_encoder encoder; + + authenticator.begin_authentication(username, username, "bad-scramble", salt, + true, encoder); + (void)authenticator.take_outbound_frames(); + + minimysql::network_buffer_type cleartext{password}; + cleartext.push_back('\0'); + BOOST_CHECK(authenticator.submit_client_frame(cleartext, encoder) == + minimysql::authentication_state::succeeded); +} + +BOOST_AUTO_TEST_CASE(BothServerRsaPathsLoadSuccessfully) { + const std::string pub_path{"/tmp/minimysql_test_server_rsa_public.pem"}; + const std::string priv_path{"/tmp/minimysql_test_server_rsa_private.pem"}; + + EVP_PKEY_CTX *ctx{EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)}; + BOOST_REQUIRE(ctx != nullptr); + BOOST_REQUIRE(EVP_PKEY_keygen_init(ctx) > 0); + BOOST_REQUIRE(EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) > 0); + EVP_PKEY *key{nullptr}; + BOOST_REQUIRE(EVP_PKEY_keygen(ctx, &key) > 0); + EVP_PKEY_CTX_free(ctx); + + BIO *pub_bio{BIO_new(BIO_s_mem())}; + BIO *priv_bio{BIO_new(BIO_s_mem())}; + BOOST_REQUIRE(pub_bio != nullptr); + BOOST_REQUIRE(priv_bio != nullptr); + BOOST_REQUIRE(PEM_write_bio_PUBKEY(pub_bio, key) == 1); + BOOST_REQUIRE(PEM_write_bio_PrivateKey(priv_bio, key, nullptr, nullptr, 0, + nullptr, nullptr) == 1); + + char *pub_data{nullptr}; + char *priv_data{nullptr}; + const long pub_len{BIO_get_mem_data(pub_bio, &pub_data)}; + const long priv_len{BIO_get_mem_data(priv_bio, &priv_data)}; + BOOST_REQUIRE(pub_len > 0); + BOOST_REQUIRE(priv_len > 0); + + write_temp_file( + pub_path, std::string_view{pub_data, static_cast(pub_len)}); + write_temp_file( + priv_path, + std::string_view{priv_data, static_cast(priv_len)}); + + BIO_free(pub_bio); + BIO_free(priv_bio); + EVP_PKEY_free(key); + + BOOST_CHECK_NO_THROW(minimysql::caching_sha2_password_authenticator( + "password", pub_path, priv_path)); +}