AWS SDK for Javascript. v3でCloudFrontの複数のファイルを対象とした署名付きCookieを生成する

2022.12.09

こんにちは、CX事業本部 Delivery部の夏目です。

個人的なプロダクトにおいて、完全Publicな状態で置きたくないものをCloudFrontで公開する必要が出てきました。
そのため、署名付きCookieを使うことにしたのですが、Cookieの生成時に少し問題が発生したので共有します。

CloudFrontの署名付きCookieとは

CloudFrontにおいてプライベートコンテンツを配信する際に署名がないアクセスを全てブロックする機能があります。

署名付きURLか署名付きCookieのどちらかを使用する必要があるのですが、署名付きCookieの方が自由度が非常に高いです。
IP制限とか、アクセスを許可するパス(ファイル)をワイルドカードで指定したりとかできます。

私の場合は複数のファイルを扱う必要があるので、署名付きCookieを使うことにしました。

基本的な情報は次の記事にあります。

CloudFrontの署名付きCookieでプライベートコンテンツの配信

端的に書くと、次のことを行います。

  1. 秘密鍵を生成する
  2. 秘密鍵から公開鍵を生成する
  3. 公開鍵をCloudFrontに登録し、PublicKeyIdを取得する
  4. 登録した公開鍵をCloudFrontのKeyGroupに登録する
  5. 必要ならカスタムポリシーを書く (IP制限とかパスのワイルドカードとか使うなら必要)
  6. 秘密鍵(とカスタムポリシー)を使って署名を行う

ドキュメントに従いAWS SDK for Javascript v3で生成してみる

AWS SDK for Javascript v3には @aws-sdk/cloudfront-signerというものがあり、簡単に署名付きURLや署名付きCookieを生成することができます。

ドキュメントにはサンプルコードがあったので、参考に署名付きCookieを生成してみた。

00_base_document.js

const { getSignedCookies } = require("@aws-sdk/cloudfront-signer");
const fs = require('fs');

const cloudfrontDistributionDomain = "https://d111111abcdef8.cloudfront.net";
// ワイルドカードで複数のファイルを見られるようにした
const s3ObjectKey = "private-content/*";  
const url = `${cloudfrontDistributionDomain}/${s3ObjectKey}`;
// Private Keyをopensslで生成したものを使用するようにした
const privateKey = fs.readFileSync("private_key.pem")
const keyPairId = "PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR";
const dateLessThan = "2022-01-01";

const cookies = getSignedCookies({
  url,
  keyPairId,
  dateLessThan,
  privateKey,
});

console.log(cookies);
$ openssl genrsa -out private_key.pem 2048
$ node 00_base_document.js
{
  'CloudFront-Key-Pair-Id': 'PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR',
  'CloudFront-Signature': 'XfjWArwMalXWKiyHhsmL9qTXsvp-H0~3mur20o677l7fZA~5FQ1GmDfcvgnDSdiVPAqk3XaMgMjjnMvf1EHbAPpr8-kZKboj2ZjJlTWmSiSPnSR2WsfskmzoYpZmgUqW36SNZpj63JxDkNV~Y~v7N7UvyoK68tiivuwN7p1OmTAkbDdt66shUnHwrAT-8pgPEq4tl3kVu~KqkF3COJ1i28EHxd2JKSy9p6P4aR173aLUKvGwrFzbTQxlSVuQz6XDl4v-b45N9brDwDcty797JN1-1GgttY3eUoFZqH~KLdMXW~qMTrsifrtQlbV-c1q3rAGoj2Q3tcSu4EnqP6bHGw__',
  'CloudFront-Expires': 1640995200
}

Cookieの値が生成されましたが、実はワイルドカードを使用した署名付きCookieとしては使用できません。

対処法

正しくはないかもしれないが、すぐにできる対処法はdateGreaterThanを指定する方法です。
これを十分に古い値にすることで、とりあえずの対処法になります。

01_custom.py

const { getSignedCookies } = require("@aws-sdk/cloudfront-signer");
const fs = require('fs');

const cloudfrontDistributionDomain = "https://d111111abcdef8.cloudfront.net";
// ワイルドカードで複数のファイルを見られるようにした
const s3ObjectKey = "private-content/*";  
const url = `${cloudfrontDistributionDomain}/${s3ObjectKey}`;
// Private Keyをopensslで生成したものを使用するようにした
const privateKey = fs.readFileSync("private_key.pem")
const keyPairId = "PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR";
const dateLessThan = "2022-01-01";
const dateGreaterThan = "2000-01-01";

const cookies = getSignedCookies({
  url,
  keyPairId,
  dateLessThan,
  dateGreaterThan,
  privateKey,
});

console.log(cookies);
$ node 01_custom.py
{
  'CloudFront-Key-Pair-Id': 'PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR',
  'CloudFront-Signature': 'eHcd4gO2OiKL8Iy9~BP7XAi19n9Hk7HDO367FBZua-6G4sFG5~CVTyfEO4P3ce0VJI6BJ2u-AwaEJ5TsoGd3d7QSYf-xk2hkxEZbcnELK86gebzd~bAjrG4FvrxmtsD-SxfKvjJxcPVy3f5q~z6rPy-fuGXRjFaR-zccfZo5zzaFr8EIEZdmSFClbRvaII7cGcUaGyBMRb3hl~B9dSgJ2eRBmHhINujQkgmb1JjcVSjlWFIGcfdVzhv7t9NLF9wLtd7qouilNkr~ka3uvzVXdi5vueDKvEcA6WtIssDixd1dViayAPXNIMFrLrU6EbBCMu1VpT92BqQL559NdySDEQ__',
  'CloudFront-Policy': 'eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kMTExMTExYWJjZGVmOC5jbG91ZGZyb250Lm5ldC9wcml2YXRlLWNvbnRlbnQvKiIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTY0MDk5NTIwMH0sIkRhdGVHcmVhdGVyVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjo5NDY2ODQ4MDB9fX1dfQ__'
}

