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" + "MIIJKQIBAAKCAgEAvV2VNbsQPG0Bh0KC8F4zCGXvMNcSicCiLXxeLWrJsmKZl0gg\n" + "f2ydymYUUewq+dVxDdh85sdSvxEmtIWvKSRK+RRCAURztq2Succd+24SF5IZYjlI\n" + "JE/U0AYUxHzUcOsannfzui60IaTHpcBFHTJK6myxGx9MORZmhfv580mfvz4yvgLj\n" + "S5yGOIS6rlxD9YV1Y04Rx3SXQQBnC7rDBL91ktNWvbclsonfytY19N9p+Gprms30\n" + "yRT+BmPFB7TqpReeZa3ivg15g/z3BLNyvj3YKiQM3cd7ENJC2x2LRxL5pG684cFN\n" + "StSjT4FvA+oh45UnU45aOSEjrxNkBG8ci0e+VKX539rK+nDzTE/MHpnvfHp4DB+k\n" + "SYBPuKHY2Eaw31NwPpfLWwEJPiDrktJJmRZqENMHLXksdiqGhvYmI33wZaZAfjbD\n" + "ZFMfPF5yBMBGDZ3aeNz5Le7uqS6g6XMOoiz/d2S5RzRrCol1yqCBPtODjfFPC4K8\n" + "GGYVkWZgSCf/PRt/DgDnZOfZSSYIQNeyr21emqgqQ+yhXEGKVjcDTKcbSLiWAdA+\n" + "GkAzLAXXhafM8mrhpnGKdO4Or6ySz7G1vk2Jt2ZSdP740oVSJi59P9NEgXcbd3c4\n" + "FzjXSOOsxfhPQfobUk3ikt55lN3fBX3mBvUduxNhAcQ02ZD5zXrX6+loiV8CAwEA\n" + "AQKCAgAfFO45zIOEt4uprOQbGgscVMbm6FZVn/W+q4w1vjJvAjodl6wl3ikkII8z\n" + "RyViroMI98DAjHTrgaAtv0eZ5CgeLBINbTPlByZvMdyc+Vsk3UknUymhNC1FG8pq\n" + "2eZwxlYvLpcltya/4vEWJrHxceDUC5UiU4fKUv/u/AXxxeLfnBDuGUE/luh8/GQ7\n" + "3E8XTJmQ/C5045E0DSHczgHWlKpyuBejuh0I6hJ+k5x1nfoh2S3iUe3c14I+gD/F\n" + "3Q8qm+7W16zA7ytD29Cbx+yMh1Ak0pf+CxELGMf6eSX0O4wYTkjYcUcDglVv5lnX\n" + "daWsWj4DO/lZKTRXN0KSa75uqg72Q1FjK//UNEigO99HYMsOWHBtaRzAwkklY5Da\n" + "5WHn3sxmfotlFDiyT30R/T0dpAjvgH18A235KOpgLnM7Kaxc3kjMmorIJrkD25oG\n" + "OmRRTvdZ5rQ+IuBzaGUOD4ZwTwQ9HMieMjjLCcmkhhzzIZni1eNMva7MJyws4qcH\n" + "tjOPQvtb8m8ZXzT77nnkKirbJLVk+FqzL93/w1Kp/BRgVVChrXhdDFW2KSI8sx7Z\n" + "T7J8Dir4Oz2JFgpuBLKTz2Bnu6EDNEdGmomP79DO2IGoPNwhhBRDNM2oYR2nPTME\n" + "0f9moTJBghsi6rutgxkf1KDY6z2oysJKoJowegEYaUh0J0aHqQKCAQEA8hEL2y5C\n" + "iq2fzLRulXEVLG4di6ZZ0ZcyuV6rwQRWrhqv//+csagNmvguz6mFF9iNciv8FT2Z\n" + "crIgJUPefslKXuqqm/zEhhafDBXypMHsk4yReIdlxQDkmnamoGJZRd3CSsNFm68a\n" + "52hkl3gniMprMp8wWyr2UNeahD9cgtooyua/hyaXewh57L9pJGHlLayvqEn6Rs0V\n" + "0lpSzMTJWqFrDPuSc+ufsd3sk1MfvdnDw5oh7cHjZhlHJVtPSrjneCTbEnNpXIr/\n" + "yGL+qamZD+a8a318KMz72y3RwA0VMkhhkAYFYV+S5qYrlbFxjacVOS0Zi0LOklrl\n" + "jGMj6RzcD2W35QKCAQEAyEP27OgVTkaEr3bmNHYMBqYZ2snYMUgJF5GOitfLGSGM\n" + "55Io++BO6NMDbcNyCtWu2RYbHfdF1qjlTxPHjqsy6z4+tpxjpnPQEbO5eN1PG3iZ\n" + "+YO6z1yXLMwglkK4Acv1YWkMZ6l2V55MyntdiCWG/UYOlVw1kxqxlhgzmyq1ZMj5\n" + "4IOGqjsjPsMs2ZVANE54y/SriocnM/2Z08440SElOtheu5G/PfTF2j3ZZRBvuggu\n" + "MVnl2+5c0PpT1DGS74327WhRWDixmgEPEgLTd9hSpCWN/5nj67zskHKv6pmOLS+I\n" + "jd+rpzrnqDallDmTm/DqcLLDuaxsxEV/788pRllf8wKCAQEAoxcfENZTGNIv9yCd\n" + "3OvqoxuxplQ28cJX95K0T4BX0kfCyszySrP6Lq4GA/2n4VASxJij57+v8hnXFKRs\n" + "dKm0BM1Ak4Yy9lCpaeAjsiPB/AtaO4Wl6JxYaUWFsEty8GKfs/VqoaDRlJW+KFtY\n" + "743JubqNPu9sMz2AKpfyAWtwznu3ERzMNKWaWAsCkPOwEBzn4I+vIyKsECSw4qu3\n" + "KevVj1Kz8owO9SybZws7OJNOlSv0rhbS2ggv6hhiDOsVcNoMC5tconA4M0+XWsIc\n" + "kR0ZV6adD3REQADX7/ggjtc7fGjCGT/mXqYYeWurIRAweWxMaIpjWTIKtJJbMIU0\n" + "Mt+KjQKCAQAbtzw/QUdhk+TdG8l0TToQ2YAOhYzEFUIc3uopUQAstDX5/oJpiXui\n" + "QUHiOQBZe4U9Sg/qr8QclzdVIFmn5w2e/PhU8YPhD3omWQc8MPS3ypMUsyRxelD5\n" + "xC5mXUl2BjIpjw5Gcm+MZL4f777cDsWF2+I8zYwklbcqHKNXwCtmjWH3rnw+pvyT\n" + "vRNB8aP3GT0ijPQIsfe8/EYDyDCY0MuEP1ms/9jFzFBtic3CbOnphyRNdDGZpH13\n" + "9o0PeuTo/m7EIIHRgdcihy78wSNfHLMjQIdMbpHamETtINIz15iTrFZrvB7XgBF7\n" + "eESmJOnG1Sq8+iCYW8KZzzyLhdIiiE/9AoIBAQDGZG7/r8feIMKUWGJmm+uWDAEi\n" + "FRn0gZap3HZRDkmgYE6Xwr6CwUBp1YWvjQGQdln9BSrc6kXazOQrX+wpaNmW5x90\n" + "EMinO3Ekg+c5ivYgw1IxN26bbOnlDUpeUDH2mp4OV9MhMmPB6EfRWbztflK7545j\n" + "SJ0sOADajDCq5WeR3IyXT9Pq99wZ1BI4qw/MD7HUzx38n7G3qa/BOQcdyETN1L1l\n" + "BZgRlbpzktD2AjX71p8FaVfeRA2R4/BWPAzBEhGdLgitXL1UVZDC/TzZBKwQcwpG\n" + "JvKExITQBoOQmIOPbEYoLZ7UAiiOmCi/QlOjswP94gTKW4YHEqu6dqMHaaw+\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 f859bac..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 @@ -179,16 +265,96 @@ void connection_context::parse_client_greeting( } [[nodiscard]] network_buffer_type -connection_context::generate_encoded_fast_auth() { +connection_context::generate_encoded_auth_method_switch() { + std::string result_buffer{}; + + 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( + {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(); +} + +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::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) { @@ -423,12 +589,28 @@ 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_view connection_context::get_frame_payload( + const network_buffer_type &payload) noexcept { + if (std::size(payload) <= get_frame_header_length()) { + return {}; + } + + // 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() { return sequence_number_++; } diff --git a/src/minimysql/connection_context.hpp b/src/minimysql/connection_context.hpp index a2c69d7..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_; @@ -130,7 +144,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_fast_auth(); + [[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_ok(); [[nodiscard]] network_buffer_type generate_encoded_eof(); [[nodiscard]] network_buffer_type @@ -225,12 +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::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 412472f..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,15 +293,67 @@ 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"; - // TODO: send SwitchAuthentication packet - co_return; + 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"); + 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()) { + + 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()}; @@ -310,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)"); @@ -480,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(); @@ -504,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 (...) { @@ -517,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)); +}