MySQLプロトコルへの接続メモ
MySQLプロトコルに関して、公式ドキュメントを読み実装を行い理解する
MySQLのバージョンは8系
参考文献
参考実装
MySQLプロトコルを勉強および仕様に沿って実装する際に便利だったもの
ngrep
ngrepを使用することでMySQLサーバーとクライアント間のパケットのやり取りを監視できる。
sudo ngrep -x -q -d lo0 '' 'port {MySQLのポート番号}'
上記コマンドで監視を開始し、MySQLサーバーとクライアント間で通信を開始すると以下のようにパケットのやり取りが確認できるようになる。
interface: lo0 (127.0.0.0/255.0.0.0)
filter: ( port 13306 ) and (ip || ip6)
T 127.0.0.1:13306 -> 127.0.0.1:57537 [AP] #5
4a 00 00 00 0a 38 2e 30 2e 33 33 00 5f 1e 00 00 J....8.0.33._...
0c 20 40 4c 62 63 61 4f 00 ff ff ff 02 00 ff df . @LbcaO.���..��
15 00 00 00 00 00 00 00 00 00 00 56 68 25 2a 51 ...........Vh%*Q
19 12 54 64 31 74 12 00 63 61 63 68 69 6e 67 5f ..Td1t..caching_
73 68 61 32 5f 70 61 73 73 77 6f 72 64 00 sha2_password.
T 127.0.0.1:57537 -> 127.0.0.1:13306 [AP] #7
6f 00 00 01 07 a2 3e 19 ff ff ff 00 ff 00 00 00 o....�>.���.�...
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 6c 64 62 63 5f 6d 79 73 71 6c 5f 6e ....ldbc_mysql_n
61 74 69 76 65 5f 75 73 65 72 00 20 39 9b 1e 96 ative_user. 9...
8f fc ff 2c aa a6 5d 2e ab d6 88 ec 2a 17 f7 ff .��,��].��.�*.��
57 30 58 63 23 7a 72 d2 37 82 78 9e 63 61 63 68 W0Xc#zr�7.x.cach
69 6e 67 5f 73 68 61 32 5f 70 61 73 73 77 6f 72 ing_sha2_passwor
64 00 00 d..
これを参考に世にあるライブラリと自作したライブラリでのパケットの違いを見比べるとどこがおかしいのかがよくわかる。
接続のライフサイクル
MySQLとの接続はConnection Phase
とCommand Phase
の2種類が存在しており、クライアントとのTCP接続が確立されたら以下のようなフェーズで通信が行われる。
https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_lifecycle.html
Connection Phase
コネクションフェーズは主に以下を行う
- クライアントとサーバーの能力を交換
- SSL通信チャネルをセットアップ (要求があれば)
- クライアントとサーバーの認証
クライアントはERRパケットを送り、ハンドシェイクを終了するか、あるいはイニシャルハンドシェイクパケットを送り、クライアントはハンドシェイク応答パケットでそれに答える。この段階でクライアントはSSL接続を要求することができ、その場合はクライアントが認証応答を送る前にSSL通信チャネルが確立される。
https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html
クライアントとサーバーの能力を交換
クライアントとのTCP接続が確立されたらサーバーからERRパケットもしくは、Initial Handshakeパケットが送られてくる。
各種バージョンや認証プラグイン、文字セットや照合順序、Server Capabilitiesなどの情報が含まれる
値 | 型 | 概要 |
---|---|---|
プロトコルバージョン | int<1> | 常に10 |
サーバーバージョン | int<1> | MySQLのバージョン |
スレッドID | int<4> | 接続に使用されるスレッドのID |
認証プラグイン データ [1] | string<8> | 認証に使用するランダムな文字列 |
フィルター | string<8> | 0x00バイト、スクランブルの最初の部分を終了判定の値 |
サーバー機能 | int<2> | サーバーの機能のビットマスク |
サーバーキャラクタセット番号 | int<2> | サーバーのデフォルトの文字セットの番号 |
サーバー状態 | int<1> | 0x02: サーバーが実行中、0x08: SSL接続が使用されている |
認証プラグイン データ [2] | $length | 残りのプラグイン提供データ(スクランブル), $len=MAX(13, auth-plugin-dataの長さ - 8) |
認証プラグイン名 | NULL | MySQLデフォルトの認証プラグイン名 |
上記の他にも値の条件によって追加される項目がある。(v10参照)
最初のハンドシェイクはサーバーが Protocol::Handshake パケットを送ることから始まる。この後、オプションとしてクライアントは Protocol::SSLRequest: パケットで SSL 接続の確立を要求することができ、その後クライアントは Protocol::HandshakeResponse: パケットを送信する。
Plain Handshake
Plain Handshake
- サーバーからInitial Handshakeパケットが送られてくる。
- クライアントはProtocol::HandshakeResponseを送信する。
SSL Handshake
SSL Handshake
- サーバーからInitial Handshakeパケットが送られてくる。
- クライアントはProtocol::SSLRequestを送信する。
- SSL接続の確立につながる通常のSSL交換を行う。
- クライアントはProtocol::HandshakeResponseを送信する。
Capabilities Flags
MySQL プロトコルで使用されるビットマスクの値。
この情報を交換することで、クライアント/サーバー間で期待されていないフォーマットでデータを送るような事を防げる。
フラグ | 値 | 概要 | 条件 |
---|---|---|---|
CLIENT_LONG_PASSWORD | 1 << 0 | 旧パスワード認証の改良版を使用するための値。現在は使用されない。 | |
CLIENT_FOUND_ROWS | 1 << 1 | EOF_Packetで、影響を受けた行の代わりに見つかった行を送信するための値。 | |
CLIENT_LONG_FLAG | 1 << 2 | カラム定義を取得する際に、すべての列フラグを取得するための値。Protocol::ColumnDefinition320で使用。 | |
CLIENT_CONNECT_WITH_DB | 1 << 3 | データベース(スキーマ)名を、接続時にで指定できるようにするための値。 | |
CLIENT_NO_SCHEMA | 1 << 4 |
database.table.column.Column 形式のフォーマットを禁止するための値。現在は廃止された。 |
|
CLIENT_COMPRESS | 1 << 5 | 圧縮プロトコルに対応するための値。 | |
CLIENT_ODBC | 1 << 6 | ODBC(Open Database Connectivity) 動作の特別な処理を行うための値。 (3.22以降、特別な動作はない。) | |
CLIENT_LOCAL_FILES | 1 << 7 | LOAD DATA LOCALを使えるようにするための値。 LOAD DATA ステートメント | |
CLIENT_IGNORE_SPACE | 1 << 8 |
( の前のスペースは無視するための値。 |
|
CLIENT_PROTOCOL_41 | 1 << 9 | 新しい4.1プロトコルを使用するための値。 | |
CLIENT_INTERACTIVE | 1 << 10 | 対話型のクライアントに対応するかの値? | |
CLIENT_SSL | 1 << 11 | SSL接続で暗号化を行うための値。 | |
CLIENT_IGNORE_SIGPIPE | 1 << 12 | ネットワーク障害が発生しても SIGPIPE を発行しないための値。クライアント専用のフラグで、現在は使用されない。 | |
CLIENT_TRANSACTIONS | 1 << 13 | OK_Packet / EOF_Packetにサーバーのステータスフラグを追加するための値。 | |
CLIENT_RESERVED | 1 << 14 | 4.1プロトコル用の古いフラグ。 現在は廃止された。 | |
CLIENT_RESERVED2 | 1 << 15 | 4.1プロトコル用の古いフラグ。 現在は廃止された。 | |
CLIENT_MULTI_STATEMENTS | 1 << 16 | 複数Statementの送信を許可するための値。 | |
CLIENT_MULTI_RESULTS | 1 << 17 | 複数の実行結果取得を許可するための値。COM_QUERYに対して複数の結果セットを送信できる。 サーバがそれらを送信する必要があり、クライアントがそれらをサポートしていない場合はエラーになりま。 | CLIENT_PROTOCOL_41フラグの有効化が必須 |
CLIENT_PS_MULTI_RESULTS | 1 << 18 | COM_STMT_EXECUTE で複数の結果セットを扱えるようにする値。 | CLIENT_PROTOCOL_41フラグの有効化が必須 |
CLIENT_PLUGIN_AUTH | 1 << 19 | クライアントがプラグイン認証をサポートするための値。 | CLIENT_PROTOCOL_41フラグの有効化が必須 |
CLIENT_CONNECT_ATTRS | 1 << 20 | クライアントは接続属性を付与できるようにするための値。 | |
CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA | 1 << 21 | 認証応答パケットを255バイトより大きくできるようするための値。デフォルトのプラグインを変更する機能で、Protocol::HandshakeResponse41の初期パスワードフィールドが任意のサイズであることが要求される場合。しかし、4.1クライアントサーバプロトコルでは、クライアントからサーバに送信されるauth-data-fieldの長さを255バイトに制限している。解決策は、フィールドのタイプを真の長さにエンコードされた文字列に変更し、このクライアント能力フラグでプロトコルの変更を示すことである。 | |
CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS | 1 << 22 | パスワードが期限切れのユーザー・アカウントの接続を閉じないようにするための値。 | |
CLIENT_SESSION_TRACK | 1 << 23 | サーバーの状態変化情報を扱うことができるようにするための値。 OK_Packetに状態変化情報を含めるようにサーバにヒントを与えることができる。 | |
CLIENT_DEPRECATE_EOF | 1 << 24 | クライアントはEOF_Packetを必要としなくなり、代わりにOK_Packetを使うようにするための値。 | |
CLIENT_OPTIONAL_RESULTSET_METADATA | 1 << 25 | クライアントは、結果セットに含まれるオプションのメタデータ情報を扱うことができるようにするための値。 | |
CLIENT_ZSTD_COMPRESSION_ALGORITHM | 1 << 26 | zstd 圧縮方式をサポートするように拡張された圧縮プロトコルを使用するための値。このcapability flagは、クライアントとサーバーの両方がこのフラグで有効になっている場合に、クライアントとサーバー間でzstd圧縮レベルを送信するために使用される。 | |
CLIENT_QUERY_ATTRIBUTES | 1 << 27 | COM_QUERY および COM_STMT_EXECUTE パケットへのクエリパラメータのオプション拡張をサポートするための値。 | |
MULTI_FACTOR_AUTHENTICATION | 1 << 28 | 多要素認証をサポートするための値。 | |
CLIENT_CAPABILITY_EXTENSION | 1 << 29 | このフラグは、32ビットの能力構造体を64ビットに拡張するために予約される値。 | |
CLIENT_SSL_VERIFY_SERVER_CERT | 1 << 30 | サーバ証明書を検証するための値。クライアント専用のフラグ。 | |
CLIENT_REMEMBER_OPTIONS | 1 << 31 | 接続に失敗してもオプションをリセットしないための値。クライアント専用のフラグ。 |
クライアントとサーバーの認証
クライアントがMySQLサーバーへ接続するときにLoginRequestというフェーズでユーザ情報を送信する。そして、サーバー側では送られたユーザがmysql.user
テーブルにあるか検索し、どの認証プラグインを使用するか決定する。認証プラグインが決定した後に、サーバーはそのプラグインを呼び出してユーザー認証を開始し、その結果をクライアント側に送信する。このようにMySQLでは認証がプラガブル(様々なタイプのプラグインを付け外しできる)になっている。選べる認証プラグインは公式ページに記載されている。
今回は主に以下3つの認証プラグインに関してまとめる
- ネイティブプラガブル認証
- SHA-256 プラガブル認証
- SHA-2 プラガブル認証のキャッシュ
認証が正常に行われない場合はクライアント・サーバー間では以下の処理が行われる
- 認証方法の変更
- 接続の拒否
ネイティブプラガブル認証
ネイティブプラガブル認証のプラグインの名前はmysql_native_password
です。
mysql_native_password
はパスワードハッシュ方式に基づいた認証を実装している。ハッシュ関数はSHA1が用いられている。以下のようにユーザ作成を行うと、mysql.user
のauthentication_string
カラムにSHA1(SHA1(password))
の結果が格納される。よって同じパスワードであれば同じauthentication_string
になる。
mysql> select user, host, plugin, authentication_string from mysql.user Where user = 'ldbc_mysql_native_user';
+------------------------+------+-----------------------+-------------------------------------------+
| user | host | plugin | authentication_string |
+------------------------+------+-----------------------+-------------------------------------------+
| ldbc_mysql_native_user | % | mysql_native_password | *88528E8B55AE11968B8861A21809294988EECA28 |
+------------------------+------+-----------------------+-------------------------------------------+
1 row in set (0.01 sec)
クライアント・サーバー間ではチャレンジ&レスポンス認証が行われる。 サーバ側からInitial Handshakeパケット送信時に、クライアント側に20バイトのランダムデータ(Nonce)が送信され、クライアント側でその値を使用して、以下の計算をしてサーバーに送信する。
SHA1( password ) XOR SHA1( "20-bytes random data from server" <concat> SHA1( SHA1( password ) ) )
サーバーはauthentication_string
の値、すなわちSHA1( SHA1( password )
とランダムデータの値を知っているので、それらとクライアントから送られてきた計算結果にXORとSHA1を作用させることで、パスワードハッシュを照合することができる。言い換えれば、ユーザが入力したパスワードに2回SHA1を作用させた値(クライアント側)とauthentication_string
の値(サーバー側)の照合をすることができる。
Scalaでの実装はこのようになる。
def hashPassword(password: String, scramble: Array[Byte]): Array[Byte] =
if password.isEmpty then Array[Byte]()
else
val sha1 = MessageDigest.getInstance("SHA-1")
val hash1 = sha1.digest(password.getBytes("UTF-8"))
val hash2 = sha1.digest(hash1)
val hash3 = sha1.digest(scramble ++ hash2)
hash1.zip(hash3).map { case (a, b) => (a ^ b).toByte }
SHA-256 プラガブル認証
SHA-256 プラガブル認証のプラグインの名前はsha256_password
です。
SHA-256 プラガブル認証ではサーバー側でsalt付きのパスワードハッシュ値を作成しており、クライアント側へはそのsalt値を送信しないので、サーバーと同様のパスワードハッシュ値を生成できない。そのため、クライアント側で先ほどの計算式のような計算結果をサーバー側に送ったとしても、salt無しでハッシュ化されたパスワードが渡るのみで、サーバー側ではauthentication_string
との照合ができない。
SHA-256 プラガブル認証ではSSL/TLS接続とRSA暗号化通信で送信するパスワードの形式が異なる。
- SSL/TLS接続時は安全な暗号化された通信経路での通信となるためパスワードの値をハッシュ化せずにそのままサーバーへ送信を行う。
- RSA暗号化通信を行う場合はサーバーからRSA 公開鍵が送られてくるため、クライアントはその公開鍵を使用して暗号化したパスワードをサーバーへ送信する。
MySQLデフォルトの公開鍵は以下クエリで確認できる。
mysql> SHOW STATUS LIKE 'Rsa_public_key'\G
*************************** 1. row ***************************
Variable_name: Rsa_public_key
Value: -----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDO9nRUDd+KvSZgY7cNBZMNpwX6
MvE1PbJFXO7u18nJ9lwc99Du/E7lw6CVXw7VKrXPeHbVQUzGyUNkf45Nz/ckaaJa
aLgJOBCIDmNVnyU54OT/1lcs2xiyfaDMe8fCJ64ZwTnKbY2gkt1IMjUAB5Ogd5kJ
g8aV7EtKwyhHb0c30QIDAQAB
-----END PUBLIC KEY-----
SHA-2 プラガブル認証のキャッシュ
SHA-256 プラガブル認証のプラグインの名前はcaching_sha2_password
です。
sha256_password
認証プラグインはソルト付きパスワードに対して複数回のSHA256ハッシュを使用して、ハッシュ変換の安全性を高めていました。ただし、暗号化された接続または RSA キー ペアのサポートが必要です。したがって、パスワードのセキュリティは強化されますが、安全な接続と複数回のハッシュ変換により、認証プロセスにより多くの時間がかかってしまうというデメリットがありました。
caching_sha2_password
ではこのデメリットを解消するために以下のような処理を行なっています。
以下2つのフェーズで動作します。
- Fast authentication
- Complete authentication
サーバは、指定されたユーザのハッシュエントリをメモリにキャッシュしている場合、クライアントから送信されたスクランブルを使用して高速認証を実行する。成功すれば認証は完了し、接続はコマンドフェーズに移行する。エラーが発生した場合、サーバはクライアントに、安全な接続サーバ経由で パスワードを送信する完全認証に切り替えるよう合図を送る。サーバは次に、指定されたユーザアカウントの authentication_string とパスワードを照合する。成功した場合、サーバはそのアカウントのハッシュエントリをキャッシュし、接続はコマンドフェーズに入る。エラーの場合、サーバはクライアントにエラー情報を送信し、接続は終了する。
つまり
- SHA256を使用したパスワードハッシュで接続を行う(
mysql_native_password
と同じ) - ハッシュ化されたパスワードのキャッシュがサーバー内に存在するか確認を行う
ここでサーバー内にキャッシュが存在するかしないかによって後続の処理が変わる。
キャッシュが存在する場合
キャッシュが存在する場合はmysql_native_password
の時と同じようにパスワードのハッシュ値がクライアント・サーバー間で一致する場合は接続が成功となる。
キャッシュが存在しない場合
- SSL/TLS接続時もしくはRSA暗号化通信を行いクライアント・サーバー間で認証を行う
- 認証が成立した場合、サーバーはその認証情報でパスワードのキャッシュをキャッシュ内に保存を行う
これにより次回以降の接続は、サーバー内のキャッシュを使用して高速に認証を行うことができるようになる。
このようにcaching_sha2_password
ではユーザのパスワードハッシュの値をキャッシュ内に保存するため、クライアントがサーバーへ接続する際に該当するキャッシュが見つかれば、クライアントから送られてきたその値とパスワードハッシュの照合ができるようになるので、暗号化されていないチャネルを介した安全な認証が可能になります。
これはmysql_native_password
のSHA1 ベースのチャレンジ/レスポンス メカニズムと比較してより高速なものとなっています。
サーバーとクライアント間の状態遷移とメッセージ交換の条件分岐は以下のようになる。
https://dev.mysql.com/doc/dev/mysql-server/latest/page_caching_sha2_authentication_exchanges.html
認証方法の変更
認証方法の不一致が発生した場合、サーバーはクライアントにProtocol::AuthSwitchRequest
を送信する。Protocol::AuthSwitchRequest
には、使用するクライアント認証方法の名前と、新しい方法によって生成される最初の認証ペイロードが含まれる。クライアントは、要求された認証メソッドに切り替え、そのメソッドに従った交換を継続する。
例えば、MySQLサーバーのデフォルト認証方式がcaching_sha2_password
であった場合、Initial Handshakeにはcaching_sha2_password
のプラグイン情報が送られてくるので、クライアントはまずcaching_sha2_password
形式で認証を開始する。
しかし、クライアントログインを希望していたユーザーアカウントが認証方法にmysql_native_password
を使用していた場合、認証方式が異なるためサーバーはmysql_native_password
で使用する認証ペイロードをクライアントに送信し、クライアントはそのデータを使用して再度パスワードをハッシュ化してサーバーへ送信を行う。