ハッシュ(HMAC)を使って有効期限付きURL/URIを作る方法

(Last Updated On: )

より高度なCSRF対策 – URL/URI個別にバリデーションする方法でハッシュ(HMAC)を使えばデータベースを使わずに有効期限付きのURLを作れる、と紹介しました。今回は有効期限付きURL(URI)の作り方を紹介します。

ハッシュ値(鍵)の安全性

より高度なCSRF対策 – URI個別にバリデーションする方法のハッシュ関数の安全な使い方の紹介が不十分なところもあるので、このエントリでもう少し詳しく説明します。RFC 5869のHKDFのようなハッシュ関数は以下のように書けます。

<?php
// RFC 5869のHKDFのようなハッシュ値を求める
$prk =hash_hmac('sha256', 'some key', 'salt');
$hkdf_like_key = hash_hmac('sha256', $prk, 'context');
// 上記は下記のような文字列連結がないので安全
$unsecure_key = hash_hmac('sha256', 'some key', 'salt' . 'context');
?>

HKDFでは”salt”(事前共有鍵として利用されることも多い)と”context”(導出される鍵の適用範囲。普通は秘密でない情報)を複数のHMACで別パラメータとして分ける事により安全性を高めています。

鍵(秘密鍵の”some key”と導出される鍵 “$hkdf_like_key”)の安全性は

$prk = hash_hmac('sha256', 'some key', 'salt');

で確保しなければなりません。その為には条件があります。

  • “some key”は秘密であること。
    • “some key”は暗号学的に強い鍵(ハッシュサイズと同じ以上のランダム値。例:SHA 256の場合、random_bytes(32)など)であることが望ましい。弱い場合(ユーザー入力のパスワードなど)は暗号学的に強い秘密の”salt”が必要
  • “some key”または”salt”のどちらかが暗号学的に安全(予測/推測できない)な値であること。
    • “salt”は秘密である必要はないが、秘密にできるなら秘密にした方がよい。”some key”が弱い場合、秘密の強い”salt”が必要。
  • salt”の値をユーザーが制御して導出される鍵($hkdf_like_key)の値を取得できないこと。

これらが守られていれば鍵(秘密鍵の”some key”と導出される鍵)の安全性は保たれます。有効期限付きURIの場合、”some key”は強い鍵にできるので”salt”を秘密にする必要はありません。

暗号学的に安全性が保たれる、とは言ってもブルートフォースによる攻撃は可能なので”絶対的な安全性”とまでは言えない点には注意が必要です。

 

ハッシュ(HMAC)を使って有効期限付きURIを作る方法

ハッシュ(HMAC)を使って有効期限付きURLを作る方法を解っているとデータベースを使わない有効期限付きURIは簡単に作れます。

<?php
// 有効期限付きURIの作成

// 有効期限付きURIにするURI 
$uri = 'http://example.com/some_path';
// 秘密鍵 - 'secure random data'はrandom_bytes()などで生成した安全な値
define('SUPER_SECRET_MASTER_KEY', 'secure random data');

$salt = bin2hex(randome_bytes(32)); // 事前共有鍵
$expire = time() + 3600; // 有効期限
$context = serialize([$uri, $expire]); // コンテクスト - $uriと$expire
$prk = hash_hmac('sha256', SUPER_SECRET_MASTER_KEY, $salt);
$derived_key = hash_hmac('sha256', $prk, $context); // 導出鍵

$params = http_build_query(['key1' => $derived_key, 'key2' => $salt, 'context' => $context]);

// 有効期限付きURI - これをユーザーに渡す
$uri_with_expiration = $uri . '?' . $params);
?>

<?php
// 有効期限付きURIのチェック

// 送られてきた導出鍵
$derived_key_from_request = $_GET['key1'];
// 送られてきた事前共有鍵
$salt_from_request = $_GET['key2'];
// コンテクスト - $uriとexpire
$uri_requested = get_uri_without_keys($_SERVER['REQUEST_URI']); // この関数でkey1,key2とexpireを削除
$context = serialize([$uri_requested, $_GET['expire']);

$prk = hash_hmac('sha256', SUPER_SECRET_MASTER_KEY, $salt_from_request);
$derived_key = hash_hmac('sha256', $prk, $context);

// 暗号学的に安全な導出鍵が一致すればアクセスOK
if (!hash_equals($derived_key_from_request, $derived_key)) {
   throw new Exception('不正なリクエスト');
}
if ($_GET['expire'] < time()) {
    throw new Exception('有効期限切れ');
}
// アクセスOK
?>

$contextには何を入れても構わないので、ユーザーIDやグループIDなど、公開範囲などのコンテクストを追加して入れることも可能です。

 

注意事項

暗号学的なハッシュ関数に対してもブルートフォース攻撃は常に可能です。

// 秘密鍵 - 'secure random data'はrandom_bytes()などで生成した安全な値
define('SUPER_SECRET_MASTER_KEY', 'secure random data');

秘密鍵がブルートフォース攻撃で解析されるリスク1も考慮しなければなりません。長期間同じ鍵を使い続けると解析されるリスクが高くなります。長期間同じ秘密鍵を使うのは避けるべきです。例えば、このサンプルコードと同じようにHMAC+SHA256を利用するAWS S3のPreSigned URLは最長でも1週間で秘密鍵を破棄しています。リスクを軽減するため秘密鍵のバージョン番号や生成日情報を一緒に保存し、定期的に更新する仕組みを実装すべきです。

論理的な背景などはより高度なCSRF対策 – URI個別にバリデーションする方法を参照してください。

 

まとめ

この方法のメリットはより高度なCSRF対策 – URI個別にバリデーションする方法と同じくデータベースを使わずに大量のURIにアクセス許可を与えることが出来ることです。データベースでアクセス許可を管理できるなら、それぞれに付与した鍵情報と有効期限を管理するデータベースを使っても構いません。

データベースを使わないので大量の有効期限付きURIでも簡単かつ効率的にアクセス管理ができることがこの方法のメリットです。


  1.  ビットコインマイニングはこの例の秘密鍵をブルートフォースで解析することと同類の仕組みです。同じように秘密鍵が攻略されるリスクがあります。 

投稿者: yohgaki