簡體   English   中英

如何在 HTTPS 客戶端中使用來自 Android KeyChain 的用戶證書?

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

這失敗了,因為encodedpk中是nullpk本身不是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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM