简体   繁体   English

Xamarin Forms (Android) 来自 KeyStore 与 PFX 文件的客户端证书

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

I have an issue with client certification in a Xamarin.Forms app (android only, no IOS project).我在 Xamarin.Forms 应用程序(仅限 Android,没有 IOS 项目)中遇到客户端认证问题。 I have a.pfx file what I included in my solution as EmbeddedResource.我有一个 .pfx 文件,我将其作为 EmbeddedResource 包含在我的解决方案中。 I also installed this pfx on my Android 11 device, so it appeared in the security settings user certificates tab.我还在我的 Android 11 设备上安装了这个 pfx,因此它出现在安全设置用户证书选项卡中。 This a fully valid user cert.这是一个完全有效的用户证书。

I would like to use this client certificate in order to do Post requests to the backend.我想使用此客户端证书向后端发送 Post 请求。 When I use the.pfx file from my solution then it's working perfectly.当我使用我的解决方案中的 .pfx 文件时,它运行良好。 The problem is that I'm unable to do the same when I read the certificate from the device's keystore (and I have to do that way, because in production there will be no.pfx in the solution).问题是,当我从设备的密钥库中读取证书时,我无法做同样的事情(我必须这样做,因为在生产中,解决方案中将没有 .pfx)。

In both scenario I'm using a custom AndroidClientHandler as you will see.在这两种情况下,我都使用自定义的 AndroidClientHandler,如您所见。

In the first scenario when I read the.pfx file I create the http call somewhere in my code like this:在第一个场景中,当我读取 .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);

The response is 201 Created, so everything is fine.响应是 201 Created,所以一切正常。 The magic happens in the AndroidHttpsClientHandler class. The full code of the class is:魔法发生在 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;
  }
}

Scenario2: When I would like to use the certificate from the Device's installed certificates, I read it with this code:场景 2:当我想使用设备安装的证书中的证书时,我使用以下代码读取它:

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

var client = new AndroidHttpsClientHandler(clientCertByArray);

And the rest code is the same as Scenario1, but now I get an IOException when the keyStore.Load(memoryStream, clientCertPassword.ToCharArray()) runs in the ctor of the AndroidHttpsClientHandler. rest 代码与场景 1 相同,但现在当 keyStore.Load(memoryStream, clientCertPassword.ToCharArray()) 在 AndroidHttpsClientHandler 的构造函数中运行时,我得到一个 IOException。

I suspected that the pfxByteArray and the clientCertByArray was not the same in the two scenarios.我怀疑 pfxByteArray 和 clientCertByArray 在这两种情况下是不一样的。

We have a X509Certificate2 class in the System.Security.Cryptography.X509Certificates namespace, what has a public X509Certificate2(byte[] rawData) constructor.我们在 System.Security.Cryptography.X509Certificates 命名空间中有一个 X509Certificate2 class,它有一个 public X509Certificate2(byte[] rawData) 构造函数。 I passed the pfxByteArray and the clientCertByArray to it in order to check the differences.我将 pfxByteArray 和 clientCertByArray 传递给它以检查差异。

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

I noticed one mayor difference: the notWorkingClientCert instace's PrivateKey property is null, and the HasPrivateKey property is false.我注意到一个主要区别:notWorkingClientCert 实例的 PrivateKey 属性为 null,而 HasPrivateKey 属性为 false。

So my question is how can I read the certificate from the KeyStore in the proper way like when I'm reading a.pfx file?所以我的问题是如何以正确的方式从 KeyStore 读取证书,就像我在读取 a.pfx 文件时一样?

Something I would like to mention, this code returns null to me, but the alias of the certificate is "MyDeviceCert":我想提一下,此代码向我返回 null,但证书的别名是“MyDeviceCert”:

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

Answer by @szg1993 is almost right or might be correct depending upon the version and usage. @szg1993 的回答几乎是正确的,或者可能是正确的,具体取决于版本和用法。

Some changes I made to his answer to get it working for me:我对他的回答进行了一些更改以使其对我有用:

  • from .NET 6 AndroidClientHandler is deprecated.来自 .NET 6 AndroidClientHandler 已弃用。 Use AndroidMessageHandler, which works almost the same.使用 AndroidMessageHandler,其工作方式几乎相同。 This would also mean to return HttpMessageHandler instead of HttpClientHandler and then use that to configure HttpClient.这也意味着返回 HttpMessageHandler 而不是 HttpClientHandler,然后使用它来配置 HttpClient。
  • I have also configured SSL using SSLContext function given below and added list of keyManager and trustManager (if required) and also implemented ConfigureCustomSSLSocketFactory我还使用下面给出的 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 extends X509ExtendedKeyManager and KeyChainKeyManager class 扩展 X509ExtendedKeyManager 和

   returns new KeyChainKeyManager(alias, certificateChain, privateKey);

And finally I used the interface as最后我将界面用作

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

After 1 week of research and sleepless nights I finally figured it out.经过 1 周的研究和不眠之夜,我终于弄明白了。 I decided to post here a detailed answer, because the exact answer for this problem cannot be found on the inte.net.我决定在这里发布一个详细的答案,因为在 inte.net 上找不到这个问题的确切答案。

