Skip to content

Commit d8752f1

Browse files
committed
feat(mysql): clean-room auth-handshake codec (scramble + handshake + packet)
Picks up the connection-phase authentication layer of MySQL protocol support per the staged plan announced on dev@brpc.apache.org and PR #2093. Three modules under src/brpc/policy/mysql_auth/: - mysql_auth_scramble: mysql_native_password (SHA1 XOR), plus caching_sha2_password fast path (SHA256 XOR) and slow path (RSA-OAEP via OpenSSL EVP_PKEY_encrypt with PKCS1_OAEP_PADDING). - mysql_auth_packet: length-encoded int/string, 4-byte packet header, NUL-terminated string. - mysql_auth_handshake: HandshakeV10 parse, HandshakeResponse41 build, AuthSwitchRequest parse, AuthMoreData parse. All written clean-room from MySQL's public protocol documentation; no GPL-licensed source was consulted. SHA-256 and RSA paths use OpenSSL EVP — works under both OpenSSL and BoringSSL. 59 unit tests across three files under test/mysql_auth/: 21 in brpc_mysql_auth_scramble_unittest.cpp 18 in brpc_mysql_auth_handshake_unittest.cpp 20 in brpc_mysql_auth_packet_unittest.cpp Mirrors every client-relevant case from mysql/mysql-server's GPLv2 unittest/gunit/sha2_password-t.cc + sha256_scramble_t.cc with independently re-derived hex vectors. Server-side cases (cache, storage format, *_verification_*) are out of scope for a client library; full mapping table tracked in the contributor-tree MYSQL_AUTH_SPEC.md. Scope limits in this CL: auth codec only. No PROTOCOL_MYSQL registration, no Socket integration, no caching_sha2 TLS-plaintext shortcut, no compressed packets, no prepared statements, no transactions. All land in the follow-up CL. Replaces the earlier src/brpc/policy/mysql_auth_hash.{h,cpp} + test/brpc_mysql_auth_hash_unittest.cpp; those files are moved into the new mysql_auth/ subdirectory and renamed.
1 parent 477fa49 commit d8752f1

11 files changed

