2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-28

mTLSとは

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

詳しくはこちら

必要なもの

  • android側

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

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

接続方法

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

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

  2. サーバ側の秘密鍵・証明書の生成

    AWSやGCPで行うのであれば、

    こちらを参考に行えばできるはずです。

  3. クライアントを使い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
}

}

	
## 参考記事

- [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)))
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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?