署名検証付きカスタムオーソライザー(ユーザー名/パスワード)を使用して AWS Iot Core に接続する方法


## 概要

IoT Core はカスタムオーソライザーを使用して認証し、IoT Core に接続することができます。
カスタムオーソライザーは署名の検証を行うことができ、また実装方法によっては
ユーザー名とパスワードを使用して認証することができます。

本記事は、この実装に関する備忘録です。

## 前提条件

-   Mac 環境であること
-   AWS のリソース作成は CDK を利用
-   Iot Core への接続認証は MQTTX のソフトを利用

## 手順

### CDK のプロジェクトを作成

適当なディレクトリを作成します。

```shell
mkdir cdk-iot-custom-auth && cd cdk-iot-custom-auth
```

CDK プロジェクトを作成します。

```shell
cdk init app --language typescript
```

### オーソライザー用の Lambda の作成

Lambda のコード格納先のディレクトリを作成します。

```shell
mkdir -p lambda/custom-auth && touch lambda/custom-auth/index.ts
```

AWS のドキュメントをもとに認証様の関数を作成する。[参照](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/custom-auth-lambda.html)

この Lambda は IoT Core のポリシーを返却する必要があります。
password=test の場合、publish/subscribe などの操作を許可する IoT ポリシーを返却します。

```typescript:lib/cdk-iot-custom-auth-stack.js
exports.handler = function (event, context, callback) {
  var uname = event.protocolData.mqtt.username;
  var pwd = event.protocolData.mqtt.password;
  var buff = Buffer.from(pwd, "base64");
  var passwd = buff.toString("ascii");
  switch (passwd) {
    case "test":
      callback(null, generateAuthResponse(passwd, "Allow"));
      break;
    default:
      callback(null, generateAuthResponse(passwd, "Deny"));
  }
};

interface AuthResponse {
  isAuthenticated: boolean;
  principalId: string;
  disconnectAfterInSeconds: number;
  refreshAfterInSeconds: number;
  policyDocuments: [
    {
      Version: string;
      Statement: Statement[];
    }
  ];
}

interface Statement {
  Action: string;
  Effect: "Allow" | "Deny";
  Resource: string;
}

// Helper function to generate the authorization response.
const generateAuthResponse = function (token, effect) {
  const authResponse: AuthResponse = {
    isAuthenticated: effect === "Allow" ? true : false,
    principalId: "TEST123",
    policyDocuments: [
      {
        Version: "2012-10-17",
        Statement: [],
      },
    ],
    disconnectAfterInSeconds: 3600,
    refreshAfterInSeconds: 300,
  };

  const publishStatement: Statement = {
    Action: "iot:Connect",
    Effect: effect,
    Resource: "*",
  };
  const connectStatement: Statement = {
    Action: "iot:Publish",
    Effect: effect,
    Resource: "*",
  };
  const subscribeStatement: Statement = {
    Action: "iot:Subscribe",
    Effect: effect,
    Resource: "*",
  };
  const receiveStatement: Statement = {
    Action: "iot:Receive",
    Effect: effect,
    Resource: "*",
  };
  authResponse.policyDocuments[0].Statement[0] = connectStatement;
  authResponse.policyDocuments[0].Statement[1] = publishStatement;
  authResponse.policyDocuments[0].Statement[2] = subscribeStatement;
  authResponse.policyDocuments[0].Statement[3] = receiveStatement;

  return authResponse;
};
```

password=test の場合、publish/subscribe などを許可する、iot ポリシーを返却するようにします。
上記のコードで返却される、Iot ポリシーは以下になります。

```json:Iotポリシー
{
    "isAuthenticated": true,
    "principalId": "TEST123",
    "policyDocuments": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "iot:Publish",
                "Effect": "Allow",
                "Resource": "*"
            },
            {
                "Action": "iot:Connect",
                "Effect": "Allow",
                "Resource": "*"
            },
            {
                "Action": "iot:Subscribe",
                "Effect": "Allow",
                "Resource": "*"
            },
            {
                "Action": "iot:Receive",
                "Effect": "Allow",
                "Resource": "*"
            }
        ]
    },
    "disconnectAfterInSeconds": 3600,
    "refreshAfterInSeconds": 300
}
```

CDK にて Lambda を定義します。

```typescript:lib/cdk-iot-custom-auth-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";

export class CdkIotCustomAuthStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // オーソライザー用の関数を作成
    const iotAuthFunc = new nodejs.NodejsFunction(this, "IotAuthFunc", {
      functionName: "iot-custom-auth",
      runtime: lambda.Runtime.NODEJS_22_X,
      entry: "lambda/custom-auth/index.ts",
    });
  }
}
```

### オーソライザー用の公開鍵と秘密鍵の作成

