ユーザーが間違った使い方をしないよう、PHP 7.1に追加されたhash_hkdf関数のシグニチャが出鱈目である件について書いておきます。使い方を間違えると脆弱な実装になるので注意してください。
hash_hkdf関数
hash_hkdf関数はRFC5869のHKDFハッシュの実装で、既存の鍵から新しい鍵を導出する鍵導出関数(Key Derivation Function – KDF)です。鍵導出とは
- 既に存在する鍵から別の鍵を作ること
です。マスター鍵から暗号化に利用する別の鍵を作る、といった場合に利用します。
hash_hkdf関数は以下のシグニチャを持っています。
string hash_hkdf ( string
$algo
, string$ikm
[, int$length
= 0 [, string$info
= ” [, string$salt
= ” ]]] )
HKDF(HMAC based Key Derivation Function – HMACベースの鍵導出関数)は、”汎用”のKDF(鍵導出関数)として標準化された物です。RFC 5869では明確に様々な用途に利用可能な汎用の鍵導出関数であると記載しています。
4. Applications of HKDF
HKDF is intended for use in a wide variety of KDF applications.
(HKDFは多岐に渡るKDFアプリケーションに利用できるよう意図されています。)
このブログではhash_hkdf関数を直接利用した使用例はまだ紹介していません。しかし、hash_hmac関数を使った鍵導出についてまとめています。HKDFはHMACの拡張です。適当な場合にHKDFに置き換えるのは簡単です。
これらの鍵導出はhash_hkdf関数でも可能です。hash_hkdf関数の使い方はこちらを参考にしください。
hash_hkdf関数と他のハッシュ関数のシグニチャ
一見して判りますが、hash_hkdf関数のシグニチャは他のハッシュ関数と異ります。
string hash ( string
$algo
, string$data
[, bool$raw_output
= false ] )string hash_hmac ( string
$algo
, string$data
, string$key
[, bool$raw_output
= false ] )
更にパスワードハッシュ化用に設計された鍵導出関数であるhash_pbkdf2関数とも異るシグニチャになっています。
string hash_pbkdf2 ( string
$algo
, string$password
, string$salt
, int$iterations
[, int$length
= 0 [, bool$raw_output
= false ]] )
hash_hkdf関数には bool $raw_output 引数がありません。hash_hkdfは常に”バイナリ”の値を返します。他の関数はデフォルトでは”16進数表記の文字列”を返します。
hash_hmac関数とhash_pbkdf2関数は第一引数として$keyまたは$saltを取ります。$keyも$saltもハッシュ値の安全性に重要な影響を及ぼす鍵情報です。 RFC 5689ではHKEFのsaltは”pre-shared key”(事前共有鍵)として利用することも多いと解説しています。しかし、hash_hkdf関数では重要である$saltが最後のオプション引数になっています。
hash_hkdf関数シグニチャの問題点
関数の戻り値が違う程度ならそれほど大きな問題(整合性のないAPIも問題ですが・・・)ではありません。hash_hkdf関数の本当の問題は
- RFCの解説/推奨に反して、シグニチャ通りに使うと脆弱な利用方法となるシグニチャになっている
ことです。
オプション引数はlength(導出鍵の長さ)、info(鍵コンテクスト – 非秘密情報)、salt(共有鍵 – 秘密または非秘密でユーザーが制御できない物)の順番になっています。
string hash_hkdf ( string
$algo
, string$ikm
[, int$length
= 0 [, string$info
= ” [, string$salt
= ” ]]] )
hash_hkdf関数はHMACつまりhash_hmac関数がベースになっています。hash_hmacのシグニチャと比べると悪い箇所が分かります。
string hash_hmac ( string
$algo
, string$data
, string$key
[, bool$raw_output
= false ] )
hash_hkdfのsalt引数はhash_hamc関数のkey引数にあたります。hash_hkdf関数では”鍵”となる引数が最後の引数になっています。”鍵”がない”鍵導出関数”が脆弱になるのは当たり前のことです。hash_hmac関数をkey引数無しで使っても意味がないことと同じで、hash_hkdf関数をsalt引数無しで使う意味はありません。1最後のオプション引数では、どうしようもないAPIデザインである、と言わざるを得ません。
あるべきhash_hkdf関数シグニチャ
既存のAPIとの整合性、RFC5869の推奨事項を考慮すると、在るべきシグニチャは以下のようになります。
string hash_hkdf ( string
$algo
, string$ikm
, string$salt
, string$info
[, int$length
= 0 [, bool$raw_output
= false ]] )
info引数が必須引数である理由は、info引数を利用しない場合、hash_hmac関数で事足りるからです。
hash_hkdf関数を利用する場合、salt引数がない設計は99%誤った設計(脆弱な設計)です。
info引数が必要ない場合、99%のケースでHKDFではなくHMACで十分なはずです。
salt、infoには”(空文字列)を指定できるので1%も無いレアケース1でも、これらの引数が必須であって困ることはありません。
info引数だけが必要なケースもありますが、この場合もhash_hmac関数で十分です。
なぜHMACやHKDFが考案されたのか?
HMACは文書改ざんを検出する場合に
$signature = hash('sha3-256', $document . $singing_key);
などと”文字列連結”を使ってハッシュ値を取得した場合に、暗号理論的なハッシュ関数を利用していても$singing_keyが解析されるリスクを軽減するために考案されました。
HMACでは文書に署名する鍵を別個の引数とした上で、複数回指定のハッシュ関数を適用します。
$signature = hash_hmac('sha3-256', $document , $singing_key);
鍵が引数として独立しているので、$singing_keyが解析されるリスクがかなり低くなりました。HMACを使っても鍵導出は可能です。
$derived_key = hash_hmac('sha3-256', $master_key , $derivation_key);
HKDFは鍵を導出する場合に、鍵のバージョン/鍵の有効期限/鍵が利用できるユーザーなど鍵のコンテクストを指定したい、というニーズに応えるために考案されました。(任意長の長さの鍵のニーズもありますが、PHPではほとんど使われないので省略)
文書の署名(HMAC)に”文字列連結”をするとリスクが高くなる、ことと同じく
$derived_key = hash_hmac('sha3-256', $master_key , $derivation_key . $key_version);
とすると脆弱になります。この為、$key_versionは別引数として分離し安全に処理できるようにしなければなりません。そこでHKDFが考案されました。(実際にはHMACさえ使わない脆弱な鍵導出が多数あったので標準化されたと思われます)
なぜこのような出鱈目なAPIデザインになったのか?
あまりにおかしいAPI設計なので変更提案を行いましたが、残念ながら修正することは出来ませんでした。なぜこのようなAPIになったのか?理由は開発者の思い込みです。
- HKDFは特定暗号コード専用の関数 (実際はRFCに汎用だと明記。わざわざ汎用である旨のセクションが作ってあっても、思いこんでいるので”一部のつまみ食い”と反論。)
- 広く誤ったKDF関数の利用が存在し、 鍵の長さだけ長くするなどの誤った利用方法が”正しい”と盲目的に信じた (RFCには”このような誤りをしないよう”注意事項も記載されているが、これを思い込みで曲解。)
- HKDFは”特別な関数”なので他のハッシュ関数APIとの整合性は必要ない (本当はHKDFはHMACの拡張なので、HMAC関数と同類のシグニチャを持つべき。)
私もですが、人は”正しいと思い込む”となかなか誤りに気がつきません。。(不合理 誰もがまぬがれない思考の罠100 この本は本当にオススメです。読んでいない方は是非!)
鍵導出のセキュリティとForward Secrecy – FS, PFS
鍵導出を行う場合に使い捨てランダムSaltを使うべきであることは、ハッシュ関数の特性を知っていれば当然のことです。使い捨てランダムSaltを使うべきであることはForward SecrecyまたはPerfect Forward Secrecyとしても知られています。
まとめ
少なくとも引数の順序が異る方が良いですが、
string hash_hkdf ( string $algo , string $ikm [, int $length = 0 [, string $info = ” [, string $salt = ” ]]] )
このシグニチャでも安全に利用することは可能です。
HKDFは秘密鍵と導出鍵の安全性を最大限にできるよう考えられています。鍵の安全性を最大限にするには、hash_hkdf関数を利用する際に以下の事項に注意します。
- $saltは最後のオプション引数だが、必ず指定する。
- $saltが必要ない鍵導出のプロトコル設計(認証プロトコルなど)の99%は脆弱な設計である。
- $saltが必要ない鍵導出のコード設計(データ暗号化など)のほとんどは脆弱な設計である。(本当に$saltが必要ない場合、鍵導出の必要性がないか、hash_hmacで十分である場合が多い)
- 低いエントロピーでも構わないので必ず有効な$saltを設定する。勿論、エントロピーは多い方が良い。($saltにはrandom_bytes($hash_size)などが好ましい)
- $saltの値はユーザーが制御できないこと。(ユーザー入力の$saltで導出鍵が参照できないこと)
- 秘密鍵($ikm)のエントロピーが少ない場合(ユーザー入力のパスワードなど)、$saltは十分なエントロピーを持った秘密鍵でなければ、秘密鍵の安全性は保てない。
- $infoが不必要な場合、または、$saltが必要なく$infoのみ必要な場合、hash_hkdf関数ではなくhash_hmac関数が利用できる。(そして、HMACの方が効率が良い)
- $infoの値はユーザーが制御できても構わない。
- $lengthは特別な理由がない限り”0”(デフォルトのハッシュサイズ)を利用する。
最適な$saltサイズは使用しているハッシュ関数と同じサイズのランダム値です。例:random_bytes($hash_size); // sha3-256なら$hash_size=32
hash_hkdf関数をオプション引数無しで利用する意味はありません。$lengthが最初のオプション引数ですが、これだけ使用するのは危険です。$infoまで指定するのもNGです。間違っても
$aes256key = hash_hkdf('sha-1', $aes128key, 32); $newkey = hash_hkdf('sha3-256', $userpassword, 64); $newkey = hash_hkdf('sha3-256', $userpassword, 64, $expires);
のような脆弱な使い方をしないようにしてください。後者2つは特に危険です!
参考: hash_hkdf()でわざわざバイナリキー/バイナリSaltを使うことに意味はない