openssl_seal()にご用心


openssl_seal()にご用心

初投稿です。お手柔らかにお願いいたします。

前書き

PHPには公開鍵暗号を用いて簡単にデータを暗号化できる関数として、OpenSSLモジュールを使用した openssl_seal() が存在します。

詳細は 公式マニュアル を参照していただければと思いますが、簡単に説明するとこの関数は

  1. 与えられたデータをランダムな鍵を用いた共通鍵暗号で暗号化
  2. 共通鍵を公開鍵で暗号化
  3. 暗号化したデータと鍵を返す

という動作をします。
また、 openssl_seal() には配列で複数の公開鍵を渡すことができて、それぞれに対応した暗号文を簡単に生成可能です。面白いですね。

このように一見便利に見えるこの関数ですが、使用には注意が必要、というのが今回の内容です。

問題点

openssl_seal() の公式マニュアル にもある通り、この関数は デフォルトで共通鍵暗号に RC4 を使用します。
RC4 は脆弱であると指摘されている暗号化方式です。安全性はあまり高くないと評価された上、問題点があったこともあり、SSL界からは抹消されかけています。今後技術が進めばいよいよ攻略されてしまう可能性も出てくるでしょう。できれば使いたくありません。
良い知らせとして、 openssl_seal() には PHP 5.3.0 から第五引数に method パラメータが追加され、ここを変えることで RC4 以外の暗号化方式に切り替えることができるのです……一応は。

「なら解決じゃないか! 万々歳!」

と思われた方、ちょっと待ってください。何かが足りないと思いませんか?

そう、初期化ベクトルです。初期化ベクトルに関する引数がありません。

つまり、このままでは iv,nonce を使用する暗号化方式は使用できないということになります。ECBモードの暗号化方式を使えば iv 無しに利用できますが、ECBモードは一般に強度が弱いのでお勧めできません。
他にもTriple DESなんかが利用できますが、速度が遅かったりとあまり人気はないようですし、どうせなら広く使われているAESを使いたいところです。

PHP7以降なら第六引数に iv パラメータが追加されている!

さて、ここまで話を引っ張っておいて何なのですが、実はPHP7.0.0以降ではひっそりと第六引数に iv が追加されており、ここに適当な変数を 参照渡しで 指定することで iv,nonce を使用する暗号化方式があっさり使えます。注意すべきポイントは、 暗号化時に使用された iv が変数に代入される という点ですね。また、復号用の関数である openssl_open() にも同様の修正が加えられています。

しかしこれ、openssl_seal(), openssl_open() のどちらのマニュアルにも書かれていません(編集して投稿しておきましたが、反映はまだのようです)。Changelogにはしっかりと載せられていますし、PHPのCソースを追っても確かに実装されています。
とりあえず、PHP5.6.21の環境、PHP7.0.6の環境のそれぞれでAES256(CBC)を試してみます。PHP7.0.6の方では iv パラメータを使いました。
事前に公開鍵と秘密鍵は用意してあるものとします。

PHP7.0.6 環境:

# php -v
PHP 7.0.6 (cli) (built: May  1 2016 07:39:38) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies
    with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies

PHP5.6.21 環境:

# php56 -v
PHP 5.6.21 (cli) (built: May  5 2016 00:23:57)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
    with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies

使用したテストコードは以下の通りです。

test.php
<?php
$pubkey = openssl_pkey_get_public(file_get_contents(__DIR__ . "/pub.key"));
$privkey = openssl_pkey_get_private(file_get_contents(__DIR__ . "/priv.key"));
$encrypted = "";
$decrypted = "";
$ekeys = [];
$iv = "";
$data = 'testdatatestdatatestdatatestdata123123123123123123123';
echo "\$data : " . $data.PHP_EOL;
$result = openssl_seal($data, $encrypted, $ekeys, [$pubkey], 'aes256', $iv); // PHP7 未満であれば第六引数は存在しない
echo "\$encrypted : ".bin2hex($encrypted).PHP_EOL;
echo "\$iv : ".bin2hex($iv).PHP_EOL;
openssl_open($encrypted, $decrypted, $ekeys[0], $privkey, 'aes256', $iv); // PHP7 未満であれば第六引数は存在しない
echo "\$decrypted : ".$decrypted.PHP_EOL;

if($data === $decrypted) {
  echo "OK".PHP_EOL;
} else {
  echo "NG".PHP_EOL;
}

