AWS IoT Coreに証明書を使わずユーザー名/パスワード認証で接続する

AWSのIoT Coreに接続するためには、証明書が必須と思っていましたが、カスタム認証を使うと、ユーザー名とパスワードで認証ができます。

証明書を使った認証は、IoT Coreが自動でやってくれますが、ユーザー名とパスワードによる認証の場合は、認証用のLambdaを作成して認証を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180441/616cd98d-052f-48e1-3502-978d313630e1.png)
※図は公式ドキュメントからの引用です。

MQTT接続で行う場合には以下の制約があります。

* 接続ポートはMQTTSの8883ではなく443である必要がある
* ALPN拡張に`MQTT`の値を指定する必要がある

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/custom-auth.html

> オーソライザーを呼び出すには、MQTT とカスタム認証を使用して AWS IoT Core に接続するデバイスがポート 443 に接続する必要があります。また、mqtt の値を持つ Application Layer Protocol Negotiation (ALPN) TLS 拡張と、AWS IoT Core データエンドポイントのホスト名を持つ Server Name Indication (SNI) 拡張を渡す必要があります。

# 手順

## カスタム認証用のLambdaの作成

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/config-custom-auth.html

公式ドキュメントのサンプルをそのまま使ってみます。

この例では、パスワードが`test`だったら認証OKとなっています。
LambdaにはBase64エンコードされたパスワードが渡されるのでエンコードして使用します。

```javascript:Lambda
// A simple Lambda function for an authorizer. It demonstrates 
// how to parse an MQTT password and generate a response.

exports.handler = function(event, context, callback) { 
    var uname = event.protocolData.mqtt.username;
    var pwd = event.protocolData.mqtt.password;
    var buff = new Buffer(pwd, 'base64');
    var passwd = buff.toString('ascii');
    switch (passwd) { 
        case 'test': 
            callback(null, generateAuthResponse(passwd, 'Allow')); 
        default: 
            callback(null, generateAuthResponse(passwd, 'Deny'));  
    }
};

// Helper function to generate the authorization response.
var generateAuthResponse = function(token, effect) { 
    var authResponse = {}; 
    authResponse.isAuthenticated = true; 
    authResponse.principalId = 'TEST123'; 
    
    var policyDocument = {}; 
    policyDocument.Version = '2012-10-17'; 
    policyDocument.Statement = []; 
    var publishStatement = {}; 
    var connectStatement = {};
    connectStatement.Action = ["iot:Connect"];
    connectStatement.Effect = effect;
    connectStatement.Resource = ["arn:aws:iot:us-east-1:123456789012:client/myClientName"];
    publishStatement.Action = ["iot:Publish"]; 
    publishStatement.Effect = effect; 
    publishStatement.Resource = ["arn:aws:iot:us-east-1:123456789012:topic/telemetry/myClientName"]; 
    policyDocument.Statement[0] = connectStatement;
    policyDocument.Statement[1] = publishStatement; 
    authResponse.policyDocuments = [policyDocument]; 
    authResponse.disconnectAfterInSeconds = 3600; 
    authResponse.refreshAfterInSeconds = 300;
    
    return authResponse; 
}
```

認証がOKだった場合に、IoTポリシーを返却します。この例ではこのようなポリシーが返却されます。
クライアントIDが`myClientName`でのiot:Connectと、`telemetry/myClientName`トピックへのiot:Publishが許可されます。

```json:IoTポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "iot:Connect"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:iot:us-east-1:123456789012:client/myClientName"
            ]
        },
        {
            "Action": [
                "iot:Publish"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:iot:us-east-1:123456789012:topic/telemetry/myClientName"
            ]
        }
    ]
}
```

## カスタムオーソライザーの登録

IoT Coreのマネジメントコンソールを開き、左メニューのオーソライザーを選択します。

![ap-northeast-1.console.aws.amazon.com_iot_home_region=ap-northeast-1(iPad).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180441/0dfbb510-49d9-9a91-12b9-411e5ea34480.png)

作成ボタンをクリックします。