カスタムオーソライザーでは、認証に公開鍵と秘密鍵が必要なので、それぞれ作成していきます。

1. 鍵を格納するためのディレクトリを作成します

    ```shell
    mkdir secret
    ```

2. 秘密鍵を作成します。

    ```shell
    openssl genrsa -out secret/private-key.pem 4096
    ```

3. 秘密鍵に問題がないか検証します。

    ```shell
    openssl rsa -check -in secret/private-key.pem -noout
    ```

4. 公開鍵を作成します。

    ```shell
    openssl rsa -in secret/private-key.pem -pubout -out secret/public-key.pem
    ```

5. 公開鍵に問題がないか確認します。

    ```shell
    openssl pkey -inform PEM -pubin -in secret/public-key.pem -noout
    ```

    特にエラーが表示されなければ問題ないです。

### カスタムオーソライザーの作成

CDK にて、カスタムオーソライザーを定義します。
この際、署名の検証を有効にします。

```typescript:lib/cdk-iot-custom-auth-stack.js
import * as iot from "aws-cdk-lib/aws-iot";

//<一部省略>

// カスタムオーソライザーの作成
const iotCustomAuthorizer = new iot.CfnAuthorizer(this, "IotCustomAuthorizer", {
    authorizerName: "iot-custom-auth",
    status: "ACTIVE",
    authorizerFunctionArn: iotAuthFunc.functionArn,
    signingDisabled: false,
    tokenKeyName: "authorizer-token",
    tokenSigningPublicKeys: {
        Key1: process.env.PUBLIC_KEY || "",
    },
});
```

また、PUBLIC_KEY の環境変数を設定するよう、以下のコマンドも実行しておきます。

```shell:terminal
export PUBLIC_KEY="$(cat secret/public-key.pem)"
```

### Iot Core に Lambda 関数の呼び出しを許可する。

カスタムオーソライザーに Lambda 関数を実行するための許可を付与します

```typescript:lib/cdk-iot-custom-auth-stack.js
    // IotにLambda関数の実行許可を付与
    iotAuthFunc.addPermission("iotAuthFuncPermission", {
      principal: new iam.ServicePrincipal("iot.amazonaws.com"),
      sourceArn: iotCustomAuthorizer.attrArn,
      action: "lambda:InvokeFunction",
    });
```

### AWS のリソースを生成

CDK の準備が完了しましたので、`cdk deploy`を実行し、リソースを生成していきます。
この際、`PUBLIC_KEY`に環境変数を設定しているか、再確認してください。

### MQTTX を使用して接続確認をする

MQTTX をインストールし、MQTTX のアプリを開きます。
インストールは以下の記事がわかりやすかいと思います。

https://qiita.com/emqx_japan/items/1ff6097fdfe273c5e22f

接続に以下の情報が必要になるので、各情報を取得します。

-   AWS の Iot エンドポイント
-   トークンの署名

#### AWS の Iot エンドポイントの確認

CLI の以下のコマンドを実行し、エンドポイントの値を取得します。

```shell
aws iot describe-endpoint --endpoint-type iot:Data-ATS
```

#### トークンの署名

次のコマンドを実行してトークンに署名をしていきます。

```shell
echo -n TOKEN_VALUE | openssl dgst -sha256 -sign PEM encoded RSA private key | openssl base64
```

本環境で実際に実行したコマンドはこちらになります。
また、トークンの署名の値は変数に格納しておきます。

```shell
TOKEN_SIGNATURE=$(echo -n "Key1" | openssl dgst -sha256 -sign secret/private-key.pem | openssl base64 -A)
```

署名したトークンの値を使用して MQTT 接続するには、署名したトークンの値を URL エンコードする必要があるので、
以下のコマンドを実行し、エンコードしていきます。

```shell
echo -n $TOKEN_SIGNATURE | jq -sRr @uri
```

出力結果をメモしておきます。

#### MQTTX から Iot Core への接続

接続に必要な情報が集まったので、MQTTX から Iot Core に接続していきます。

1. MQTTX にて、「新しい接続」を押下する
2. 各項目に以下の値を設定し、「接続」を押下する。

    - 名前：<任意>
    - ホスト：wss://<AWS Iot エンドポイント>
    - Path：/mqtt
    - ユーザ名：<ユーザ名>?x-amz-customauthorizer-name=<オーソライザー名>&x-amz-customauthorizer-signature=<エンコード後の TOKEN_SIGNATURE>&<トークンのキー名>=<トークンの値>
        - 例：mqttuser?x-amz-customauthorizer-name=iot-custom-auth&x-amz-customauthorizer-signature=<エンコード後の TOKEN_SIGNATURE>&authorizer-token=Key1
    - パスワード：<パスワード>
        - 例：test
    - SSL/TLS：有効
    - SSL 証明書：無効

無事に接続できれば成功です！