繁体   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