Amazon Web Services ブログ
CloudFront Functions を使用したコンテンツの保護
AWS の高速コンテンツ配信ネットワーク(CDN)サービスである Amazon CloudFront から配信されるビデオコンテンツを保護するには、署名付き Cookieと署名付き URL の 2つの方法があります。 お客様はユースケースに応じて、どちらか一方または両方を使用することができます。
例えば HTTP Live Streaming(HLS)のセキュアなビデオ再生のために、メインマニフェスト、サブマニフェスト、トランスポートストリームセグメントを含む、全てのコンテンツに権限を与えるワイルドカード文字を持つリソース URL を承認するために、署名付き Cookie を選択することができます。
一方、クライアントが Cookie をサポートしていない、または Cookie を無効にしている場合、お客様は署名付き URL に頼ることになりますが、認証されるためには各ビデオセグメントの URL に署名が必要になります。このため HLSのマニフェスト操作では、ビデオ再生時に各 URL にセキュリティ署名を入れる必要があり、実装上のオーバーヘッドやパフォーマンスの低下を招いてしまいます。
これらの機能に加えて、お客様は CloudFront Functions を使用してカスタムセキュリティワークフローを作成することもできます。これは、大規模で遅延の影響を受けやすい CDN のカスタマイズのために、JavaScript で軽量な関数を書くことができる機能です。有効期限、リソース、クライアントの IP アドレスなどの限定された属性を使用する、署名付き Cookie や署名付き URL の方法とは異なり、CloudFront Functions を使用したカスタムワークフローでは、セキュリティ署名に好みのプロパティ(ユーザーエージェントなど)を含める機能を提供し、配信中のビデオコンテンツの全体的なセキュリティを強化します。また、セキュリティ手法を統一することで、クライアントの能力に応じて適切な手法を選択する必要がありません。
この記事では、CloudFront Functionsを使用してカスタムセキュリティワークフローを構築し、不正な外部の脅威からビデオコンテンツを保護する手順について説明します。
概略図
手順
・リージョンの選択:このソリューションではAmazon API Gateway、AWS Lambdaを利用します。以降の手順では同じリージョンになるように選択してください
・Key Management Service (KMS)/カスタマーマスターキーの登録:このソリューションでは AWS Lambda を利用したカスタムオーソライザーを利用しますので、KMS カスタマーマスターキーを利用します。KMS コンソールなどから事前に作成しておく必要があります
AWS Lambda を使用してカスタムオーソライザーの関数を作成する
このステップでは AWS Lambda のコンソールを使用します。AWS Lambda はサーバーレスコンピューティングサービスのため、お客様はサーバーのプロビジョニングや管理、ワークロード対応のクラスタースケーリングロジックの作成、イベント統合の維持、ランタイムの管理などを行わずにコードを実行できます。
1. AWS Lambda コンソールを開きます
2. ナビゲーションペインで「Functions」を選択します
3. 「Create function」を選択します
4. Basic Information で Function name を「custom-authorizer」と入力します
5. Runtime は「Node.js 14.x」を選択します
6. 「Create function」を選択します
7. Code タブで「js」をダブルクリックします
8. 次のコードスニペットをコピーし、エディタセクションに表示されている既存のコードを置き換えます
カスタムオーソライザー Lambdaのコードスニペット:
1. const AWS = require('aws-sdk');
2. const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
3. const secret = process.env['SECRET'];
4. let decrypted_secret;
5.
6. function inHouseAuthCheck(event) {
7. //Perform your in house authorization
8. return true;
9. }
10.
11. function processEvent(event) {
12. console.log(event);
13. //Perform in house auth continue only if it succeeds.
14. if (!inHouseAuthCheck(event)) {
15. const response = {
16. statusCode: 500,
17. body: JSON.stringify({"Error": "In House Authorization Failed."}),
18. };
19. return response;
20. }
21. //Parse the body for the information; say in this case PlaybackURL.
22. var url = event['body-json']['playback_url'];
23. var expires_duration = 86400;
24. var d = new Date();
25. var expires = Math.floor(d.getTime() / 1000) + expires_duration;
26. var url = new URL(url);
27. var hostname = url.hostname;
28. var path = url.pathname;
29. //Including wildcard to support ABR playback
30. var manifestname = path.split("/").pop();
31. path = path.replace(manifestname, "*");
32.
33. //Stream key Generation using properties (Client IP, URL path, Hostname, Expires and User Agent)
34. var client_ip = event["params"]["header"]["X-Forwarded-For"];
35. var user_agent = event["params"]["header"]["User-Agent"];
36. var customdata = {"client_ip": client_ip, "path": path, "hostname": hostname, "expires": expires, "user_agent": user_agent};
37. console.log("Custom Data => " + JSON.stringify(customdata));
38. var crypto = require('crypto');
39. var secret = decrypted_secret;
40. var stream_hash = crypto.createHmac('sha256', secret);
41. var stream_key = stream_hash.update(JSON.stringify(customdata)).digest('base64');
42. console.log("Stream Key => " + stream_key);
43.
44. //Stream policy Generation using reconstructed URL Path and Expires
45. var stream_policy_json = {
46. "url": path,
47. "expires": expires
48. };
49. var stream_policy_str = new Buffer(JSON.stringify(stream_policy_json));
50. var stream_policy = stream_policy_str.toString('base64');
51. console.log("Stream Policy =>" + stream_policy);
52.
53. //Stream Key response
54. const response = {
55. statusCode: 200,
56. body: {"stream_key": stream_key, "stream_policy": stream_policy},
57. };
58. return response;
59. };
60.
61. exports.handler = async (event) => {
62. if (!decrypted_secret) {
63. // Decrypt code should run once and variables stored outside of the
64. // function handler so that these are decrypted once per container
65. const kms = new AWS.KMS();
66. try {
67. const req = {
68. CiphertextBlob: Buffer.from(secret, 'base64'),
69. EncryptionContext: {LambdaFunctionName: functionName},
70. };
71. const data = await kms.decrypt(req).promise();
72. decrypted_secret = data.Plaintext.toString('ascii');
73. } catch (err) {
74. console.log('Decrypt error:', err);
75. throw err;
76. }
77. }
78. return processEvent(event);
79. };
9. 「Deploy」を選択します
10. 「Configuration」を選択します
11. Environment variables(環境変数)で「Edit」を選択します
12. 「Add environment variable」を選択します
13. 「SECRET」という名前のキーと任意の値を入力します。キー値はアプリケーションでのみ認識される 32 文字以上のランダム化された文字列にする必要があります。後で CloudFront Functions に組み込むためキー値は安全に保管します
14. Encryption configuration(暗号化設定)を開きます。
15. 「Enable helpers for encryption in transit」(転送中の暗号化のヘルパーを有効にする)を選択します
16. 変数の横にある 「Encrypt」を選択して、その値を暗号化します
17. 「Save」を選択します
18. AWS Lambda ロールに対して AWS Key Management Service (AWS KMS)(暗号化キーを作成、管理するための安全でレジリエントなサービス)を使用した復号化を許可するために、次のポリシーを追加します
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "<<INSERT THE ARN OF YOUR KMS KEY FOR LAMBDA HERE>>"
}
]
}
Amazon API Gateway にカスタムオーソライザーの Lambda 関数を関連付け
このステップでは Amazon API Gateway のコンソールを使用します。Amazon API Gateway は、デベロッパーが規模にかかわらず簡単に API を作成、公開、保守、監視、保護することができるフルマネージド型のサービスです。
1. API gatewayを作成する
a) Amazon API Gateway コンソールを開きます
b) 「Create API」を選択します
c) 「REST API」から「Build」を選択します
d) API name(「secure-video」など)を入力します
e) 任意の Description(詳細説明)を入力します
f) Endpoint Type(「Regional」など)を選択します
g) 「Create API」を選択します
2. リソースの作成
a) Amazon API Gateway ナビゲーションペインで「Resources」を選択します
b) Actions で「Create Resource」を選択します
c) Resource Name(「getStreamKey」など)を入力します
d) 「Create Resource」を選択します
3. メソッドを作成して Lambda 関数を関連付ける
a) 「/getStreamKey」リソースを選択します
b) Actions で「Create Method」を選択します
c) Resource の下のドロップダウンリストオプションから「GET」メソッドを選択します
d) チェックボックスを選択します
e) Setup の Integration type(統合タイプ)にて Lambda Function を選択します
f) Lambda Function に「custom-authorizer」を入力します
g) 「Save」を選択します
h) Integration Request の「Mapping Templates」セクションを選択します
i) 「Add mapping template」を選択し、Content-Type セクションで application/json を使用します
j) テンプレートのドロップダウンから、「Method Request passthrough」テンプレートを選択します
k) 「Save」を選択します
4. APIをデプロイする
a) Actions で「Deploy API」を選択します
b) Deployment stage に対して「New Stage」オプションを選択します
c) 任意のステージ名 (「secure-video」など) を入力します
d) 「Deploy」を選択します
セキュアストリームキーを生成する
認証されたユーザーが再生したいビデオコンテンツを選択すると、クライアントはセキュアストリームキーの生成を要求します。クライアントは Amazon API Gateway を介してカスタムオーソライザー Lambda 関数にリクエストを委ね、ストリームキーが特定の推奨プロパティに従って生成されます。さらに、ビデオ再生のセキュリティを強化するために、任意の特定のプロパティを追加で含めることができます。例えば購読しているユーザーに生成される認証トークン、クエリ文字列、配信識別子などです。
1. クライアントIP
a) クライアント IP は HTTP リクエストヘッダーの「X-FORWARDED-FOR」から抽出されます。これはリクエストの発信元である IP アドレスを特定するための一般的な方法です
b) このプロパティを含む場合、要求されたクライアントのみが映像コンテンツにアクセスできるようになります
2. ユーザーエージェント
a) ユーザーエージェントは、HTTP リクエストヘッダーの「User-Agent」から抽出されます
このフィールドはリクエスト元のクライアントのアプリケーション、オペレーティングシステム、ベンダー、バージョンを識別します
b) このプロパティを含む場合、異なるユーザーエージェントを持つデバイスでビデオが再生されるのを防ぐことができます
3. リソース URL パス
a) ストリームキーを使用して承認が必要となるリソースの URL パスを指定します
この URL はプログレッシブ再生を行う場合は、目的のアセットを指す完全な URL パス、または HTTP ストリーミングに必要なすべてのリソース(マニフェスト、サブマニフェスト、セグメントなど)を許可する場合には(*)を付けた URL パスにすることができます
b) このプロパティを含む場合、指定したリソースセットのみが、ストリームキーによって許可されることになります
4. 有効期限
a) ストリームキーの有効期限を決定します
これはストリームキーの有効期間を決定するために必要な値です
b) このプロパティを含めることで、生成されたストリームキーは一定期間だけ有効となり、有効期限が過ぎると使用できなくなります
5. ホスト名
a) ホスト名は、リクエストが送信されるサーバーを指定するリクエストヘッダーの「host」属性から抽出されます
b) このプロパティは、コンテンツが確実に意図したオリジン(ホスト名)から再生されることを確認します
以下のように、全てのプロパティを含む JSON を作成します。
(プロトコル、ポートなど他のプロパティを含めることもできます)
81. //Stream key Generation using properties (Client IP, URL path, Hostname, Expires and User Agent)
82. var client_ip = event["params"]["header"]["X-Forwarded-For"];
83. var user_agent = event["params"]["header"]["User-Agent"];
84. var customdata = {"client_ip": client_ip, "path": path, "hostname": hostname, "expires": expires, "user_agent": user_agent};
85. console.log("Custom Data => " + JSON.stringify(customdata));
信頼されたシークレット(Trusted secret)は、下記のように AWS KMS からフェッチされます。
86. if (!decrypted_secret) {
87. // Decrypt code should run once and variables stored outside of the
88. // function handler so that these are decrypted once per container
89. const kms = new AWS.KMS();
90. try {
91. const req = {
92. CiphertextBlob: Buffer.from(secret, 'base64'),
93. EncryptionContext: {LambdaFunctionName: functionName},
94. };
95. const data = await kms.decrypt(req).promise();
96. decrypted_secret = data.Plaintext.toString('ascii');
97. } catch (err) {
98. console.log('Decrypt error:', err);
99. throw err;
100. }
101. }
SHA256 アルゴリズムを使用してセキュアストリームキーを作成します。
102. var crypto = require('crypto');
103. var secret = decrypted_secret;
104. var stream_hash = crypto.createHmac('sha256', secret);
105. var stream_key = stream_hash.update(JSON.stringify(customdata)).digest('base64');
106. console.log("Stream Key => " + stream_key);
クライアントが認識していないプロパティや、ストリームキー生成の一部として使用されたプロパティは、シンプルな JSON base64 でエンコードされた文字列でストリームポリシーとして共有されます。例えば、有効期限の決定やパスの変更がクライアントに認識されていない場合、その情報はストリームポリシーの一部としてクライアントに提供され、後に検証で使用することができます。
107. //Stream policy Generation using reconstructed URL Path and Expires
108. var stream_policy_json = {
109. "url": path,
110. "expires": expires
111. };
112. var stream_policy_str = new Buffer(JSON.stringify(stream_policy_json));
113. var stream_policy = stream_policy_str.toString('base64');
114. console.log("Stream Policy =>" + stream_policy);
カスタムオーソライザーからのストリームキー応答は、ストリームキーとストリームポリシー情報を JSON 形式でカプセル化したものです。
115. //Stream Key response
116. const response = {
117. statusCode: 200,
118. body: {"stream_key": stream_key, "stream_policy": stream_policy},
119. };
CloudFront Functions の準備
1. シンプルな CloudFront Functions を作成します。
a) Amazon CloudFront コンソールで Functions のページを開きます
b) 「Create function」を選択します
c) 関数名(「secure-streams」など)を入力し「Create function」を選択します
d) 次のコードを Function code セクションにコピーします
注意:この関数を使用するには、関数コードにシークレットキーを入れる必要があります。
1. function handler(event) {
2. // NOTE: This example function is for a viewer request event trigger.
3. // Choose viewer request for event trigger when you associate this function with a distribution.
4.
5. //Importing Crypto for stream key validation
6. var crypto = require('crypto');
7. //Secret Key that is used; this is known to only custom authorizer and CloudFront function.
8. var secret = "*<<INSERT THE KEY VALUE USED IN YOUR CUSTOM-AUTHORIZER LAMBDA FUNCTION HERE>>*";
9.
10. console.log(event.request);
11. var request = event.request;
12. var headers = request.headers;
13. var hostname = headers.host.value;
14. var user_agent = headers["user-agent"].value;
15. console.log("User Agent => "+ user_agent);
16. var uri = request.uri;
17. var client_ip = event.viewer.ip;
18.
19.
20. var stream_key = headers.stream_key.value;
21. var stream_policy = headers.stream_policy.value;
22. var stream_policy_decoded = String.bytesFrom(stream_policy, 'base64');
23. var stream_policy = JSON.parse(stream_policy_decoded);
24. var expires = stream_policy["expires"];
25. var path = stream_policy["url"];
26.
27. //Construct Stream key and validate
28. var customdata = {"client_ip": client_ip, "path": path, "hostname": hostname, "expires": expires, "user_agent": user_agent};
29. console.log(JSON.stringify(customdata));
30. var srv_hmac_key = crypto.createHmac('sha256', secret);
31. var srv_stream_key = srv_hmac_key.update(JSON.stringify(customdata)).digest('base64');
32. console.log(srv_stream_key);
33. if (stream_key != srv_stream_key) {
34. var response = {
35. statusCode: 500,
36. statusDescription: 'HMAC Key Mismatched!!',
37. }
38. return response;
39.
40. }
41. //Add the true-client-ip header to the incoming request
42. request.headers['true-client-ip'] = {value: client_ip};
43.
44. var current_time = Math.round(Date.now() / 1000);
45. console.log("current time "+ current_time);
46. console.log("expires "+ expires);
47. if(current_time > expires) {
48. var response = {
49. statusCode: 500,
50. statusDescription: 'time expired!!',
51. }
52. return response;
53. }
54. var path_check = path.replace("/", "\\/").replace("*", ".*");
55. var regex_str = new RegExp(path_check);
56. if(!regex_str.test(path)) {
57. var response = {
58. statusCode: 500,
59. statusDescription: 'URL pattern Mismatched!!',
60. }
61. return response;
62. }
63. console.log("All Validation Succeeded!!. Good to Go!");
64. return event.request;
65. }
66.
e) CloudFront Functions のコードをテストします
2. CloudFront Functions を公開します
a) Functions のページで「Publish」タブを選択し、「Publish function」を選択します
b) 正常に公開されるとページの上部に「Successfully published secure-streams.」というバナーが表示されます
3. CloudFront Functions を関連付けします
a) CloudFront Functions が正常に公開されたら下の「Add association」を選択して、関数を Amazon CloudFront ディストリビューションに関連付けます
Distribution では関数に関連付けるディストリビューションを選択します
Event Type では「Viewer Request」を選択し、CloudFront が再生リクエストを受信する度に関数を実行できるようにします
Cache behavior では、この関数に関連付けるキャッシュ動作を選択します(デフォルト動作には「Default(*)」を選択します)
例えば HLS マニフェストに対してのみセキュアストリーム関数を呼び出したい場合は、Cache behavior で「*.m3u8」を選択します
「Add Association」を選択します
再生中にセキュアストリームキーを関連付ける
再生に使用されるクライアントは、ビデオ再生に関連するすべてのリクエストのヘッダーの一部にストリームキーを含める必要があります。再生中にヘッダーを入れる方法は、すべてのビデオプレーヤー(再生クライアント)において異なります。以下 2 つの例について説明します。
1. JW Player でカスタムヘッダーを追加する方法については、こちらのリンクを参照してください。
2. ExoPlayer でカスタムヘッダーを追加する方法については、こちらのリンクを参照してください。
セキュアストリームキーの検証
Amazon CloudFront ディストリビューションに対して設定された CloudFront Functions は、再生リクエスト中に、リクエストヘッダーとして提供されるストリームキーとストリームポリシーを受け取ります。CloudFront Functions はカスタムオーソライザーによって使用されたのと同じプロパティと、Trusted shared secret(信頼された共有シークレット)を使用してストリームキーを生成します。その情報が標準ヘッダーに含まれない場合、CloudFront Functions はストリームポリシーを使用して、ストリームキー生成プロセスと整合するようプロパティを選択します。
67. JavaScript
68. var hostname = headers.host.value;
69. var user_agent = headers["user-agent"].value;
70. console.log("User Agent => "+ user_agent);
71. var uri = request.uri;
72. var client_ip = event.viewer.ip;
73.
74.
75. var stream_key = headers.stream_key.value;
76. var stream_policy = headers.stream_policy.value;
77. var stream_policy_decoded = String.bytesFrom(stream_policy, 'base64');
78. var stream_policy = JSON.parse(stream_policy_decoded);
79. var expires = stream_policy["expires"];
80. var path = stream_policy["url"];
81.
82. //Construct Stream key and validate
83. var customdata = {"client_ip": client_ip, "path": path, "hostname": hostname, "expires": expires, "user_agent": user_agent};
84. console.log(JSON.stringify(customdata));
CloudFront Functions によって生成されたストリームキーが、クライアントから提供されたストリームキーと一致する場合にのみ、リクエストのパススルーを認証します。ストリームキーが一致しない場合は、HTTP ステータスコード 500 を返します。
85. if (stream_key != srv_stream_key) {
86. var response = {
87. statusCode: 500,
88. statusDescription: 'HMAC Key Mismatched!!',
89. }
90. return response;
91.
92. }
フローをテストする
1. ストリームキーを取得:
curl --location --request GET 'https://<<API Gateway Endpoint>>/secure-video/getstreamkey' --header 'Content-Type: application/json' --data-raw '{ "playback_url": "https://<<CloudFront Endpoint>>/transcoded-out/high/1.m3u8"}'
2. ストリームキー応答の例:
{"statusCode":200,"body":{"stream_key":"Zpz8a2d+NbOA3dVzFLTzBgfvO9NfEEpluAT+ny9cEKo=","stream_policy":"eyJ1cmwiOiIvdHJhbnNjb2RlZC1vdXQvaGlnaC8qIiwiZXhwaXJlcyI6MTYzNzExMDYxNX0="}}%
3. ストリームキーとストリームポリシーを使用したマニフェストのリクエスト:
curl -v GET "https://<<CloudFront Endpoint>>/transcoded-out/high/1.m3u8" --header 'stream_key: Zpz8a2d+NbOA3dVzFLTzBgfvO9NfEEpluAT+ny9cEKo=' --header 'stream_policy: eyJ1cmwiOiIvdHJhbnNjb2RlZC1vdXQvaGlnaC8qIiwiZXhwaXJlcyI6MTYzNzExMDYxNX0='
1. #EXTM3U
2. #EXT-X-VERSION:3
3. #EXT-X-INDEPENDENT-SEGMENTS
4. #EXT-X-STREAM-INF:BANDWIDTH=2820417,AVERAGE-BANDWIDTH=2779558,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25.000
5. 11080/1.m3u8
6. #EXT-X-STREAM-INF:BANDWIDTH=1854376,AVERAGE-BANDWIDTH=1816108,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=1080x720,FRAME-RATE=25.000
7. 1720/1.m3u8
8. #EXT-X-STREAM-INF:BANDWIDTH=1185653,AVERAGE-BANDWIDTH=1178677,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=1024x580,FRAME-RATE=25.000
9. 1480/1.m3u8
4. 同じストリームキーとストリームポリシーを使用したサブマニフェストのリクエスト:
curl -v GET "https://<<CloudFront Endpoint>>/transcoded-out/high/11080/1.m3u8" --header 'stream_key: txn6IZLhfnoGBcNKUc56Mu6zAwjL8nrUve5yuKgWg44=' --header 'stream_policy: eyJ1cmwiOiIvdHJhbnNjb2RlZC1vdXQvaGlnaC8qIiwiZXhwaXJlcyI6MTYyODQ0NTczMH0='
1. #EXTM3U
2. #EXT-X-VERSION:3
3. #EXT-X-TARGETDURATION:11
4. #EXT-X-MEDIA-SEQUENCE:1
5. #EXT-X-PLAYLIST-TYPE:VOD
6. #EXTINF:11,
7. 1_00001.ts
8. #EXTINF:11,
9. 1_00002.ts
10. #EXTINF:8,
11. 1_00003.ts
12. #EXT-X-ENDLIST
5. 同じストリームキーとストリームポリシーを使用したセグメントのリクエスト
curl -v GET “https://d326kci5pvyw99.cloudfront.net/transcoded-out/high/11080/1_00001.ts” –header ‘stream_key: txn6IZLhfnoGBcNKUc56Mu6zAwjL8nrUve5yuKgWg44=’ –header ‘stream_policy: eyJ1cmwiOiIvdHJhbnNjb2RlZC1vdXQvaGlnaC8qIiwiZXhwaXJlcyI6MTYyODQ0NTczMH0=’
まとめ
この記事では、ビデオ配信用のカスタムセキュリティワークフローを CloudFront Functionsと API Gateway, Lambda を使用して構築する方法を紹介しました。また、お客様がセキュリティワークフローのビルドを迅速に進められるよう、詳細な設定手順とサンプルコードスニペットを提供するとともに、ビデオ配信において高まるセキュリティニーズに対応できるよう、メディアワークフローをカスタマイズする柔軟性について詳しく説明しました。
AWS はメディアワークフローの構築を支援するために設計されたサービスを多数提供し続けています。AWS サービスを使用したビデオストリーミング、処理、配信について、さらに詳しくご覧になりたい場合は、AWSでのメディアサービスを参照してください。また、ビデオコンテンツのグローバル配信については Amazon CloudFront をご覧ください。コンテンツ配信のエッジでカスタムロジックを追加したい場合は CloudFront Functions を参照してください。
参考リンク
AWS Media Services
AWS Media & Entertainment Blog (日本語)
AWS Media & Entertainment Blog (英語)
AWSのメディアチームの問い合わせ先: awsmedia@amazon.co.jp
※ 毎月のメルマガをはじめました。最新のニュースやイベント情報を発信していきます。購読希望は上記宛先にご連絡ください。
翻訳は SA 森が担当しました。原文はこちらをご覧ください。