So the first scenario is that one what I described in the question, when you have the.pfx file and you are able to store it somewhere where you can read it.因此,第一种情况是我在问题中描述的情况,当您拥有 .pfx 文件并且能够将其存储在可以读取的位置时。 In that scenario you don't have to call the KeyChain.GetPrivateKey method, because the.pfx file contains the private key.在那种情况下,您不必调用 KeyChain.GetPrivateKey 方法,因为 .pfx 文件包含私钥。 In the AndroidHttpsClientHandler you can init a custom truststore if you have your own ca.在 AndroidHttpsClientHandler 中,如果您有自己的 ca,则可以初始化自定义信任库。 Just put this code in the ctor of the class, right above the "sslContext = SSLContext.GetInstance("TLS")" line:只需将这段代码放在 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();
        }
    }

The customCa is a file what you have to read like the pfxByteArray. customCa 是一个您必须像 pfxByteArray 一样阅读的文件。 And also you have to pass it to the ctor of the AndroidHttpsClientHandler like the keystoreRaw variable.而且你还必须像 keystoreRaw 变量一样将它传递给 AndroidHttpsClientHandler 的构造函数。 (I don't need that code, this is the reason why it not present in the question) (我不需要那个代码,这就是问题中没有它的原因)

Scenario 2 - when you have to read your client certificate from the Android's user cert store:场景 2 - 当您必须从 Android 的用户证书库中读取您的客户端证书时:

Somewhere in your code you like to post data something like this:在您的代码中的某处,您喜欢发布如下数据:

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

As you can see the HttpClient uses the certificationService.GetAuthAndroidClientHander() method which returns an Android specific HttpClientHandler.如您所见,HttpClient 使用 certificationService.GetAuthAndroidClientHander() 方法,该方法返回 Android 特定的 HttpClientHandler。 The class lives in the.Android project, not in the shared, and the code is the following: 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;
    }
}

This is not fully optimized, for example you don't have to read all the trusted certs, but I was so happy when it worked that I don't wan't to change it.这没有完全优化,例如你不必阅读所有受信任的证书,但当它工作时我很高兴我不想改变它。

The next piece is the ClientCertificate class what is the parameter for the AndroidHttpsClientHander.下一块是 ClientCertificate class AndroidHttpsClientHander 的参数是什么。 The code is (this also lives in the Android project):代码是(这也存在于 Android 项目中):

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

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

The properties of this call are setted by a CertificationService class, what is a singleton of mine:这个调用的属性是通过一个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);
            }
        }
    }
}

It's interface (in the shared project, because I want to use everywhere):它的界面(在共享项目中,因为我想到处使用):

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

The PrivateKeyCallback is a class what has a method which will be fired, after the user select the certificate from the popup (more on that later in this answer): 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();
        }
    }
}

And the final piece of the puzzle is the place where I create the CertificationService and use it's methods.最后一块拼图是我创建 CertificationService 并使用它的方法的地方。 I just put this code of the MainActivity's method as it's first line (Task.Run in order to not block the main thread):我只是把 MainActivity 方法的这段代码放在第一行(Task.Run 为了不阻塞主线程):

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

And the RegisterCertificationService is just a private method in the MainActivity: 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);
    }
}

The SetPrivateKeyFromUser tries to get the private key and if it fail with exception or stays null the user will be asked to choose one. SetPrivateKeyFromUser 尝试获取私钥,如果失败并出现异常或保持 null,将要求用户选择一个。 After that the KeyChain.GetPrivateKey will has permission to return it.之后 KeyChain.GetPrivateKey 将有权返回它。

Just to be sure: MyConstants.DeviceCertAlias is a string, what is the alias of you certificate (what you can see in the Android's user cert store in the settings).请确定:MyConstants.DeviceCertAlias 是一个字符串,您证书的别名是什么(您可以在设置中的 Android 用户证书存储区中看到的内容)。 Note that the CertificationService is a singleton, so whenever I get the from the ioc container I got the same instance.请注意,CertificationService 是 singleton,因此每当我从 ioc 容器中获取时,我都会得到相同的实例。 This is why my way can work.这就是为什么我的方法可以奏效。 Another important tip: there are classes what I placed in the plarform specific project.另一个重要提示:我在平台特定项目中放置了一些类。 That is important, because we need to reach the java version of the X509Certification (etc) classes.这很重要,因为我们需要达到 X509Certification(等)类的 java 版本。

If this solution not works for you (my certs are not self signed) then you should probably check hacks like this:如果这个解决方案不适合你(我的证书不是自签名的)那么你应该检查这样的黑客:

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

One important tip: Try to get the certification in a proper format and use it with postman to test it first: https://learning.postman.com/docs/sending-requests/certificates/一个重要的提示:尝试以正确的格式获得认证并将其与 postman 一起使用以首先对其进行测试: https://learning.postman.com/docs/sending-requests/certificates/

You should be able to do requests with it when the "Enable SSL certificate validation" is turned ON.当“启用 SSL 证书验证”打开时,您应该能够使用它进行请求。 If it's not working with verification ON, than you probably will end up trying hack the ServerCertificateCustomValidationCallback, which is really poorly supported in Xamarin. Try to avoid it.如果它不能与验证一起工作,那么你可能最终会尝试破解 ServerCertificateCustomValidationCallback,它在 Xamarin 中的支持真的很差。尽量避免它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM