1290 lines
52 KiB
C++
1290 lines
52 KiB
C++
// Licensed to the Apache Software Foundation (ASF) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
#include <gflags/gflags.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <openssl/err.h>
|
|
#include <openssl/ssl.h>
|
|
#include <pthread.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "brpc/policy/mysql/mysql_auth_handshake.h"
|
|
#include "brpc/policy/mysql/mysql_auth_packet.h"
|
|
#include "brpc/policy/mysql/mysql_auth_scramble.h"
|
|
#include "butil/logging.h"
|
|
#include "butil/strings/string_piece.h"
|
|
|
|
// When true, the server-integration tests connect to an already-running
|
|
// MySQL server (on -mysql_host:-mysql_port, as -mysql_user/-mysql_password)
|
|
// that the test neither starts nor stops. When false (the default), the
|
|
// fixture spawns and tears down its own throwaway server, exactly like
|
|
// test/brpc_redis_unittest.cpp.
|
|
DEFINE_bool(mysql_use_running_server, false,
|
|
"Use an already-running MySQL server instead of spawning a "
|
|
"throwaway one; the running server is neither started nor "
|
|
"stopped by the test.");
|
|
DEFINE_string(mysql_host, "127.0.0.1",
|
|
"Host of the running MySQL server "
|
|
"(only with -mysql_use_running_server).");
|
|
DEFINE_int32(mysql_port, 13306,
|
|
"TCP port of the MySQL server (used for both the running "
|
|
"server and the spawned throwaway server).");
|
|
DEFINE_string(mysql_user, "root",
|
|
"User for the authentication tests against a running server.");
|
|
DEFINE_string(mysql_password, "",
|
|
"Password for -mysql_user (empty for the spawned server).");
|
|
|
|
namespace {
|
|
|
|
using brpc::policy::mysql::AuthMoreData;
|
|
using brpc::policy::mysql::AuthSwitchRequest;
|
|
using brpc::policy::mysql::BuildHandshakeResponse41;
|
|
using brpc::policy::mysql::DecodePacketHeader;
|
|
using brpc::policy::mysql::EncodePacketHeader;
|
|
using brpc::policy::mysql::HandshakeResponse41;
|
|
using brpc::policy::mysql::HandshakeV10;
|
|
using brpc::policy::mysql::PacketHeader;
|
|
using brpc::policy::mysql::ParseAuthMoreData;
|
|
using brpc::policy::mysql::ParseAuthSwitchRequest;
|
|
using brpc::policy::mysql::ParseHandshakeV10;
|
|
using brpc::policy::mysql::kAuthMoreDataTag;
|
|
using brpc::policy::mysql::kAuthSwitchRequestTag;
|
|
using brpc::policy::mysql::kErrPacketTag;
|
|
using brpc::policy::mysql::kHandshakeV10Tag;
|
|
using brpc::policy::mysql::kOkPacketTag;
|
|
using brpc::policy::mysql::kPacketHeaderLen;
|
|
using brpc::policy::mysql::kSaltLen;
|
|
using brpc::policy::mysql::CLIENT_CONNECT_WITH_DB;
|
|
using brpc::policy::mysql::CLIENT_PLUGIN_AUTH;
|
|
using brpc::policy::mysql::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
|
|
using brpc::policy::mysql::CLIENT_PROTOCOL_41;
|
|
using brpc::policy::mysql::CLIENT_SECURE_CONNECTION;
|
|
using brpc::policy::mysql::NativePasswordScramble;
|
|
using brpc::policy::mysql::CachingSha2PasswordScramble;
|
|
using brpc::policy::mysql::CachingSha2PasswordRsaEncrypt;
|
|
using brpc::policy::mysql::CachingSha2PasswordCleartext;
|
|
using brpc::policy::mysql::CachingSha2PasswordSlowPath;
|
|
using brpc::policy::mysql::kNativePasswordResponseLen;
|
|
using brpc::policy::mysql::kCachingSha2PasswordResponseLen;
|
|
|
|
// Constructs a synthetic HandshakeV10 packet payload matching the wire
|
|
// format described at:
|
|
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_v10.html
|
|
std::string MakeHandshakeV10Payload(
|
|
const std::string& server_version,
|
|
uint32_t connection_id,
|
|
const std::string& salt,
|
|
uint32_t capability_flags,
|
|
uint8_t character_set,
|
|
uint16_t status_flags,
|
|
const std::string& auth_plugin_name) {
|
|
std::string out;
|
|
out.push_back(static_cast<char>(kHandshakeV10Tag));
|
|
out.append(server_version);
|
|
out.push_back('\0');
|
|
for (int i = 0; i < 4; ++i) {
|
|
out.push_back(static_cast<char>((connection_id >> (8 * i)) & 0xff));
|
|
}
|
|
// Salt part 1 (first 8 bytes).
|
|
out.append(salt.data(), 8);
|
|
// Filler.
|
|
out.push_back('\0');
|
|
// Capability flags low 16 bits.
|
|
out.push_back(static_cast<char>(capability_flags & 0xff));
|
|
out.push_back(static_cast<char>((capability_flags >> 8) & 0xff));
|
|
// Character set.
|
|
out.push_back(static_cast<char>(character_set));
|
|
// Status flags.
|
|
out.push_back(static_cast<char>(status_flags & 0xff));
|
|
out.push_back(static_cast<char>((status_flags >> 8) & 0xff));
|
|
// Capability flags high 16 bits.
|
|
out.push_back(static_cast<char>((capability_flags >> 16) & 0xff));
|
|
out.push_back(static_cast<char>((capability_flags >> 24) & 0xff));
|
|
// Length of auth-plugin-data: 21 (8 + 12 + 1 NUL filler) when
|
|
// CLIENT_PLUGIN_AUTH set, 0 otherwise.
|
|
const uint8_t apd_total = (capability_flags & CLIENT_PLUGIN_AUTH) ? 21 : 0;
|
|
out.push_back(static_cast<char>(apd_total));
|
|
// 10 reserved zeros.
|
|
out.append(10, '\0');
|
|
if (capability_flags & CLIENT_SECURE_CONNECTION) {
|
|
// Salt part 2: 12 bytes plus 1 NUL filler.
|
|
out.append(salt.data() + 8, salt.size() - 8);
|
|
out.push_back('\0');
|
|
}
|
|
if (capability_flags & CLIENT_PLUGIN_AUTH) {
|
|
out.append(auth_plugin_name);
|
|
out.push_back('\0');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// HandshakeV10 parser
|
|
// ----------------------------------------------------------------------
|
|
|
|
TEST(HandshakeV10Test, HappyPath_Mysql8Style) {
|
|
std::string salt;
|
|
for (int i = 1; i <= 20; ++i) salt.push_back(static_cast<char>(i));
|
|
const uint32_t caps =
|
|
CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH;
|
|
|
|
const std::string payload = MakeHandshakeV10Payload(
|
|
"8.0.32", 42, salt, caps,
|
|
/*character_set=*/0xff, /*status_flags=*/0x0002,
|
|
"mysql_native_password");
|
|
|
|
HandshakeV10 hs;
|
|
ASSERT_TRUE(ParseHandshakeV10(payload, &hs));
|
|
EXPECT_EQ(hs.protocol_version, kHandshakeV10Tag);
|
|
EXPECT_EQ(hs.server_version, "8.0.32");
|
|
EXPECT_EQ(hs.connection_id, 42u);
|
|
EXPECT_EQ(hs.auth_plugin_data, salt);
|
|
EXPECT_EQ(hs.auth_plugin_data.size(), kSaltLen);
|
|
EXPECT_TRUE(hs.capability_flags & CLIENT_PLUGIN_AUTH);
|
|
EXPECT_TRUE(hs.capability_flags & CLIENT_SECURE_CONNECTION);
|
|
EXPECT_EQ(hs.character_set, 0xff);
|
|
EXPECT_EQ(hs.status_flags, 0x0002);
|
|
EXPECT_EQ(hs.auth_plugin_name, "mysql_native_password");
|
|
}
|
|
|
|
TEST(HandshakeV10Test, HappyPath_CachingSha2Server) {
|
|
std::string salt;
|
|
for (int i = 0; i < 20; ++i) salt.push_back(static_cast<char>('A' + i));
|
|
const uint32_t caps =
|
|
CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH;
|
|
|
|
const std::string payload = MakeHandshakeV10Payload(
|
|
"8.0.32", 7, salt, caps, 0xff, 0x0002, "caching_sha2_password");
|
|
|
|
HandshakeV10 hs;
|
|
ASSERT_TRUE(ParseHandshakeV10(payload, &hs));
|
|
EXPECT_EQ(hs.auth_plugin_name, "caching_sha2_password");
|
|
EXPECT_EQ(hs.auth_plugin_data, salt);
|
|
}
|
|
|
|
TEST(HandshakeV10Test, RejectsBadProtocolVersion) {
|
|
std::string payload(1, static_cast<char>(0x09)); // not 10
|
|
payload.append("ignored");
|
|
HandshakeV10 hs;
|
|
EXPECT_FALSE(ParseHandshakeV10(payload, &hs));
|
|
}
|
|
|
|
TEST(HandshakeV10Test, RejectsTruncatedAtServerVersion) {
|
|
// Tag, but no NUL anywhere -> server_version unterminated.
|
|
std::string payload(1, static_cast<char>(kHandshakeV10Tag));
|
|
payload.append(20, 'x'); // no NUL
|
|
HandshakeV10 hs;
|
|
EXPECT_FALSE(ParseHandshakeV10(payload, &hs));
|
|
}
|
|
|
|
TEST(HandshakeV10Test, RejectsEmptyPayload) {
|
|
HandshakeV10 hs;
|
|
EXPECT_FALSE(ParseHandshakeV10(butil::StringPiece(""), &hs));
|
|
}
|
|
|
|
TEST(HandshakeV10Test, RejectsTruncatedBeforeSalt) {
|
|
// Build a payload then chop after capability_flags_lo.
|
|
std::string salt(20, '\x01');
|
|
const std::string full = MakeHandshakeV10Payload(
|
|
"8.0.32", 1, salt, CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION,
|
|
0xff, 0, "");
|
|
// Chop early — keep only protocol+server_version+conn_id+part1+filler+caps_lo.
|
|
const std::string truncated(full.data(), 6 + 1 + 4 + 8 + 1 + 2);
|
|
HandshakeV10 hs;
|
|
EXPECT_FALSE(ParseHandshakeV10(truncated, &hs));
|
|
}
|
|
|
|
TEST(HandshakeV10Test, ExtractsFull20ByteSalt) {
|
|
std::string salt(20, 0);
|
|
for (int i = 0; i < 20; ++i) salt[i] = static_cast<char>(0xA0 + i);
|
|
const std::string payload = MakeHandshakeV10Payload(
|
|
"8.0.32", 1, salt,
|
|
CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH,
|
|
0xff, 0, "mysql_native_password");
|
|
HandshakeV10 hs;
|
|
ASSERT_TRUE(ParseHandshakeV10(payload, &hs));
|
|
EXPECT_EQ(hs.auth_plugin_data.size(), kSaltLen);
|
|
EXPECT_EQ(hs.auth_plugin_data, salt);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// HandshakeResponse41 builder
|
|
// ----------------------------------------------------------------------
|
|
|
|
TEST(HandshakeResponse41Test, BuildsExpectedLayout) {
|
|
HandshakeResponse41 req;
|
|
req.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH
|
|
| CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
|
|
req.max_packet_size = 1u << 24;
|
|
req.character_set = 0x21;
|
|
req.username = "root";
|
|
req.auth_response = std::string(20, '\x42'); // canned scramble
|
|
req.auth_plugin_name = "mysql_native_password";
|
|
|
|
std::string payload;
|
|
ASSERT_TRUE(BuildHandshakeResponse41(req, &payload));
|
|
|
|
// 4 caps + 4 max_pkt + 1 charset + 23 reserved = 32 bytes fixed prefix
|
|
ASSERT_GE(payload.size(), 32u);
|
|
// Caps roundtrip
|
|
uint32_t caps = static_cast<unsigned char>(payload[0])
|
|
| (static_cast<uint32_t>(static_cast<unsigned char>(payload[1])) << 8)
|
|
| (static_cast<uint32_t>(static_cast<unsigned char>(payload[2])) << 16)
|
|
| (static_cast<uint32_t>(static_cast<unsigned char>(payload[3])) << 24);
|
|
EXPECT_EQ(caps, req.capability_flags);
|
|
// Username + NUL + lenenc(20) + 20 bytes + plugin + NUL
|
|
const char* p = payload.data() + 32;
|
|
EXPECT_EQ(std::string(p, 5), std::string("root\0", 5));
|
|
p += 5;
|
|
EXPECT_EQ(static_cast<unsigned char>(*p), 20u); // lenenc(20) = 0x14
|
|
++p;
|
|
EXPECT_EQ(std::string(p, 20), std::string(20, '\x42'));
|
|
p += 20;
|
|
const std::string plugin_nul("mysql_native_password\0", 22);
|
|
EXPECT_EQ(std::string(p, plugin_nul.size()), plugin_nul);
|
|
}
|
|
|
|
TEST(HandshakeResponse41Test, OmitsDatabaseWhenFlagAbsent) {
|
|
HandshakeResponse41 req;
|
|
req.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH
|
|
| CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
|
|
req.max_packet_size = 1u << 24;
|
|
req.character_set = 0x21;
|
|
req.username = "u";
|
|
req.auth_response = std::string(20, '\x01');
|
|
req.database = "mydb"; // should be ignored
|
|
req.auth_plugin_name = "mysql_native_password";
|
|
|
|
std::string payload;
|
|
ASSERT_TRUE(BuildHandshakeResponse41(req, &payload));
|
|
EXPECT_EQ(payload.find("mydb"), std::string::npos);
|
|
}
|
|
|
|
TEST(HandshakeResponse41Test, IncludesDatabaseWhenFlagSet) {
|
|
HandshakeResponse41 req;
|
|
req.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH | CLIENT_CONNECT_WITH_DB
|
|
| CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
|
|
req.max_packet_size = 1u << 24;
|
|
req.character_set = 0x21;
|
|
req.username = "u";
|
|
req.auth_response = std::string(20, '\x01');
|
|
req.database = "mydb";
|
|
req.auth_plugin_name = "mysql_native_password";
|
|
|
|
std::string payload;
|
|
ASSERT_TRUE(BuildHandshakeResponse41(req, &payload));
|
|
EXPECT_NE(payload.find("mydb"), std::string::npos);
|
|
}
|
|
|
|
TEST(HandshakeResponse41Test, HandlesLargeAuthResponseViaLenEncoding) {
|
|
// 256-byte RSA ciphertext — exercises lenenc 0xfc 2-byte branch.
|
|
HandshakeResponse41 req;
|
|
req.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH
|
|
| CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
|
|
req.max_packet_size = 1u << 24;
|
|
req.character_set = 0x21;
|
|
req.username = "u";
|
|
req.auth_response = std::string(256, '\xAA');
|
|
req.auth_plugin_name = "caching_sha2_password";
|
|
|
|
std::string payload;
|
|
ASSERT_TRUE(BuildHandshakeResponse41(req, &payload));
|
|
// lenenc 256 -> 0xfc 0x00 0x01
|
|
const std::string lenenc("\xfc\x00\x01", 3);
|
|
EXPECT_NE(payload.find(lenenc), std::string::npos);
|
|
}
|
|
|
|
TEST(HandshakeResponse41Test, RejectsOversizeAuthResponseWithoutLenEnc) {
|
|
// CLIENT_SECURE_CONNECTION without the lenenc flag uses a 1-byte length
|
|
// prefix, so a >255-byte auth_response cannot be represented. The builder
|
|
// must hard-fail (return false) and write nothing, rather than silently
|
|
// truncating to 255 bytes.
|
|
HandshakeResponse41 req;
|
|
req.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH; // deliberately no LENENC flag
|
|
req.max_packet_size = 1u << 24;
|
|
req.character_set = 0x21;
|
|
req.username = "u";
|
|
req.auth_response = std::string(256, '\xAA'); // 256 > 255
|
|
req.auth_plugin_name = "caching_sha2_password";
|
|
|
|
std::string payload;
|
|
EXPECT_FALSE(BuildHandshakeResponse41(req, &payload));
|
|
EXPECT_TRUE(payload.empty())
|
|
<< "no bytes must be written to out on failure";
|
|
}
|
|
|
|
// Exactly 255 bytes is the boundary that still fits the 1-byte length prefix.
|
|
TEST(HandshakeResponse41Test, AcceptsMaxSizeAuthResponseWithoutLenEnc) {
|
|
HandshakeResponse41 req;
|
|
req.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH;
|
|
req.max_packet_size = 1u << 24;
|
|
req.character_set = 0x21;
|
|
req.username = "u";
|
|
req.auth_response = std::string(255, '\xAA'); // fits in one byte
|
|
req.auth_plugin_name = "caching_sha2_password";
|
|
|
|
std::string payload;
|
|
ASSERT_TRUE(BuildHandshakeResponse41(req, &payload));
|
|
// After "u\0" we expect length byte 0xFF (255) then 255 payload bytes.
|
|
const size_t u_end = payload.find('u') + 2;
|
|
EXPECT_EQ(static_cast<unsigned char>(payload[u_end]), 255u);
|
|
}
|
|
|
|
TEST(HandshakeResponse41Test, UsesSingleByteLengthWithoutLenEncFlag) {
|
|
HandshakeResponse41 req;
|
|
req.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH;
|
|
req.max_packet_size = 1u << 24;
|
|
req.character_set = 0x21;
|
|
req.username = "u";
|
|
req.auth_response = std::string(20, '\x77');
|
|
req.auth_plugin_name = "mysql_native_password";
|
|
|
|
std::string payload;
|
|
ASSERT_TRUE(BuildHandshakeResponse41(req, &payload));
|
|
// After username "u\0", we expect 1-byte length 0x14 (20).
|
|
const size_t u_end = payload.find('u') + 2; // skip 'u' + NUL
|
|
EXPECT_EQ(static_cast<unsigned char>(payload[u_end]), 20u);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// AuthSwitchRequest parser
|
|
// ----------------------------------------------------------------------
|
|
|
|
TEST(AuthSwitchRequestTest, HappyPath) {
|
|
std::string payload(1, static_cast<char>(kAuthSwitchRequestTag));
|
|
payload.append("caching_sha2_password");
|
|
payload.push_back('\0');
|
|
payload.append(20, '\xAA');
|
|
payload.push_back('\0'); // trailing NUL filler
|
|
AuthSwitchRequest sw;
|
|
ASSERT_TRUE(ParseAuthSwitchRequest(payload, &sw));
|
|
EXPECT_EQ(sw.auth_plugin_name, "caching_sha2_password");
|
|
EXPECT_EQ(sw.auth_plugin_data, std::string(20, '\xAA'));
|
|
}
|
|
|
|
TEST(AuthSwitchRequestTest, RejectsBadTag) {
|
|
std::string payload(1, static_cast<char>(0x00));
|
|
payload.append("x\0", 2);
|
|
AuthSwitchRequest sw;
|
|
EXPECT_FALSE(ParseAuthSwitchRequest(payload, &sw));
|
|
}
|
|
|
|
TEST(AuthSwitchRequestTest, RejectsMissingPluginNameNul) {
|
|
std::string payload(1, static_cast<char>(kAuthSwitchRequestTag));
|
|
payload.append("no_nul_here_at_all");
|
|
AuthSwitchRequest sw;
|
|
EXPECT_FALSE(ParseAuthSwitchRequest(payload, &sw));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// AuthMoreData parser
|
|
// ----------------------------------------------------------------------
|
|
|
|
TEST(AuthMoreDataTest, FastAuthOkMarker) {
|
|
const char data[] = {static_cast<char>(kAuthMoreDataTag), '\x03'};
|
|
AuthMoreData mod;
|
|
ASSERT_TRUE(ParseAuthMoreData(butil::StringPiece(data, sizeof(data)), &mod));
|
|
EXPECT_EQ(mod.data, std::string("\x03", 1));
|
|
}
|
|
|
|
TEST(AuthMoreDataTest, RequestPubKeyMarker) {
|
|
const char data[] = {static_cast<char>(kAuthMoreDataTag), '\x04'};
|
|
AuthMoreData mod;
|
|
ASSERT_TRUE(ParseAuthMoreData(butil::StringPiece(data, sizeof(data)), &mod));
|
|
EXPECT_EQ(mod.data, std::string("\x04", 1));
|
|
}
|
|
|
|
TEST(AuthMoreDataTest, PubKeyPayload) {
|
|
std::string payload(1, static_cast<char>(kAuthMoreDataTag));
|
|
const std::string pem = "-----BEGIN PUBLIC KEY-----\nABC\n-----END PUBLIC KEY-----\n";
|
|
payload.append(pem);
|
|
AuthMoreData mod;
|
|
ASSERT_TRUE(ParseAuthMoreData(payload, &mod));
|
|
EXPECT_EQ(mod.data, pem);
|
|
}
|
|
|
|
TEST(AuthMoreDataTest, RejectsBadTag) {
|
|
std::string payload(1, static_cast<char>(0x00));
|
|
payload.append("\x03", 1);
|
|
AuthMoreData mod;
|
|
EXPECT_FALSE(ParseAuthMoreData(payload, &mod));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// End-to-end handshake against a real mysqld.
|
|
//
|
|
// Two modes, selected by the -mysql_use_running_server flag:
|
|
//
|
|
// * Self-spawned throwaway server (the DEFAULT, flag false). The
|
|
// fixture brings up its own mysqld and tears it down on exit,
|
|
// exactly like test/brpc_redis_unittest.cpp; --initialize-insecure
|
|
// leaves root with an empty password, so caching_sha2_password
|
|
// completes via its fast path with no RSA round trip. Keeps CI
|
|
// self-contained.
|
|
//
|
|
// * Already-running server (flag true). The tests connect to a
|
|
// server you started yourself on -mysql_host:-mysql_port and do
|
|
// NOT start or stop it. Run that server in a terminal with
|
|
// --log-error-verbosity=3 to watch the handshake; see
|
|
// test/README_mysql_auth.md for the bring-up commands. With a real
|
|
// -mysql_password, caching_sha2_password takes its RSA full-auth
|
|
// path over plain TCP, exercising CachingSha2PasswordRsaEncrypt
|
|
// against a real server.
|
|
//
|
|
// MySQL 8.4+/9.x ship without the mysql_native_password server plugin,
|
|
// so both modes authenticate with caching_sha2_password.
|
|
// ----------------------------------------------------------------------
|
|
|
|
#define MYSQLD_BIN "mysqld"
|
|
|
|
static pthread_once_t start_mysqld_once = PTHREAD_ONCE_INIT;
|
|
// >0 : we forked a throwaway mysqld with this pid.
|
|
// -2 : an already-running server (-mysql_use_running_server) is reachable.
|
|
// -1 : no server available; server tests skip.
|
|
static pid_t g_mysqld_pid = -1;
|
|
|
|
// Connection parameters, resolved once in RunMysqlServer().
|
|
static std::string g_mysql_host = "127.0.0.1";
|
|
static int g_mysql_port = 13306;
|
|
static std::string g_mysql_user = "root";
|
|
static std::string g_mysql_password; // empty for the self-spawned server
|
|
|
|
// A (user, password) pair the auth tests exercise. An empty password
|
|
// takes caching_sha2's fast path; a non-empty password against a cold
|
|
// cache takes the RSA full-auth path. Populated once in
|
|
// RunMysqlServer(): the spawned server gets BOTH an empty-password and a
|
|
// non-empty-password account so it can exercise both paths; a running
|
|
// server contributes the single -mysql_user/-mysql_password credential.
|
|
struct AuthCase {
|
|
std::string label;
|
|
std::string user;
|
|
std::string password;
|
|
bool use_ssl; // drive the login over a SSL connection (set at every init site)
|
|
};
|
|
static std::vector<AuthCase> g_auth_cases;
|
|
|
|
// Non-empty-password accounts created on the spawned server. Two distinct
|
|
// accounts so the plaintext (RSA) and SSL (cleartext) full-auth tests each
|
|
// hit a COLD caching_sha2 cache deterministically (one login would
|
|
// otherwise warm the cache for the other).
|
|
static const char* const kSpawnPwUser = "brpc_test";
|
|
static const char* const kSpawnSslUser = "brpc_ssl";
|
|
static const char* const kSpawnPwPassword = "brpc_test_password";
|
|
|
|
// True when this process spawned its own throwaway mysqld (vs. a running
|
|
// server). Spawned servers are brand-new, so credentials are cold.
|
|
static bool IsSpawnedServer() { return g_mysqld_pid > 0; }
|
|
|
|
// Returns the first non-empty-password credential matching |use_ssl|, or
|
|
// NULL when the active server exposes none (so the caller can skip).
|
|
static const AuthCase* FindNonEmptyCase(bool use_ssl) {
|
|
for (size_t i = 0; i < g_auth_cases.size(); ++i) {
|
|
if (!g_auth_cases[i].password.empty() &&
|
|
g_auth_cases[i].use_ssl == use_ssl) {
|
|
return &g_auth_cases[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Absolute path to the throwaway data directory. mysqld resolves a
|
|
// relative --datadir against its basedir (not the current working
|
|
// directory), so the path handed to mysqld must be absolute.
|
|
static std::string TestDataDir() {
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof(cwd)) == NULL) {
|
|
return std::string("/tmp/mysql_data_for_test");
|
|
}
|
|
return std::string(cwd) + "/mysql_data_for_test";
|
|
}
|
|
|
|
static void RemoveMysqlServer() {
|
|
if (g_mysqld_pid > 0) {
|
|
puts("[Stopping mysqld]");
|
|
char cmd[1280];
|
|
snprintf(cmd, sizeof(cmd), "kill %d", g_mysqld_pid);
|
|
CHECK(0 == system(cmd));
|
|
// Wait for mysqld to flush and exit before removing its datadir.
|
|
usleep(500000);
|
|
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", TestDataDir().c_str());
|
|
CHECK(0 == system(cmd));
|
|
}
|
|
}
|
|
|
|
// Opens a TCP connection to g_mysql_host:g_mysql_port. Returns the fd
|
|
// on success or -1 on failure (without logging, so callers can poll).
|
|
static int ConnectTestMysql() {
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
return -1;
|
|
}
|
|
struct sockaddr_in addr;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(static_cast<uint16_t>(g_mysql_port));
|
|
addr.sin_addr.s_addr = inet_addr(g_mysql_host.c_str());
|
|
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
static void RunMysqlServer() {
|
|
// Mode 1 (flag true): connect to a server the caller started; do not
|
|
// start or stop it.
|
|
if (FLAGS_mysql_use_running_server) {
|
|
g_mysql_host = FLAGS_mysql_host;
|
|
g_mysql_port = FLAGS_mysql_port;
|
|
g_mysql_user = FLAGS_mysql_user;
|
|
g_mysql_password = FLAGS_mysql_password;
|
|
printf("[Using running mysqld at %s:%d as user '%s']\n",
|
|
g_mysql_host.c_str(), g_mysql_port, g_mysql_user.c_str());
|
|
int fd = ConnectTestMysql();
|
|
if (fd >= 0) {
|
|
close(fd);
|
|
g_mysqld_pid = -2; // running server reachable
|
|
g_auth_cases.push_back(
|
|
{"flag-credential", g_mysql_user, g_mysql_password, false});
|
|
g_auth_cases.push_back(
|
|
{"flag-credential-ssl", g_mysql_user, g_mysql_password, true});
|
|
} else {
|
|
printf("Cannot reach running mysqld at %s:%d, "
|
|
"following tests will be skipped\n",
|
|
g_mysql_host.c_str(), g_mysql_port);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Mode 2 (default): spawn a throwaway server with an empty-password
|
|
// root and tear it down on exit (the redis-unittest pattern).
|
|
if (system("which " MYSQLD_BIN) != 0) {
|
|
puts("Fail to find " MYSQLD_BIN ", following tests will be skipped");
|
|
return;
|
|
}
|
|
g_mysql_host = "127.0.0.1";
|
|
g_mysql_port = FLAGS_mysql_port;
|
|
g_mysql_user = "root";
|
|
g_mysql_password.clear();
|
|
const std::string datadir = TestDataDir();
|
|
char cmd[2048];
|
|
// Start from a clean, empty data directory every run; mysqld
|
|
// --initialize-insecure requires the directory to exist and be empty.
|
|
snprintf(cmd, sizeof(cmd), "rm -rf '%s' && mkdir -p '%s'",
|
|
datadir.c_str(), datadir.c_str());
|
|
if (system(cmd) != 0) {
|
|
puts("Fail to create datadir, following tests will be skipped");
|
|
return;
|
|
}
|
|
// Initialize root with an empty password. mysqld auto-detects its
|
|
// basedir from the binary location, so no --basedir is needed.
|
|
snprintf(cmd, sizeof(cmd),
|
|
MYSQLD_BIN " --initialize-insecure --datadir='%s'"
|
|
" --log-error='%s/init.err'",
|
|
datadir.c_str(), datadir.c_str());
|
|
if (system(cmd) != 0) {
|
|
puts("Fail to initialize mysqld datadir, following tests will be skipped");
|
|
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", datadir.c_str());
|
|
CHECK(0 == system(cmd));
|
|
return;
|
|
}
|
|
atexit(RemoveMysqlServer);
|
|
|
|
g_mysqld_pid = fork();
|
|
if (g_mysqld_pid < 0) {
|
|
puts("Fail to fork");
|
|
exit(1);
|
|
} else if (g_mysqld_pid == 0) {
|
|
puts("[Starting mysqld]");
|
|
char port_arg[32];
|
|
snprintf(port_arg, sizeof(port_arg), "--port=%d", FLAGS_mysql_port);
|
|
const std::string datadir_arg = "--datadir=" + datadir;
|
|
const std::string socket_arg = "--socket=" + datadir + "/mysqld.sock";
|
|
const std::string pidfile_arg = "--pid-file=" + datadir + "/mysqld.pid";
|
|
const std::string logerr_arg = "--log-error=" + datadir + "/mysqld.err";
|
|
char* const argv[] = {
|
|
(char*)MYSQLD_BIN,
|
|
(char*)datadir_arg.c_str(),
|
|
(char*)port_arg,
|
|
(char*)socket_arg.c_str(),
|
|
(char*)pidfile_arg.c_str(),
|
|
(char*)logerr_arg.c_str(),
|
|
(char*)"--mysqlx=OFF",
|
|
(char*)"--bind-address=127.0.0.1",
|
|
NULL };
|
|
if (execvp(MYSQLD_BIN, argv) < 0) {
|
|
puts("Fail to run " MYSQLD_BIN);
|
|
exit(1);
|
|
}
|
|
}
|
|
// Poll until mysqld accepts TCP connections (it has to recover its
|
|
// freshly created tablespace first), giving up after ~30s.
|
|
for (int i = 0; i < 300; ++i) {
|
|
int fd = ConnectTestMysql();
|
|
if (fd >= 0) {
|
|
close(fd);
|
|
// The spawned server always tests the empty-password root.
|
|
g_auth_cases.push_back(
|
|
{"empty-password", "root", std::string(), false});
|
|
// Additionally create two non-empty-password accounts (over the
|
|
// unix socket, where root has an empty password): one for the
|
|
// plaintext/RSA full-auth path and one for the SSL/cleartext
|
|
// full-auth path, each cold so both are deterministic.
|
|
// Best-effort: if the mysql client is missing both are skipped.
|
|
char create[2048];
|
|
snprintf(create, sizeof(create),
|
|
"mysql --socket='%s/mysqld.sock' -u root -e \""
|
|
"CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED WITH "
|
|
"caching_sha2_password BY '%s'; "
|
|
"GRANT ALL PRIVILEGES ON *.* TO '%s'@'%%'; "
|
|
"CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED WITH "
|
|
"caching_sha2_password BY '%s'; "
|
|
"GRANT ALL PRIVILEGES ON *.* TO '%s'@'%%';\" 2>/dev/null",
|
|
datadir.c_str(), kSpawnPwUser, kSpawnPwPassword,
|
|
kSpawnPwUser, kSpawnSslUser, kSpawnPwPassword,
|
|
kSpawnSslUser);
|
|
if (system(create) == 0) {
|
|
g_auth_cases.push_back(
|
|
{"nonempty-password", kSpawnPwUser, kSpawnPwPassword,
|
|
false});
|
|
g_auth_cases.push_back(
|
|
{"nonempty-password-ssl", kSpawnSslUser, kSpawnPwPassword,
|
|
true});
|
|
} else {
|
|
puts("mysql client unavailable; spawned server will test "
|
|
"only the empty-password path");
|
|
}
|
|
return;
|
|
}
|
|
usleep(100000);
|
|
}
|
|
puts("mysqld did not become ready, following tests will be skipped");
|
|
g_mysqld_pid = -1;
|
|
}
|
|
|
|
// Reads exactly |n| bytes into |buf|. When |ssl| is non-null the bytes
|
|
// come from the SSL session; otherwise from the raw fd. Returns true on
|
|
// success.
|
|
static bool ReadFull(int fd, char* buf, size_t n, SSL* ssl = NULL) {
|
|
size_t off = 0;
|
|
while (off < n) {
|
|
ssize_t r = ssl ? SSL_read(ssl, buf + off, static_cast<int>(n - off))
|
|
: read(fd, buf + off, n - off);
|
|
if (r > 0) {
|
|
off += static_cast<size_t>(r);
|
|
} else if (!ssl && r < 0 && errno == EINTR) {
|
|
continue;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Writes all of |data| (over SSL when |ssl| is non-null). Returns true
|
|
// on success.
|
|
static bool WriteFull(int fd, const std::string& data, SSL* ssl = NULL) {
|
|
size_t off = 0;
|
|
while (off < data.size()) {
|
|
ssize_t w = ssl ? SSL_write(ssl, data.data() + off,
|
|
static_cast<int>(data.size() - off))
|
|
: write(fd, data.data() + off, data.size() - off);
|
|
if (w > 0) {
|
|
off += static_cast<size_t>(w);
|
|
} else if (!ssl && w < 0 && errno == EINTR) {
|
|
continue;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Reads one MySQL packet (4-byte header + payload). On success stores
|
|
// the payload in *payload, the sequence id in *seq, and returns true.
|
|
static bool ReadPacket(int fd, std::string* payload, uint8_t* seq,
|
|
SSL* ssl = NULL) {
|
|
char hdr[kPacketHeaderLen];
|
|
if (!ReadFull(fd, hdr, sizeof(hdr), ssl)) {
|
|
return false;
|
|
}
|
|
PacketHeader header;
|
|
if (!DecodePacketHeader(butil::StringPiece(hdr, sizeof(hdr)), &header)) {
|
|
return false;
|
|
}
|
|
*seq = header.seq;
|
|
payload->resize(header.payload_len);
|
|
if (header.payload_len > 0 &&
|
|
!ReadFull(fd, &(*payload)[0], header.payload_len, ssl)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Frames |payload| with a packet header carrying |seq| and writes it.
|
|
static bool WritePacket(int fd, const std::string& payload, uint8_t seq,
|
|
SSL* ssl = NULL) {
|
|
std::string out;
|
|
PacketHeader header;
|
|
header.payload_len = static_cast<uint32_t>(payload.size());
|
|
header.seq = seq;
|
|
EncodePacketHeader(header, &out);
|
|
out.append(payload);
|
|
return WriteFull(fd, out, ssl);
|
|
}
|
|
|
|
// CLIENT_SSL capability flag (0x00000800) -- not part of the codec's
|
|
// CapabilityFlag enum; defined here for the test's SSL upgrade.
|
|
static const uint32_t kClientSSL = 0x00000800;
|
|
|
|
// Sends the MySQL SSLRequest packet (the 32-byte HandshakeResponse41
|
|
// fixed prefix with CLIENT_SSL set, no username) at sequence |seq|, then
|
|
// performs a SSL client handshake on |fd|. Returns the SSL* on success
|
|
// (caller owns it) or NULL on failure.
|
|
static SSL* UpgradeToSSL(int fd, uint32_t capability_flags, uint8_t seq) {
|
|
// SSLRequest payload: 4B caps + 4B max_packet_size + 1B charset + 23B
|
|
// reserved = 32 bytes, with CLIENT_SSL set.
|
|
const uint32_t caps = capability_flags | kClientSSL;
|
|
std::string payload;
|
|
for (int i = 0; i < 4; ++i)
|
|
payload.push_back(static_cast<char>((caps >> (8 * i)) & 0xff));
|
|
const uint32_t max_packet = 1u << 24;
|
|
for (int i = 0; i < 4; ++i)
|
|
payload.push_back(static_cast<char>((max_packet >> (8 * i)) & 0xff));
|
|
payload.push_back(static_cast<char>(0x21)); // charset utf8_general_ci
|
|
payload.append(23, '\0');
|
|
if (!WritePacket(fd, payload, seq)) {
|
|
return NULL;
|
|
}
|
|
// One client SSL_CTX for the whole process; certificate not verified
|
|
// (mysqld's auto-generated cert is self-signed).
|
|
static SSL_CTX* ctx = NULL;
|
|
if (ctx == NULL) {
|
|
ctx = SSL_CTX_new(TLS_client_method());
|
|
if (ctx == NULL) {
|
|
return NULL;
|
|
}
|
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
|
|
}
|
|
SSL* ssl = SSL_new(ctx);
|
|
if (ssl == NULL) {
|
|
return NULL;
|
|
}
|
|
SSL_set_fd(ssl, fd);
|
|
if (SSL_connect(ssl) != 1) {
|
|
SSL_free(ssl);
|
|
return NULL;
|
|
}
|
|
return ssl;
|
|
}
|
|
|
|
// Outcome of an SHA2-password client handshake, recording which
|
|
// authentication path the server drove so tests can assert on it.
|
|
struct LoginTrace {
|
|
bool ok = false; // server answered with an OK packet
|
|
bool full_auth = false; // server sent AuthMoreData 0x04
|
|
// (perform_full_authentication)
|
|
bool fast_auth = false; // server sent AuthMoreData 0x03
|
|
// (fast_auth_success; credential was cached)
|
|
bool auth_switched = false; // server sent an AuthSwitchRequest
|
|
bool used_ssl = false; // handshake ran over a SSL connection
|
|
bool used_cleartext = false;// full-auth sent the cleartext password
|
|
// (the is_ssl secure-transport branch)
|
|
std::string switched_plugin;// plugin the server switched us to
|
|
std::string err; // human-readable reason when !ok
|
|
|
|
// Convenience: which authentication path this login took.
|
|
const char* path() const {
|
|
if (full_auth) {
|
|
return used_cleartext ? "full-authentication (cleartext over SSL)"
|
|
: "full-authentication (RSA)";
|
|
}
|
|
if (fast_auth) return "cached fast-authentication";
|
|
return "direct OK (empty password / immediate)";
|
|
}
|
|
};
|
|
|
|
// Performs a complete SHA2-password client handshake against an
|
|
// already-greeted connection. Drives every branch the codec implements:
|
|
//
|
|
// 1. initial scramble in HandshakeResponse41, using |initial_plugin| if
|
|
// given (e.g. "mysql_native_password" to provoke an auth switch),
|
|
// otherwise the plugin the server advertised in its greeting;
|
|
// 2. AuthSwitchRequest (server asks for a different plugin / new salt) ->
|
|
// LoginTrace::auth_switched is set;
|
|
// 3. AuthMoreData fast-auth-success (0x03) -> cached path -> wait for OK;
|
|
// 4. AuthMoreData full-auth-required (0x04) -> full-auth path: request the
|
|
// RSA public key (send 0x02), receive the PEM, send the RSA-OAEP
|
|
// ciphertext.
|
|
//
|
|
// When |use_ssl| is true the client upgrades the connection to SSL
|
|
// (MySQL SSLRequest + SSL_connect) before sending HandshakeResponse41,
|
|
// and on full authentication routes through CachingSha2PasswordSlowPath
|
|
// with is_ssl=true -- i.e. the password is sent in the clear, protected
|
|
// by SSL, with no RSA exchange. When false, full auth takes the RSA
|
|
// public-key path (CachingSha2PasswordSlowPath with is_ssl=false).
|
|
//
|
|
// The returned LoginTrace records success, which path the server took,
|
|
// whether SSL was used, and (verified by inspecting the slow-path output)
|
|
// whether the cleartext or RSA branch was taken.
|
|
static LoginTrace PerformSha2Login(int fd, const std::string& user,
|
|
const std::string& password, bool use_ssl,
|
|
const std::string& initial_plugin =
|
|
std::string()) {
|
|
LoginTrace t;
|
|
SSL* ssl = NULL;
|
|
std::string payload;
|
|
uint8_t seq = 0;
|
|
if (!ReadPacket(fd, &payload, &seq)) { // greeting is always plaintext
|
|
t.err = "failed to read greeting";
|
|
goto done;
|
|
}
|
|
{
|
|
HandshakeV10 hs;
|
|
if (!ParseHandshakeV10(payload, &hs)) {
|
|
t.err = "failed to parse greeting";
|
|
goto done;
|
|
}
|
|
// The nonce used for both the fast scramble and the RSA-path XOR.
|
|
std::string salt = hs.auth_plugin_data;
|
|
// Initial client plugin: a caller-forced one (to provoke an auth
|
|
// switch) if given, else the plugin the server advertised.
|
|
std::string plugin =
|
|
!initial_plugin.empty()
|
|
? initial_plugin
|
|
: (hs.auth_plugin_name.empty() ? "caching_sha2_password"
|
|
: hs.auth_plugin_name);
|
|
|
|
HandshakeResponse41 resp;
|
|
resp.capability_flags = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
|
| CLIENT_PLUGIN_AUTH
|
|
| CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
|
|
resp.max_packet_size = 1u << 24;
|
|
resp.character_set = 0x21; // utf8_general_ci
|
|
|
|
// Greeting is seq 0; the client's next packet is seq 1. A SSL
|
|
// upgrade inserts the SSLRequest at seq 1, pushing the
|
|
// HandshakeResponse41 to seq 2.
|
|
uint8_t next_seq = static_cast<uint8_t>(seq + 1);
|
|
if (use_ssl) {
|
|
ssl = UpgradeToSSL(fd, resp.capability_flags, next_seq);
|
|
if (ssl == NULL) {
|
|
t.err = "SSL upgrade (SSLRequest + SSL_connect) failed";
|
|
goto done;
|
|
}
|
|
t.used_ssl = true;
|
|
resp.capability_flags |= kClientSSL;
|
|
next_seq = static_cast<uint8_t>(next_seq + 1);
|
|
}
|
|
resp.username = user;
|
|
resp.auth_plugin_name = plugin;
|
|
if (plugin == "caching_sha2_password") {
|
|
resp.auth_response = CachingSha2PasswordScramble(salt, password);
|
|
} else {
|
|
resp.auth_response = NativePasswordScramble(salt, password);
|
|
}
|
|
|
|
std::string resp_payload;
|
|
if (!BuildHandshakeResponse41(resp, &resp_payload)) {
|
|
t.err = "failed to build HandshakeResponse41";
|
|
goto done;
|
|
}
|
|
if (!WritePacket(fd, resp_payload, next_seq, ssl)) {
|
|
t.err = "failed to write HandshakeResponse41";
|
|
goto done;
|
|
}
|
|
|
|
// Continuation loop: follow the server through any auth-switch /
|
|
// more-data exchange to the terminal OK or ERR packet.
|
|
for (int guard = 0; guard < 8; ++guard) {
|
|
std::string pkt;
|
|
uint8_t pkt_seq = 0;
|
|
if (!ReadPacket(fd, &pkt, &pkt_seq, ssl)) {
|
|
t.err = "failed to read server reply";
|
|
goto done;
|
|
}
|
|
if (pkt.empty()) {
|
|
t.err = "empty server reply";
|
|
goto done;
|
|
}
|
|
const uint8_t tag = static_cast<uint8_t>(pkt[0]);
|
|
if (tag == kOkPacketTag) {
|
|
t.ok = true;
|
|
goto done;
|
|
}
|
|
if (tag == kErrPacketTag) {
|
|
t.err = "ERR packet: " + (pkt.size() > 9 ? pkt.substr(9)
|
|
: std::string("(no message)"));
|
|
goto done;
|
|
}
|
|
if (tag == kAuthSwitchRequestTag) {
|
|
t.auth_switched = true;
|
|
AuthSwitchRequest sw;
|
|
if (!ParseAuthSwitchRequest(pkt, &sw)) {
|
|
t.err = "failed to parse AuthSwitchRequest";
|
|
goto done;
|
|
}
|
|
plugin = sw.auth_plugin_name;
|
|
salt = sw.auth_plugin_data;
|
|
t.switched_plugin = sw.auth_plugin_name;
|
|
std::string scramble =
|
|
(plugin == "caching_sha2_password")
|
|
? CachingSha2PasswordScramble(salt, password)
|
|
: NativePasswordScramble(salt, password);
|
|
if (!WritePacket(fd, scramble,
|
|
static_cast<uint8_t>(pkt_seq + 1), ssl)) {
|
|
t.err = "failed to write auth-switch response";
|
|
goto done;
|
|
}
|
|
continue;
|
|
}
|
|
if (tag == kAuthMoreDataTag) {
|
|
AuthMoreData mod;
|
|
if (!ParseAuthMoreData(pkt, &mod) || mod.data.empty()) {
|
|
t.err = "failed to parse AuthMoreData";
|
|
goto done;
|
|
}
|
|
const uint8_t marker = static_cast<uint8_t>(mod.data[0]);
|
|
if (marker == 0x03) {
|
|
t.fast_auth = true; // cached credential; OK packet follows
|
|
continue;
|
|
}
|
|
if (marker == 0x04) {
|
|
t.full_auth = true; // perform_full_authentication
|
|
// On a secure channel the slow path ignores the pubkey
|
|
// and salt and sends the cleartext password, so we don't
|
|
// even request the RSA key. On plain TCP we must fetch
|
|
// the server's RSA public key first.
|
|
std::string pubkey;
|
|
uint8_t resp_after = static_cast<uint8_t>(pkt_seq + 1);
|
|
if (!use_ssl) {
|
|
if (!WritePacket(fd, std::string("\x02", 1),
|
|
static_cast<uint8_t>(pkt_seq + 1),
|
|
ssl)) {
|
|
t.err = "failed to request public key";
|
|
goto done;
|
|
}
|
|
std::string key_pkt;
|
|
uint8_t key_seq = 0;
|
|
if (!ReadPacket(fd, &key_pkt, &key_seq, ssl)) {
|
|
t.err = "failed to read public key";
|
|
goto done;
|
|
}
|
|
AuthMoreData key_mod;
|
|
if (!ParseAuthMoreData(key_pkt, &key_mod)) {
|
|
t.err = "failed to parse public-key AuthMoreData";
|
|
goto done;
|
|
}
|
|
pubkey = key_mod.data;
|
|
resp_after = static_cast<uint8_t>(key_seq + 1);
|
|
}
|
|
// Route through the dispatcher so the test exercises the
|
|
// is_ssl decision end to end.
|
|
const std::string slow =
|
|
CachingSha2PasswordSlowPath(password, salt, pubkey,
|
|
use_ssl);
|
|
if (slow.empty()) {
|
|
t.err = "slow-path produced empty payload";
|
|
goto done;
|
|
}
|
|
// Verify which branch the dispatcher actually took by
|
|
// comparing its output to the cleartext form.
|
|
t.used_cleartext =
|
|
(slow == CachingSha2PasswordCleartext(password));
|
|
if (!WritePacket(fd, slow, resp_after, ssl)) {
|
|
t.err = "failed to write slow-path response";
|
|
goto done;
|
|
}
|
|
continue;
|
|
}
|
|
t.err = "unexpected AuthMoreData marker";
|
|
goto done;
|
|
}
|
|
t.err = "unexpected packet tag";
|
|
goto done;
|
|
}
|
|
t.err = "handshake did not terminate";
|
|
goto done;
|
|
}
|
|
done:
|
|
if (ssl != NULL) {
|
|
SSL_shutdown(ssl);
|
|
SSL_free(ssl);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
class MysqlHandshakeServerTest : public testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
pthread_once(&start_mysqld_once, RunMysqlServer);
|
|
}
|
|
// True when no server (spawned or external) is available.
|
|
static bool NoServer() { return g_mysqld_pid == -1; }
|
|
};
|
|
|
|
// Parses the greeting packet that a real mysqld sends on connect.
|
|
TEST_F(MysqlHandshakeServerTest, ParsesRealServerGreeting) {
|
|
if (NoServer()) {
|
|
puts("Skipped due to absence of mysqld");
|
|
return;
|
|
}
|
|
int fd = ConnectTestMysql();
|
|
ASSERT_GE(fd, 0);
|
|
|
|
std::string payload;
|
|
uint8_t seq = 0xff;
|
|
ASSERT_TRUE(ReadPacket(fd, &payload, &seq));
|
|
EXPECT_EQ(seq, 0u); // greeting is always sequence 0
|
|
|
|
HandshakeV10 hs;
|
|
ASSERT_TRUE(ParseHandshakeV10(payload, &hs));
|
|
EXPECT_EQ(hs.protocol_version, kHandshakeV10Tag);
|
|
EXPECT_FALSE(hs.server_version.empty());
|
|
EXPECT_EQ(hs.auth_plugin_data.size(), kSaltLen);
|
|
EXPECT_TRUE(hs.capability_flags & CLIENT_PROTOCOL_41);
|
|
EXPECT_TRUE(hs.capability_flags & CLIENT_PLUGIN_AUTH);
|
|
EXPECT_FALSE(hs.auth_plugin_name.empty());
|
|
close(fd);
|
|
}
|
|
|
|
// Generates both scrambles (mysql_native_password and
|
|
// caching_sha2_password) -- the "intermediate" auth response -- from the
|
|
// salt in a real server greeting, parameterized on password length. An
|
|
// empty (zero-length) password must yield an empty wire response for
|
|
// both plugins per spec; a non-empty password must yield the fixed-width
|
|
// digests (20 bytes for native, 32 for caching_sha2). Confirms a wire
|
|
// salt from a live server is usable as scramble input.
|
|
TEST_F(MysqlHandshakeServerTest, GeneratesScramblesFromRealSalt) {
|
|
if (NoServer()) {
|
|
puts("Skipped due to absence of mysqld");
|
|
return;
|
|
}
|
|
int fd = ConnectTestMysql();
|
|
ASSERT_GE(fd, 0);
|
|
std::string payload;
|
|
uint8_t seq = 0;
|
|
ASSERT_TRUE(ReadPacket(fd, &payload, &seq));
|
|
HandshakeV10 hs;
|
|
ASSERT_TRUE(ParseHandshakeV10(payload, &hs));
|
|
close(fd);
|
|
ASSERT_EQ(hs.auth_plugin_data.size(), kSaltLen);
|
|
|
|
// Parameterized on password length: zero-length and non-empty.
|
|
const std::string passwords[] = {std::string(),
|
|
std::string("some_password")};
|
|
for (const std::string& password : passwords) {
|
|
SCOPED_TRACE(password.empty() ? "zero-length-password"
|
|
: "nonzero-length-password");
|
|
const std::string native =
|
|
NativePasswordScramble(hs.auth_plugin_data, password);
|
|
const std::string sha2 =
|
|
CachingSha2PasswordScramble(hs.auth_plugin_data, password);
|
|
if (password.empty()) {
|
|
EXPECT_TRUE(native.empty());
|
|
EXPECT_TRUE(sha2.empty());
|
|
} else {
|
|
EXPECT_EQ(native.size(), kNativePasswordResponseLen);
|
|
EXPECT_EQ(sha2.size(), kCachingSha2PasswordResponseLen);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Empty-password login takes caching_sha2's fast path and never triggers
|
|
// perform_full_authentication (0x04). Uses the spawned server's
|
|
// empty-password root; skipped when no empty-password credential exists.
|
|
TEST_F(MysqlHandshakeServerTest, AuthenticatesEmptyPasswordFastPath) {
|
|
if (NoServer()) {
|
|
puts("Skipped due to absence of mysqld");
|
|
return;
|
|
}
|
|
const AuthCase* empty = NULL;
|
|
for (size_t i = 0; i < g_auth_cases.size(); ++i) {
|
|
if (g_auth_cases[i].password.empty() && !g_auth_cases[i].use_ssl) {
|
|
empty = &g_auth_cases[i];
|
|
break;
|
|
}
|
|
}
|
|
if (empty == NULL) {
|
|
puts("Skipped: no empty-password credential on this server");
|
|
return;
|
|
}
|
|
int fd = ConnectTestMysql();
|
|
ASSERT_GE(fd, 0);
|
|
const LoginTrace t =
|
|
PerformSha2Login(fd, empty->user, empty->password, /*use_ssl=*/false);
|
|
close(fd);
|
|
EXPECT_TRUE(t.ok) << "login failed: " << t.err;
|
|
EXPECT_FALSE(t.full_auth)
|
|
<< "empty-password login unexpectedly took the full-auth path";
|
|
}
|
|
|
|
// Full authentication over PLAIN TCP (is_ssl=false): a non-empty password
|
|
// against a cold caching_sha2 cache must take the full-auth path and route
|
|
// CachingSha2PasswordSlowPath down the RSA branch (NOT cleartext).
|
|
TEST_F(MysqlHandshakeServerTest, FullAuthenticationNotSSL) {
|
|
if (NoServer()) {
|
|
puts("Skipped due to absence of mysqld");
|
|
return;
|
|
}
|
|
const AuthCase* c = FindNonEmptyCase(/*use_ssl=*/false);
|
|
if (c == NULL) {
|
|
puts("Skipped: no non-empty-password credential for plaintext "
|
|
"full-auth (need a running server with -mysql_password, or the "
|
|
"mysql client for the spawned account)");
|
|
return;
|
|
}
|
|
int fd = ConnectTestMysql();
|
|
ASSERT_GE(fd, 0);
|
|
const LoginTrace t =
|
|
PerformSha2Login(fd, c->user, c->password, /*use_ssl=*/false);
|
|
close(fd);
|
|
|
|
EXPECT_TRUE(t.ok) << "login as '" << c->user << "' failed: " << t.err;
|
|
EXPECT_FALSE(t.used_ssl) << "this login must not be SSL-wrapped";
|
|
if (IsSpawnedServer()) {
|
|
// The spawned account is brand-new -> guaranteed cold cache.
|
|
EXPECT_TRUE(t.full_auth)
|
|
<< "cold account should require full authentication (0x04)";
|
|
}
|
|
if (t.full_auth) {
|
|
EXPECT_FALSE(t.used_cleartext)
|
|
<< "plain-TCP full-auth must use the RSA branch, not cleartext";
|
|
}
|
|
}
|
|
|
|
// Full authentication over SSL (is_ssl=true): the client upgrades the
|
|
// connection to SSL, and on a cold cache the full-auth path routes
|
|
// CachingSha2PasswordSlowPath down the CLEARTEXT branch (no RSA) -- the
|
|
// secure channel protects the password.
|
|
TEST_F(MysqlHandshakeServerTest, FullAuthenticationSSL) {
|
|
if (NoServer()) {
|
|
puts("Skipped due to absence of mysqld");
|
|
return;
|
|
}
|
|
const AuthCase* c = FindNonEmptyCase(/*use_ssl=*/true);
|
|
if (c == NULL) {
|
|
puts("Skipped: no non-empty-password credential for SSL full-auth");
|
|
return;
|
|
}
|
|
int fd = ConnectTestMysql();
|
|
ASSERT_GE(fd, 0);
|
|
const LoginTrace t =
|
|
PerformSha2Login(fd, c->user, c->password, /*use_ssl=*/true);
|
|
close(fd);
|
|
|
|
EXPECT_TRUE(t.ok) << "SSL login as '" << c->user << "' failed: " << t.err;
|
|
EXPECT_TRUE(t.used_ssl) << "login should have upgraded the connection to SSL";
|
|
if (IsSpawnedServer()) {
|
|
EXPECT_TRUE(t.full_auth)
|
|
<< "cold account should require full authentication (0x04)";
|
|
}
|
|
if (t.full_auth) {
|
|
EXPECT_TRUE(t.used_cleartext)
|
|
<< "SSL full-auth must use the cleartext branch, not RSA";
|
|
}
|
|
}
|
|
|
|
// Caching behavior, parameterized over every credential. caching_sha2
|
|
// caches a credential after the first successful authentication, so a
|
|
// second login reuses the cache (fast-auth) instead of repeating the full
|
|
// RSA exchange. For each credential we log in twice on fresh
|
|
// connections: the first populates the cache, the second must NOT take
|
|
// the full-auth path. Runs in both modes (with the spawned empty-password
|
|
// account both logins are trivially fast).
|
|
TEST_F(MysqlHandshakeServerTest, CachesCredentialOnSecondLogin) {
|
|
if (NoServer()) {
|
|
puts("Skipped due to absence of mysqld");
|
|
return;
|
|
}
|
|
ASSERT_FALSE(g_auth_cases.empty());
|
|
for (const AuthCase& c : g_auth_cases) {
|
|
SCOPED_TRACE(c.label);
|
|
// First login: establishes the credential in the server's cache.
|
|
int fd1 = ConnectTestMysql();
|
|
ASSERT_GE(fd1, 0);
|
|
const LoginTrace first =
|
|
PerformSha2Login(fd1, c.user, c.password, c.use_ssl);
|
|
close(fd1);
|
|
ASSERT_TRUE(first.ok) << "first login failed: " << first.err;
|
|
|
|
// Second login: the credential is now cached, so the server must
|
|
// take the fast-auth path, never perform_full_authentication.
|
|
int fd2 = ConnectTestMysql();
|
|
ASSERT_GE(fd2, 0);
|
|
const LoginTrace second =
|
|
PerformSha2Login(fd2, c.user, c.password, c.use_ssl);
|
|
close(fd2);
|
|
EXPECT_TRUE(second.ok) << "second login failed: " << second.err;
|
|
EXPECT_FALSE(second.full_auth)
|
|
<< "second login unexpectedly took the full-auth (0x04) path; the "
|
|
"credential should have been cached by the first login";
|
|
}
|
|
}
|
|
|
|
// Auth-switch path. The client advertises mysql_native_password in its
|
|
// HandshakeResponse41, but the account uses caching_sha2_password, so the
|
|
// server replies with an AuthSwitchRequest telling the client to switch.
|
|
// PerformSha2Login follows the switch (recomputing the scramble with the
|
|
// server-provided plugin and salt) and the login still reaches OK.
|
|
TEST_F(MysqlHandshakeServerTest, SwitchesFromNativePasswordToServerPlugin) {
|
|
if (NoServer()) {
|
|
puts("Skipped due to absence of mysqld");
|
|
return;
|
|
}
|
|
ASSERT_FALSE(g_auth_cases.empty());
|
|
const AuthCase& c = g_auth_cases.front();
|
|
int fd = ConnectTestMysql();
|
|
ASSERT_GE(fd, 0);
|
|
const LoginTrace t =
|
|
PerformSha2Login(fd, c.user, c.password, /*use_ssl=*/false,
|
|
"mysql_native_password");
|
|
close(fd);
|
|
|
|
EXPECT_TRUE(t.ok) << "login as '" << c.user << "' failed: " << t.err;
|
|
EXPECT_TRUE(t.auth_switched)
|
|
<< "server did not send an AuthSwitchRequest after the client "
|
|
"advertised mysql_native_password";
|
|
EXPECT_EQ(t.switched_plugin, "caching_sha2_password")
|
|
<< "server switched to an unexpected plugin: " << t.switched_plugin;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char* argv[]) {
|
|
testing::InitGoogleTest(&argc, argv);
|
|
GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
|
|
return RUN_ALL_TESTS();
|
|
}
|