簡體   English   中英

Xamarin Forms (Android) 來自 KeyStore 與 PFX 文件的客戶端證書

[英]Xamarin Forms (Android) Client certificate from KeyStore vs PFX file

我在 Xamarin.Forms 應用程序(僅限 Android,沒有 IOS 項目)中遇到客戶端認證問題。 我有一個 .pfx 文件,我將其作為 EmbeddedResource 包含在我的解決方案中。 我還在我的 Android 11 設備上安裝了這個 pfx,因此它出現在安全設置用戶證書選項卡中。 這是一個完全有效的用戶證書。

我想使用此客戶端證書向后端發送 Post 請求。 當我使用我的解決方案中的 .pfx 文件時,它運行良好。 問題是,當我從設備的密鑰庫中讀取證書時,我無法做同樣的事情(我必須這樣做,因為在生產中,解決方案中將沒有 .pfx)。

在這兩種情況下,我都使用自定義的 AndroidClientHandler,如您所見。

在第一個場景中,當我讀取 .pfx 文件時,我在代碼中的某處創建了 http 調用,如下所示:

var ms = new MemoryStream();
Assembly.GetExecutingAssembly().GetManifestResourceStream("CertTest.MyDeviceCert.pfx").CopyTo(ms);
var pfxByteArray = ms.ToArray();

string url = @"https://my-backend-hostname:443/api/endpoint-name";

var objectToPost = someObjectWhatIWantToPost.

var client = new AndroidHttpsClientHandler(pfxByteArray);

var httpClient = new HttpClient(client);

var request = new HttpRequestMessage(HttpMethod.Post, url);

request.Content = JsonContent.Create(objectToPost);

var response = await httpClient.SendAsync(request);

響應是 201 Created,所以一切正常。 魔法發生在 AndroidHttpsClientHandler class 中。class 的完整代碼是:

public class AndroidHttpsClientHandler : AndroidClientHandler
{
  private SSLContext sslContext;
  private const string clientCertPassword = "123456";
        
  public AndroidHttpsClientHandler(byte[] keystoreRaw) : base()
  {
    IKeyManager[] keyManagers = null;
    ITrustManager[] trustManagers = null;

    if (keystoreRaw != null)
    {
      using (MemoryStream memoryStream = new MemoryStream(keystoreRaw))
      {
        KeyStore keyStore = KeyStore.GetInstance("pkcs12");
        keyStore.Load(memoryStream, clientCertPassword.ToCharArray());
        KeyManagerFactory kmf = KeyManagerFactory.GetInstance("x509");
        kmf.Init(keyStore, clientCertPassword.ToCharArray());
        keyManagers = kmf.GetKeyManagers();
      }
    }

    sslContext = SSLContext.GetInstance("TLS");
    sslContext.Init(keyManagers, trustManagers, null);
  }
        
  protected override SSLSocketFactory ConfigureCustomSSLSocketFactory(HttpsURLConnection 
  connection)
  {
    SSLSocketFactory socketFactory = sslContext.SocketFactory;
    if (connection != null)
    {
      connection.SSLSocketFactory = socketFactory;
    }
    return socketFactory;
  }
}

場景 2:當我想使用設備安裝的證書中的證書時,我使用以下代碼讀取它:

var keyChain = KeyChain.GetCertificateChain(Android.App.Application.Context, alias);
var clientCert = keyChain.FirstOrDefault();
var clientCertByArray = clientCert.GetEncoded();

var client = new AndroidHttpsClientHandler(clientCertByArray);

rest 代碼與場景 1 相同,但現在當 keyStore.Load(memoryStream, clientCertPassword.ToCharArray()) 在 AndroidHttpsClientHandler 的構造函數中運行時,我得到一個 IOException。

我懷疑 pfxByteArray 和 clientCertByArray 在這兩種情況下是不一樣的。

我們在 System.Security.Cryptography.X509Certificates 命名空間中有一個 X509Certificate2 class,它有一個 public X509Certificate2(byte[] rawData) 構造函數。 我將 pfxByteArray 和 clientCertByArray 傳遞給它以檢查差異。

