Webアプリケーションの機能をサービスとして提供する場合、ランダムな値の秘密のAPIキーを鍵とすることが多いです。
// 何らかのAPIを呼び出す http://example.com/api/v2/get_something?api_key=qwertyuiop
シンプルな方法で使いやすいですが、鍵となるAPIキーをそのまま使っているので鍵が漏洩する可能性があります。HMACやHKDFを使うと鍵となるAPIキーを直接使わないでAPIへのアクセスを認証できます。
HMACを使ったAPIキーによる認証
前提条件: $api_keyは暗号学的に安全な鍵。例:$api_key = base64_encode(random_bytes(32));
鍵となるAPIキーを直接GETやPOSTで渡さなければ、鍵が漏れる心配がなくなります。HMACを使う場合の例です。有効期限を付けないとAPIキーを直接使う場合と変わらないので、鍵に30秒の有効期限をつけています。普通は30秒もあれば十分すぎるでしょう。
<?php // クライアント側 // API利用者のIDとキー $api_user_id = 123456789; $api_key = 'some secure random api key'; // 一時的に利用する事前共有鍵 $tmp_key = bin2hex(random_bytes(32)); // 導出鍵の有効期限 30秒 $expire = time() + 30; // 上記の情報から安全な鍵を導出 $prk = hash_hmac('sha256', $api_key, $tmp_key); $info = json_encode(['api_user_id'=>$api_user_id, 'expire'=>$expire]); $key = hash_hmac('sha256', $prk, $info); // クエリパラメータを作成 $query = http_build_query(['api_user_id'=>$api_user_id, 'key'=>$key, 'tmp_key'=>$tmp_key, 'info'=>$info]); // リクエスト用のURL $url = 'http://example.com/api/v2/get_something?' . $query; ?>
<?php // サーバー側 // ユーザーIDからAPIキーを取得 $api_key = get_api_key_from_id($_GET['api_user_id']); // リクエストから鍵を導出 $prk = hash_hmac('sha256', $api_key, $_GET['tmp_key']); $key = hash_hmac('sha256', $prk, $_GET['info']); // 鍵をチェック - こういう鍵となる情報はhash_equals()でチェック。タイミング攻撃防止。 if (!hash_equals($key, $_GET['key'])) { throw new exception('無効なリクエストです。'); } // 有効期限をチェック $info = json_decode($_GET['info']); if ($info['expire'] < time()) { throw new exception('有効期限切れの鍵です。'); } // OK ?>
HKDFの場合、鍵と追加情報が別パラメーターなので一度の関数呼び出しで処理できます。HKDFはHMACの拡張なので、上記の処理はHKDFによる鍵の導出と同等です。
$api_keyが十分に強いランダム鍵の場合、salt無しでも安全に処理できます。しかし、可能な場合はsaltも利用します。弱いsaltでも飛躍的に強い導出鍵を生成できますが、十分に強いランダム値の方がより強い鍵導出になります。
HKDFによるより安全な鍵導出:
$more_secure_derived_api_key = bin2hex(hash_hkdf('sha265', $api_key, 0, $_GET['info'], $_GET['salt']));
$_GET[‘salt’]は base64_encode(random_bytes(32)) などとして生成します。
HMACを複数回に分けて利用する理由
SHA256が理想的な暗号学的ハッシュ関数かつ$api_keyが強いランダム値あれば、hash(‘sha256’, $api_key . $_GET[‘info’]) のように文字列連結をしても問題ないはずです。しかし、暗号学的ハッシュとされるSHA256でもあっても”理想的”ではありません。
SHA256には「雪崩効果」がありますが、入力となるデータに1 bitの違いがある場合にハッシュ値に半分以上の違いがある程度に過ぎません。この特性を利用すると秘密鍵の解析を総当たりではなく、統計学的分析を利用した攻撃が可能になります。
文字列連結せず、HMACを複数回呼ぶことにより雪崩効果以上に鍵の分析を困難にします。
HKDFとsaltを利用した鍵導出を行うと、もう一回追加のHMACによるランダム化が行われ、鍵分析が更に困難になります。HKDFが使える環境であれば、saltも利用して鍵を導出した方がより安全です。
HMACでも同類の鍵導出が可能です。saltを含めたHMAC値を取得するだけです。ここではAPIキーを直接やり取りするよりは安全な方法を紹介することが目的なので省略しています。
まとめ
上記の方法を使うと秘密であるべきAPIキーをURLやPOSTリクエストの中に埋め込まずに認証できます。多少追加のCPUリソースが必要ですが、重要なAPIキーが漏洩しづらくなるメリットは大きいです。
直接APIキーを使う場合に比べ面倒に見えますが、この処理はAPI利用のクライアントライブラリで行えばユーザーから見るとAPIキーを直接使う場合と手間は変わりません。
この例では”salt”をユーザーが制御しています。ユーザーが”salt”($tmp_key)を制御できると、秘密のはずの$api_keyを解析されるリスクが高まります。この場合、そもそも正規ユーザーが自分の秘密鍵に対して”salt”を設定しているので、このリスクは考慮する必要がありません。1
ここで紹介した鍵導出方法はFS(Forward Secrecy)/PFS(Perfect Forward Secrecy)という概念です。
HKDF, HMACなどのハッシュ関数を使う場合に知っておくべきFS/PFS
性能を向上させる方法
ハッシュ関数の特性を利用して、結果をキャッシュすると性能を向上できます。
- ハッシュ関数は入力値が同じであれば、必ず同じ結果を返す
つまり、最終的に生成される$keyは入力パラメーターである$_GET[‘tmp_key’]、$_GET[‘info’]が同じであれば変わりません。PHP配列のキーはバイナリセーフでどんなキーでも大丈夫なので、例えば
$_SESSION[‘key_cache’][$tmp_key . $info] = $key;
とキャッシュし、キャッシュした値と同じであるか比較するだけで十分です。このようにキャッシュするとハッシュ関数(HMAC/HKDF)によるサーバー側のオーバーヘッドはほとんど気にしなくても良い程度にまで軽減できます。
参考:
HMACハッシュの使い方のまとめ
出鱈目なシグニチャのhash_hkdf関数を安全に使う方法
- 秘密鍵を知らない攻撃者が任意の”salt”を使って導出鍵を取得できる場合、秘密鍵を解析されるリスクが高まります。このような使い方はしてはなりません。 ↩