LoginSignup
7
4

More than 5 years have passed since last update.

Amazon API Gateway の Lambda オーソライザーで SORACOM Beam の署名検証 (Python 2.7)

Last updated at Posted at 2018-12-02

SORACOM アドベントカレンダー 12/3 担当の Max こと松下です。

Amazon API Gateway の API 呼び出しの認証(エンドポイントセキュリティ)に x-api-key のみを使う方法は、公式にも書いてある通り推奨されていません

ここでは、Amazon API Gateway の Lambda オーソライザーで SORACOM Beam の署名検証を行う話です。

Lambda オーソライザー使用時 / lambda_authorizer / after

設定手順

AWS / Lambda 関数の作成

Lambda オーソライザーは設計図(Blueprint) に入っているので、これを流用します。今回は "api-gatewayauthorizer-python" を使います。(関数名は soracom_beam_sign_validator_func としました)

Blueprint との diff は以下の通りです。

by_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_ としています)
image.png

その他、コード解説;

  • x-soracom-signature を検証
  • principalIdThe 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 ソース: (行を全て削除)
  • 認証のキャッシュ: 無効

image.png

テスト

  • ヘッダー / x-soracom-signature
    • 21820f94db77f56c5d90c35b6fe06f64f185cb0133bf9b48d41ced4715c26ca7
  • ヘッダー / x-soracom-timestamp
    • 1542029454636
  • ヘッダー / x-soracom-imsi
    • 440XXXXXXXXXX91
  • ヘッダー / x-soracom-imei
    • 35XXXXXXXXXX195

image.png

値を適当に編集して署名検証に失敗することも確認してください。

API Gateway / メソッドリクエストの設定

メソッドリクエストの設定で 認証soracom_beam_sign_validator を指定してください。

image.png

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 オーソライザーの環境変数に設定した値と同じ文字列にしてください)

image.png

SORACOM / SORACOM Beam の設定

基本的な設定手順は SORACOM Beam の設定 に記載されています。
ここでは SORACOM Beam の転送設定の内容を解説します。

HTTP エントリーポイントを選び、以下の設定をします。

  • エントリーポイント
    • 設定名: API-Gateway (任意)
    • パス: / (任意)
  • 転送先
    • プロトコル: HTTPS
    • ホスト名:
    • ポート番号: (空)
    • パス:
  • ヘッダ操作
    • IMSI ヘッダ: ON
    • IMEI ヘッダ: ON
    • 署名ヘッダ付与: ON
    • 事前共有鍵: (認証情報ストアで作成したエントリーを選択)

image.png

以上です。

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_authorizer / before

Lambda オーソライザー使用時;

Lambda オーソライザー使用時 / lambda_authorizer / after

SORACOM Beam の署名機能

SORACOM Beam は「データ転送支援サービス」です。
SORACOM の SIM を挿したデバイスでセルラー通信をすると beam.soracom.io というアドレスが見えるようになります。ここへ対してデータを HTTP/TCP/UDP で送信すると、SORACOM Beam 上の設定に従ってデータを送信します。

要するに Proxy サーバっぽく振舞うのですが、データ転送の際、SORACOM Beam はペイロードに対して SHA256 で署名することが可能です。
この署名を受付先のサーバで検証することで SORACOM Beam から送信されたのか否かを確認することが可能となります。

この機能を使うと API サーバの保護が簡単になるので、特に本番においては是非とも使っていただきたいわけですが、この署名検証を Lambda オーソライザーでやろうというのが今回の主旨です。

署名無しの場合;

lambda_authorizer / Beam none-signature

署名ありの場合;

lambda_authorizer / Beam signature

※そのほかの方法としては送信先 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

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4