AWS IoT Bootstrapping - CSR作成・証明書発行・MQTT接続 （秘密鍵はエッジで生成）

#実現したいこと
* IoTデバイスをできる限りセキュアに保ち、かつ運用も効率化したい。
  * 独自CAの運用管理は手間なのでできれば避けたい。
  * 秘密鍵を通信路に流したくない。
  * AWS IoT と接続してMQTT通信したい。

この実現手段として以下を考えます。

* クライアントで鍵ペア、CSR生成を行う
* REST経由で証明書を取得
    * Edge -> API GW -> Lambda -> AWS IoT

* MQTT接続 & Publish
  * Edge -> AWS IoT

## RSA鍵の生成、CSR作成
エッジ側（Mac）での操作です。ここではOpensslを使います。

```sh
openssl genrsa -out privatekey.pem 2048
openssl req -new -subj "/C=JP/ST=Tokyo/L=Meguro/O=Amazon Web Services Japan K.K./CN=AWS IoT Certificate" -key privatekey.pem -out cert.csr
```

## Lambdaで証明書をリクエストするコードをかく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102681/647c2015-e221-4a2a-6084-2a4dad00ef30.png)

LambdaのExecution roleに、以下のようなポリシーをアタッチします。

``` json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "iot:CreateCertificateFromCsr",
            "Resource": "*"
        }
    ]
}
```

Lambdaはこのような感じで、一旦CSRをハードコーディングしてテストします。

```python
import json
import boto3

def lambda_handler(event, context):
    client = boto3.client('iot')
    response = client.create_certificate_from_csr(
        certificateSigningRequest='''-----BEGIN CERTIFICATE REQUEST-----
(CSR)
-----END CERTIFICATE REQUEST-----''',
        setAsActive=True
    )
    return {
        'statusCode': 200,
        'body': json.dumps(response)
    }
```

Responseは以下のようになっており、certificatePemが取れています。

```json
{
  "statusCode": 200,
  "body": {
    "ResponseMetadata": {
      "RequestId": "d6fe4c43-004a-46c3-9a47-bab473a210ef",
      "HTTPStatusCode": 200,
      "HTTPHeaders": {
        "date": "Thu, 23 Jan 2020 06:09:32 GMT",
        "content-type": "application/json",
        "content-length": "1594",
        "connection": "keep-alive",
        "x-amzn-requestid": "d6fe4c43-004a-46c3-9a47-bab473a210ef",
        "access-control-allow-origin": "*",
        "x-amz-apigw-id": "GvXIZFqVtjMFnDw=",
        "x-amzn-trace-id": "Root=1-5e29389c-199aa1001fb6b8f098ea85b8"
      },
      "RetryAttempts": 0
    },
    "certificateArn": "arn:aws:iot:ap-northeast-1:aaa",
    "certificateId": "aaa",
    "certificatePem": "-----BEGIN CERTIFICATE-----(中略)-----END CERTIFICATE-----\n"
  }
}

```

## csrをLambdaに渡す

クライアントからのリクエストに応じてHTTP経由でCSR取得可能にします。
まずは、API Gatewayの設定。

簡単のため、Beta機能ですが、HTTP APIを使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102681/633f16a5-23a5-cc4f-d07b-b51947796b9d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102681/1128bae0-6659-0031-fce0-8c025f71bfeb.png)

POSTでCSRを送るように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102681/6052b910-09a7-ee20-2d03-306b139fadbe.png)


これで、APIが作成されました。
Lambda側は、CSRを受け付けるように少し改変します。

```python
import json
import boto3

def lambda_handler(event, context):
    csr = event['body']
    client = boto3.client('iot')
    response = client.create_certificate_from_csr(
        certificateSigningRequest=csr,
        setAsActive=True
    )
    return {
        'statusCode': 200,
        'body': json.dumps(response)
    }

```

PCにPostmanをインストールして、BodyにCSRを入れてAPIを叩きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102681/e44221b6-943a-213b-59e1-3e1cef76c0c8.png)

レスポンスを確認すると、certificatePemが取れていることが分かりました。
これで、有効な証明書がゲットできました。

## 証明書にPolicyをアタッチ

証明書にPolicyをアタッチすることで、特定の操作をエッジデバイスに認可することができます。

AWS IoTで以下のようなポリシーを作っておきます。
名前は、getCertificateTestとしました。

```json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect",
        "iot:Publish"
      ],
      "Resource": "*"
    }
  ]
}
```

