【问题标题】:Xamarin Forms (Android) Client certificate from KeyStore vs PFX file来自 KeyStore 与 PFX 文件的 Xamarin Forms (Android) 客户端证书
【发布时间】:2022-11-21 12:50:58
【问题描述】:

我在 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,所以一切正常。 魔法发生在 Android HttpClientHandler 类中。 该类的完整代码是:

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);

其余代码与场景 1 相同,但现在当 keyStore.Load(memoryStream, clientCertPassword.ToCharArray()) 在 AndroidHttpsClientHandler 的构造函数中运行时,我得到一个 IOException。

我怀疑 pfxByteArray 和 clientCertByArray 在这两种情况下是不一样的。

我们在 System.Security.Cryptography.X509Certificates 命名空间中有一个 X509Certificate2 类,它有一个 public X509Certificate2(byte[] rawData) 构造函数。 我将 pfxByteArray 和 clientCertByArray 传递给它以检查差异。

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

我注意到一个主要区别:notWorkingClientCert 实例的 PrivateKey 属性为 null,而 HasPrivateKey 属性为 false。

所以我的问题是如何以正确的方式从 KeyStore 中读取证书,就像在读取 .pfx 文件时一样?

我想提一下,此代码向我返回 null,但证书的别名是“MyDeviceCert”:

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

【问题讨论】:

  • docs 中有很多关于不从主线程调用、权限、上下文和异常的说明。你读过它们吗?
  • 是的,但对我的具体情况没有任何用处。
  • “如果调用者无权访问它,则返回 null”——您确定不是这种情况吗?您是否尝试过备用调用来请求权限?
  • 如果是这样,我需要什么许可? :O
  • 现在我明白了,我必须在我的 MainActivity 中调用这段代码: string certAlias = "MyCertName"; KeyChain.ChoosePrivateKeyAlias(activity: this, response: new Callback(), keyTypes: new string[] { "RSA", "DSA" }, issuers: null, uri: null, alias: certAlias);用户选择证书后,以下代码将返回私钥: var privateKey = KeyChain.GetPrivateKey(Android.App.Application.Context, certAlias);

标签: android xamarin keystore x509certificate2


【解决方案1】:

经过 1 周的研究和不眠之夜,我终于弄明白了。我决定在这里发布一个详细的答案,因为在互联网上找不到这个问题的确切答案。

因此,第一种情况是我在问题中描述的情况,当您拥有 .pfx 文件并且能够将其存储在可以读取的位置时。在这种情况下,您不必调用 KeyChain.GetPrivateKey 方法,因为 .pfx 文件包含私钥。 在 AndroidHttpsClientHandler 中,如果您有自己的 ca,则可以初始化自定义信任库。只需将此代码放在类的构造函数中,就在“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。该类位于 .Android 项目中,而不是共享中,代码如下:

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 类,AndroidHttpsClientHander 的参数是什么。代码是(这也存在于 Android 项目中):

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

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

这个调用的属性是通过一个CertificationService类来设置的,什么是我的单例:

[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 是一个类,它有一个方法,在用户从弹出窗口中选择证书后将被触发(本答案稍后会详细介绍):

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 尝试获取私钥,如果失败并出现异常或保持为空,则将要求用户选择一个。之后 KeyChain.GetPrivateKey 将有权返回它。

请确定:MyConstants.DeviceCertAlias 是一个字符串,您证书的别名是什么(您可以在设置中的 Android 用户证书存储区中看到的内容)。 请注意,CertificationService 是一个单例,因此每当我从 ioc 容器中获取时,我都会得到相同的实例。这就是为什么我的方法可以奏效。 另一个重要提示:我在平台特定项目中放置了一些类。这很重要,因为我们需要达到 X509Certification(等)类的 java 版本。

如果这个解决方案不适合你(我的证书不是自签名的)那么你应该检查这样的黑客:

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

一个重要的提示:尝试以正确的格式获得认证,并首先将其与邮递员一起使用以进行测试: https://learning.postman.com/docs/sending-requests/certificates/

当“启用 SSL 证书验证”打开时,您应该能够使用它进行请求。如果它不能与验证一起工作,那么你可能最终会尝试破解 ServerCertificateCustomValidationCallback,这在 Xamarin 中的支持真的很差。尽量避免它。

【讨论】:

    【解决方案2】:

    @szg1993 的回答几乎是正确的,或者可能是正确的,具体取决于版本和用法。

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

    • .NET 6 中的 AndroidClientHandler 已弃用。使用 AndroidMessageHandler,其工作方式几乎相同。这也意味着返回 HttpMessageHandler 而不是 HttpClientHandler,然后使用它来配置 HttpClient。
    • 我还使用下面给出的 SSLContext 函数配置了 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 类扩展 X509ExtendedKeyManager 和

       returns new KeyChainKeyManager(alias, certificateChain, privateKey);
    

    最后我将界面用作

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

    【讨论】:

      猜你喜欢
      • 2020-02-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多