如何在 HTTPS 客户端中使用来自安卓钥匙串的用户证书?



我的目标是在Android中开发一个小型HTTPS客户端应用程序,允许用户从Android KeyChain中选择一个用户证书,并向要求客户端使用自己的证书进行身份验证的服务器执行HTTPS请求。

我已经使用SCEP服务器在Intune注册的Android 11设备中安装了用户证书,它在设置中正确显示:

系统设置中的用户证书

所有证书都有一个公钥和一个私钥。

根据Android KeyChain文档,我实现了这一点,让用户选择一个证书:

// Brings up the user certificate picker
KeyChain.choosePrivateKeyAlias(
this,   // activity
// Callback for the user selection
{
Log.d("choosePrivateKeyAlias", "User has chosen this alias: $it")
if (it != null) {
// Get private key and certificate chain
val pk = KeyChain.getPrivateKey(this, it)
val chain = KeyChain.getCertificateChain(this, it)
// TODO use full chain instead of only last certificate
val certEncoded =
"-----BEGIN CERTIFICATE-----n" +
Base64.toBase64String(chain!!.last().encoded) +
"n-----END CERTIFICATE-----n" +
"-----BEGIN PRIVATE KEY-----n" +
// Fails because encoded is null
Base64.toBase64String(pk!!.encoded) +
"n-----END PRIVATE KEY-----"

// Decode into HeldCertificate
val heldCertificate = HeldCertificate.decode(certEncoded)

// heldCertificate is then passed to OkHttp [...]
}
},
arrayOf("RSA"), null, "example.com", 443, null
)

这是因为encodedpk中的null(pk本身不是null(。

在进一步阅读了Android KeyStore文档后,Android中似乎有一些保护措施,可以防止出于安全原因导出私钥。

因此出现了一个问题:如何为HTTPS客户端应用程序使用客户端证书?

注意:如果需要,我愿意使用除OkHttp之外的其他库。

我最终找到了前进的道路。

首先,我们应该提供X509KeyManager:的实现

X509Impl.kt:

package com.example.myapp
import android.content.Context
import kotlin.Throws
import life.evam.configurationtest.X509Impl
import android.security.KeyChain
import android.security.KeyChainException
import java.lang.RuntimeException
import java.lang.UnsupportedOperationException
import java.net.Socket
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.Principal
import java.security.PrivateKey
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.KeyManager
import javax.net.ssl.SSLContext
import javax.net.ssl.X509KeyManager
class X509Impl(
private val alias: String,
private val certChain: Array<X509Certificate>,
private val privateKey: PrivateKey
) : X509KeyManager {
override fun chooseClientAlias(
arg0: Array<String>,
arg1: Array<Principal>,
arg2: Socket
): String {
return alias
}
override fun getCertificateChain(alias: String): Array<X509Certificate> {
return if (this.alias == alias) certChain else emptyArray()
}
override fun getPrivateKey(alias: String): PrivateKey? {
return if (this.alias == alias) privateKey else null
}
// Methods unused (for client SSLSocket callbacks)
override fun chooseServerAlias(
keyType: String,
issuers: Array<Principal>,
socket: Socket
): String {
throw UnsupportedOperationException()
}
override fun getClientAliases(keyType: String, issuers: Array<Principal>): Array<String> {
throw UnsupportedOperationException()
}
override fun getServerAliases(keyType: String, issuers: Array<Principal>): Array<String> {
throw UnsupportedOperationException()
}
companion object {
fun setForConnection(
con: HttpsURLConnection,
context: Context?,
alias: String
): SSLContext {
var sslContext: SSLContext? = null
sslContext = try {
SSLContext.getInstance("TLS")
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException("Should not happen...", e)
}
sslContext!!.init(arrayOf<KeyManager>(fromAlias(context, alias)), null, null)
con.sslSocketFactory = sslContext.getSocketFactory()
return sslContext
}
fun fromAlias(context: Context?, alias: String): X509Impl {
val certChain: Array<X509Certificate>?
val privateKey: PrivateKey?
try {
certChain = KeyChain.getCertificateChain(context!!, alias)
privateKey = KeyChain.getPrivateKey(context, alias)
} catch (e: KeyChainException) {
throw CertificateException(e)
} catch (e: InterruptedException) {
throw CertificateException(e)
}
if (certChain == null || privateKey == null) {
throw CertificateException("Can't access certificate from keystore")
}
return X509Impl(alias, certChain, privateKey)
}
}
}

然后我们可以使用它来创建一个socketFactory,以便在改装:中注入

MainActivity.kt:

val trustManager = HandshakeCertificates.Builder()
.addPlatformTrustedCertificates()
.build()
.trustManager
KeyChain.choosePrivateKeyAlias(
this,   // activity
// Callback for the user selection
{
if (it != null) {
Log.d("choosePrivateKeyAlias", "User has chosen this alias: $it")
val x509 = X509Impl.setForConnection(
URL("https://example.com/").openConnection() as HttpsURLConnection,
this, it
)
val socketFactory = x509.socketFactory
// Get private key and certificate chain
val pk = KeyChain.getPrivateKey(this, it)
val chain = KeyChain.getCertificateChain(this, it)
val pke = PrivateKeyEntry(pk, chain)
val interceptor = HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
var clientBuilder = OkHttpClient.Builder()
.addInterceptor(interceptor)
clientBuilder = clientBuilder
.sslSocketFactory(socketFactory, trustManager)
val client = clientBuilder.build()
val contentType = "application/json"
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://example.com/")
.client(client)
.addConverterFactory(Json.asConverterFactory(contentType = contentType.toMediaType()))
.build()
// Use this retrofit instance as usual

客户端证书现在附加到此改造实例的请求。您可以通过替换";example.com";在上面的代码中,由您自己的服务器配置为需要客户端证书。

最新更新