SORACOM アドベントカレンダー 12/3 担当の Max こと松下です。
Amazon API Gateway の API 呼び出しの認証(エンドポイントセキュリティ)に x-api-key
のみを使う方法は、公式にも書いてある通り推奨されていません。
ここでは、Amazon API Gateway の Lambda オーソライザーで SORACOM Beam の署名検証を行う話です。
設定手順
AWS / Lambda 関数の作成
Lambda オーソライザーは設計図(Blueprint) に入っているので、これを流用します。今回は "api-gatewayauthorizer-python" を使います。(関数名は soracom_beam_sign_validator_func
としました)
Blueprint との diff は以下の通りです。
--- blueprint.py 2018-11-28 09:06:12.833433300 +0900
+++ soracom_beam_signature_validator.py 2018-11-28 09:06:00.735645400 +0900
@@ -2,10 +2,26 @@
import re
+import os
+SHARED_PSK = os.environ['SHARED_PSK']
def lambda_handler(event, context):
- print("Client token: " + event['authorizationToken'])
- print("Method ARN: " + event['methodArn'])
+ s = ''
+ for header in ['x-soracom-imei', 'x-soracom-imsi', 'x-soracom-timestamp']:
+ try:
+ s += '{}={}'.format(header, event['headers'][header])
+ except:
+ pass
+ print(s) # to CloudWatch Log
+
+ import hashlib
+ h = hashlib.sha256(SHARED_PSK + s).hexdigest()
+ print(h) # to CloudWatch Log
+ print(event['headers']['x-soracom-signature']) # to CloudWatch Log
+
+ if h != event['headers']['x-soracom-signature']:
+ raise Exception('Unauthorized')
'''
Validate the incoming token and produce the principal user identifier
@@ -15,7 +31,7 @@
2. Decode a JWT token inline
3. Lookup in a self-managed DB
'''
- principalId = 'user|a1b2c3d4'
+ principalId = s
'''
You can send a 401 Unauthorized response to the client by failing like so:
@@ -45,7 +61,8 @@
policy.restApiId = apiGatewayArnTmp[0]
policy.region = tmp[3]
policy.stage = apiGatewayArnTmp[1]
- policy.denyAllMethods()
+ #policy.denyAllMethods()
+ policy.allowAllMethods()
#policy.allowMethod(HttpVerb.GET, '/pets/*')
# Finally, build the policy
@@ -54,6 +71,7 @@
# new! -- add additional key-value pairs associated with the authenticated principal
# these are made available by APIGW like so: $context.authorizer.<key>
# additional context is cached
+ '''
context = {
'key': 'value', # $context.authorizer.key -> value
'number': 1,
@@ -63,6 +81,7 @@
# context['obj'] = {'foo':'bar'} <- also invalid
authResponse['context'] = context
+ '''
return authResponse
このコードでは事前共有鍵として利用する文字列を環境変数 SHARED_PSK
から読み出すようにしています。
値は任意ですが、後述する SORACOM の認証情報ストアに設定する文字列と同じにしてください。(例では _YOUR_SECRET_KEY_
としています)
その他、コード解説;
-
x-soracom-signature
を検証 -
principalId
は The principal user identification associated with the token sent by the client. ということなので、識別できる値として署名生成元文字列を入れています - 認証後の認可として allow all を返すようにしています
-
context
には何も入れないようにコメントアウトしています
テスト
{
"methodArn": "arn:aws:execute-api:ap-northeast-1:123456789012:example/prod/POST/{proxy+}",
"headers": {
"x-soracom-imsi": "440XXXXXXXXXX91",
"x-soracom-imei": "35XXXXXXXXXX195",
"x-soracom-timestamp": "1542029454636",
"x-soracom-signature": "21820f94db77f56c5d90c35b6fe06f64f185cb0133bf9b48d41ced4715c26ca7"
}
}
値を適当に編集して署名検証に失敗することも確認してください。
AWS / Amazon API Gateway のオーソライザーの設定
- 名前:
soracom_beam_sign_validator
- タイプ: Lambda
- Lambda 関数:
soracom_beam_sign_validator_func
(先ほど作ったやつ。リージョンまたぎ可能) - Lambda 呼び出しロール: (空で OK。後から付けてくれる)
- Lambda イベントペイロード: リクエスト
- ID ソース: (行を全て削除)
- 認証のキャッシュ: 無効
テスト
-
ヘッダー /
x-soracom-signature
21820f94db77f56c5d90c35b6fe06f64f185cb0133bf9b48d41ced4715c26ca7
-
ヘッダー /
x-soracom-timestamp
1542029454636
-
ヘッダー /
x-soracom-imsi
440XXXXXXXXXX91
-
ヘッダー /
x-soracom-imei
35XXXXXXXXXX195
値を適当に編集して署名検証に失敗することも確認してください。
API Gateway / メソッドリクエストの設定
メソッドリクエストの設定で 認証 に soracom_beam_sign_validator
を指定してください。
API Gateway それ以降の設定
残りは;
- 統合リクエストの設定
- 統合リクエストから呼び出される Lambda 関数の作成
- API のデプロイ
以上の作業となりますが、この辺は解説しません。ごめんなさい。
テスト
API のデプロイまで終われば curl 等で検証できます。
$ curl -v -X POST \
-H "x-soracom-signature: 21820f94db77f56c5d90c35b6fe06f64f185cb0133bf9b48d41ced4715c26ca7" \
-H "x-soracom-timestamp: 1542029454636" \
-H "x-soracom-imsi: 440XXXXXXXXXX91" \
-H "x-soracom-imei: 35XXXXXXXXXX195" \
https://as2eqedq7d.execute-api.ap-northeast-1.amazonaws.com/prod/beam
SORACOM / 認証情報ストアを作成
SORACOM 上で [セキュリティ] > [認証情報ストア] で 事前共有鍵 で認証情報を作成します。
- 認証情報 ID:
shared-psk
(任意) - 概要:
SORACOM Beam 署名生成用 共有鍵
(任意) - 種別: 事前共有鍵
- 事前共有鍵:
_YOUR_SECRET_KEY_
(任意ですが Lambda オーソライザーの環境変数に設定した値と同じ文字列にしてください)
SORACOM / SORACOM Beam の設定
基本的な設定手順は SORACOM Beam の設定 に記載されています。
ここでは SORACOM Beam の転送設定の内容を解説します。
HTTP エントリーポイントを選び、以下の設定をします。
- エントリーポイント
- 設定名:
API-Gateway
(任意) - パス:
/
(任意)
- 設定名:
- 転送先
- プロトコル: HTTPS
- ホスト名:
- ポート番号: (空)
- パス:
- ヘッダ操作
- IMSI ヘッダ: ON
- IMEI ヘッダ: ON
- 署名ヘッダ付与: ON
- 事前共有鍵: (認証情報ストアで作成したエントリーを選択)
以上です。
NOTE:
IMEI は環境によっては取得できない場合があります。その場合は IMSI だけでも問題ありません。(Lambda 関数側も対応できるようにコーディングされています)
テスト
このテストが最終です。
SORACOM Beam が有効になっている SORACOM Air SIM の回線から curl コマンドでテストできます。
$ curl -v -X POST beam.soracom.io:8888
いろいろ解説します
Amazon API Gateway の Lambda オーソライザー
Amazon API Gateway (以下、API Gateway) には Lambda オーソライザー※という、認証処理を AWS Lambda に実装する機能があります。
※カスタムオーソライザーと言われてました
Lambda オーソライザーを使う事で;
- 実処理を行う Lambda 関数の実装がシンプル化が可能
- Lambda 関数を分けられるため、認証処理・実処理それぞれ別の言語が選択可能
- 実処理に影響無く後から有効化&無効化が可能なので、特に初期における開発が容易
以上の利点があります。
従来;
Lambda オーソライザー使用時;
SORACOM Beam の署名機能
SORACOM Beam は「データ転送支援サービス」です。
SORACOM の SIM を挿したデバイスでセルラー通信をすると beam.soracom.io
というアドレスが見えるようになります。ここへ対してデータを HTTP/TCP/UDP で送信すると、SORACOM Beam 上の設定に従ってデータを送信します。
要するに Proxy サーバっぽく振舞うのですが、データ転送の際、SORACOM Beam はペイロードに対して SHA256 で署名することが可能です。
この署名を受付先のサーバで検証することで SORACOM Beam から送信されたのか否かを確認することが可能となります。
この機能を使うと API サーバの保護が簡単になるので、特に本番においては是非とも使っていただきたいわけですが、この署名検証を Lambda オーソライザーでやろうというのが今回の主旨です。
署名無しの場合;
署名ありの場合;
※そのほかの方法としては送信先 IP アドレスによる ACL ベース認証があります。それを実現するためのサービスとして SORACOM の VPG 固定グローバル IP アドレスオプション もあります。
TIPS;
ヘッダの case-insensitive
結論、小文字に集約されます。
X-FOO-BAR
で API Gateway に送信しても、Lambda 内では event['header']['x-foo-bar']
で取得できます。
そのため、Lambda 関数でのテストや、オーソライザー上のテストにおいてはすべて小文字で行ってください。
Lambda オーソライザー上の認証のキャッシュ
認証結果をキャッシュする機能です。キーは ID ソースになります。
SORACOM Beam が生成する署名は SORACOM が付与する x-soracom-timestamp
を基に算出されているため、都度違う署名になることが確定します。
そのため、キャッシュ機能を有効活用できないので無効にしています。
一方、都度 Lambda 関数が起動しますので、回数や実行時間については注意するようにしてください。
Lambda オーソライザーで得たデータを実処理の Lambda 関数への値の引き渡し
TIPS というか実装検討ポイントになります。
オーソライザーとして起動した Lambda 関数内で context
というキーにマップ(Key-Value)を渡し return することで、実処理を担当する後続の Lambda 関数と連携することができます。
ですが、これですと密結合になってしまうのでせっかく別実装できるようになった Lambda の運用が面倒くさくなるという課題を抱えますので、ご利用は計画的に。
Amazon API Gateway / Lambda オーソライザーの event
に引き渡されるオブジェクト
Lambda オーソライザーの設定で "Lambda イベントペイロード" を リクエスト に設定した時に、オーソライザーとして指定された Lambda 関数の event
に引き渡されるオブジェクトは以下の通りです。
{
"methodArn": "arn:aws:execute-api:REGION:ACCOUNT_ID:API-GATEWAY/prod/POST/beam",
"resource": "/beam",
"requestContext": {
"requestTime": "12/Nov/2018:13:30:54 +0000",
"protocol": "HTTP/1.1",
"domainName": "API-GATEWAY.execute-api.REGION.amazonaws.com",
"resourceId": "XXXXXXX",
"apiId": "API-GATEWAY",
"resourcePath": "/beam",
"httpMethod": "POST",
"domainPrefix": "API-GATEWAY",
"requestId": "XX-XX-XX-XX",
"extendedRequestId": "XXXXXXXXXXXXX=",
"path": "/prod/beam",
"stage": "prod",
"requestTimeEpoch": 1542029454651,
"identity": {
"userArn": null,
"cognitoAuthenticationType": null,
"accessKey": null,
"caller": null,
"userAgent": "curl/7.58.0",
"user": null,
"cognitoIdentityPoolId": null,
"cognitoIdentityId": null,
"cognitoAuthenticationProvider": null,
"sourceIp": "X.X.X.X",
"accountId": null
},
"accountId": "ACCOUNT_ID"
},
"queryStringParameters": {},
"httpMethod": "POST",
"multiValueQueryStringParameters": {},
"pathParameters": {},
"headers": {
"Content-Length": "7",
"x-soracom-signature": "21820f94db77f56c5d90c35b6fe06f64f185cb0133bf9b48d41ced4715c26ca7",
"x-soracom-imsi": "440XXXXXXXXXX91",
"X-Forwarded-Port": "443",
"X-Forwarded-For": "X.X.X.X",
"accept": "*/*",
"user-agent": "curl/7.58.0",
"X-Amzn-Trace-Id": "Root=1-TRACE-ID",
"Host": "API-GATEWAY.execute-api.REGION.amazonaws.com",
"X-Forwarded-Proto": "https",
"x-soracom-signature-version": "20151001",
"x-soracom-imei": "35XXXXXXXXXX195",
"x-soracom-timestamp": "1542029454636",
"content-type": "application/json"
},
"multiValueHeaders": {
"Content-Length": [
"7"
],
"x-soracom-signature": [
"21820f94db77f56c5d90c35b6fe06f64f185cb0133bf9b48d41ced4715c26ca7"
],
"x-soracom-imsi": [
"440XXXXXXXXXX91"
],
"X-Forwarded-Port": [
"443"
],
"X-Forwarded-For": [
"X.X.X.X"
],
"accept": [
"*/*"
],
"user-agent": [
"curl/7.58.0"
],
"X-Amzn-Trace-Id": [
"Root=1-TRACE-ID"
],
"Host": [
"API-GATEWAY.execute-api.REGION.amazonaws.com"
],
"X-Forwarded-Proto": [
"https"
],
"x-soracom-signature-version": [
"20151001"
],
"x-soracom-imei": [
"35XXXXXXXXXX195"
],
"x-soracom-timestamp": [
"1542029454636"
],
"content-type": [
"application/json"
]
},
"stageVariables": {},
"path": "/beam",
"type": "REQUEST"
}
SORACOM Beam の署名の仕様
署名は以下の値を基に SHA256 で行われます。
-
事前共有鍵文字列 (shared_key_string)
(任意の文字列) -
IMEI
(モデムに割り当てられている一意の番号) -
IMSI
(SIM に割り当てられている一意の番号) -
TIMESTAMP
は SORACOM 側で自動的に付与します
※IMSI もしくは IMEI は and/or です
署名は各種プログラム言語で生成可能です。たとえば Python による実装は以下の通りで、 hashlib を使えば一撃で算出できます。これと同じことを Lambda オーソライザー内で行ったのが今回の実装です。
shared_key_string = '_YOUR_SECRET_KEY_'
imei = '35XXXXXXXXXX195'
imsi = '440XXXXXXXXXX91'
timestamp = '1542029454636'
s = "{}x-soracom-imei={}x-soracom-imsi={}x-soracom-timestamp={}".format(shared_key_string, imei, imsi, timestamp).encode('UTF-8')
import hashlib
h = hashlib.sha256(s).hexdigest()
print(h)
#=> 21820f94db77f56c5d90c35b6fe06f64f185cb0133bf9b48d41ced4715c26ca7
※ 詳しくは SORACOM Beam / Beamが付与する署名ヘッダと事前共有鍵について をご覧ください。
あとがき
やっと\(^o^)/オワタ
EoT