Android 9 以下で最低 TLS バージョンが 1.3 の API へアクセスさせてみる
いわさです。
最近 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 様に手伝ってもらいました。
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
で優先使用させるようにしてみます。
- How to enable TLSv1.3 for OkHttp 3.12.x on Android 8/9? - Stack Overflow
- How integrate the new conscrypt library to Android and use it by default - Stack Overflow
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 が切り捨てれないかまず検討したいところですが、どうにもならない場合のワークアラウンドを検討する際の情報として今回の検証結果は覚えておきたいと思います。