ストアドファンクションを使った暗号鍵の守り方(MySQL Enterprise Encryption版)
始めに
以前の記事でストアドファンクションを使って暗号鍵をアプリケーションに渡さずに暗号化/復号化を行うサンプルを例を取り上げました。そこで用いたのは、MySQL Community Editionでも使える共通鍵暗号方式のAES暗号化関数でしたが、今回はMySQL Enterprise Editionで使える公開鍵暗号方式のRSA暗号化関数を使った例の紹介になります。
Oracle Cloud Infrastructure (以下OCI)で提供されているMySQLのPaaSサービスではEnterprise Editionが提供されています。そのためEnterprise Editionの機能を追加費用無しで利用可能です。今回のサンプルはOCI上で動作検証をしているので、Enterprise Editionのライセンスが無い方でもOCIが使えれば今回のサンプル例を試すことが可能です。
暗号化データの保存方法
2つのアプリケーション用DBアカウントを作成して、2つのアプリケーション間のデータのやり取りの暗号化と署名検証も行うサンプルです。
アプリケーション用データベース & 暗号化データ保存テーブル
データの受け渡し用のテーブル定義です。
1 2 3 4 5 6 7 8 9 10 11 12 |
CREATE DATABASE share_db; USE share_db; CREATE TABLE encrypt_data_store ( id int NOT NULL, /* データ保存用ID */ from_app VARCHAR(512) NOT NULL, /* 送信元アプリ名 */ to_app VARCHAR(512) NOT NULL, /* 受信元アプリ名 */ encypted_data BLOB NOT NULL, /* 暗号化済みデータ本体 */ sign_data BLOB NOT NULL, /* 署名データ */ PRIMARY KEY (id) ) ENGINE=InnoDB; |
アプリケーション用DB接続ユーザーの作成
サンプルとして2つのDBアカウントを作成ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
-- Application A CREATE USER 'app_a'@'%' IDENTIFIED BY '##_Example_Password_00'; GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON share_db.* TO 'app_a'@'%'; -- Application B CREATE USER 'app_b'@'%' IDENTIFIED BY '##_Example_Password_00'; GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON share_db.* TO 'app_b'@'%'; |
RSAの秘密鍵と公開鍵の保存用データベース & テーブル作成
RSAの秘密鍵の保管用なのでアクセス権の付与には十分注意を払ってください。
1 2 3 4 5 6 7 8 9 10 11 |
CREATE DATABASE key_store; USE key_store; CREATE TABLE rsa_keys ( app_name VARCHAR(512) NOT NULL, /* DBアカウント名 */ priv_key BLOB NOT NULL, /* 秘密鍵 */ pub_key BLOB NOT NULL, /* 公開鍵 */ PRIMARY KEY (app_name) ) ENGINE=InnoDB; |
暗号化/復号化ストアドファンクション専用ユーザーの作成
多くのクラウドのDBaaSがそうですが、OCIでもMySQLインスタンスを作成時に作った管理者ユーザーにはSUPER権限やSET_USER_ID権限が与えられていません。そのためストアドファンクションの実行ユーザーを指定して作成できません。
そのためencrypt_func_userアカウントでMySQLにログインをして、ストアドファンクションを作成します。ストアドファンクションを作成するときに実行権限の指定を行わない場合、ストアドファンクションを作成したアカウントが持っている権限で実行されます。
encrypt_func_userアカウントには、rsa_keysテーブルの読み取り権限とshare_dbデータベースでストアドファンクションの作成と実行する権限のみを与えます。
1 2 3 4 5 6 7 8 |
CREATE USER 'encrypt_func_user'@'%' IDENTIFIED BY '##_Example_Password_00'; GRANT SELECT ON key_store.rsa_keys TO 'encrypt_func_user'@'%'; GRANT EXECUTE, CREATE ROUTINE ON share_db.* TO 'encrypt_func_user'@'%'; |
ストアドファンクション作成
encrypt_func_userアカウントでMySQLにログインをして各ストアドファンクションを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
USE share_db; DELIMITER // /* RSA 暗号化関数 */ CREATE FUNCTION rsa_encrypt_func(plain_text TEXT, app_user TEXT) RETURNS BLOB DETERMINISTIC READS SQL DATA BEGIN DECLARE pub BLOB; SELECT rsa_keys.pub_key INTO pub FROM key_store.rsa_keys WHERE rsa_keys.app_name = app_user; IF pub IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'App user not found'; END IF; RETURN asymmetric_encrypt('RSA', plain_text, pub); END; // /* RSA 復号化関数 */ CREATE FUNCTION rsa_decrypt_func(encypt_data BLOB) RETURNS TEXT DETERMINISTIC READS SQL DATA BEGIN DECLARE priv BLOB; SELECT rsa_keys.priv_key INTO priv FROM key_store.rsa_keys WHERE user() like rsa_keys.app_name; IF priv IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Private key not found'; END IF; RETURN asymmetric_decrypt('RSA', encypt_data, priv); END; // /* RSA 署名関数 */ CREATE FUNCTION rsa_sign_func(sign_data BLOB) RETURNS BLOB DETERMINISTIC READS SQL DATA BEGIN DECLARE priv BLOB; DECLARE dig BLOB; SET dig = create_digest('SHA512', sign_data); SELECT rsa_keys.priv_key INTO priv FROM key_store.rsa_keys WHERE user() like rsa_keys.app_name; IF priv IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Private key not found'; END IF; RETURN asymmetric_sign('RSA', dig, priv, 'SHA512'); END; // /* RSA 署名検証関数 */ CREATE FUNCTION rsa_verify_func(verify_data BLOB, verify_sign BLOB, verify_user TEXT) RETURNS INT DETERMINISTIC READS SQL DATA BEGIN DECLARE pub BLOB; DECLARE dig BLOB; SET dig = create_digest('SHA512', verify_data); SELECT rsa_keys.pub_key INTO pub FROM key_store.rsa_keys WHERE rsa_keys.app_name = verify_user; IF pub IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'App user not found'; END IF; RETURN asymmetric_verify('RSA', dig, verify_sign, pub, 'SHA512'); END; // DELIMITER ; |
ストアドファンクションの作成が終わればencrypt_func_userアカウントでログインする必要がなくなるのでストアドファンクションを作成する権限を削除してアカウントをロックします。
1 2 3 4 |
REVOKE CREATE ROUTINE ON share_db.* FROM 'encrypt_func_user'@'%'; ALTER USER 'encrypt_func_user'@'%' ACCOUNT LOCK; |
ストアドファンクションの使い方
これで準備ができたので、app_aアカウントからapp_bアカウントへ暗号化をして安全にデータを受け渡してみましょう。
各DBアカウント用の鍵ペアを作成
管理者アカウントなどで、各アプリケーションのDBアカウントに紐づく鍵ペアを登録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
USE key_store; -- app_a用の秘密鍵と公開鍵を登録 SET @app_a_priv = create_asymmetric_priv_key('RSA', 2048); SET @app_a_pub = create_asymmetric_pub_key('RSA', @app_a_priv); INSERT rsa_keys VALUES ('app_a@%', @app_a_priv, @app_a_pub); -- app_b用の秘密鍵と公開鍵を登録 SET @app_b_priv = create_asymmetric_priv_key('RSA', 2048); SET @app_b_pub = create_asymmetric_pub_key('RSA', @app_b_priv); INSERT rsa_keys VALUES ('app_b@%', @app_b_priv, @app_b_pub); |
app_aからapp_bへ渡す暗号化したデータの作成
app_aアカウントで暗号化したデータを登録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
mysql> select current_user(); +----------------+ | current_user() | +----------------+ | app_a@% | +----------------+ 1 row in set (0.00 sec) mysql> SELECT rsa_encrypt_func('Private message', 'app_b@%') INTO @enc_data; -- app_bの公開鍵で暗号化 Query OK, 1 row affected (0.00 sec) mysql> INSERT INTO encrypt_data_store VALUES (1, current_user(), 'app_b@%', @enc_data, rsa_sign_func(@enc_data)); Query OK, 1 row affected (0.00 sec) |
暗号化したデータ(@enc_data)に対してapp_aの秘密鍵で署名をしているので、このデータの送信元がapp_aだと検証可能になります。
app_bがapp_aからの暗号化したデータを復号化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
mysql> SELECT current_user(); +----------------+ | current_user() | +----------------+ | app_b@% | +----------------+ 1 row in set (0.00 sec) mysql> SELECT id, rsa_decrypt_func(encypted_data), rsa_verify_func(encypted_data, sign_data, from_app) FROM encrypt_data_store WHERE id = 1; +----+---------------------------------+-----------------------------------------------------+ | id | rsa_decrypt_func(encypted_data) | rsa_verify_func(encypted_data, sign_data, from_app) | +----+---------------------------------+-----------------------------------------------------+ | 1 | Private message | 1 | +----+---------------------------------+-----------------------------------------------------+ 1 row in set (0.00 sec) |
暗愚化されたデータを平文に戻せて、署名の検証も成功しました。
それ以外のDBアカウントでアクセスした場合
app_eというDBアカウントの作成と鍵ペアを登録して、先ほど作成したapp_bへの平文の読み取りと偽メッセージの作成を試みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
mysql> SELECT current_user(); +----------------+ | current_user() | +----------------+ | app_e@% | +----------------+ 1 row in set (0.00 sec) mysql> SELECT id, rsa_decrypt_func(encypted_data), rsa_verify_func(encypted_data, sign_data, from_app) FROM encrypt_data_store WHERE id =1; +----+---------------------------------+-----------------------------------------------------+ | id | rsa_decrypt_func(encypted_data) | rsa_verify_func(encypted_data, sign_data, from_app) | +----+---------------------------------+-----------------------------------------------------+ | 1 | NULL | 1 | +----+---------------------------------+-----------------------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT rsa_encrypt_func('Fake message', 'app_b@%') into @enc_data; -- 偽メッセージ作成 Query OK, 1 row affected (0.00 sec) mysql> INSERT INTO encrypt_data_store VALUES (2, 'app_a@%', 'app_b@%', @enc_data, rsa_sign_func(@enc_data)); -- app_aの名前を騙る Query OK, 1 row affected (0.01 sec) |
app_aやapp_bのDBアカウントでは無い他のユーザーでも署名の検証は成功しますが、暗号データの復号化には失敗してNULLが返ってきています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
mysql> SELECT current_user(); +----------------+ | current_user() | +----------------+ | app_b@% | +----------------+ 1 row in set (0.00 sec) mysql> SELECT id, rsa_decrypt_func(encypted_data), rsa_verify_func(encypted_data, sign_data, from_app) FROM encrypt_data_store WHERE id =2; +----+---------------------------------+-----------------------------------------------------+ | id | rsa_decrypt_func(encypted_data) | rsa_verify_func(encypted_data, sign_data, from_app) | +----+---------------------------------+-----------------------------------------------------+ | 2 | Fake message | NULL | +----+---------------------------------+-----------------------------------------------------+ 1 row in set (0.00 sec) |
app_bの秘密鍵で暗号化されているデータは復号化して平文に戻せましたが、署名の検証に失敗しているので署名元を偽っていると判断できます。
終わりに
DBに接続するアプリケーションが悪意を持った挙動をする可能性は低いですが、RSA暗号に代表される公開鍵暗号方式の利点が必要とされるアプリケーションは多くあります
例えば、Web上の様々なサービスと連携するのに使われるJSON Web tokenなどでは認証/認可/署名/検証などでRSA暗号方式が必要とされ、適切な鍵管理が求められるアプリケーションは増えています。
この記事がアプリケーションごとに必要とされる適切な鍵の管理を行う際に参考になれば幸いです。