Debugging mysqlnd.so ~mysql_native_passwordが廃止される未来に備えて~

こんにちわ。せじまです。PHPには疎いんですが、今回は珍しくPHPの話をします。

はじめに

2023-07-18 に MySQL 8.1.0 がリリースされました。いくつか気になる変更が入っているのですが、今回は

The mysql_native_password authentication plugin now is deprecated and subject to removal in a future version of MySQL. CREATE USER, ALTER USER, and SET PASSWORD operations now insert a deprecation warning into the server error log if an account attempts to authenticate using mysql_native_password as an authentication method. (Bug #35336317)

に関連するところについてです。

MySQL8.0でcaching_sha2_passwordがデフォルトの認証プラグインになりましたが、8.0がリリースされた当時、古いクライアントライブラリはcaching_sha2_passwordに対応できていませんでした。default_authentication_plugin=mysql_native_password という設定を入れてMySQL8.0を導入された方は、少なくなかったでしょう。
将来、MySQLの新しいバージョンでmysql_native_passwordが廃止されたとき、こういった対応は取れなくなってしまいますので、mysql_native_passwordからcaching_sha2_passwordに移行していく必要が発生します。

caching_sha2_password の仕様など

caching_sha2_passwordの仕様は、Doxygenで生成されたソースコードのドキュメントを読むのが確実でしょう。
caching_sha2_passwordに対応するということは、Fast authenticationに対応するだけでなく、Complete authenticationにどのように対応するかが問題になります。

Fast authentication

雑にいいますと、クライアントはパスワードのハッシュ(のScramble)を最初に送信するんですが、それがmysqld上のキャッシュに存在していたらそこで認証終了で、それがFast authenticationになります。

Complete authentication

パスワードのハッシュがmysqld上のキャッシュに存在していなければ、"TCP with TLS OR Socket Or Shared Memory connection"、安全な通信経路であれば、そのままパスワードを送信して認証します。安全な通信経路でなければ、mysqldから公開鍵を取得し(クライアントがすでに公開鍵を持っているならそれを使い)パスワードを暗号化し、mysqldで復号化して認証します。パスワード認証できたら、そのパスワードのハッシュをmysqld上にキャッシュします。これがComplete authenticationです。

libmysqlclientや公式のmysqlコマンドラインクライアントの場合

mysql や mysqladmin などの標準 MySQL クライアントは libmysqlclient ベースであるため、話はシンプルです。

ほとんどの場合はデフォルトでSSL経由で接続し、Complete authenticationについて意識することもないでしょう。もしSSLで繋げない環境でも、--get-server-public-keyを指定するなどすれば良いだけの話です。

PHPの場合は?

MySQLの中のひと曰く、PHP7.4はcaching_sha2_password対応されてる とのことで、実際、7.4.2でFix caching_sha2_password authがmergeされているようです。
なるほど確かにcaching_sha2_password動きそうなんですが、mysqlndのドキュメントを見ると、SHA-256 Authentication Pluginについての記述はあるんですが、--get-server-public-keyのようなオプションは見当たりません。

どないなっとんねん???

と思ったので、mysqlndのソースコード眺めたり、PHPで検証するためのコードを書いたんですが、libmysqlclientと異なる振る舞いだったので、確証が持てませんでした。ただ、

PHPはOSSなんだからgdbでbreakpointはればいいか

と思ったので、デバッガで検証してみることにしました。
今回はお手軽に、 Ubuntu 20.04 LTS on WSL2で検証しました。便利な時代になったものです。

テスト環境のMySQL

WSL2上でデバッグビルド済みのMySQL8.0.34があったので、そこで

などしました。

PHPでComplete authenticationを検証するためのスクリプト

実行例は次のようなものになります。

細かいところを補足します。

127.0.0.1

ここをlocalhostにすると socket file 経由の接続になり、安全な通信経路とみなされてしまい、公開鍵を取得する可能性がなくなってしまいます。

SHOW SESSION STATUS LIKE 'Ssl_cipher';

SSL経由であればTLS_AES_256_GCM_SHA384のような値が、SSL経由でなければemptyが返ってきます。これにより、SSL経由で接続されているかが確認できます。

FLUSH PRIVILEGES

mysqld上のパスワードのハッシュのキャッシュがクリアされます。次回接続時、Complete authenticationを強制するためのクリーンアップの処理として入れています。

mysqlndが公開鍵を取得しているところをgdbで確認する

上記の検証用スクリプトを実行したらSsl_cipherが空だったので、SSL経由じゃないんだろうと思うわけですが、公開鍵を取得しているその瞬間を確認したいわけです。というわけで、Debug Symbol Packageなどを入れていきます。

PHPのインストール

PHPのソースコードをインストール

/etc/apt/sources.list でdeb-srcがコメントアウトされているようであれば、コメントアウトを無効化して

して

します。

Debug Symbol Packages

Ubuntu公式の Debug Symbol Packages を参考にやっていきます。

あとは

します。

公開鍵ファイルの確認

caching_sha2_password_auto_generate_rsa_keysはデフォルトでONで、caching_sha2_password_public_key_pathのデフォルトはpublic_key.pemなので、

な環境であれば

というように配置されています。

gdbでbreakpoint張って検証用スクリプトを実行

apt-get source して ~/php7.4-7.4.3 とかにソースコードが展開されてると思いますので、

します。もし万が一gdbが入ってなかったら、sudo apt install gdbしてください。

とりあえず、
https://github.com/php/php-src/blob/php-7.4.3/ext/mysqlnd/mysqlnd_auth.c#L1112

https://github.com/php/php-src/blob/php-7.4.3/ext/mysqlnd/mysqlnd_auth.c#L978
あたりにbreakpointはって、mysqlnd_auth.c:978で止まったら

して、さきほどの公開鍵が来てるかどうか確認すれば良いんじゃないか、というところです。というわけで

という感じです。
ちなみに、ここが今回のポイントです。info shareで 'No shared libraries loaded at this time.' で、 info break で Address が <PENDING> になっており、 mysqlnd.so はまだロードされていない状態です。 rbreak mysqlnd_caching_sha2_get_key とかやってもこの段階では張れません。gdbは賢いので、mysqlnd.soがロードされるとbreakpointで止まるようになります。(本当はvscodeでやれればよかったんですが、vscode力が低いおっさんなので、今回は手っ取り早くgdbのCLIでやりました。今後の課題にしたいなと思います。)

で、 run /home/sejma/test.php などやりますと

さきほどの公開鍵を取得しているところが確認できました。これは p pk_resp_packet.public_key してから迅速に c しているので、正常終了していますが、ゆっくりやると

となります。内部的なタイムアウトとかに引っかかるか、なんらかの timing issue な気がしますが、今回の主題から外れるので、いったん忘れます。

せっかくなので、Breakpoint 1で止まってるときの info share や info break なども掲載しておきます。

というように、Addressは解決され、/usr/lib/php/20190902/mysqlnd.soがロードされているのも確認できます。mysqlnd.soがアンロードされると、breakpointが無効化されるのも確認できます。

確認できた mysqlnd の caching_sha2_password の振る舞い

MySQLの公式ドキュメントには

For connections by accounts that authenticate with caching_sha2_password and RSA key pair-based password exchange, the server does not send the RSA public key to clients by default.

とあるので、Complete authenticationに公開鍵を用いるのは、あくまでオプション扱いになっています。実際、libmysqlclientを使った公式のmysqlコマンドラインクライアントは、デフォルトではSSLで接続してComplete authenticationを行います。
一方、公式ドキュメントには

RSA-based password exchange is available regardless of the SSL library against which MySQL is linked.

ともあります。(このあたりの経緯は知りませんが)もしかするとmysqlnd開発者は、SSLのライブラリへの依存度を下げるべく、公開鍵を用いてComplete authenticationするように実装したのでしょうか?

せっかくなので、require_secure_transportを有効にして、先程の検証スクリプトを実行してみましょう。

コケました。
PHP 7.4.3-4ubuntu2.19 で mysqlnd のデフォルトは --ssl-mode=PREFERREDではなく、--ssl-mode=DISABLED --get-server-public-key 相当なんだろうなというのが確認できました。

おわりに

少し調べてみたのですが、

  • mysqlndがcaching_sha2_passwordのComplete authenticationに対して、どのように振る舞っているのか
  • mysqlndをgdbでデバッグする

といった話が、インターネット上にあまり転がってなさそうだったので、少々難儀していたのですが、とりあえずデバッガで動かせば良いんだからOSS最高ですねと改めて再認識しました。もし万が一、PHPでcaching_sha2_password使うために、公開鍵のファイルやSSL通信するための証明書のファイルをmysqlndにオプションで渡すのが必須になるなら、caching_sha2_passwordに移行するハードル上がるのではと危惧していました。しかし、それはどうやら杞憂に終わりそうでホッとしています。

デバッガと言いますと、MySQL 8.1.0や8.0.34で、ビルド周りにまた変更が入りました。macOSやWindowsでは、8.0.33と同じようにはビルドできない場合がありそうなので、MySQL 8.1.0をWindows/macOS/WSL2でデバッグビルドする話について、近日中にまた書こうかなと思っています。

Special Thanks

今回、MYSQL_OPT_SSL_MODEのSSL_MODE_PREFERREDやMYSQL_OPT_GET_SERVER_PUBLIC_KEY的なものがPHP側になさそうだと嘆いていたら、yoku0825さん曰く「(sha256_passwordのプラグインの方には)ありそうな気がしますー」とのことだったので、「PHPでcaching_sha2_passwordは別のプラグインだしmysqlndのデフォルトの振る舞いがlibmysqlclientと異なりそうだし、PHPのcaching_sha2_password対応についてはデバッガで動かしてみないと何ともわからんなぁ」となったので、gdbを持ち出す運びとなりました。

yoku0825さんには、この場を借りて御礼申し上げます。