先程LambdaにAttachしたPolicyを編集し、AttachPolicyのActionを追加します。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iot:CreateCertificateFromCsr",
                "iot:AttachPolicy"
            ],
            "Resource": "*"
        }
    ]
}
```

また、Lambdaの実装の最後に以下を追加します。

```py

    certificateArn = response['certificateArn']
    
    client.attach_policy(
        policyName='getCertificateTest',
        target=certificateArn
    )
```

ここまで設定して、もう一度POSTを実行してみます。AWS IoTで証明書が追加されており、Policyもアタッチされていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102681/69a1a484-072c-c3be-e4a7-a61d47cb0810.png)

## クライアントからMQTTのPublishをしてみる

MQTTのPublishを行うためには、AWS IoT側で、エッジデバイスに対応するThingを登録し、Thingと証明書を紐付ける必要があります。
そこで、クライアント側からThingNameとCSRをJson形式で送るようにし、Lambda上でそのThingNameでThingの作成を行い、さらに作成した証明書を紐付けます。
まずはそれらに必要な権限をLambdaに与えるため、Policyを更新します。

```json
            "Action": [
                "iot:CreateThing",
                "iot:AttachPolicy",
                "iot:AttachThingPrincipal",
                "iot:CreateCertificateFromCsr"
            ]
```

最終的にはLambdaはこうなりました。

```py
import json
import boto3
import base64

def lambda_handler(event, context):

    body = json.loads(event['body'])
    client = boto3.client('iot')
    response = client.create_certificate_from_csr(
        certificateSigningRequest=body['csr'],
        setAsActive=True
    )
    
    certificatePem = response['certificatePem']
    certificateArn = response['certificateArn']
    
    client.attach_policy(
        policyName='getCertificateTest',
        target=certificateArn
    )
    
    response = client.create_thing(
        thingName=body['thingName']
    )
    
    response = client.attach_thing_principal(
        thingName=body['thingName'],
        principal=certificateArn
    )
    
    return {
        'statusCode': 200,
        'body': certificatePem
    }
```

クライアント側の実装では、Python のDevice SDKを使います。
https://github.com/aws/aws-iot-device-sdk-python

認証で必要なルート証明書を取得します。

```sh
curl https://www.amazontrust.com/repository/AmazonRootCA1.pem >> Amazon_Root_CA_1.pem
```


実行ごとに新たなThingを作成し、一発だけMQTTのPublishを行います。

```py
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import requests
import time
import json

thingName = "SN-" + str(int(time.time()))

file = open('cert.csr', 'r')
csr = file.read()
file.close()

payload = {"thingName": thingName, "csr": csr}
header = {"content-type": "application/json"}

r = requests.post('https://aaa.execute-api.ap-northeast-1.amazonaws.com/getCertificate',
                  data=json.dumps(payload), headers=header)

file = open('cert', 'w')
file.write(r.content)
file.close()

myMQTTClient = AWSIoTMQTTClient(thingName)
myMQTTClient.configureEndpoint("aaa-ats.iot.ap-northeast-1.amazonaws.com", 8883)
myMQTTClient.configureCredentials("Amazon_Root_CA_1.pem", "privatekey.pem", "cert")

myMQTTClient.configureOfflinePublishQueueing(-1)  # Infinite offline Publish queueing
myMQTTClient.configureDrainingFrequency(2)  # Draining: 2 Hz
myMQTTClient.configureConnectDisconnectTimeout(10)  # 10 sec
myMQTTClient.configureMQTTOperationTimeout(5)  # 5 sec

myMQTTClient.connect()
myMQTTClient.publish("certtest/{}".format(thingName), json.dumps({"message": "hello", "thingName": thingName}), 1)

time.sleep(10)

myMQTTClient.disconnect()

```

これでPythonのスクリプトを実行するとMQTTのメッセージがPublishされるようになります。
AWS IoTのTestを使って、Subscribeするとメッセージを受信しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102681/274491cd-3707-d7c4-1dab-8e7af289d8eb.png)

### 補足

実際のユースケースだと、有効な鍵ペアや証明書がローカルに無い場合のみREST APIを叩く形になると思います。
CSRは、環境によって使えるライブラリを探す必要があるかもしれません。組み込み系だとmbed TLSでも作成できそうです。
https://tls.mbed.org/kb/how-to/generate-a-certificate-request-csr

### 注意点

作成した秘密鍵の保管方法は別途検討が必要です。

今回の方法はBootstrappingのあくまで１パターンに過ぎないです。実際にはセキュリティリスクアセスメントを実施し、ビジネスとセキュリティの両面から実現方法を検討することになると思います。

環境にあわせてREST APIの認証も別途考える必要があります。