LoginSignup
0
0

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

Last updated at Posted at 2023-12-18

はじめに

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コードなど)を必要とするように設定できます。

Keystoreのセキュリティ強化

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

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

自身の理解を図にすると下記の形。
image.png

手持ちの京セラ Digno BX はQualcomm SDM429が搭載され、Qualcomm® Processor Securityというセキュリティ機能が内蔵されている。
その中で重要なのが、ARM TrustZoneに構築されたQSEEと呼ばれるTrusted Execution Environmentになる。

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

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

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)での鍵保護を有効にする。

        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)を経由したプロビジョニングを行って接続させる。

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

image.png

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

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

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

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

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

        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


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

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

    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を記載する。

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

                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

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

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

image.png
image.png

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0