【AWS IoT】Thing用の個別のクライアント証明書を使ってS3/DynamoDBにアクセスする

# はじめに
AWS IoTをはじめとし、多くのIoTシステムでは、各デバイスそれぞれに固有のクライアント証明書を発行し、これを利用してAuthentication/Authorizationを実現しているものと思います。
しかしながら、ほとんどのAWSサービスはクライアント証明書に対応しておらず、例えば特定のクライアント証明書を持つデバイスのみ、特定のS3 Bucketにアクセスさせたい、といったことはそのままでは実現できません。
しかしAuthorizing Direct Callsという仕組みを利用することで、これを実現することができるようになります。

Authorizing Direct Calls to AWS Services - AWS IoT
https://docs.aws.amazon.com/iot/latest/developerguide/authorizing-direct-aws.html

# TL;DR
- Authorizing Direct Callsとは、個別のクライアント証明書をTemporary Security Credentialsに変換する仕組み
- Temporary Security Credentialsがあれば、（権限があれば）AWS APIを何でも実行できる
- IoT Thingから見た場合、Certificate→IoT Policy→Role Alias→IAM Roleというかたちで権限情報が紐づく

# 検証した環境
- AWSアカウントID: 123456789099
- S3 Bucket:
    - S3 Bucket名: investigate-aws-iot
    - テスト対象ファイル名: s3://investigate-aws-iot/file_needs_retrieving.txt
- IAM Role: 
    - Role名: InvestigateAuthorizingDirectCallsRole
    - Role ARN: arn:aws:iam::123456789099:role/InvestigateAuthorizingDirectCallsRole
- AWS IoT:
    - IoT Endpoint: abcdefgh123456.credentials.iot.ap-northeast-1.amazonaws.com
    - Role Alias:
        - Role Alias名: InvestigateIotRoleAlias
        - Role Alias ARN: arn:aws:iot:ap-northeast-1:123456789099:rolealias/InvestigateIotRoleAlias
    - Thing/Certificate/Policy
        - Thing: InvestigateAuthorizingDirectCallsThing
        - Certificate: c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e
        - Policy: InvestigateAuthorizingDirectCallsPolicy

# 作業概要
1. S3 Bucketの準備
2. IoT ThingからAssumeRoleするIAM Roleの作成
3. AWS IoT Role Aliasの作成
4. AWS IoT Thing/Certificate/Policyの準備
    1. Thingの作成
    2. IoT Policyの作成
    3. Certificateの作成
    4. Certificateのactivate
    5. CertificateへのPolicyのattach
    6. CertificateをThingにattach
5. Authorizing Direct Callsの確認
    1. AWS IoTのEndpoint確認
    2. curlを実行し、Temporary Security Credentialsを取得する
    3. S3 Bucketからファイルを取得できることを確認する

# 各ステップの詳細
## 1. S3 Bucketの準備
手順詳細は省略しますが、下記のように、必要なS3 Bucketを作成し、取得対象のファイルを格納しておきます。

```console
$ aws s3 ls
2019-08-21 15:14:05 investigate-aws-iot
    
$ aws s3 ls s3://investigate-aws-iot/
2019-08-21 15:16:09         11 file_needs_retrieving.txt
```

上記ファイルの内容は何でもよいです。今回は下記の内容でつくっています。

```file_needs_retrieving.txt
retrieved


```


## 2. IoT ThingからAssumeRoleするIAM Roleの作成
本稿で確認するAuthorizing Direct Callsの仕組みは、最終的にThingはここで定義するIAM Roleの権限を取得することとなります。したがって、このRoleに必要なPermissionsを定義する必要があります。

Role名は上述の通り、「InvestigateAuthorizingDirectCallsRole」とします。

AWS IoTのサービスからAssumeRoleされる形になるため、Trust Relationshipで、「credentials.iot.amazonaws.com」からAssumeRoleできるようにしておきます。
またPermissionsとして、S3 Bucket一覧の取得と、対象S3 Bucketのオブジェクト取得に必要な権限を付与します。
それぞれ具体的には下記のようになります。