var workingCert = new X509Certificate2(pfxByteArray);
var notWorkingClientCert = new X509Certificate2(clientCertByArray);

我注意到一個主要區別:notWorkingClientCert 實例的 PrivateKey 屬性為 null,而 HasPrivateKey 屬性為 false。

所以我的問題是如何以正確的方式從 KeyStore 讀取證書,就像我在讀取 a.pfx 文件時一樣?

我想提一下,此代碼向我返回 null,但證書的別名是“MyDeviceCert”:

var privateKey = KeyChain.GetPrivateKey(Android.App.Application.Context, "MyDeviceCert");

@szg1993 的回答幾乎是正確的,或者可能是正確的,具體取決於版本和用法。

我對他的回答進行了一些更改以使其對我有用:

  • 來自 .NET 6 AndroidClientHandler 已棄用。 使用 AndroidMessageHandler,其工作方式幾乎相同。 這也意味着返回 HttpMessageHandler 而不是 HttpClientHandler,然后使用它來配置 HttpClient。
  • 我還使用下面給出的 SSLContext function 配置了 SSL,並添加了 keyManager 和 trustManager 列表(如果需要),還實現了 ConfigureCustomSSLSocketFactory
      private SSLContext GetSSLContext() {
           string protocol;
           if (SslProtocols == SslProtocols.Tls11)
           {
                protocol = "TLSv1.1";
           }
           else if (SslProtocols == SslProtocols.Tls || SslProtocols == SslProtocols.Tls12)
           {
                protocol = "TLSv1.2";
           }
           else
           {
                throw new IOException("unsupported ssl protocol: " + SslProtocols.ToString());
           }
    
            IKeyManager keyManager;
            # KeyChainKeyManager is a custom class
            keyManager = KeyChainKeyManager.fromAlias(Application.Context, mAlias);
    
            SSLContext ctx = SSLContext.GetInstance("TLS");
            ctx.Init(new IKeyManager[] { keyManager }, null, null);
            return ctx;
      }

      protected override SSLSocketFactory ConfigureCustomSSLSocketFactory(HttpsURLConnection connection)
      {
            SSLSocketFactory socketFactory = sslContext.SocketFactory;
            if (connection != null)
            {
                connection.SSLSocketFactory = socketFactory;
            }
            return socketFactory;
      }

KeyChainKeyManager class 擴展 X509ExtendedKeyManager 和

   returns new KeyChainKeyManager(alias, certificateChain, privateKey);

最后我將界面用作

    ICertificationService certificationService = DependencyService.Get<ICertificationService>();
    var httpClient = new HttpClient(certificationService.GetAuthAndroidClientHander());

經過 1 周的研究和不眠之夜,我終於弄明白了。 我決定在這里發布一個詳細的答案,因為在 inte.net 上找不到這個問題的確切答案。

因此,第一種情況是我在問題中描述的情況,當您擁有 .pfx 文件並且能夠將其存儲在可以讀取的位置時。 在那種情況下,您不必調用 KeyChain.GetPrivateKey 方法,因為 .pfx 文件包含私鑰。 在 AndroidHttpsClientHandler 中,如果您有自己的 ca,則可以初始化自定義信任庫。 只需將這段代碼放在 class 的構造函數中,就在“sslContext = SSLContext.GetInstance("TLS")”行的正上方:

    if (customCA != null)
    {
        CertificateFactory certFactory = CertificateFactory.GetInstance("X.509");

        using (MemoryStream memoryStream = new MemoryStream(customCA))
        {
            KeyStore keyStore = KeyStore.GetInstance("pkcs12");
            keyStore.Load(null, null);
            keyStore.SetCertificateEntry("MyCA", certFactory.GenerateCertificate(memoryStream));
            TrustManagerFactory tmf = TrustManagerFactory.GetInstance("x509");
            tmf.Init(keyStore);
            trustManagers = tmf.GetTrustManagers();
        }
    }

customCa 是一個您必須像 pfxByteArray 一樣閱讀的文件。 而且你還必須像 keystoreRaw 變量一樣將它傳遞給 AndroidHttpsClientHandler 的構造函數。 (我不需要那個代碼,這就是問題中沒有它的原因)