※繰り返しになりますが、 第六引数の iv は参照渡し扱いになり、中身は自動的に更新されます。 もし既に値が入っていた場合、内容が破壊されるためご注意ください。 iv の固定はできません。
また、今回はaes256(AES-256-CBCのエイリアス)を使っていますが、 RC4 のようなストリーム暗号をお望みでしたら、 CTR モードか GCM モードを使うと良いでしょう。

では、実行してみます。

PHP7.0.6:

ivパラメータ込みで実行 :

$data : testdatatestdatatestdatatestdata123123123123123123123
$encrypted : e3833f913f182454cf30f82ec3706e1f1ca59e17f87c14d9f28045f4a7d9d75e938de623f2d055d14c90877cefca7b74d6bff039e5061034aef767ee338e50e2
$iv : 8002cb115c48ceb2a0eea915acc64a93
$decrypted : testdatatestdatatestdatatestdata123123123123123123123
OK

ivパラメータを抜く :

$data : testdatatestdatatestdatatestdata123123123123123123123
PHP Warning:  openssl_seal(): Cipher algorithm requires an IV to be supplied as a sixth parameter in /home/testuser/test/test.php on line 10

Warning: openssl_seal(): Cipher algorithm requires an IV to be supplied as a sixth parameter in /home/testuser/test/test.php on line 10
$encrypted :
$iv :
PHP Notice:  Undefined offset: 0 in /home/testuser/test/test.php on line 13

Notice: Undefined offset: 0 in /home/testuser/test/test.php on line 13
PHP Warning:  openssl_open(): Cipher algorithm requires an IV to be supplied as a sixth parameter in /home/testuser/test/test.php on line 13

Warning: openssl_open(): Cipher algorithm requires an IV to be supplied as a sixth parameter in /home/testuser/test/test.php on line 13
$decrypted :
NG

PHP5.6.21

ivパラメータ込みで実行 :

$data : testdatatestdatatestdatatestdata123123123123123123123
PHP Warning:  openssl_seal() expects at most 5 parameters, 6 given in /home/testuser/test/test.php on line 10
$encrypted :
$iv :
PHP Notice:  Undefined offset: 0 in /home/testuser/test/test.php on line 13
PHP Warning:  openssl_open() expects at most 5 parameters, 6 given in /home/testuser/test/test.php on line 13
$decrypted :
NG

ivパラメータを抜く :

$data : testdatatestdatatestdatatestdata123123123123123123123
PHP Warning:  openssl_seal(): Ciphers with modes requiring IV are not supported in /home/testuser/test/test.php on line 10
$encrypted :
$iv :
PHP Notice:  Undefined offset: 0 in /home/testuser/test/test.php on line 13
$decrypted :
NG

となり、確かにPHP7以降であれば動作していることが分かります。
PHP7以降であれば、第六引数を使わない手はありませんね。

PHP7未満での対応

追記 : @mpyw さんがPHP7未満の環境でも動くよう、以下の関数群を用いてバックポート実装を書いてくださいました。
http://qiita.com/clvs7/items/d754ceab6cd1e87bcd9d#comment-e376870dceac00502c78
これにより、 openssl_encrypt() / openssl_decrypt() に iv パラメータが追加された PHP 5.3.3 以降の環境であれば、 PHP7 以降の openssl_seal() 同等の処理を行うことができます。
@mpyw さん、ありがとうございました!


結果の通り、PHP7未満の環境で openssl_seal() を用いた場合、AES等の iv を用いる暗号化方式を RC4 の代替とすることはできません。
しかし、結局の所 openssl_seal() がやっている処理は
openssl_encrypt() / openssl_decrypt() / openssl_random_pseudo_bytes() / openssl_public_encrypt() / openssl_private_decrypt()
などで代用可能なので、PHP7未満の環境で openssl_seal() にAESを使いたい場合は代用関数群を使うのが良さそうです。

最後に

そもそもPHPで秘密鍵/公開鍵を扱うパターンが多くない、なんて夢のないことは言わないお約束です。

参考文献

第4回暗号勉強会 (https://docs.google.com/presentation/d/1xxAtkuP3s_OysDWxLaMDOZQLn-0dfTqXiPnTpM3Gvsg)
PHP公式マニュアル (openssl_seal())
PHP公式マニュアル (openssl_open())
PHP公式マニュアル (OpenSSL 関数)
PHPの該当部分 C実装 (openssl.c)