名前をつけて、先程作成したLambdaを選択します。

![ap-northeast-1.console.aws.amazon.com_iot_home_region=ap-northeast-1(iPad) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180441/a62a2633-0fdd-0f3f-611a-5e29c46f605f.png)

トークンの検証は今回は有効にはせず、`オーソライザーのアクティブ化`にチェックを入れ、オーソライザーの作成ボタンをクリックします。

![ap-northeast-1.console.aws.amazon.com_iot_home_region=ap-northeast-1(iPad) (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180441/21408278-73e7-306b-6f24-808820bf971c.png)

## デフォルトのオーソライザーを登録

作成したオーソライザーが呼び出されるようにするには、デフォルトのオーソライザーの登録が必要です。
マネジメントコンソールからはできないようですので、CLIで行います。

```shell:AWSCLI
aws iot set-default-authorizer --authorizer-name custom-authorizer
```

これでAWS IoT Coreの設定は完了です。

# 接続するクライアントの作成

PahoクライアントのPython版を使います。
[こちら](https://aws.amazon.com/jp/blogs/news/how-to-implement-mqtt-with-tls-client-authentication-on-port-443-from-client-devices-python/)のブログ記事を参考にしました。

```shell
pip install paho-mqtt
```

ブログのものから、以下の項目を変更します。

* `IoT_protocol_name`を`mqtt`に変更
* 証明書認証に関する部分をコメントアウト
* Publishするトピック名を`telemetry/myClientName`に変更
* クライアントIDに`myClientName`を指定
* `username_pw_set`でユーザー名とパスワードを指定

もちろんエンドポイントの変更も必要です。

```python
from __future__ import print_function
import sys
import ssl
import time
import datetime
import logging, traceback
import paho.mqtt.client as mqtt

IoT_protocol_name = "mqtt" ### 変更
aws_iot_endpoint = "AWS_IoT_ENDPOINT_HERE" # <random>.iot.<region>.amazonaws.com
url = "https://{}".format(aws_iot_endpoint)

# ca = "YOUR/ROOT/CA/PATH" ### 変更
# cert = "YOUR/DEVICE/CERT/PATH" ### 変更
# private = "YOUR/DEVICE/KEY/PATH" ### 変更

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
log_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(log_format)
logger.addHandler(handler)

def ssl_alpn():
    try:
        #debug print opnessl version
        logger.info("open ssl version:{}".format(ssl.OPENSSL_VERSION))
        ssl_context = ssl.create_default_context()
        ssl_context.set_alpn_protocols([IoT_protocol_name])
        # ssl_context.load_verify_locations(cafile=ca) ### 変更
        # ssl_context.load_cert_chain(certfile=cert, keyfile=private) ### 変更

        return  ssl_context
    except Exception as e:
        print("exception ssl_alpn()")
        raise e

if __name__ == '__main__':
    topic = "telemetry/myClientName" ### 変更
    try:
        mqttc = mqtt.Client(client_id='myClientName') ### 変更
        ssl_context= ssl_alpn()
        mqttc.tls_set_context(context=ssl_context)
        mqttc.username_pw_set('username', 'test') ### 変更
        logger.info("start connect")
        mqttc.connect(aws_iot_endpoint, port=443)
        logger.info("connect success")
        mqttc.loop_start()

        while True:
            now = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
            logger.info("try to publish:{}".format(now))
            mqttc.publish(topic, now)
            time.sleep(1)

    except Exception as e:
        logger.error("exception main()")
        logger.error("e obj:{}".format(vars(e)))
        logger.error("message:{}".format(e.message))
        traceback.print_exc(file=sys.stdout)
```

##　接続テスト

![ap-northeast-1.console.aws.amazon.com_iot_home_region=ap-northeast-1(iPad) (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180441/97efbaea-2061-6848-a476-782169e3dab7.png)

無事にPublishできました。


パスワードが間違っていたり、クライアントIDやトピック名が許可されたもの以外の場合にPublishしたメッセージが届かないことも確認しました。