mTLSとは
ざっくり言うと双方向で行うTLS認証である。
詳しくはこちら
必要なもの
-
android側
- クライアントの秘密鍵
- クライアント証明書
- サーバ証明書のルート証明書
-
サーバ側
- サーバの秘密鍵
- サーバ証明書
- クライアント証明書のルート証明書
接続方法
-
クライアントの秘密鍵・証明書の生成
生成方法はこちらの記事を参照
- https://tech.at-iroha.jp/?p=734
- https://android.benigumo.com/20201130/keytool/
- https://qiita.com/KNaito/items/66dc67e15b71f2bb1f98
-
サーバ側の秘密鍵・証明書の生成
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
こちらを参考に行えばできるはずです。
-
クライアントを使いmTLS認証を行う
- 公式ドキュメントを参考にサーバへアクセス
- KeyChain.choosePrivateKeyAliasを使ってクライアント証明書へアクセスし、秘密鍵とサーバ証明書のルート証明書を取得する。
- 秘密鍵とサーバ証明書のルート証明書を渡す。
サンプルコード@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 システム
- KeyChain
- Android ICS のキーストアアクセスの一元化
- Android HTTPS two-way authentication (based on OkHttp + Retrofit + Rxjava)
- 鍵ストアファイルとアプリの署名に関する情報の整理
- キーストアとエイリアスのパスワード確認
- 【コピペ用】Android「Google アプリ署名」コマンドまとめ
- KotlinでSSLクライアント認証を実現する
- HTTPS と SSL を使用したセキュリティ
- p12ファイルの中身を確認する方法
- TLSハンドシェイクでは何が起こるのか?| SSLハンドシェイク
- TCP/IP - TCP three-way handshaking
- mtls-android
- Android 4.3 で Android Keystore を使う
- Android app client Mutual TLS with java server
- Amazon API GatewayでmTLSを試してみた。
- 相互 TLS を使用して有効期間の短い認証情報を取得する
- mTLS(Mutual TLS)メモ