AWS IoTで、クレームによるフリートプロビジョニング(CreateKeysAndCertificate)を使いデバイスの証明書を取得する

by

@wapa5pow

AWS IoTではデバイスを認証してAWS IoTのMQTTに接続する必要があります。
(以下が参考になります)

デバイスが1台の場合は、AWS IoTで発行したデバイス証明書と秘密鍵を使えばいいのですが、問題になるのは複数台になったときです。
デバイス複数台で同じデバイス証明書と秘密鍵を使うと、流出したときにすべてのデバイスの証明書を入れ替える必要があります。
よって、デバイスごとに異なるデバイス証明書と秘密鍵を使うのが本番環境では好ましいです。

では異なるデバイス証明書を発行するときにどの登録方式を使うのがいいのでしょうか。AWS IoTにおけるデバイスへの認証情報のプロビジョニングにまとまっています。

/assets/2025-01-10--aws-iot-fleet-provisioning/registration_methods.png

Amazonが管理するCAで証明書を発行し、製造時に同じ証明書をデバイスに登録する運用にしたい場合は、フリートプロビジョニングで登録します。
フリートプロビジョニングも2種類あり、クレームによるフリートプロビジョニングとユーザーによるフリートプロビジョニングがあります。
さらにクレームによるフリートプロビジョニングも2つの方法でデバイス証明書を取得します。まとめると以下です。

  • クレームによるフリートプロビジョニング: デバイスがクレーム証明書を使ってデバイス証明書を取得する。
  • ユーザーによるフリートプロビジョニング: デバイスがCognito経由でユーザを識別した状態でクレーム証明書を取得し、それを使ってデバイス証明書を取得する。

今回は、デバイスがCognitoで認証されていないことを想定、かつ、シンプルに実装するため、クレームによるフリートプロビジョニングで、CreateKeysAndCertificateを使います。
その場合、デバイスの出荷時に共通の「デバイス証明書発行のためのクレーム証明書と秘密鍵」をデバイスに設定し出荷します。
デバイスの初回のAWS IoT接続時に、クレーム証明書を使ってデバイス証明書と秘密鍵を生成して取得することができます。

詳細なフロー

AWS Black Belt Online Seminar - AWS IoT Core プロビジョニング編の資料の23ページ目にクレームによるフリートプロビジョニングのフロー図があります。

/assets/2025-01-10--aws-iot-fleet-provisioning/fleet_provisioning_by_claim.png

「パラメータ・H/Wシークレット・トークン」の部分はAPI名が書いてないですが、RegisterThingのことです。

設定

ドキュメントを参考にしながら実際の画面を見ながら設定していきます。

事前準備

AWS IoT Policyの作成

以下のポリシーを作ります。

  • クレーム証明書と秘密鍵を使ってAWS IoTにアクセスした時の権限を設定するポリシー (wapa5pow-claim-policy)
  • 生成されたデバイス証明書と秘密鍵を使ってAWS IoTにアクセスした時の権限を設定するポリシー(wapa5pow-device-policy)

wapa5pow-claim-policyの作成

証明書の取得は、量産デバイスや大量のデバイスに個別の認証情報を発行する方法についての記事によると以下のMQTTの通信をしています。

/assets/2025-01-10--aws-iot-fleet-provisioning/provisioning_flow.png

よって、ドキュメントにあるように以下のようなプロビジョニングに必要な権限のみついたポリシーを作成します。
(ポリシーの詳細な説明は、ドキュメントにあります。)
aws-regionaws-account-idtemplateNameは適当に入れ替えます。templateNameに関しては、wapa5pow-templateとします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": ["iot:Connect"],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": ["iot:Publish","iot:Receive"],
            "Resource": [
                "arn:aws:iot:aws-region:aws-account-id:topic/$aws/certificates/create/*",
                "arn:aws:iot:aws-region:aws-account-id:topic/$aws/provisioning-templates/templateName/provision/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "iot:Subscribe",
            "Resource": [
                "arn:aws:iot:aws-region:aws-account-id:topicfilter/$aws/certificates/create/*",
                "arn:aws:iot:aws-region:aws-account-id:topicfilter/$aws/provisioning-templates/templateName/provision/*"
            ]
        }
    ]
}

/assets/2025-01-10--aws-iot-fleet-provisioning/wapa5pow_claim_policy.png

wapa5pow-device-policy の作成

デバイス証明書にアタッチするポリシーですがこちらはプロジェクトに応じて必要に応じた権限をつけます。権限が広いと不要なトピックをサブスクライブできるなどセキュリティリスクがあるので必要なものだけつけておきます。
(実際の画面は省略します)

クレーム証明書と秘密鍵の作成

