[英]I want to use Android KeyChain to store public key and private key
[英]How can I use a User Certificate from Android's KeyChain in a HTTPS client?
我的目標是在 Android 中開發一個小型 HTTPS 客戶端應用程序,它允許用戶從 Android KeyChain select 獲取他們的一個用戶證書,並向服務器執行 HTTPS 請求,要求客戶端使用自己的證書進行身份驗證。
我已經使用 SCEP 服務器在 Intune 中注冊的 Android 11 設備中安裝了用戶證書,它在設置中正確顯示:
所有證書都有 1 個公鑰和 1 個私鑰。
按照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
)
這失敗了,因為encoded
在pk
中是null
( pk
本身不是null
)。
進一步閱讀Android KeyStore 文檔后,似乎 Android 中有一些保護措施可以防止出於安全原因導出私鑰。
因此出現了問題:如何為我的 HTTPS 客戶端應用程序使用客戶端證書?
注意:如果需要,我願意使用 OkHttp 以外的其他庫。
我最終找到了通往 go 的路。
首先,我們應該提供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
注入到 Retrofit 中:
主要活動.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
客戶端證書現在附加到從此 retrofit 實例發出的請求。 您可以通過將上面代碼中的“example.com”替換為您自己配置為需要客戶端證書的服務器來驗證它。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.