Android 9 以下で最低 TLS バージョンが 1.3 の API へアクセスさせてみる

2024.04.14

いわさです。

最近 AWS の様々なサービスで TLS 1.3 をサポートするようになりました。
その多くは TLS 1.2 を最小バージョンとしつつ TLS 1.3 も使えるというポリシーが多いですが、一部のサービスは TLS 1.3 を最小バージョンとすることも可能です。

その兼ね合いから TLS 1.3 をサポート出来ないクライアントを調べていたのですが、Android の古いバージョンでサポートされていないことを知りました。

どうやら Android 10 (API 29) 以降はサポートされているようです。

やってみましょう

ALB のセキュリティポリシーで最低 TLS バージョンに 1.3 を指定することが可能なので、そちらで適当な API をホスティングしてモバイルアプリケーションからアクセスさせてみます。

モバイルアプリの実装は以下です。
今回はOkHttpなどは使わずに標準のHttpsURLConnectionで検証を行っています。
なお、実装は Claude 3 Sonnet 様に手伝ってもらいました。

MainActivity.kt

package com.example.hoge0414tls

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.hoge0414tls.ui.theme.Hoge0414tlsTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.URL
import javax.net.ssl.HttpsURLConnection

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Hoge0414tlsTheme {
                val responseLines = remember { mutableStateListOf<String>() }
                Column(
                    modifier = Modifier.fillMaxSize().padding(16.dp)
                ) {
                    Button(onClick = { sendHttpsRequest(responseLines) }) {
                        Text("Send HTTPS Request")
                    }
                    responseLines.forEach { line ->
                        Text(line)
                    }
                }
            }
        }
    }

    private fun sendHttpsRequest(responseLines: MutableList<String>) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val url = URL("https://hoge0414.tak1wa.com/")
                val connection = url.openConnection() as HttpsURLConnection
                connection.requestMethod = "GET"

                val responseCode = connection.responseCode
                if (responseCode == HttpsURLConnection.HTTP_OK) {
                    val inputStream = connection.inputStream
                    val reader = BufferedReader(InputStreamReader(inputStream))
                    var line: String?
                    while (reader.readLine().also { line = it } != null) {
                        withContext(Dispatchers.Main) {
                            responseLines.add(line!!)
                        }
                    }
                } else {
                    withContext(Dispatchers.Main) {
                        responseLines.add("Error: $responseCode")
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    responseLines.add("Error: ${e.message}")
                }
            }
        }
    }
}

ボタンを押したら ALB パブリックなエンドポイントへ GET リクエストを送信するだけのものです。
こちらのアプリーケーションを Android 9 (API 28) と Android 10 (API 29) で動作確認してみます。

最低 TLS バージョン 1.2 (ELBSecurityPolicy-TLS13-1-2-2021-06)

まずは最低 TLS バージョンが 1.2 のセキュリティポリシーであるELBSecurityPolicy-TLS13-1-2-2021-06から動作確認してみます。

サポートされる TLS バージョンは次のようになっています。

% sslscan https://hoge0414.tak1wa.com/
Version: 2.1.3
OpenSSL 3.2.1 30 Jan 2024

Connected to 18.176.40.218

Testing SSL server hoge0414.tak1wa.com on port 443 using SNI name hoge0414.tak1wa.com

  SSL/TLS Protocols:
SSLv2     disabled
SSLv3     disabled
TLSv1.0   disabled
TLSv1.1   disabled
TLSv1.2   enabled
TLSv1.3   enabled

モバイルアプリから実行してみたところ、どちらも正常にレスポンスを受信することが出来ました。

最低 TLS バージョン 1.3 (ELBSecurityPolicy-TLS13-1-3-2021-06)

次に最低 TLS バージョンを 1.3 に変更して検証してみましょう。

サポートされる TLS バージョンは次のようになっています。

% sslscan https://hoge0414.tak1wa.com/
Version: 2.1.3
OpenSSL 3.2.1 30 Jan 2024

Connected to 54.248.138.32

Testing SSL server hoge0414.tak1wa.com on port 443 using SNI name hoge0414.tak1wa.com

  SSL/TLS Protocols:
SSLv2     disabled
SSLv3     disabled
TLSv1.0   disabled
TLSv1.1   disabled
TLSv1.2   disabled
TLSv1.3   enabled

モバイルアプリで確認してみます。
Android 10 (API 29) だと先ほどと同様に正常にアクセス出来ます。TLS 1.3 で通信が出来ていますね。

Android 9 (API 28) の場合、ハンドシェイクエラーが発生しました。

ドキュメントに記載のとおり Android 9 以下は標準では TLS 1.3 が使えないようですね。

Android 9 以下で TLS 1.3 は使えないのか?

とはいえ、モバイルアプリはレガシー OS バージョンを切り捨てにくいのが悩ましいところ。
シェアにもよると思いますが、どうしても TLS 1.3 のみをサポートするサーバーでレガシー OS をサポートする必要がある場合はどうしたらよいでしょうか。

どうやら調べてみたところ、最新のセキュリティモジュールを使いたい場合はセキュリティプロバイダに Conscrypt を使うことで古い API バージョンでも利用出来るようになるとのこと。

Conscrypt.newProvider()で取得したセキュリティプロバイダを HTTP クライアントで使えるようにすることで対応出来そうです。
一部のドキュメントではSecurity.addProvider(Conscrypt.newProvider())が使われていましたが、私が検証したところそれだとデフォルトプロバイダが使われてしまいました。

次は OkHttp の例ではありますが、HttpsURLConnection の実装までは見てないですが Security モジュールが使われていれば同じように動作するだろうということでSecurity.insertProviderAtで優先使用させるようにしてみます。

MainActivity.kt

package com.example.hoge0414tls

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.hoge0414tls.ui.theme.Hoge0414tlsTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.URL
import javax.net.ssl.HttpsURLConnection
import org.conscrypt.Conscrypt
import java.security.Security

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Hoge0414tlsTheme {
                val responseLines = remember { mutableStateListOf<String>() }
                Column(
                    modifier = Modifier.fillMaxSize().padding(16.dp)
                ) {
                    Button(onClick = { sendHttpsRequest(responseLines) }) {
                        Text("Send HTTPS Request")
                    }
                    responseLines.forEach { line ->
                        Text(line)
                    }
                }
            }
        }
    }

    private fun sendHttpsRequest(responseLines: MutableList<String>) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val conscrypt = Conscrypt.newProvider()
                Security.insertProviderAt(conscrypt, 1)
                val url = URL("https://hoge0414.tak1wa.com/")
                val connection = url.openConnection() as HttpsURLConnection
                connection.requestMethod = "GET"

                val responseCode = connection.responseCode
                if (responseCode == HttpsURLConnection.HTTP_OK) {
                    val inputStream = connection.inputStream
                    val reader = BufferedReader(InputStreamReader(inputStream))
                    var line: String?
                    while (reader.readLine().also { line = it } != null) {
                        withContext(Dispatchers.Main) {
                            responseLines.add(line!!)
                        }
                    }
                } else {
                    withContext(Dispatchers.Main) {
                        responseLines.add("Error: $responseCode")
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    responseLines.add("Error: ${e.message}")
                }
            }
        }
    }
}

実行してみると次のように Android 9 (API 28) でも TLS 1.3 のみを許可するサーバーへ通信することが出来ました。

今回はデフォルトプロバイダよりも前に設定しているので、他に副作用が出る可能性もありそうですが、Conscrypt を利用することでサポートされていない OS バージョンでも通信させることが出来ました。

さいごに

本日は Android 9 以下で最低 TLS バージョンが 1.3 の API へアクセスさせてみました。

大人しく古い OS が切り捨てれないかまず検討したいところですが、どうにもならない場合のワークアラウンドを検討する際の情報として今回の検証結果は覚えておきたいと思います。