dateGreaterThanを指定することで、カスタムポリシーのCookieと認識してくれます。

URLにワイルドカードを指定するだけじゃだめだった理由

署名付きCookieの規定ポリシーとカスタムポリシー

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html#private-content-choosing-canned-custom-cookies

署名付きURLでは、規定ポリシーとカスタムポリシーの二種類があります。

有効期限と特定の一つのファイルだけアクセスできる場合は規定ポリシーが使用されます。
規定ポリシーの場合には次の3つのCookieが必要です。

  • CloudFront-Expires
  • CloudFront-Signature
  • CloudFront-Key-Pair-Id

複数のファイルを対象にしたりする場合にはカスタムポリシーを使用します。 カスタムポリシーでは次の3つのCookieが必要です。

  • CloudFront-Policy
  • CloudFront-Signature
  • CloudFront-Key-Pair-Id

最初のJSを実行したときの結果を見ると、規定ポリシーのCookieとして出力されていることがわかります。

getSignedCookies()

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_cloudfront_signer.html#getsignedcookies-1

getSignedCookies()ではカスタムポリシーでしか制限できない内容も引数として渡すことができます。
このドキュメントを見る限りだと、なんでダメだったのかわかりません。

ソースコードを読んでみると、CloudfrontSignBuilderというクラスで署名を行っていることがわかります。

https://github.com/aws/aws-sdk-js-v3/blob/208c93b0da339330ddcd9198f24f9016ac154be9/packages/cloudfront-signer/src/sign.ts#L119-L122

https://github.com/aws/aws-sdk-js-v3/blob/208c93b0da339330ddcd9198f24f9016ac154be9/packages/cloudfront-signer/src/sign.ts#L133-L143

CloudfrontSignBuildersetPolicyParameters()を読むと、dateGreaterThanipAddressの両方もしくは片方を指定するか、policyを直接渡すかしないとカスタムポリシーと解釈してくれないみたい。

https://github.com/aws/aws-sdk-js-v3/blob/208c93b0da339330ddcd9198f24f9016ac154be9/packages/cloudfront-signer/src/sign.ts#L345-L371

本来、URLにワイルドカードを示す文字列が入っていればカスタムポリシーが必要になるが、それは考慮していないことがわかります。

対処法

カスタムポリシーと解釈される必要があるのでdateGreaterThanを指定するのが一番楽な気がします。
十分に古い値にしておけば問題ないと思われるので。

01_custom.py

const { getSignedCookies } = require("@aws-sdk/cloudfront-signer");
const fs = require('fs');

const cloudfrontDistributionDomain = "https://d111111abcdef8.cloudfront.net";
// ワイルドカードで複数のファイルを見られるようにした
const s3ObjectKey = "private-content/*";  
const url = `${cloudfrontDistributionDomain}/${s3ObjectKey}`;
// Private Keyをopensslで生成したものを使用するようにした
const privateKey = fs.readFileSync("private_key.pem")
const keyPairId = "PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR";
const dateLessThan = "2022-01-01";
// これを指定することでカスタムポリシーと解釈される
const dateGreaterThan = "2000-01-01";

const cookies = getSignedCookies({
  url,
  keyPairId,
  dateLessThan,
  dateGreaterThan,
  privateKey,
});

console.log(cookies);
$ node 01_custom.py
{
  'CloudFront-Key-Pair-Id': 'PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR',
  'CloudFront-Signature': 'eHcd4gO2OiKL8Iy9~BP7XAi19n9Hk7HDO367FBZua-6G4sFG5~CVTyfEO4P3ce0VJI6BJ2u-AwaEJ5TsoGd3d7QSYf-xk2hkxEZbcnELK86gebzd~bAjrG4FvrxmtsD-SxfKvjJxcPVy3f5q~z6rPy-fuGXRjFaR-zccfZo5zzaFr8EIEZdmSFClbRvaII7cGcUaGyBMRb3hl~B9dSgJ2eRBmHhINujQkgmb1JjcVSjlWFIGcfdVzhv7t9NLF9wLtd7qouilNkr~ka3uvzVXdi5vueDKvEcA6WtIssDixd1dViayAPXNIMFrLrU6EbBCMu1VpT92BqQL559NdySDEQ__',
  'CloudFront-Policy': 'eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kMTExMTExYWJjZGVmOC5jbG91ZGZyb250Lm5ldC9wcml2YXRlLWNvbnRlbnQvKiIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTY0MDk5NTIwMH0sIkRhdGVHcmVhdGVyVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjo5NDY2ODQ4MDB9fX1dfQ__'
}

カスタムポリシーのCookieが生成されました。

まとめ

  • AWS SDK for Javascript v3において、 @aws-sdk/cloudfront-signergetSignedCookies()を使うとCloudFrontの署名付きCookieを簡単に生成できる
  • urlとpublicKeyId, privateKey, dateLessThanを指定すれば既定ポリシーの署名付きCookieを生成できる
  • dateGreaterThanとipAddressの両方もしくは片方を指定するか、policyを指定すればカスタムポリシーの署名付きCookieを生成する