Android Keystoreを利用したAzure IoTへのセキュアな接続

### はじめに

IoTデバイスとして、さらには他のIoTデバイスの表示/操作器としてAndroidデバイスは手ごろな選択肢となる。
IoTデバイスで用いられるMQTT接続をAndroidデバイスから行ってみたい。
その際、Azure IoTを利用し、TLS相互接続によるセキュアな接続ができないか試した。

### Android Keystoreについて

Android KeyStoreは、Android 4.3（API レベル 18）以降で利用可能となった、Androidデバイス上で暗号鍵を安全に保管するためのシステムである。以下、主な特徴

1. セキュリティの強化: Android KeyStoreは、デバイスのセキュリティハードウェア（存在する場合）を利用してキーを保護します。これにより、キーがデバイス外部に露出するリスクを最小限に抑えます。

2. キーの生成と管理: アプリはKeyStoreを通じて、新しいキーを生成したり、既存のキーを取得したりできます。また、キーの使用方法（例えば、暗号化、署名など）に関する制限を設定することも可能です。

3. アクセスコントロール: KeyStoreはアプリケーションごとにキーを分離して保管し、他のアプリからの不正アクセスを防ぎます。アプリは自分が生成または取得したキーのみにアクセスできます。

4. ユーザー認証に基づく制約: 開発者は、特定のキーを使用するためにデバイスのユーザー認証（例えば、指紋認証、PINコードなど）を必要とするように設定できます。



https://developer.android.com/training/articles/keystore?hl=ja

### Keystoreのセキュリティ強化

キーがデバイス外部に露出するリスクを下げるため、Android デバイスのセキュア ハードウェア（Trusted Execution Environment（TEE））を利用できるようになっており、Android 9（API レベル 28）でさらに強化され、StrongBoxというオプションにより、セキュア エレメント（SE）を利用することができるようになる。

StrongBox対応しているデバイスとしては代表的なのはGoogle Pixelシリーズとなる。

自身の理解を図にすると下記の形。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/e30badc9-5af3-b1a3-1451-c2ea982caec5.png)


手持ちの京セラ　Digno BX は[Qualcomm SDM429](https://www.qualcomm.com/products/mobile/snapdragon/smartphones/snapdragon-4-series-mobile-platforms/snapdragon-429-mobile-platform)が搭載され、Qualcomm® Processor Securityというセキュリティ機能が内蔵されている。
その中で重要なのが、ARM TrustZoneに構築されたQSEEと呼ばれるTrusted Execution Environmentになる。

Androidデバイスは採用されているSoCが多岐にわたり、ハードウェア実装は様々となる。
実際のところ、どのデバイスが何のセキュリティ強化機能に対応しているかは、試してみないとはっきりしないようである。

Android Studioにて調べることができるコードを動かしてみた。

``` Kotlin
val keyFactory = KeyFactory.getInstance(keypair.public.algorithm, "AndroidKeyStore")
val keyInfo = keyFactory.getKeySpec(keypair.private, KeyInfo::class.java)

// Android 11 (APIレベル30）以前
val securityLevel = keyInfo.isInsideSecureHardware
if (securityLevel == true)
    Log.d("SecurityLevel","Security Level is StrongBox or TEE")
    else if (securityLevel == false)
    Log.d("SecurityLevel", "Security Level is Software")

// Android 12 (APIレベル31）以降
val securityLevel = keyInfo.securityLevel
if (securityLevel == KeyProperties.SECURITY_LEVEL_STRONG_BOX)
    Log.d("SecurityLevel","Security Level is StrongBox")
    else if (securityLevel == KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT)
    Log.d("SecurityLevel", "Security Level is TEE")
    else if (securityLevel == KeyProperties.SECURITY_LEVEL_SOFTWARE)
    Log.d("SecurityLevel", "Security Level is Software")
```

手持ちのDigno BXはAndroid10の環境だったので以下結果となった。

```
Security Level is StrongBox or TEE
```
Android Studio のシミュレーターでは、どちらも
```
Security Level is Software
```
となった。

StrongBoxが有効な環境では、鍵生成の際に以下のように記述し、セキュアエレメント（SE）での鍵保護を有効にする。
``` kotlin
        val keyPairGenerator = KeyPairGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_EC,
            "AndroidKeyStore"
        )

        val spec = KeyGenParameterSpec.Builder(
            keyPairAlias,
            KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
        )
            .setDigests(KeyProperties.DIGEST_NONE, KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
            .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
            .setIsStrongBoxBacked(true)
            .build()
        keyPairGenerator.initialize(spec)
```

setIsStrongBoxBacked(true)を追記するが、このオプションはStrongBoxが利用できないデバイス、シミュレーターではエラーとなるため、コメントアウトする。


### Azure IoTへの接続
こうして生成した鍵ペアを用いて、Azure IoTへ接続を行ってみる。
Azure IoTへの接続に関しては、Azure IoT Hub Device Provisioning Service（DPS）を経由したプロビジョニングを行って接続させる。


https://learn.microsoft.com/ja-jp/azure/iot-dps/about-iot-dps

接続の流れとしては下記の通り、Azure IoT Hubへの仲介をAzure IoT Hub Device Provisioning Serviceが行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/8b28321a-b04f-35e7-1b5a-469b553252b8.png)

今回Azure IoT SDKを利用しないMQTT接続を行う。

