JITR(Just In Time Registration)でAWS IoT Coreにデバイスや証明書を登録し、APIGatewayでの認証にデバイス証明書を利用する

JITRからAPIGatewayで認証するまでを、自分なりに考えて実装してみました
2022.06.06

AWS IoT Coreの「JITR(Just In Time Registration)」の仕組みを利用してThingとデバイス証明書を登録し、APIGatewayでそのデバイス証明書を利用した認証をするところまで実装してみました。

TL;DR

  • JITRの仕組みを使って「AWS IoT Core」でデバイス証明書を簡単に管理できる
  • APIGatewayの認証でこの証明書を利用することでセキュアに接続できる
  • JITRではThingの登録ができないので、そこを実現するためにゴニョゴニョやり方を模索してみました

1.はじめに

APIGatewayにてデバイス証明書での認証をするためには「カスタムドメイン名」が必要です。本記事では用意できている前提で執筆します。
また、本エントリーで言及しきれていない箇所についてはこちらにコードを置いてあるのでご確認ください。

2.本記事の内容について

本記事で最終的に実現することは「TL;DR」にも一部記載している通り、JITRの流れでThingやデバイス証明書を「AWS IoT Core」に登録し、そのデバイス証明書をAPIGatewayの認証に利用するまで、を実際にやってみたものです。
本エントリーでは以下のような作業を実施します。

  • 1.CA証明書の登録
    • JITR、APIGatewayの認証の両方にて利用する証明書
    • JITRで利用するために「AWS IoT Core」に、APIGatewayにて利用するために「S3」にCA証明書を登録する
  • 2.必要なAWS側リソースを準備
    • ex.AWS IoT Core Policy/Lambda関数
  • 3.JITR、デバイス証明書の作成
  • 4.APIGatewayへの接続確認

処理のイメージはこんな感じです。

この方法でAPIGatewayの認証を実施すると、IoTCore側でのデバイス証明書のライフサイクルとAPIGatewayの認証が連携していない点に注意してください。(ex.IoTCore側でデバイス証明書を無効化してもAPIGatewayの認証に成功してしまう) IoTCoreでのデバイス証明書のライフサイクル管理とAPIGatewayの認証を連携させたい場合は、「AWS IoT Core認証情報プロバイダー」にてデバイスに必要なクレデンシャルを一時的に付与し、そのクレデンシャルを利用してAPIGateway(IAM認証)にリクエストする方法が良さそうです。

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/authorizing-direct-aws.html

3.やってみる

3-1.CA証明書の登録

以下を参考にして「AWS IoT Core」にCA証明書を登録します。

# キーペアを生成
openssl genrsa -out ./certs/rootCA.key 2048

# CA証明書を生成
# 値を入れるように促されるところは全てEnterでOK(本リポジトリに保存している「openssl.conf」に情報を事前に記載しておくこと)
openssl req -x509 -new -nodes -key ./certs/rootCA.key -sha256 -days 1024 -out ./certs/rootCA.pem -config openssl.conf -extensions v3_ca

# AWS IoTから登録コードを取得
CODE=`aws iot get-registration-code --output text`

# プライベートキー検証証明書のキーペアを生成
openssl genrsa -out ./certs/verificationCert.key 2048

# プライベートキー検証証明書のCSRを作成
VC_SUBJ="/C=JP/ST=Tokyo/L=Tokyo/O=MyOrg/OU=verification/CN=${CODE}"
openssl req -new -key ./certs/verificationCert.key -out ./certs/verificationCert.csr -subj "$VC_SUBJ"

# プライベートキー検証証明書を作成
openssl x509 -req -in ./certs/verificationCert.csr -CA ./certs/rootCA.pem -CAkey ./certs/rootCA.key -CAcreateserial -out ./certs/verificationCert.pem -days 500 -sha256

# AWS IoTにCA証明書を登録
aws iot register-ca-certificate \
--ca-certificate file://certs/rootCA.pem \
--verification-cert file://certs/verificationCert.pem \
--set-as-active \
--allow-auto-registration

CA証明書の「証明書の自動登録」を有効化しています。
これで、「このCA証明書を利用したJITRのリクエスト」はデバイス証明書が自動的にAWS IoT Coreに登録されるようになります。
また、APIGatewayでの認証にもこのCA証明書を利用するので、S3バケットにCA証明書を保存しておきます。

3-2.必要なAWS側リソースを準備

今回の検証をするために以下のリソースをデプロイします。

  • AWS IoT Core Policy
    • JITRとアプリケーション用の2種類
  • Lambda関数
    • JITR用の2つ、APIGatewayでの認証確認用1つ
  • APIGateway
    • カスタムドメインも必要

こちらのリポジトリをcloneして、「cdk/.env」で環境変数を変更した後に以下の通りデプロイしてください。

