AndroidによるmTLS接続方法について自分用メモ

## mTLSとは
ざっくり言うと双方向で行うTLS認証である。

詳しくは[こちら](https://milestone-of-se.nesuke.com/nw-basic/tls/mutual-tls-token-binding/)

## 必要なもの
- android側
	- クライアントの秘密鍵
	- クライアント証明書
	- サーバ証明書のルート証明書

- サーバ側
	- サーバの秘密鍵
	- サーバ証明書
	- クライアント証明書のルート証明書

## 接続方法

1. クライアントの秘密鍵・証明書の生成

	生成方法はこちらの記事を参照

	- https://tech.at-iroha.jp/?p=734
	- https://android.benigumo.com/20201130/keytool/
	- https://qiita.com/KNaito/items/66dc67e15b71f2bb1f98

1. サーバ側の秘密鍵・証明書の生成
	
	AWSやGCPで行うのであれば、
	
	- https://aws.amazon.com/jp/about-aws/whats-new/2021/02/announcing-aws-app-mesh-controller-for-kubernetes-version-1-3-0-with-mtls-support/
	- https://cloud.google.com/architecture/using-mutual-tls-to-obtain-short-lived-credentials?hl=ja

	こちらを参考に行えばできるはずです。
	
1. クライアントを使いmTLS認証を行う

	1. [公式ドキュメント](https://developer.android.com/training/articles/security-ssl?hl=ja)を参考にサーバへアクセス
	2. KeyChain.choosePrivateKeyAliasを使ってクライアント証明書へアクセスし、秘密鍵とサーバ証明書のルート証明書を取得する。
	3. 秘密鍵とサーバ証明書のルート証明書を渡す。

	サンプルコード@webview

	```
	class MtlsWebViewClient(
    private val activity: Activity,
    private val trustAll: Boolean = false
) : WebViewClient() {
    override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) {
        KeyChain.choosePrivateKeyAlias(activity, { alias ->
            if (alias == null) {
                super.onReceivedClientCertRequest(view, request)
                return@choosePrivateKeyAlias
            }
            try {
                val certChain = KeyChain.getCertificateChain(activity, alias)
                val privateKey = KeyChain.getPrivateKey(activity, alias)
                if (certChain == null || privateKey == null) {
                    super.onReceivedClientCertRequest(view, request)
                } else {
                    request.proceed(privateKey, certChain)
                }
            } catch (e: Exception) {
                Log.e(
                    "MtlsWebViewClient",
                    "Error when getting CertificateChain or PrivateKey for alias '${alias}'",
                    e
                )
                super.onReceivedClientCertRequest(view, request)
            }

        }, request.keyTypes, request.principals, request.host, request.port, null)
    }

    override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
        if (trustAll) handler.proceed() else super.onReceivedSslError(view, handler, error)
    }
}
	```
	
	サンプルコード@API
	
	```
	class OkHttpFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_ok_http, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val resultTextView = view.findViewById<TextView>(R.id.text_view_result)
        val cardView = view.findViewById<CardView>(R.id.card_view)
        KeyChain.choosePrivateKeyAlias(requireActivity(), { alias ->
            val httpClient = OkHttpClient.Builder().apply {
                alias?.let {
                    val helper =
                        MtlsHelper(requireContext(), alias, trustAll = BuildConfig.SKIP_TLS_VERIFY)
                    sslSocketFactory(helper.sslSocketFactory, helper.trustManager)
                }
            }.build()

            val request =
                Request.Builder().url(BuildConfig.OK_HTTP_URL).build()
            val response = httpClient.newCall(request).execute()
            val responseBody = response.body ?: throw IOException("Response body is null")
            val result = responseBody.string()
            view.post {
                cardView.visibility = View.VISIBLE
                resultTextView.text = result
            }

            val clientCertificatePath = JSONObject(result).getString("client_certificate_path")
            val clientCertificateUrl = BuildConfig.OK_HTTP_URL.toHttpUrl()
                .newBuilder()
                .encodedPath(clientCertificatePath)
                .toString()
            val button = view.findViewById<Button>(R.id.button_download_cert)

            button.visibility = Button.VISIBLE
            button.setOnClickListener {
                val intent =
                    Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(clientCertificateUrl) }
                startActivity(intent)
            }
        }, null, null, null, null)

    }
}
class MtlsHelper internal constructor(context: Context, alias: String, trustAll: Boolean = false) {
    val trustManager: X509TrustManager
    val sslSocketFactory: SSLSocketFactory

    init {
        val trustManagers = if (trustAll) arrayOf(TrustAllTrustManager()) else systemTrustManagers()
        trustManager = trustManagers[0] as X509TrustManager

        val sslContext = SSLContext.getInstance("TLS")
        sslContext.init(arrayOf(keyManagerFromAlias(context, alias)), trustManagers, null)
        sslSocketFactory = sslContext.socketFactory
    }

    class CertificateNotFoundException(message: String?) : Exception(message)
    class PrivateKeyNotFoundException(message: String?) : Exception(message)

    @Throws(
        InterruptedException::class,
        KeyChainException::class,
        CertificateNotFoundException::class,
        PrivateKeyNotFoundException::class
    )
    private fun keyManagerFromAlias(context: Context, alias: String): KeyManager {
        val certChain = KeyChain.getCertificateChain(context, alias)
            ?: throw PrivateKeyNotFoundException("PrivateKey for alias '${alias}' not found")
        val privateKey = KeyChain.getPrivateKey(context, alias)
            ?: throw CertificateNotFoundException("Certificate for alias '${alias}' not found")
        return ClientCertKeyManager(alias, certChain, privateKey)
    }

    private fun systemTrustManagers(): Array<TrustManager> {
        val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
        factory.init(null as KeyStore?)
        val trustManagers = factory.trustManagers
        if (trustManagers.size != 1 || (trustManagers[0] !is X509TrustManager)) {
            throw IllegalStateException(
                "Unexpected default trust managers: ${Arrays.toString(trustManagers)}"
            )
        }

        return factory.trustManagers
    }
}
```
	
## 参考記事

- [Android Keystore システム](https://developer.android.com/training/articles/keystore?hl=ja)
- [KeyChain](https://developer.android.com/reference/android/security/KeyChain)
- [Android　ICS のキーストアアクセスの一元化](http://y-anz-m.blogspot.com/2012/03/androidics.html)
- [Android HTTPS two-way authentication (based on OkHttp + Retrofit + Rxjava)](https://www.programmersought.com/article/58093920050/)
- [鍵ストアファイルとアプリの署名に関する情報の整理](https://tech.at-iroha.jp/?p=734)
- [キーストアとエイリアスのパスワード確認
](https://qiita.com/KNaito/items/66dc67e15b71f2bb1f98)
- [【コピペ用】Android「Google アプリ署名」コマンドまとめ](https://android.benigumo.com/20201130/keytool/)
- [KotlinでSSLクライアント認証を実現する](https://qiita.com/rs_tukki/items/3c7abc4181e7ac58127f)
- [HTTPS と SSL を使用したセキュリティ](https://developer.android.com/training/articles/security-ssl?hl=ja)
- [p12ファイルの中身を確認する方法](http://www.asnm4.com/2018/01/p12%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%B8%AD%E8%BA%AB%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/)
- [TLSハンドシェイクでは何が起こるのか？| SSLハンドシェイク](https://www.cloudflare.com/ja-jp/learning/ssl/what-happens-in-a-tls-handshake/)
- [TCP/IP - TCP three-way handshaking](https://www.infraexpert.com/study/tcpip9.html)
- [mtls-android](https://github.com/diebietse/mtls-android)
- [Android 4.3 で Android Keystore を使う](https://qiita.com/Koganes/items/e8253f13ecb534ca11a1)
- [Android app client Mutual TLS with java server](https://stackoverflow.com/questions/58438885/android-app-client-mutual-tls-with-java-server)
- [Amazon API GatewayでmTLSを試してみた。](https://qiita.com/horit0123/items/8eb45bfcef6b848971a4)
- [相互 TLS を使用して有効期間の短い認証情報を取得する](https://cloud.google.com/architecture/using-mutual-tls-to-obtain-short-lived-credentials?hl=ja)
- [mTLS（Mutual TLS）メモ](https://blog.bobuhiro11.net/2021/01-12-mtls.html[]([https://blog.bobuhiro11.net/2021/01-12-mtls.html](https://blog.bobuhiro11.net/2021/01-12-mtls.html)))