場景 2 - 當您必須從 Android 的用戶證書庫中讀取您的客戶端證書時:

在您的代碼中的某處,您喜歡發布如下數據:

var httpClient = new HttpClient(certificationService.GetAuthAndroidClientHander());

var request = new HttpRequestMessage(HttpMethod.Post, url);

request.Content = new StringContent(JsonConvert
.SerializeObject(objectToPost), Encoding.UTF8, "application/json");

var response = await httpClient.SendAsync(request);

如您所見,HttpClient 使用 certificationService.GetAuthAndroidClientHander() 方法,該方法返回 Android 特定的 HttpClientHandler。 class存在於.Android項目中,不在shared中,代碼如下:

public class AndroidHttpsClientHander : AndroidClientHandler
{
    private readonly ClientCertificate clientCertificate;

    public AndroidHttpsClientHander(ClientCertificate clientCertificate)
    {
        this.clientCertificate = clientCertificate;

        var trustManagerFactory = TrustManagerFactory
            .GetInstance(TrustManagerFactory.DefaultAlgorithm);

        trustManagerFactory.Init((KeyStore)null);

        var x509trustManager = trustManagerFactory
            .GetTrustManagers()
            .OfType<IX509TrustManager>()
            .FirstOrDefault();

        var acceptedIssuers = x509trustManager.GetAcceptedIssuers();

        TrustedCerts = clientCertificate.X509CertificateChain
            .Concat(acceptedIssuers)
            .ToList<Certificate>();
    }

    protected override KeyStore ConfigureKeyStore(KeyStore keyStore)
    {
        keyStore = KeyStore.GetInstance("PKCS12");

        keyStore.Load(null, null);

        keyStore.SetKeyEntry("privateKey", clientCertificate.PrivateKey,
            null, clientCertificate.X509CertificateChain.ToArray());

        if (TrustedCerts?.Any() == false)
            return keyStore;

        for (var i = 0; i < TrustedCerts.Count; i++)
        {
            var trustedCert = TrustedCerts[i];
            
            if (trustedCert == null)
                continue;

            keyStore.SetCertificateEntry($"ca{i}", trustedCert);
        }

        return keyStore;
    }

    protected override KeyManagerFactory ConfigureKeyManagerFactory(KeyStore keyStore)
    {
        var keyManagerFactory = KeyManagerFactory.GetInstance("x509");
        keyManagerFactory.Init(keyStore, null);
        return keyManagerFactory;
    }

    protected override TrustManagerFactory ConfigureTrustManagerFactory(KeyStore keyStore)
    {
        var trustManagerFactory = TrustManagerFactory.GetInstance(TrustManagerFactory.DefaultAlgorithm);
        trustManagerFactory.Init(keyStore);
        return trustManagerFactory;
    }
}

這沒有完全優化,例如你不必閱讀所有受信任的證書,但當它工作時我很高興我不想改變它。

下一塊是 ClientCertificate class AndroidHttpsClientHander 的參數是什么。 代碼是(這也存在於 Android 項目中):

public class ClientCertificate
{  
    public IPrivateKey PrivateKey { get; set; }

    public IReadOnlyCollection<X509Certificate> X509CertificateChain { get; set; }
}

這個調用的屬性是通過一個CertificationService class來設置的,什么是我的singleton:

[assembly: Dependency(typeof(CertificationService))]
namespace MyProject.Droid.Services
{
    public class CertificationService : ICertificationService
    {
        public ClientCertificate ClientCertificate { get; set; }

        private readonly ILoggerService loggerService;

        public CertificationService()
        {
            ClientCertificate = new ClientCertificate();
            loggerService = Startup.ServiceProvider.GetRequiredService<ILoggerService>();
        }

        public void SetPrivateKeyFromUser(object activity)
        {
            SetPrivateKey();

            if (ClientCertificate.PrivateKey != null)
                return;

            KeyChain.ChoosePrivateKeyAlias(
                activity: (Activity)activity,
                response: new PrivateKeyCallback(this),
                keyTypes: new string[] { "RSA" },
                issuers: null,
                uri: null,
                alias: MyConstants.DeviceCertAlias);
        }