$ yarn
$ yarn deploy

3-3.デバイス証明書の作成とJITR

続いて、デバイス証明書を作成します。
CA証明書、鍵を使ってデバイス証明書に署名する必要があります。

# キーペアを生成
openssl genrsa -out ./certs/deviceCert.key 2048

# デバイス証明書のCSRを作成
openssl req -new -key ./certs/deviceCert.key -out ./certs/deviceCert.csr

# デバイス証明書を作成
openssl x509 -req -in ./certs/deviceCert.csr -CA ./certs/rootCA.pem -CAkey ./certs/rootCA.key -CAcreateserial -out ./certs/deviceCert.pem -days 500 -sha256
Point!
デバイス証明書の有効期限を指定できます(今回は500日間)。 今回と同じようなことを実現したい場合に「Fleet provisioning」も選択肢に上がりますが、「Fleet provisioning」では有効期限の指定ができない(本記事執筆時点)点に注意してください。

続いて、デバイス証明書をAWS IoT Coreに登録します。
これはIoTCoreでのJITRのお作法ですが、CA証明書とデバイス証明書を結合してリクエストします。
また、AWSのrootCAを事前に用意しておいてください。

# デバイス証明書とCA証明書を結合
cat ./certs/deviceCert.pem ./certs/rootCA.pem > ./certs/deviceCertAndCACert.crt

# JITR開始
curl --tlsv1.2 \
--cacert ./certs/amazonRootCa.cert \
--cert ./certs/deviceCertAndCACert.crt \
--key ./certs/deviceCert.key \
--request POST \
--data "{}" \
"https://{IoTCoreのデータエンドポイント}:8443/topics/jitr/start?qos=1"

初回リクエストは必ず失敗します。(AWSにこの証明書が登録されていないので)

# こんな返信がくる
curl: (52) Empty reply from server

AWS側はこのリクエストを受けたら非同期処理を開始します。
以下に本処理について補足しますので、気になる方は以下をクリックしてご参照ください。

ここをクリック
IoT Coreに登録しているCA証明書の「証明書の自動登録」を有効化している場合、この証明書で署名されたデバイス証明書での初回リクエスト時に、IoT Coreはデバイス証明書を自動的に登録します。 登録されるとそのイベント内容が「$aws/events/certificates/registered/#」という予約済みトピックに発行されるので、このメッセージをトリガーにIoT RuleでLambda関数を実行し、必要な処理を実行することができます。 (今回の実装では「デバイス証明書の有効化」、「IoT Policyをデバイス証明書にアタッチ」)

なお、MQTTプロトコルでリクエストしてもいいのですが、以下のように考えた結果HTTPプロトコルでリクエストしました。

- 実行完了がデバイス側で確認できるようにしたい
- MQTTプロトコルで、となるとconnectの権限も付与する必要がある
- しかし、connectを許可するためには「クライアントID: *」か「全デバイス固有のクライアントID」をIoTPolicyで指定する必要がある
- AWS IoT Coreの仕様として、あるクライアントIDで複数の接続が確認された場合は先に接続していた方のコネクションが切断されてしまいトラブルの元になる
- https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/audit-chk-conflicting-client-ids.html
- connectに「*」か「全デバイスに固有のクライアントID」のいずれを採用するとしても、この問題に遭遇しうる
- 今回はデバイスがAPIGatewayにHTTPリクエストすることが前提なので、HTTPリクエストとした
- MQTTのコネクションの権限不要なので

# AWS側ではJITRに伴いデバイス証明書が登録された際に、「$aws/events/certificates/registered/#」トピックに以下のようなメッセージが発行されます。  
{
  "certificateId": "xxxxxxxxxxxxxxxxxx",
  "caCertificateId": "yyyyyyyyyyyyyyyyyyy",
  "timestamp": 1654051712321,
  "certificateStatus": "PENDING_ACTIVATION",
  "awsAccountId": "123456789012",
  "certificateRegistrationTimestamp": null
}

今回の実装ではAWS側の処理が完了したらこのリクエストが成功するように権限を付与しているので、デバイス側はこの処理が成功するまで実行し続けます。

# こんな返信が来たらOK
{"message":"OK","traceId":"1870a050-b5f0-6fab-36ef-633e6790c64a"}

この処理完了時には、AWS側では以下の状態です。

  • デバイス証明書が登録&有効化
  • JITRに必要なIoTポリシーをデバイス証明書にアタッチ
    • 「jitr/start」、「jitr/request」トピックへのパブリッシュ権限
    • アプリケーションに必要なIoTポリシーはまだ付与されていない

続いて以下のリクエストを実行します。
この処理は正常終了します。(上記の通り、デバイス証明書に対してアタッチされているIoTポリシーに必要な権限が付与されているため)

