mTLSとは
ざっくり言うと双方向で行うTLS認証である。
詳しくはこちら
必要なもの
-
android側
- クライアントの秘密鍵
- クライアント証明書
- サーバ証明書のルート証明書
-
サーバ側
- サーバの秘密鍵
- サーバ証明書
- クライアント証明書のルート証明書
接続方法
-
クライアントの秘密鍵・証明書の生成
生成方法はこちらの記事を参照
-
サーバ側の秘密鍵・証明書の生成
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 システム](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)))