        public void SetCertificateChain()
            => ClientCertificate.X509CertificateChain = KeyChain
                .GetCertificateChain(Android.App.Application.Context, MyConstants.DeviceCertAlias);

        public HttpClientHandler GetAuthAndroidClientHander()
            => new AndroidHttpsClientHander(ClientCertificate);

        public string GetCertificationDetailsError()
        {
            if (ClientCertificate?.X509CertificateChain == null)
                return $"{LogMessages.CertificationChainEmpty}";

            if (ClientCertificate?.PrivateKey == null)
                return $"{LogMessages.PrivateKeyIsNull}";

            if (string.IsNullOrEmpty(ClientCertificate?.CN))
                return $"{LogMessages.DeviceCnIsNull}";

            return null;
        }

        public void SetPrivateKey()
        {
            try
            {
                ClientCertificate.PrivateKey = KeyChain
                    .GetPrivateKey(Android.App.Application.Context, MyConstants.DeviceCertAlias);
            }
            catch (Exception e)
            {
                loggerService.LogError(e);
            }
        }
    }
}

它的界面(在共享項目中,因為我想到處使用):

namespace MyProject.Services
{
    public interface ICertificationService
    {  
        void SetPrivateKeyFromUser(object activity);
        void SetCertificateChain();
        HttpClientHandler GetAuthAndroidClientHander();
        string GetCertificationDetailsError();
        void SetPrivateKey();
    }
}

PrivateKeyCallback 是一個 class 有一個方法,在用戶 select 從彈出窗口中獲取證書后將被觸發(此答案稍后會詳細介紹):

namespace MyProject.Droid.Utils
{
    public class PrivateKeyCallback : Java.Lang.Object, IKeyChainAliasCallback
    {
        private readonly ICertificationService certificationService;

        public PrivateKeyCallback(ICertificationService certificationService)
            => this.certificationService = certificationService;

        public void Alias(string alias)
        {
            if (alias != MyConstants.DeviceCertAlias)
                return;

            certificationService.SetPrivateKey();
        }
    }
}

最后一塊拼圖是我創建 CertificationService 並使用它的方法的地方。 我只是把 MainActivity 方法的這段代碼放在第一行(Task.Run 為了不阻塞主線程):

Task.Run(() => RegisterCertificationService());

RegisterCertificationService 只是 MainActivity 中的私有方法:

private void RegisterCertificationService()
{
    var certificationService = new CertificationService();

    try
    {
        certificationService.SetPrivateKeyFromUser(this);
        certificationService.SetCertificateChain();

        DependencyService.RegisterSingleton<ICertificationService>(certificationService);
    }
    catch (Exception e)
    {
        Startup.ServiceProvider
            .GetRequiredService<ILoggerService>()
            .LogError(e);
    }
}

SetPrivateKeyFromUser 嘗試獲取私鑰,如果失敗並出現異常或保持 null,將要求用戶選擇一個。 之后 KeyChain.GetPrivateKey 將有權返回它。

請確定:MyConstants.DeviceCertAlias 是一個字符串,您證書的別名是什么(您可以在設置中的 Android 用戶證書存儲區中看到的內容)。 請注意,CertificationService 是 singleton,因此每當我從 ioc 容器中獲取時,我都會得到相同的實例。 這就是為什么我的方法可以奏效。 另一個重要提示:我在平台特定項目中放置了一些類。 這很重要,因為我們需要達到 X509Certification(等)類的 java 版本。

如果這個解決方案不適合你(我的證書不是自簽名的)那么你應該檢查這樣的黑客:

yourHttpClientHandler.ServerCertificateCustomValidationCallback +=
    (sender, cert, chain, sslPolicyErrors) =>
    {
        return true;
    };

一個重要的提示:嘗試以正確的格式獲得認證並將其與 postman 一起使用以首先對其進行測試: https://learning.postman.com/docs/sending-requests/certificates/

當“啟用 SSL 證書驗證”打開時,您應該能夠使用它進行請求。 如果它不能與驗證一起工作,那么你可能最終會嘗試破解 ServerCertificateCustomValidationCallback,它在 Xamarin 中的支持真的很差。盡量避免它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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