```json:Trust&nbsp;Relationship
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "credentials.iot.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

```json:Permissions
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:GetRole",
                "iam:PassRole"
            ],
            "Resource": [
                "arn:aws:iam::123456789099:role/InvestigateAuthorizingDirectCallsRole"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::investigate-aws-iot/*",
                "arn:aws:s3:::investigate-aws-iot"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "s3:ListAllMyBuckets",
            "Resource": "arn:aws:s3:::*"
        }
    ]
}
```

## 3. AWS IoT Role Aliasの作成
AWS IoTのRole Aliasを作成します。これはAWS Management Consoleからは実行できないようなので、AWS CLIから実行します。
Role Alias名は上述の通り「InvestigateIotRoleAlias」とします。
引数として、先ほど作成したIAM RoleのARNを渡します。

```console:Role&nbsp;Aliasの作成
$ aws iot create-role-alias --role-alias InvestigateIotRoleAlias --role-arn arn:aws:iam::123456789099:role/InvestigateAuthorizingDirectCallsRole
{
    "roleAlias": "InvestigateIotRoleAlias",
    "roleAliasArn": "arn:aws:iot:ap-northeast-1:123456789099:rolealias/InvestigateIotRoleAlias"
}
```

## 4. AWS IoT Thing/Certificate/Policyの準備
動作確認対象のデバイス（Thing）を作成し、個別のクライアント証明書を発行し、Policyを紐づけます。
このあたりの作業は、通常AWS IoTを利用する上では当たり前のように実施していると思いますので、できる限り省略します。

### 4-1. Thingの作成
普通にThingを作成します。何も特別なことはありません。

```console:Thingの作成
$ aws iot create-thing --thing-name InvestigateAuthorizingDirectCallsThing
{
    "thingArn": "arn:aws:iot:ap-northeast-1:123456789099:thing/InvestigateAuthorizingDirectCallsThing",
    "thingName": "InvestigateAuthorizingDirectCallsThing"
}
```

### 4-2. IoT Policyの作成
AWS IoTにはIoT Policyという概念があります。これは各クライアント証明書に紐づけるもので、当該クライアント証明書が紐づけられたThingに付与する権限を指定します。
本稿で必要な権限は、先ほど作成したRole Aliasに対する「iot:AssumeRoleWithCertificate」となります。

まずは下記JSONファイルを作成します。ここでは「iot_policy.json」とします。

```json:iot_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:AssumeRoleWithCertificate",
      "Resource": "arn:aws:iot:ap-northeast-1:123456789099:rolealias/InvestigateIotRoleAlias"
    }
  ]
}
```

上記ファイルを指定して、IoT Policyを作成します。

```console:IoT&nbsp;Policyの作成
$ aws iot create-policy --policy-name InvestigateAuthorizingDirectCallsPolicy --policy-document file://iot_policy_document.json
{
    "policyName": "InvestigateAuthorizingDirectCallsPolicy",
    "policyArn": "arn:aws:iot:ap-northeast-1:123456789099:policy/InvestigateAuthorizingDirectCallsPolicy",
    "policyDocument": "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"iot:AssumeRoleWithCertificate\",\n      \"Resource\": \"arn:aws:iot:ap-northeast-1:123456789099:rolealias/InvestigateIotRoleAlias\"\n    }\n  ]\n}\n",
    "policyVersionId": "1"
}
```

### 4-3. Certificateの作成
クライアント証明書と鍵ペアを作成し、それぞれ個別のファイルに保存します。

まずはiot create-keys-and-certificateを実行し、結果をファイルに出力します。

```console:クライアント証明書と秘密鍵の作成
$ aws iot create-keys-and-certificate > keys-and-cert.txt
$ cat keys-and-cert.txt
{
    "certificateArn": "......",
    "certificatePem": "-----BEGIN CERTIFICATE-----\n......\n-----END CERTIFICATE-----\n",
    "keyPair": {
        "PublicKey": "-----BEGIN PUBLIC KEY-----\n......\n-----END PUBLIC KEY-----\n",
        "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\n......\n-----END RSA PRIVATE KEY-----\n"
    },
    "certificateId": "c231554173......"
}
```

certificateIdの先頭10文字を取得します。これはファイル名などに用います。

```console:証明書IDの先頭10文字取得
$ cat keys-and-cert.txt | jq -r '.certificateId[0:10]'
c231554173
```

証明書と秘密鍵を個別のファイルに保存します。公開鍵は単体では特に必要ありません。

```console:クライアント証明書と秘密鍵の取り出し
$ cat keys-and-cert.txt | jq -r '.certificatePem' > c231554173-certificate.pem.crt
$ cat keys-and-cert.txt | jq -r '.keyPair.PrivateKey' > c231554173-private.pem.key
```

### 4-4. Certificateのactivate
Certificateは作成直後は「INACTIVE」の状態になっていますので、これを「ACTIVE」に変更します。

```console:クライアント証明書をactivateする
$ aws iot update-certificate --certificate-id c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e --new-status "ACTIVE"
```

iot describe-crtificateコマンドで、クライアント証明書のステータスを確認することができます。

```console:クライアント証明書のステータスを確認する
$ aws iot describe-certificate --certificate-id c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e
{
    "certificateDescription": {
        "certificateArn": "arn:aws:iot:ap-northeast-1:123456789099:cert/c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e",
        "status": "ACTIVE",
        "certificateId": "c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e",
        "lastModifiedDate": 1566750890.409,
        "certificatePem": "-----BEGIN CERTIFICATE-----\n......\n-----END CERTIFICATE-----\n",
        "transferData": {},
        "ownedBy": "123456789099",
        "creationDate": 1566750089.245
    }
}
```

### 4-5. CertificateへのPolicyのattach
クライアント証明書にIoT Policyを紐づけます。

```console:aws&nbsp;iot&nbsp;attach-principal-policy
$ aws iot attach-principal-policy --policy-name InvestigateAuthorizingDirectCallsPolicy --principal arn:aws:iot:ap-northeast-1:123456789099:cert/c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e
```

IoT Policyに紐づけられたクライアント証明書は、aws iot list-policy-principalsコマンドを実行することで確認できます。

```console:aws&nbsp;iot&nbsp;list-policy-principals
$ aws iot list-policy-principals --policy-name InvestigateAuthorizingDirectCallsPolicy
{
    "principals": [
        "arn:aws:iot:ap-northeast-1:123456789099:cert/c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e"
    ]
}
```

### 4-6. CertificateをThingにattach
クライアント証明書をThingに紐づけます。

```console:aws&nbsp;iot&nbsp;attach-thing-principal
$ aws iot attach-thing-principal --thing-name InvestigateAuthorizingDirectCallsThing --principal arn:aws:iot:ap-northeast-1:123456789099:cert/c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e
```

たクライアント証明書に紐づけられたThingは、aws iot list-principal-thingsコマンドを実行することで確認できます。

```console:aws&nbsp;iot&nbsp;list-principal-things
$ aws iot list-principal-things --principal arn:aws:iot:ap-northeast-1:123456789099:cert/c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e
{
    "things": [
        "InvestigateAuthorizingDirectCallsThing"
    ]
}
```

## 5. Authorizing Direct Callsの確認
### 5-1. AWS IoTのEndpoint確認
AWS IoTはそのサービス提供用に、各AWSアカウント・リージョンごとに専用のEndpointを用意しています。aws iot describe-endpointコマンドを実行して、当該Endpointのドメイン名を取得できます。

```console:aws&nbsp;iot&nbsp;iot-describe-endpoint
$ aws --profile playground iot describe-endpoint --endpoint-type iot:CredentialProvider
{
    "endpointAddress": "abcdefgh123456.credentials.iot.ap-northeast-1.amazonaws.com"
}
```

### 5-2. curlを実行し、Temporary Security Credentialsを取得する
上記で取得したEndpointに対して、Thingのクライアント証明書・秘密鍵を用いてHTTPSリクエストを発行することで、Temporary Security Credentialsを取得することができます。

まず、AWSのRootCA局証明書が必要になりますので、これをダウンロードしておきます。

```console:AmazonRootCA1証明書の保存
$ curl -O https://www.amazontrust.com/repository/AmazonRootCA1.pem
```

下記のように、curlの引数として以下の情報を渡すことで、Temporary Security Credentialsを取得することができます。

- クライアント証明書とその秘密鍵
- RootCA局証明書
- Thing名を「x-amzn-iot-thingname」ヘッダに指定

```console
$ curl -v --cert ./c231554173-certificate.pem.crt --key ./c231554173-private.pem.key \
  --cacert AmazonRootCA1.pem \
  -H "x-amzn-iot-thingname: InvestigateAuthorizingDirectCallsThing" \
  https://abcdefgh123456.credentials.iot.ap-northeast-1.amazonaws.com/role-aliases/InvestigateIotRoleAlias/credentials \
  | jq
```

上記が成功すると、下記のようなJSONの出力を得ることができます。この情報を用いて、S3やDynamoDBその他AWSリソースにアクセスすることが可能です。権限は、最初の方で作成したIAM Role「InvestigateAuthorizingDirectCallsRole」のPermissionsが付与されます。

```json
{
  "credentials": {
    "accessKeyId": "ASIA................",
    "secretAccessKey": "................................",
    "sessionToken": "................................................",
    "expiration": "2019-08-25T18:26:25Z"
  }
}
```

### 5-3. S3 Bucketからファイルを取得できることを確認する
上記で取得したTemporary Security Credentialsを用いて、実際にS3 Bucketにアクセスできることを確認します。

まず、先ほど取得したAccessKeyId/SecretAccessKey/SessionTokenを、環境変数にセットします。

```console
$ export AWS_ACCESS_KEY_ID=ASIA................
$ export AWS_SECRET_ACCESS_KEY=................................
$ export AWS_SESSION_TOKEN=................................................
```

なお、AWS CLIからTemporary Security Credentialsを利用する方法について、詳細は下記ドキュメントに記載があります。

Using Temporary Security Credentials to Request Access to AWS Resources - AWS Identity and Access Management
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html#using-temp-creds-sdk-cli

次に、STSのGetCallerIdentity APIを実行し、APIの発行主体がIAM Role「InvestigateAuthorizingDirectCallsRole」になっていることを確認します。

```console
$ aws sts get-caller-identity
{
    "Account": "123456789099",
    "UserId": "AROA............:c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e",
    "Arn": "arn:aws:sts::123456789099:assumed-role/InvestigateAuthorizingDirectCallsRole/c231554173c80cdb46da694a21ec92b060b07d27883214edf7ac1573843f2a3e"
}  
```

最後に、S3 Bucketからファイルを取得できることを確認します。

```console
$ aws s3 ls s3://investigate-aws-iot
2019-08-21 15:16:09         11 file_needs_retrieving.txt
     
$ aws s3 cp s3://investigate-aws-iot/file_needs_retrieving.txt ./
download: s3://investigate-aws-iot/file_needs_retrieving.txt to ./file_needs_retrieving.txt
```


# おわりに
これで、Thing個別のクライアント証明書を用いて、AWSリソースにアクセスできるようになりました。

AWSの基本的な考え方として、各AWSリソースにアクセスする際には、AWS Credentials(AccessKeyId/SecretAccessKey)か、Temporary Security Credentials(AccessKeyId/SecretAccessKey/SessionToken)を用いて、Signature V4で署名してAWS APIを発行する、という方針があると思います。
Cognito Federated Identitiesもそうですが、何らか操作する主体を特定できる情報があった場合、最終的にTemporary Security Credentialsに変換する、という考え方が根底にあるかと思います。

そのため、たとえクライアント証明書があったところで、これを用いて直接AWS APIを発行できるようになる可能性はゼロと考えた方がよいでしょう。