### MQTTを使ったAzure IoT DPSプロビジョニング

以下を参考にAndroid Kotlinによる構築を行う。

https://learn.microsoft.com/ja-jp/azure/iot/iot-mqtt-connect-to-iot-dps

以下流れでMQTT接続を作成する。事前にデバイスでのCSR生成、自己署名証明書と秘密鍵を用意し、CSRを署名しておく。


１． TLS相互認証接続を行うためのAndroid Keystoreの秘密鍵、署名されたデバイス証明書、CA証明書、サーバー認証のためのルートCA証明書を、接続に利用するPaho Android MQTTクライアントへ設定する。

``` kotlin
        val serverURI = "ssl://global.azure-devices-provisioning.net:8883"
        mqttClient = MqttAndroidClient(activity, serverURI, clientId )

        val mqttConnectOptions = MqttConnectOptions()
        mqttConnectOptions.isCleanSession = true
        mqttConnectOptions.isAutomaticReconnect = false
        mqttConnectOptions.userName = "$idscope/registrations/$clientId/api-version=2019-03-31"

        ~

            val MyCertStore = MyCertStore.getInstance()
            val signedCertificate = MyCertStore.signedCertificate
            val caCertificate = provideCACertificate()

            if (signedCertificate != null) {
                val keyStore = createKeyStore( signedCertificate, caCertificate)
            }
            else {
                Log.d("signedCertificate", "signedCertificate not found")
            }

            val keyManager = CustomX509ExtendedKeyManager(keyPairAlias)

            val caPemStrings = provideRootCertificates()
            val caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
                load(null, null)
                for ((index, caPemString) in caPemStrings.withIndex()) {
                    val caInputStream: InputStream = ByteArrayInputStream(caPemString.toByteArray(Charsets.UTF_8))
                    val caCert = CertificateFactory.getInstance("X.509").generateCertificate(caInputStream)
                    setCertificateEntry("ca$index", caCert)
                }
            }
            val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
            val trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm)
            trustManagerFactory.init(caKeyStore)

            val sslContext = SSLContext.getInstance("TLSv1.2")
            sslContext.init(arrayOf(keyManager), trustManagerFactory.trustManagers, null)
            // Create SniSSLSocketFactory
            val sslSocketFactory = sslContext.socketFactory
            val sniHostname = "global.azure-devices-provisioning.net"
            val sniSslSocketFactory = SniSSLSocketFactory(sslSocketFactory, sniHostname)

            // Set SniSSLSocketFactory in MqttConnectOptions
            mqttConnectOptions.socketFactory = sniSslSocketFactory


```

２． Azure IoT DPS グローバルエンドポイントへ接続後、デバイス登録のためのメッセージをパブリッシュする。

まず、パブリッシュした後、サブスクライブしているステータスを読み取るため、サブスクライブ結果を待ち合わせるためにCompletableFutureを利用する。その準備を行う。

``` kotlin
    suspend fun publishAndWait(client: MqttAndroidClient, topic: String, payload: String,  messageReceived: CompletableFuture<String>) {
        publishMessage(client, topic, payload)
        val receivedMessage = waitForMessage(messageReceived)
        println("Received message: $receivedMessage")
    }
```

初回の登録メッセージの作成と送信。
以下の通り、トピック名は既定のものを使用し、送信内容（ペイロード）は登録したいクライアントIDを記載する。

``` kotlin
            runBlocking {
                val topic =
                   "\$dps/registrations/PUT/iotdps-register/?\$rid=1"
                val messagePayload = "{\"registrationId\":\"$clientId\"}"
                publishAndWait(mqttClient, topic, messagePayload, message1Received)
                val messageobject =
                    Json.parseToJsonElement(message1Received.get()).jsonObject
                        dpsoperationId = messageobject["operationId"].toString().trim('"')
```

３． 登録内容を受け取るために応答待ち（ポーリング）を行う
ここでは、有効な応答を受け取るために、3秒待ち、ポーリングのためのトピックを読み取り待ち合わせを行う。
受け取ったJSONメッセージの中から、パースしてアサインされたAzure IoT HubのURLを抜き出す。
もし受け取れない場合、再度やり直す。本当はリトライの実装があるべきだが、今回は割愛。

``` kotlin
                delay(3000)
                val topic2 = 
                "\$dps/registrations/GET/iotdps-get-operationstatus/?\$rid=2&operationId=$dpsoperationId"
                val messagePayload2 = ""

                publishAndWait(
                    mqttClient,
                    topic2,
                    messagePayload2,
                    message2Received
                    )
                azureioturl = json.decodeFromString<azureioturlPayload>(message2Received.get()).registrationState.assignedHub
```

４． Azure IoT Hub DPSへの接続を切断し、取得したAzure IoT Hub URLを使って再接続をかける。

５． Azure IoT Hub上で、登録されたデバイスを見つけ、デバイスへのメッセージで通信を確認する。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/36b82c6d-cddc-ae74-9ebc-dffa862ba06d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/9c9253df-eaff-fb21-d5b5-971434992671.png)

まとめ
Android Keystoreを使ったAzure IoTへの接続をAndroid Keystoreでセキュアに行うことができた。
Azure IoT Hub DPSを用いれば、プロビジョニングのほか、負荷分散なども実現できる。
Android MQTTを使った接続でのプロビジョニングでCompletableFutureを使うことで、応答を待った次処理の開始を実現できた。