デバイス証明書を要求するためのクレーム証明書と秘密鍵を作成します。このクレーム証明書と秘密鍵はどのデバイスでも共通で使用します。

/assets/2025-01-10--aws-iot-fleet-provisioning/claim1.png
/assets/2025-01-10--aws-iot-fleet-provisioning/claim2.png
/assets/2025-01-10--aws-iot-fleet-provisioning/claim3.png

矢印のついているファイルをダウンロードしておきます。ここでは以下のファイル名とします。

  • Device certificate: claim.pem.cert
  • Public key file: public.pem.key
  • Private key file: private.pem.key
  • ROOT CA certificates: AmazonRootCA1.pem

Device certificateと名前がついていますが、実際はクレーム証明書として使います。
証明書一覧では、証明書自体に説明・名前・タグなど一切つけられないので、eb7などの元のファイル名のprefixを覚えておきます。

プロビジョニングテンプレートの作成

プロビジョニングテンプレートはクレーム証明書を使ってプロビジョニングするデバイス証明書のポリシーなど設定をするJSONです。

/assets/2025-01-10--aws-iot-fleet-provisioning/template1.png
/assets/2025-01-10--aws-iot-fleet-provisioning/template2.png
/assets/2025-01-10--aws-iot-fleet-provisioning/template3.png

Create new roleでProvisioning roleを作成しておきます。

/assets/2025-01-10--aws-iot-fleet-provisioning/template4.png
/assets/2025-01-10--aws-iot-fleet-provisioning/template5.png

Pre-provisioning actionsRegisterThingでデバイス証明書を有効にするときにラムダを呼び出し、各種デバイス証明書を要求しているクライアントが正しいかを確かめます。例えば、クライアントIDがデータベースに保存されているか確認してなければエラーを返すこともできます。

今回は単純にするために使わないようにします。本番環境では設定するのが望ましいです。

/assets/2025-01-10--aws-iot-fleet-provisioning/template6.png

Thing名にどのようなprefixをつけるかの設定です。Thing名を判別しやすくします。

/assets/2025-01-10--aws-iot-fleet-provisioning/template7.png

デバイス証明書の権限を設定するポリシーを選択します。

最後確認をし、いままで選択したものを確認したらテンプレートを作成します。

以下のJSONがテンプレートとして作成されました。ParametersのところにあるSerialNumberをデバイス自ら設定してクレーム証明書を使ってデバイス証明書を取得します。Parametersにはこれ以外にも様々なパラメータを設定でき、前述のpre-provisioning時のラムダで検証できたりします。

{
  "Parameters": {
    "SerialNumber": {
      "Type": "String"
    },
    "AWS::IoT::Certificate::Id": {
      "Type": "String"
    }
  },
  "Resources": {
    "policy_wapa5pow-device-policy": {
      "Type": "AWS::IoT::Policy",
      "Properties": {
        "PolicyName": "wapa5pow-device-policy"
      }
    },
    "certificate": {
      "Type": "AWS::IoT::Certificate",
      "Properties": {
        "CertificateId": {
          "Ref": "AWS::IoT::Certificate::Id"
        },
        "Status": "Active"
      }
    },
    "thing": {
      "Type": "AWS::IoT::Thing",
      "OverrideSettings": {
        "AttributePayload": "MERGE",
        "ThingGroups": "DO_NOTHING",
        "ThingTypeName": "REPLACE"
      },
      "Properties": {
        "AttributePayload": {},
        "ThingGroups": [],
        "ThingName": {
          "Fn::Join": [
            "",
            [
              "wapa5pow-template-",
              {
                "Ref": "SerialNumber"
              }
            ]
          ]
        }
      }
    }
  }
}

クレーム証明書を利用してデバイス証明書と秘密鍵を取得

準備が整ったので実際に、クライアントから接続してみましょう。

GitHubのaws/aws-iot-device-sdk-js-v2にAWS IoTのJavaScriptのSDKがあり、ここにサンプルコードがあるのでこれを使います。

git cloneしたら以下のコマンドでパッケージを入れます。

# rootのパッケージを入れる
npm install

# サンプルが入っているディレクトリでパッケージを入れる。
cd samples/node/fleet_provisioning
npm install

samples/node/fleet_provisioning以下にクライアント証明書や秘密鍵を入れます。

/assets/2025-01-10--aws-iot-fleet-provisioning/execute1.png

以下のコマンドを実行し、デバイス証明書を取得します。
xxx.iot.ap-northeast-1.amazonaws.com --cert certificate.pem.crt のxxxとなっている部分は、Connect -> Domain configurationsのDomain nameを使います。SerialNumberはデバイスのクライアントIDです。

