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

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

image.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エンコードされたパスワードが渡されるのでエンコードして使用します。

// 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が許可されます。

{
    "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

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

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

ap-northeast-1.console.aws.amazon.com_iot_home_region=ap-northeast-1(iPad) (1).png

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

ap-northeast-1.console.aws.amazon.com_iot_home_region=ap-northeast-1(iPad) (2).png

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

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

aws iot set-default-authorizer --authorizer-name custom-authorizer

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

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

PahoクライアントのPython版を使います。 こちらのブログ記事を参考にしました。

pip install paho-mqtt

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

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

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

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

無事にPublishできました。

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