2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-28

mTLSとは

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

詳しくはこちら

必要なもの

  • 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. 公式ドキュメントを参考にサーバへアクセス
    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
    }
    }
    

参考記事

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1