Amazon CloudFront Functionsでハッシュの検証(HMAC)

こちらは『[ゆるWeb勉強会@札幌 Advent Calendar 2022](https://adventar.org/calendars/7513)』の18日目の記事です。

## はじめに
S3に保存しているコンテンツを保護したい場合、署名付きURLや署名付きCookieを利用すると思います。
もう少しライトにアクセスを制限出来ないかと思い、CloudFront Functionsで
ハッシュの検証を試してみました。
 
## 実現したいこと
クエリパラメータsに指定したハッシュ値が正しい場合のみ、S3に保存してある画像を表示するようにします。

ハッシュ化はCloudFront Functionsで用意されているビルトインモジュール(crypto)を利用します。
アルゴリズムはsha256を使用し、ハッシュベースのメッセージ認証コード (HMAC)を生成します。

今回リクエスト側の処理は省略します。
テスト用にPHPでハッシュ値を生成しました。

### 前提条件
* 画像はS3にアップロード済
* S3はCloudFront経由でのみアクセスする設定済
* CloudFrontからS3への接続設定済

## コード
### CloudFront Functions用

JWT認証のコードを参考にしています。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/example-function-validate-token.html

ハッシュ化部分
```
var crypto = require('crypto');
crypto.createHmac('sha256', {シークレット値}).update({ハッシュ化したい文字列}).digest('hex')
```

>crypto.createHmac(algorithm, secret key)
>指定された algorithm と secret key を使用する HMAC オブジェクトを作成して返します。アルゴリズムは md5、sha1、sha256 のいずれかを使用します。
>
>hmac.update(data)
>指定された data を使用して HMAC コンテンツを更新します 。
>
>hmac.digest([encoding])
>hmac.update() を使用して渡されたすべてのデータのダイジェストを計算します。エンコードは hex、base64、base64url のいずれかを使用します。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html#writing-functions-javascript-features-builtin-modules-crypto

```javascript: cloudfront functionに設定
var crypto = require('crypto');

//バリデーションエラー時のレスポンス
var response401 = {
    statusCode: 401,
    statusDescription: 'Unauthorized'
};

//ハッシュ値のバリデーション
function verifyRequest(fwdUri, token, key, signingMethod, signingType) {
  // check token
  if (!token) {
    throw new Error('No token supplied');
  }

  if (!_verify(fwdUri, token, key, signingMethod, signingType)) {
    throw new Error('Signature verification failed');
  }

  return fwdUri;
}

function _verify(input, token, key, method, type) {
  if (type === "hmac") {
    return (token === _sign(input, key, method));
  }
  else {
    throw new Error('Algorithm type not recognized');
  }
}

function _sign(input, key, method) {
  return crypto.createHmac(method, key).update(input).digest('hex');
}

function handler(event) {
  var request = event.request;

  //Secret key used to verify token.
  //Update with your own key.
  var key = "penguin";
  var signingMethod = 'sha256';
  var signingType = 'hmac';

  // If no token, then generate HTTP redirect 401 response.
  if (!request.querystring.s) {
    console.log("Error: No token in the querystring");
    return response401;
  }

  var fwdUri = event.request.uri;
  var token = request.querystring.s.value;

  try {
    verifyRequest(fwdUri, token, key, signingMethod, signingType);
  }
  catch (e) {
    console.log(e);
    return response401;
  }

  //Remove the token from the query string if valid and return.
  delete request.querystring.s;
  console.log("Valid token");
  return request;
}
```

### リクエストのテスト用ハッシュ作成
以下の内容で作成します。

参照S3：/images/sample.jpg
secret：penguin

```php: makeHash.php
<?php
    $s3Path = "/images/sample.jpg";
    $hased_string = hash_hmac('sha256', $s3Path, 'penguin');
    print_r($hased_string.PHP_EOL);
```

```bash
$ php makeHash.php
fcf3f9b449720bfc1fd32f8fe3a2d0cb72ebe14e6ea86cb7d6fdc60e4b7dd521
```




## CloudFront Functionsの作成

<details><summary>チュートリアルの通りに設定します。</summary>

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-tutorial.html

1. マネジメントコンソールからCloudFrontを開きます。
https://console.aws.amazon.com/cloudfront/v3/home

1. [関数を作成]ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/11193fc8-b583-6576-a69a-210824052f39.png)

1. 関数名を入力し、[関数を作成]ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/296ee60b-ba8f-a1e4-04e2-b3c5026ada39.png)

1. 作成された関数の画面に遷移します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/610bde3d-a8a1-0cce-233c-c775e4e6d2f2.png)

1. 構築タブをクリックし、開発のテキストにコードを貼り付けます。
1. [変更を保存]ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/9a439711-77bf-e918-7185-e1ad72a89f75.png)

1. 正常に発行されました。のメッセージが表示されます。
発行タブをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/ebe873db-0023-0a2e-e505-e0fce2cee0bc.png)

1. 関連付けられているディストリビューション欄の[関連付けを追加]ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/1d987841-b101-6166-ffe7-22f5fbdfabcd.png)

1. CloudFrontディストリビューションと紐づけます。
イベントタイプは「Viewer Request」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/bf239155-937c-f3b4-6369-9a5bc90b0087.png)

1. 紐づけ成功のメッセージが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/c9a0f250-9021-29f0-2abe-5953a721676a.png)

1. 関数の反映を待ちます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/2b5dfcea-43ca-75c3-d8a5-f64ff2b00fc2.png)
反映されるとステータスが「デプロイ済み」になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/686f0a74-6276-f911-a7f5-49a85616f535.png)

</details>

## 実行結果
### 正しくないハッシュ値の場合
s=hogeなどを渡すと、指定した401エラーが返却されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/f0c0a408-71ca-0551-a1a4-3f943007a607.png)

### 正しいハッシュ値を渡した場合
画像が表示されました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177998/47d19a21-069d-0bcc-8c95-0e6659b68205.png)

## まとめ
初めてCloudFront Functionsを触りましたが、Lambda@Edgeよりは簡単に実装できました。
例えば指定したサイズを含むハッシュ値を作成し、[画像のリサイズ](https://aws.amazon.com/jp/blogs/news/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/)処理の前に検証すると良さそうです。
サイズを微妙に変更したリクエストが大量に届いても、Lambda@Edgeの実行前に制限できますね。

制限もありますので、[クオータ](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-functions)や[紹介ページ](https://aws.amazon.com/jp/blogs/news/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/
)で確認しておきましょう！

P.S.ヘッダに認証キーを設定する方法が書かれている記事を見つけました。
クエリでハッシュ値を渡すよりおしゃれです。

https://www.infiniteloop.co.jp/tech-blog/2022/12/sounds-good-cloudfront-functions-use-case/


## 参考資料
https://aws.amazon.com/jp/blogs/news/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html#writing-functions-javascript-features-builtin-modules-crypto

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/functions-tutorial.html

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/example-function-validate-token.html

https://aws.amazon.com/jp/blogs/news/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/