AWS IoTへport:443/ALPNを使ってmqtt publishする

# はじめに

(update 2018/6/29: ALPN対応が[python SDK1.4.0](https://www.facebook.com/takashi.koyanagawa/posts/1664165517033387?notif_id=1530231250708432&notif_t=feedback_reaction_generic)で対応されました)

先日、AWS IoTでmqttをport:443を使って利用できる[update](https://aws.amazon.com/jp/about-aws/whats-new/2018/02/aws-iot-core-now-supports-mqtt-connections-with-certificate-based-client-authentication-on-port-443/)がリリースされました。
[ブログ](https://aws.amazon.com/jp/blogs/news/mqtt-with-tls-client-authentication-on-port-443-why-it-is-useful-and-how-it-works/)には、TLS標準のALPN機能を使うことで実現できるとの記載があるので、こちらを試してみました。

# 前提条件と投稿で使った環境について

ご利用されるsslライブラリなどによりALPNの実装方式が異なります。また、ご利用される言語によりalpn設定用の関数なども異なりますので、ご自分の環境や言語での実装方式はご確認をお願いします。
本投稿で確認した環境では、

- python version : _Python 2.7.13_
- OpenSSLのversion : _OpenSSL 1.0.2k-fips  26 Jan 2017_

です。
python2.7としては、alpn設定ための[SSLContext.set_alpn_protocols()](https://docs.python.org/2/library/ssl.html#ssl.SSLContext.set_alpn_protocols)が2.7.10以降でサポートされています。OpenSSLのalpnは、OpenSSL 1.0.2以降でサポートされていますので、この辺はご自分の環境を確認してください。またalpnで通知するプロトコル名は"x-amzn-mqtt-ca"と規定されています。

追記：
Python3.6でも下記のソースのまま動作確認しました。Python3系では、3.5以降で[set_alpn_protocols](https://docs.python.jp/3/library/ssl.html#ssl.SSLContext.set_alpn_protocols)がサポートされています。

mqtt clientとしてはpahoを使っています。pahoのinstallは以下
(pyenvや、特定directoryなどinstall先は各自の環境に合わせてください。以下の参考は実行するpythonプログラムのローカル環境へinstallしています。)

```
sudo pip install paho-mqtt -t [サンプルプログラムのdir]
```

pythonが使っているOpenSSLのバージョンは以下で確認出来ます。

```python

import ssl
print ssl.OPENSSL_VERSION
```


また、テストコードを実行する際に証明証が必要になるのでAWS IoTからThingの作成や証明書の作成、ダウンロード、証明書activate、policyのattachを行ってください。詳細手順は[こちら](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/device-certs-create.html)

# コードの流れ

- ssl_contextへalpnの設定
- 同じくssl_contextへ証明書の設定
- ssl_contextを使ってmqqt clientへtls option設定
- mqtt接続

となります。

# サンプルコード

本sampleを動かすディレクトリとしては以下の環境となっています。
sample-dir
├ alpn_mqtt.py
├ paho/ (dir)
├ paho_mqtt-1.3.1-py2.7.egg-info/ (dir)
└ certs/ (dir)
　└ [root_ca](https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem) / certificate / private

certificate と private はAWS IoTで生成した証明書

```python:alpn_mqtt.py

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 = "x-amzn-mqtt-ca"
aws_iot_endpoint = "AWS IoTのエンドポイント" # <random>.iot.<region>.amazonaws.comの形式
url = "https://{}".format(aws_iot_endpoint)

ca = "root_caの相対/絶対path付きfailname"
cert = "certificateの相対/絶対path付きfailname"
private = "privateの相対/絶対path付きfailname"

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 = "test/date"
    try:
        mqttc = mqtt.Client()
        ssl_context= ssl_alpn()
        mqttc.tls_set_context(context=ssl_context)
        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)
```

# 動作確認

動かしているホストがlinuxであれば、 _tcpdump port 443_ としていただけば 443で一秒ごとにpublishしていることがわかるかと思います。また、 _tcpdump port 8883_ をみても8883で送信していないことが確認できます。
AWS IoTへの到達確認は AWS IoTのテスト画面から # or test/date をsubscribeしてください。
毎秒毎に時刻が送信されていることがわかります。

![スクリーンショット 2018-02-25 11.22.14.png](https://qiita-image-store.s3.amazonaws.com/0/100893/9056935c-c2f0-625f-8cfe-4c2ac7a996b3.png)

# 免責

本投稿は、個人の意見で、所属する企業や団体は関係ありません。
また掲載しているsampleプログラムの動作に関しても保障いたしませんので、参考程度にしてください。