Lines changed: 1873 additions & 1 deletion
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#include "brpc/policy/mysql_auth/mysql_auth_handshake.h"
19+
20+
#include <cstring>
21+
22+
#include "brpc/policy/mysql_auth/mysql_auth_packet.h"
23+
#include "brpc/policy/mysql_auth/mysql_auth_scramble.h"
24+
25+
namespace brpc {
26+
namespace policy {
27+
namespace mysql_auth {
28+
29+
namespace {
30+
31+
// MySQL HandshakeV10 fixed-size pieces and constants.
32+
const size_t kAuthPluginDataPart1Len = 8;
33+
const size_t kReservedAfterCapsLen = 10;
34+
const size_t kFillerAfterPart1Len = 1;
35+
const size_t kReservedInResponseLen = 23;
36+
37+
// Reads N little-endian bytes from |buf| at |off| into |out|.
38+
template <typename T>
39+
bool ReadLE(const butil::StringPiece& buf, size_t off, size_t n, T* out) {
40+
if (off + n > buf.size()) return false;
41+
T v = 0;
42+
for (size_t i = 0; i < n; ++i) {
43+
v |= static_cast<T>(static_cast<unsigned char>(buf[off + i])) << (8 * i);
44+
}
45+
*out = v;
46+
return true;
47+
}
48+
49+
template <typename T>
50+
void WriteLE(T value, size_t n, std::string* out) {
51+
for (size_t i = 0; i < n; ++i) {
52+
out->push_back(static_cast<char>((value >> (8 * i)) & 0xff));
53+
}
54+
}
55+
56+
} // namespace
57+
58+
bool ParseHandshakeV10(const butil::StringPiece& payload, HandshakeV10* out) {
59+
if (payload.empty()) return false;
60+
61+
size_t off = 0;
62+
out->protocol_version = static_cast<uint8_t>(payload[off++]);
63+
if (out->protocol_version != kHandshakeV10Tag) {
64+
return false;
65+
}
66+
67+
// server_version: NUL-terminated string
68+
std::string version;
69+
{
70+
const butil::StringPiece rest(payload.data() + off,
71+
payload.size() - off);
72+
const size_t consumed = DecodeNullTerminatedString(rest, &version);
73+
if (consumed == 0) return false;
74+
off += consumed;
75+
}
76+
out->server_version = std::move(version);
77+
78+
// connection_id: 4 LE bytes
79+
if (!ReadLE<uint32_t>(payload, off, 4, &out->connection_id)) return false;
80+
off += 4;
81+
82+
// auth-plugin-data-part-1: 8 bytes
83+
if (off + kAuthPluginDataPart1Len > payload.size()) return false;
84+
std::string salt(payload.data() + off, kAuthPluginDataPart1Len);
85+
off += kAuthPluginDataPart1Len;
86+
87+
// filler 0x00
88+
if (off + kFillerAfterPart1Len > payload.size()) return false;
89+
off += kFillerAfterPart1Len;
90+
91+
// capability flags (lower 2 bytes)
92+
uint16_t caps_lo = 0;
93+
if (!ReadLE<uint16_t>(payload, off, 2, &caps_lo)) return false;
94+
off += 2;
95+
out->capability_flags = caps_lo;
96+
97+
if (off == payload.size()) {
98+
// Pre-4.1 server. We don't support these — bail.
99+
return false;
100+
}
101+
102+
// character_set
103+
if (off >= payload.size()) return false;
104+
out->character_set = static_cast<uint8_t>(payload[off++]);
105+
106+
// status_flags
107+
if (!ReadLE<uint16_t>(payload, off, 2, &out->status_flags)) return false;
108+
off += 2;
109+
110+
// capability flags upper 2 bytes
111+
uint16_t caps_hi = 0;
112+
if (!ReadLE<uint16_t>(payload, off, 2, &caps_hi)) return false;
113+
off += 2;
114+
out->capability_flags |= static_cast<uint32_t>(caps_hi) << 16;
115+
116+
// length of auth-plugin-data (or 0x00 when CLIENT_PLUGIN_AUTH is absent)
117+
if (off >= payload.size()) return false;
118+
const uint8_t apd_total_len = static_cast<uint8_t>(payload[off++]);
119+
120+
// 10 reserved bytes (all 0x00)
121+
if (off + kReservedAfterCapsLen > payload.size()) return false;
122+
off += kReservedAfterCapsLen;
123+
124+
if (out->capability_flags & CLIENT_SECURE_CONNECTION) {
125+
// auth-plugin-data-part-2: max(13, apd_total_len - 8) bytes. Modern
126+
// servers send 13 (12 salt bytes + 1 NUL filler).
127+
const size_t part2_len = apd_total_len > kAuthPluginDataPart1Len
128+
? static_cast<size_t>(apd_total_len) - kAuthPluginDataPart1Len
129+
: static_cast<size_t>(13);
130+
const size_t want = part2_len < 13 ? 13 : part2_len;
131+
if (off + want > payload.size()) return false;
132+
// Concat salt parts; trim trailing NUL filler so callers see the
133+
// raw 20-byte salt.
134+
salt.append(payload.data() + off, want);
135+
off += want;
136+
if (!salt.empty() && salt.back() == '\0') {
137+
salt.pop_back();
138+
}
139+
}
140+
if (salt.size() != kSaltLen) {
141+
return false;
142+
}
143+
out->auth_plugin_data = std::move(salt);
144+
145+
if (out->capability_flags & CLIENT_PLUGIN_AUTH) {
146+
std::string name;
147+
const butil::StringPiece rest(payload.data() + off,
148+
payload.size() - off);
149+
const size_t consumed = DecodeNullTerminatedString(rest, &name);
150+
// Some servers omit the trailing NUL; tolerate by treating the
151+
// remainder of the payload as the plugin name.
152+
if (consumed == 0) {
153+
out->auth_plugin_name.assign(rest.data(), rest.size());
154+
} else {
155+
out->auth_plugin_name = std::move(name);
156+
}
157+
}
158+
159+
return true;
160+
}
161+
162+
void BuildHandshakeResponse41(const HandshakeResponse41& req, std::string* out) {
163+
WriteLE<uint32_t>(req.capability_flags, 4, out);
164+
WriteLE<uint32_t>(req.max_packet_size, 4, out);
165+
out->push_back(static_cast<char>(req.character_set));
166+
out->append(kReservedInResponseLen, '\0');
167+
out->append(req.username);
168+
out->push_back('\0');
169+
170+
if (req.capability_flags & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
171+
EncodeLengthEncodedString(req.auth_response, out);
172+
} else if (req.capability_flags & CLIENT_SECURE_CONNECTION) {
173+
// Auth response length must fit in one byte under this scheme.
174+
// Callers using payloads >255 bytes (e.g., RSA ciphertext) must
175+
// set CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA instead.
176+
const uint8_t len = static_cast<uint8_t>(
177+
req.auth_response.size() > 0xff ? 0xff : req.auth_response.size());
178+
out->push_back(static_cast<char>(len));
179+
out->append(req.auth_response.data(), len);
180+
} else {
181+
out->append(req.auth_response);
182+
out->push_back('\0');
183+
}
184+
185+
if (req.capability_flags & CLIENT_CONNECT_WITH_DB) {
186+
out->append(req.database);
187+
out->push_back('\0');
188+
}
189+
190+
if (req.capability_flags & CLIENT_PLUGIN_AUTH) {
191+
out->append(req.auth_plugin_name);
192+
out->push_back('\0');
193+
}
194+
}
195+
196+
bool ParseAuthSwitchRequest(const butil::StringPiece& payload,
197+
AuthSwitchRequest* out) {
198+
if (payload.empty() ||
199+
static_cast<uint8_t>(payload[0]) != kAuthSwitchRequestTag) {
200+
return false;
201+
}
202+
size_t off = 1;
203+
std::string name;
204+
const butil::StringPiece rest(payload.data() + off, payload.size() - off);
205+
const size_t consumed = DecodeNullTerminatedString(rest, &name);
206+
if (consumed == 0) return false;
207+
off += consumed;
208+
out->auth_plugin_name = std::move(name);
209+
210+
// Remainder is auth-plugin-data; trim a single trailing NUL filler.
211+
out->auth_plugin_data.assign(payload.data() + off, payload.size() - off);
212+
if (!out->auth_plugin_data.empty() && out->auth_plugin_data.back() == '\0') {
213+
out->auth_plugin_data.pop_back();
214+
}
215+
return true;
216+
}
217+
218+
bool ParseAuthMoreData(const butil::StringPiece& payload, AuthMoreData* out) {
219+
if (payload.empty() ||
220+
static_cast<uint8_t>(payload[0]) != kAuthMoreDataTag) {
221+
return false;
222+
}
223+
out->data.assign(payload.data() + 1, payload.size() - 1);
224+
return true;
225+
}
226+
227+
} // namespace mysql_auth
228+
} // namespace policy
229+
} // namespace brpc
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
// Codec for the four MySQL connection-phase packets the client touches
19+
// during authentication. All functions operate on raw packet payloads
20+
// (without the 4-byte packet header); the caller is responsible for
21+
// framing. Specifications:
22+
// HandshakeV10:
23+
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_v10.html
24+
// HandshakeResponse41:
25+
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_response.html
26+
// AuthSwitchRequest / AuthMoreData:
27+
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_switch_request.html
28+
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_more_data.html
29+
30+
#ifndef BRPC_POLICY_MYSQL_AUTH_MYSQL_AUTH_HANDSHAKE_H
31+
#define BRPC_POLICY_MYSQL_AUTH_MYSQL_AUTH_HANDSHAKE_H
32+
33+
#include <stdint.h>
34+
35+
#include <string>
36+
37+
#include "butil/strings/string_piece.h"
38+
39+
namespace brpc {
40+
namespace policy {
41+
namespace mysql_auth {
42+
43+
// Subset of MySQL capability flags we recognize.
44+
enum CapabilityFlag : uint32_t {
45+
CLIENT_LONG_PASSWORD = 0x00000001,
46+
CLIENT_LONG_FLAG = 0x00000004,
47+
CLIENT_CONNECT_WITH_DB = 0x00000008,
48+
CLIENT_PROTOCOL_41 = 0x00000200,
49+
CLIENT_TRANSACTIONS = 0x00002000,
50+
CLIENT_SECURE_CONNECTION = 0x00008000,
51+
CLIENT_PLUGIN_AUTH = 0x00080000,
52+
CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000,
53+
CLIENT_DEPRECATE_EOF = 0x01000000,
54+
};
55+
56+
// The leading status byte of an authentication-related packet. Used
57+
// by callers to dispatch a packet payload to the right parser before
58+
// invoking any of the functions below.
59+
enum PacketTag : uint8_t {
60+
kHandshakeV10Tag = 0x0a,
61+
kAuthSwitchRequestTag = 0xfe,
62+
kAuthMoreDataTag = 0x01,
63+
kOkPacketTag = 0x00,
64+
kErrPacketTag = 0xff,
65+
};
66+
67+
// Parsed HandshakeV10 (server greeting).
68+
struct HandshakeV10 {
69+
uint8_t protocol_version; // always 10
70+
std::string server_version; // human-readable, NUL-terminated on wire
71+
uint32_t connection_id;
72+
std::string auth_plugin_data; // 20-byte salt (parts 1 + 2 concatenated)
73+
uint32_t capability_flags; // upper 16 bits OR'd in when present
74+
uint8_t character_set;
75+
uint16_t status_flags;
76+
std::string auth_plugin_name; // e.g., "mysql_native_password"
77+
};
78+
79+
// Parses |payload| (a packet body without the 4-byte header) as a
80+
// HandshakeV10. Returns true on success. Rejects packets whose
81+
// protocol_version is not 10 or whose salt is not 20 bytes long.
82+
bool ParseHandshakeV10(const butil::StringPiece& payload, HandshakeV10* out);
83+
84+
// Inputs for building a HandshakeResponse41 payload. The caller is
85+
// expected to have already negotiated capability_flags against the
86+
// server's advertised flags and computed the scrambled auth_response.
87+
struct HandshakeResponse41 {
88+
uint32_t capability_flags;
89+
uint32_t max_packet_size;
90+
uint8_t character_set;
91+
std::string username;
92+
std::string auth_response; // bytes from NativePasswordScramble,
93+
// CachingSha2PasswordScramble, etc.
94+
std::string database; // omitted when CLIENT_CONNECT_WITH_DB
95+
// is not in capability_flags
96+
std::string auth_plugin_name; // included when CLIENT_PLUGIN_AUTH
97+
// is in capability_flags
98+
};
99+
100+
// Appends a HandshakeResponse41 payload (no header) to |out|.
101+
// auth_response encoding obeys capability_flags:
102+
// - CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA -> length-encoded string
103+
// - CLIENT_SECURE_CONNECTION -> 1-byte length + data
104+
// - neither -> NUL-terminated
105+
void BuildHandshakeResponse41(const HandshakeResponse41& req, std::string* out);
106+
107+
// Parsed AuthSwitchRequest (server asks client to switch plugins).
108+
struct AuthSwitchRequest {
109+
std::string auth_plugin_name;
110+
std::string auth_plugin_data; // 20-byte salt; trailing NUL stripped
111+
};
112+
113+
// Parses an AuthSwitchRequest payload. Returns true on success. The
114+
// caller must have already verified payload[0] == kAuthSwitchRequestTag.
115+
bool ParseAuthSwitchRequest(const butil::StringPiece& payload,
116+
AuthSwitchRequest* out);
117+
118+
// Parsed AuthMoreData (server sends RSA pubkey or fast-auth status).
119+
struct AuthMoreData {
120+
std::string data; // 0x03=fast-auth-ok, 0x04=request-pubkey, or PEM
121+
};
122+
123+
// Parses an AuthMoreData payload. Returns true on success. The
124+
// caller must have already verified payload[0] == kAuthMoreDataTag.
125+
bool ParseAuthMoreData(const butil::StringPiece& payload, AuthMoreData* out);
126+
127+
} // namespace mysql_auth
128+
} // namespace policy
129+
} // namespace brpc
130+
131+
#endif // BRPC_POLICY_MYSQL_AUTH_MYSQL_AUTH_HANDSHAKE_H

0 commit comments

Comments
 (0)