node ./dist/index.js --endpoint xxx.iot.ap-northeast-1.amazonaws.com --cert certificate.pem.crt --key private.pem.key --template_name wapa5pow-template --template_parameters '{"SerialNumber":"1"}'

実行すると以下のような出力がでます。certificateIdにかかれている9d0xxxが作成されたデバイス証明書です。

Connecting...
Connected with Mqtt3 Client!
Subscribing to CreateKeysAndCertificate Accepted and Rejected topics..
Publishing to CreateKeysAndCertificate topic..
CreateKeysAndCertificateResponse for certificateId=9d0xxx
Subscribing to RegisterThing Accepted and Rejected topics..
Publishing to RegisterThing topic..
token=yyy
RegisterThingResponse for thingName=wapa5pow-template-1
Disconnecting...
Disconnected

実際に、証明書を確認してみるとあります。

/assets/2025-01-10--aws-iot-fleet-provisioning/execute2.png

Action -> Downloadを選択するとデバイス証明書も取得できます。ただし、秘密鍵は取得できないので、プログラムに手を加えデバイス証明書と秘密鍵を保存するようにします。

@@ -45,6 +45,8 @@ async function execute_keys(identity: iotidentity.IotIdentityClient, argv: Args)
                 if (response) {
                     if (argv.is_ci == false) {
                         console.log("CreateKeysAndCertificateResponse for certificateId=" + response.certificateId);
+                        fs.writeFileSync('device.pem.crt', response.certificatePem);
+                        fs.writeFileSync('device.pem.key', response.privateKey);
                     } else {
                         console.log("Got CreateKeysAndCertificateResponse");
                     }

再度、npm installを実行してコンパイルしてから同じコマンドを実行します。以下のようにデバイス証明書と秘密鍵が取得できました。

/assets/2025-01-10--aws-iot-fleet-provisioning/execute3.png

デバイス証明書と秘密鍵を使用してAWS IoTに接続する

先ほどと同じGitHubのaws/aws-iot-device-sdk-js-v2samples/node/pub_subフォルダ以下に行き、以下を実行します。

npm install

AmazonRootCA1.pem, device.pem.crt, device.pem.keyを同じフォルダ上に配置した状態で以下を実行します。

node dist/index.js --endpoint xxx.iot.ap-northeast-1.amazonaws.com --key device.pem.key --cert device.pem.crt --ca_file AmazonRootCA1.pem --client_id 1 --topic sdk/test/js

成功すれば以下のように返ってきます。

Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":1}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":2}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":3}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":4}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":5}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":6}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":7}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":8}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":9}
Publish received. topic:"sdk/test/js" dup:false qos:1 retain:false
Payload: {"message":"Hello world!","sequence":10}

その他考慮すべきところ

証明書の期限

AWS IoTで作成された証明書はクレーム証明書とデバイス証明書どちらとも2050年となっています。
デバイス証明書の期限をもっと短くしたい場合は、デバイス自体が秘密鍵を生成し、共通のCSR(Certificate Signing Request)を使ってデバイス証明書と秘密鍵を生成する必要があります。
以下、参考URLです。

クレーム証明書と秘密鍵が盗まれた場合

デバイスが盗まれた場合、デバイスに設定してあるクレーム証明書と秘密鍵が容易に取得できると、デバイス証明書と秘密鍵が発行できてしまいます。

ただし、Pre-provisioning hooksにより、クライアントIDを認証していた場合、デバイス証明書を取得するにはクライアントIDを知っている必要があり、かつ知っていても既存ですでに接続されていた場合、Cloud WatchでDUPLICATE_CLIENT_IDログが出る。クレーム証明書が盗まれて不正に使用されているかどうかは、このログを監視することにより確認できる。

不正にデバイス証明書が作られた場合は、クレーム証明書をリセットしたあとにすべてのデバイスに配布する必要がある。配布されたクライアントは再度デバイス証明書を取得し、それを使ってAWS IoTに接続する。

まとめると以下になりそうです。

  1. 新しいクレーム証明書と秘密鍵を各デバイスに配布する
  2. 既存のデバイス証明書を失効させる
  3. 各デバイスはデバイス証明書が失効している場合は、新しいクレーム証明書と秘密鍵でデバイス証明書を再度発行し接続する

ポリシーの制限

プロビジョニングで配布されるデバイス証明書にはポリシーが紐づけられています。本来デバイスごとにサブスクライブやパブリッシュできるトピックを制限すべきです。その場合、ポリシー変数というのが使えます。

以下、参考になるURLです。

まとめ

AWS IoTでデバイスをプロビジョニングする手段の、クレームによるフリートプロビジョニング(CreateKeysAndCertificate)を見てきました。最初調べたときは様々なところにドキュメントが散らばっていて探しにくかったのでこのドキュメントが役立てば嬉しく思います。