Client Environment is Xamarin Android Native TLS 1.2 SSL/TLS implementation (boringssl aka btls), using System.Net.WebSockets.ClientWebSocket. This is running on an Android 7.0 device. Visual Studio 2017 15.8.1, Xamarin.Android 9.0.0.18.
Server Environment is Windows .NET 4.7 running Fleck (WebSocket server) configured with TLS 1.2 using a certificate issued by a homemade (non-trusted anywhere on the globe) Certificate Authority (CA).
Assuming a homemade CA Cert (.pem or .cer format) has been installed on the android device via Settings->Security->Install from SD Card, the ClientWebSocket connects using TLS 1.2 without problems, as one would expect. Since this is a global solution to a local (one part of my app) problem, not to mention opening a security hole for the larger device ecosystem, I do not wish to require this setup.
I have then tried several methods to localize the trust of the CA to only my application without success. Regardless of approach, there is always the same exception thrown by ClientWebSocket.ConnectAsync(): A call to SSPI failed
and Ssl error:1000007d:SSL routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED at /Users/builder/jenkins/workspace/xamarin-android-d15-8/xamarin-android/external/mono/external/boringssl/ssl/handshake_client.c:1132
I created a sample windows server and console app and Xamarin.Forms Android app that demonstrates the issue and the attempts to workaround it described below. Included is a custom CA cert. The server code dynamically issues a client cert with SANs bound to your IP/hostnames for ease of repro.
Attempt 1:
Apply the android:networkSecurityConfig="@xml/network_security_config"
attribute to the application element in the AndroidManifest.xml file, including resources Resources\raw\sample_ca.pem
Resources\xml\network_security_config.xml
<?xml version="1.0" encoding="utf-8" ?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="@raw/sample_ca"/>
<certificates src="system"/>
<certificates src="user"/>
</trust-anchors>
</base-config>
</network-security-config>
This had no visible effect, and I cannot see anything in the debug output that would indicate that the runtime is even loading this. I have seen references to messages like this:
D/NetworkSecurityConfig: No Network Security Config specified, using platform default
However, with or without this in place I have never seen messages like this or similar. I really have no idea if it is being applied or not, or if btls implementation even uses/respects this.
Interestingly, since the Android minSdk is set to 24 and target sdk of 27 I would expect the lack of this declaration should cause TLS 1.2 to not work if I simply added the CA to the android device user certificate store. I suspect there are a few Xamarin bugs surrounding this.
Attempt 2:
Add the CA to the X509 Store, hoping btls uses that as a source of certificates. This approach works on Windows/.NET 4 (it does bring up a dialog to accept the addition of the certificate).
X509Store store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
var certs = store.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, false);
if (certs.Count == 0)
store.Add(cert);
store.Close();
Attempt 3:
Handle ServerCertificateValidationCallback
. This never gets called in Xamarin Android, but this approach works on Windows/.NET 4.
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
return true;
//BUGBUG: Obviously there should be a much more extensive check here.
if (certificate.Issuer == caCert.Issuer)
return true;
return false;
};
There are some Mono issues surrounding btls and a pull request that makes this approach look possible in the near future.
Attempt 4:
Add the CA cert (and/or the cert issued from the CA) to the ClientWebSocket.Options ClientCertificates collection. Technically this should not work with CA Certs but should be the approach to use with self-signed certificates. I offer it here merely for completeness. As expected, it does not work either.
Full Repro Code
Easy to use code that demonstrates this issue with all of the attempted workarounds described above is available on GitHub.