はじめに
IoTデバイスとして、さらには他のIoTデバイスの表示/操作器としてAndroidデバイスは手ごろな選択肢となる。
IoTデバイスで用いられるMQTT接続をAndroidデバイスから行ってみたい。
その際、Azure IoTを利用し、TLS相互接続によるセキュアな接続ができないか試した。
Android Keystoreについて
Android KeyStoreは、Android 4.3(API レベル 18)以降で利用可能となった、Androidデバイス上で暗号鍵を安全に保管するためのシステムである。以下、主な特徴
-
セキュリティの強化: Android KeyStoreは、デバイスのセキュリティハードウェア(存在する場合)を利用してキーを保護します。これにより、キーがデバイス外部に露出するリスクを最小限に抑えます。
-
キーの生成と管理: アプリはKeyStoreを通じて、新しいキーを生成したり、既存のキーを取得したりできます。また、キーの使用方法(例えば、暗号化、署名など)に関する制限を設定することも可能です。
-
アクセスコントロール: KeyStoreはアプリケーションごとにキーを分離して保管し、他のアプリからの不正アクセスを防ぎます。アプリは自分が生成または取得したキーのみにアクセスできます。
-
ユーザー認証に基づく制約: 開発者は、特定のキーを使用するためにデバイスのユーザー認証(例えば、指紋認証、PINコードなど)を必要とするように設定できます。
Keystoreのセキュリティ強化
キーがデバイス外部に露出するリスクを下げるため、Android デバイスのセキュア ハードウェア(Trusted Execution Environment(TEE))を利用できるようになっており、Android 9(API レベル 28)でさらに強化され、StrongBoxというオプションにより、セキュア エレメント(SE)を利用することができるようになる。
StrongBox対応しているデバイスとしては代表的なのはGoogle Pixelシリーズとなる。
手持ちの京セラ 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が行う。
今回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上で、登録されたデバイスを見つけ、デバイスへのメッセージで通信を確認する。
まとめ
Android Keystoreを使ったAzure IoTへの接続をAndroid Keystoreでセキュアに行うことができた。
Azure IoT Hub DPSを用いれば、プロビジョニングのほか、負荷分散なども実現できる。
Android MQTTを使った接続でのプロビジョニングでCompletableFutureを使うことで、応答を待った次処理の開始を実現できた。