我的目標是在 Android 中開發一個小型 HTTPS 客戶端應用程式,允許用戶從 Android KeyChain 中選擇一個用戶證書,并向要求客戶端使用自己的證書進行身份驗證的服務器執行 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(本身不是)。nullpkpknull
進一步閱讀Android KeyStore 檔案后,出于安全原因,Android 中似乎有一些保護措施可以防止私鑰被匯出。
因此問題是:如何為我的 HTTPS 客戶端應用程式使用客戶端證書?
注意:如果需要,我愿意使用 OkHttp 以外的其他庫。
uj5u.com熱心網友回復:
我最終找到了要走的路。
首先,我們應該提供一個實作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 中:
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”替換為您自己的配置為需要客戶端證書的服務器來驗證它。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/511468.html
標籤:科特林ssl安卓密钥库
