Open11

MySQLプロトコルへの接続メモ

takapitakapi
takapitakapi

MySQLプロトコルを勉強および仕様に沿って実装する際に便利だったもの

ngrep

https://formulae.brew.sh/formula/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..

これを参考に世にあるライブラリと自作したライブラリでのパケットの違いを見比べるとどこがおかしいのかがよくわかる。

takapitakapi

Connection Phase

コネクションフェーズは主に以下を行う

  • クライアントとサーバーの能力を交換
  • SSL通信チャネルをセットアップ (要求があれば)
  • クライアントとサーバーの認証

クライアントはERRパケットを送り、ハンドシェイクを終了するか、あるいはイニシャルハンドシェイクパケットを送り、クライアントはハンドシェイク応答パケットでそれに答える。この段階でクライアントはSSL接続を要求することができ、その場合はクライアントが認証応答を送る前にSSL通信チャネルが確立される。


https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html

takapitakapi

クライアントとサーバーの能力を交換

クライアントとの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

  1. サーバーからInitial Handshakeパケットが送られてくる。
  2. クライアントはProtocol::HandshakeResponseを送信する。

Plain Handshake

SSL Handshake

SSL Handshake

  1. サーバーからInitial Handshakeパケットが送られてくる。
  2. クライアントはProtocol::SSLRequestを送信する。
  3. SSL接続の確立につながる通常のSSL交換を行う。
  4. クライアントはProtocol::HandshakeResponseを送信する。

SSL Handshake

takapitakapi

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 接続に失敗してもオプションをリセットしないための値。クライアント専用のフラグ。
takapitakapi

クライアントとサーバーの認証

クライアントがMySQLサーバーへ接続するときにLoginRequestというフェーズでユーザ情報を送信する。そして、サーバー側では送られたユーザがmysql.userテーブルにあるか検索し、どの認証プラグインを使用するか決定する。認証プラグインが決定した後に、サーバーはそのプラグインを呼び出してユーザー認証を開始し、その結果をクライアント側に送信する。このようにMySQLでは認証がプラガブル(様々なタイプのプラグインを付け外しできる)になっている。選べる認証プラグインは公式ページに記載されている。

https://dev.mysql.com/doc/refman/8.0/ja/authentication-plugins.html

今回は主に以下3つの認証プラグインに関してまとめる

  • ネイティブプラガブル認証
  • SHA-256 プラガブル認証
  • SHA-2 プラガブル認証のキャッシュ

認証が正常に行われない場合はクライアント・サーバー間では以下の処理が行われる

  • 認証方法の変更
  • 接続の拒否
takapitakapi

ネイティブプラガブル認証

ネイティブプラガブル認証のプラグインの名前はmysql_native_passwordです。

https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods_native_password_authentication.html

mysql_native_passwordはパスワードハッシュ方式に基づいた認証を実装している。ハッシュ関数はSHA1が用いられている。以下のようにユーザ作成を行うと、mysql.userauthentication_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 }
takapitakapi

SHA-256 プラガブル認証

SHA-256 プラガブル認証のプラグインの名前はsha256_passwordです。

https://dev.mysql.com/doc/refman/8.0/ja/sha256-pluggable-authentication.html

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-----
takapitakapi

SHA-2 プラガブル認証のキャッシュ

SHA-256 プラガブル認証のプラグインの名前はcaching_sha2_passwordです。

https://dev.mysql.com/doc/refman/8.0/ja/caching-sha2-pluggable-authentication.html

sha256_password認証プラグインはソルト付きパスワードに対して複数回のSHA256ハッシュを使用して、ハッシュ変換の安全性を高めていました。ただし、暗号化された接続または RSA キー ペアのサポートが必要です。したがって、パスワードのセキュリティは強化されますが、安全な接続と複数回のハッシュ変換により、認証プロセスにより多くの時間がかかってしまうというデメリットがありました。

caching_sha2_passwordではこのデメリットを解消するために以下のような処理を行なっています。
以下2つのフェーズで動作します。

  1. Fast authentication
  2. Complete authentication

サーバは、指定されたユーザのハッシュエントリをメモリにキャッシュしている場合、クライアントから送信されたスクランブルを使用して高速認証を実行する。成功すれば認証は完了し、接続はコマンドフェーズに移行する。エラーが発生した場合、サーバはクライアントに、安全な接続サーバ経由で パスワードを送信する完全認証に切り替えるよう合図を送る。サーバは次に、指定されたユーザアカウントの authentication_string とパスワードを照合する。成功した場合、サーバはそのアカウントのハッシュエントリをキャッシュし、接続はコマンドフェーズに入る。エラーの場合、サーバはクライアントにエラー情報を送信し、接続は終了する。

つまり

  1. SHA256を使用したパスワードハッシュで接続を行う(mysql_native_passwordと同じ)
  2. ハッシュ化されたパスワードのキャッシュがサーバー内に存在するか確認を行う

ここでサーバー内にキャッシュが存在するかしないかによって後続の処理が変わる。

キャッシュが存在する場合

キャッシュが存在する場合はmysql_native_passwordの時と同じようにパスワードのハッシュ値がクライアント・サーバー間で一致する場合は接続が成功となる。

キャッシュが存在しない場合

  1. SSL/TLS接続時もしくはRSA暗号化通信を行いクライアント・サーバー間で認証を行う
  2. 認証が成立した場合、サーバーはその認証情報でパスワードのキャッシュをキャッシュ内に保存を行う

これにより次回以降の接続は、サーバー内のキャッシュを使用して高速に認証を行うことができるようになる。

このようにcaching_sha2_passwordではユーザのパスワードハッシュの値をキャッシュ内に保存するため、クライアントがサーバーへ接続する際に該当するキャッシュが見つかれば、クライアントから送られてきたその値とパスワードハッシュの照合ができるようになるので、暗号化されていないチャネルを介した安全な認証が可能になります。
これはmysql_native_passwordのSHA1 ベースのチャレンジ/レスポンス メカニズムと比較してより高速なものとなっています。

サーバーとクライアント間の状態遷移とメッセージ交換の条件分岐は以下のようになる。


https://dev.mysql.com/doc/dev/mysql-server/latest/page_caching_sha2_authentication_exchanges.html

takapitakapi

認証方法の変更

認証方法の不一致が発生した場合、サーバーはクライアントにProtocol::AuthSwitchRequestを送信する。Protocol::AuthSwitchRequestには、使用するクライアント認証方法の名前と、新しい方法によって生成される最初の認証ペイロードが含まれる。クライアントは、要求された認証メソッドに切り替え、そのメソッドに従った交換を継続する。

例えば、MySQLサーバーのデフォルト認証方式がcaching_sha2_passwordであった場合、Initial Handshakeにはcaching_sha2_passwordのプラグイン情報が送られてくるので、クライアントはまずcaching_sha2_password形式で認証を開始する。

しかし、クライアントログインを希望していたユーザーアカウントが認証方法にmysql_native_password を使用していた場合、認証方式が異なるためサーバーはmysql_native_passwordで使用する認証ペイロードをクライアントに送信し、クライアントはそのデータを使用して再度パスワードをハッシュ化してサーバーへ送信を行う。