curl --tlsv1.2 \
--cacert ./certs/amazonRootCa.cert \
--cert ./certs/deviceCertAndCACert.crt \
--key ./certs/deviceCert.key \
--request POST \
--data "{\"thingName\": \"{デバイスごとに一意のUUID}\"}" \
"https://{IoTCoreのデータエンドポイント}:8443/topics/jitr/request?qos=1"
# 権限が付与されているので以下のような返信が来る
{"message":"OK","traceId":"8f794d1b-895f-9c11-95bd-67aefd83f05f"}

この処理を受けて、AWS側では以下を実施するように実装してあります。

  • Thingの作成
    • デバイスが送信したUUIDをThingNameに利用する
  • Thingとデバイス証明書のアタッチ
  • デバイス証明書にアプリケーションに必要なIoTポリシーをアタッチ
  • デバイス証明書から不要になったIoTポリシーをデタッチ

上記を先ほどの「jitr/start」へのパブリッシュでまとめて処理しなかった理由は以下の2点です。

  • デバイス、AWS側でクライアントIDを連絡するために処理を2段階に分ける
  • JITR完了後はJITRに必要な権限は不要。そのためIoT PolicyをJITR用とアプリ用に分けて、アプリ用ポリシーがアタッチできたらJITR用ポリシーをデタッチする

最後に、ここまでの処理が完了していることを確認するために以下のリクエストを実施します。
先ほどの処理でデバイス証明書にアタッチしたIoTポリシーのおかげでパブリッシュできるトピックへのパブリッシュです。
これが正常終了したら、デバイス証明書登録やThingの作成までひと段落です。

mosquitto_pub \
--cafile ./certs/amazonRootCa.cert \
--cert ./certs/deviceCertAndCACert.crt \
--key ./certs/deviceCert.key \
-h {IoTCoreのデータエンドポイント} \
-p 8883 \
-q 1 \
-t things/{デバイスごとに一意のUUID}/confirmation \
-i {デバイスごとに一意のUUID} \
--tls-version tlsv1.2 \
-m "{}" -d

以下のようにコネクト、パブリッシュ成功の返信を受け取ればOK!

Client {デバイスごとに一意のUUID} sending CONNECT
Client {デバイスごとに一意のUUID} received CONNACK (0)
Client {デバイスごとに一意のUUID} sending PUBLISH (d0, q1, r0, m1, 'things/{デバイスごとに一意のUUID}/confirmation', ... (2 bytes))
Client {デバイスごとに一意のUUID} received PUBACK (Mid: 1, RC:0)
Client {デバイスごとに一意のUUID} sending DISCONNECT

これでデバイスはIoTCoreにMQTTでセキュアに接続できます。

3-4.APIGatewayへの接続確認

「1.はじめに」で述べたとおり、APIGatewayでクライアント証明書を使った認証をするためにはカスタムドメインを設定する必要がありますが、本エントリーではこの手順は完了していることを前提として進めます。 カスタムドメインの設定が完了すれば、「3-2.必要なAWS側リソースを準備」で作成したAPIGatewayにて作成したAPIGatewayでデバイス証明書を利用して認証できるようになっています。
今回は認証できていることを確認したいだけなので、APIGatewayの裏には以下のようなシンプルなLambda関数があるだけです。

export const handler = async(): Promise => {
    try {
        const res = {
            statusCode: 200,
            body: '{"message": "Good."}',
        };
        return res;
    } catch (error: unknown) {
        console.log({error})
        throw new Error('Failed in handler.');
    }
};

「デバイス証明書の有無」でそれぞれリクエストしてみます。

# 証明書有でリクエスト
curl -i \
--key ./certs/deviceCert.key \
--cert ./certs/deviceCertAndCACert.crt \
-X GET "https://{APIGatewayのカスタムドメイン}/sample"

成功!

{"message": "Good."}
# 証明書無でリクエスト
curl -X GET "https://{APIGatewayのカスタムドメイン}/sample"

失敗!

curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to {APIGatewayのカスタムドメイン}:443

いい感じですね!

注意
APIGatewayのカスタムドメインにこのような認証方式を設定しても、AWSが自動で発行するAPIGatewayのエンドポイントには認証なしでリクエストできてしまいます。 AWSが自動で発行するAPIGatewayのエンドポイントは無効にしておきましょう。

4.まとめ

JITRでデバイス証明書を登録し、その流れでThingを作成しました。また、APIGatewayの認証にデバイス証明書を利用することでデバイスはAPIにセキュアに接続できます。
JITRでは証明書の登録を簡単にできますが、Thing作成やThingNameをデバイスとAWSで連絡する方法については一手間考える必要がありそうです。

5.参照