Skip to content

Commit

Permalink
[Mono.Android] custom validation callback for server certificates in …
Browse files Browse the repository at this point in the history
…HTTP handlers (#6665)

Context: dotnet/runtime#62966

The `AndroidClientHandler` and `AndroidMessageHandler` classes both
have the `ServerCertificateCustomValidationCallback` property, which
should be useful e.g. to allow running the Android app against a
server with a self-signed SSL certificate during development, but the
callback is never used. Unfortunatelly since .NET 6 the
`System.Net.Http.SocketsHttpHandler` for Android doesn't support the
use case anymore. That means that [the recommended way of connecting
to local web server][0] won't work in MAUI.

This PR introduces an implementation of `IX509TrustManger` which wraps
the default Java X509 trust manager and calls the user's callback on
top of the default validation.

It turns out that `X509Chain` `Build` function doesn't work on
Android, so I'm not calling it and I'm passing the chain to the
callback directly.

Additionally, we need a default proguard rule due to:

https://github.com/xamarin/xamarin-android/blob/46002b49d8c0b7b1a17532a8e104b4d31afee7a6/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/GenerateProguardConfiguration.cs#L50-L57

    -keep class xamarin.android.net.X509TrustManagerWithValidationCallback { *; <init>(...); }

`Mono.Android.dll` is skipped during the
`GenerateProguardConfiguration` linker step. It might be worth
addressing this in a future PR.

[0]: https://docs.microsoft.com/en-us/xamarin/cross-platform/deploy-test/connect-to-local-web-services
  • Loading branch information
simonrozsival authored Apr 29, 2022
1 parent 46002b4 commit 48540d6
Show file tree
Hide file tree
Showing 11 changed files with 843 additions and 633 deletions.
1 change: 1 addition & 0 deletions src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
<Compile Include="Xamarin.Android.Net\AuthModuleBasic.cs" />
<Compile Include="Xamarin.Android.Net\AuthModuleDigest.cs" />
<Compile Include="Xamarin.Android.Net\IAndroidAuthenticationModule.cs" />
<Compile Include="Xamarin.Android.Net\X509TrustManagerWithValidationCallback.cs" />
<Compile Condition=" '$(TargetFramework)' == 'monoandroid10' " Include="Xamarin.Android.Net\OldAndroidSSLSocketFactory.cs" />
</ItemGroup>

Expand Down
65 changes: 43 additions & 22 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,15 @@ public CookieContainer CookieContainer

public bool CheckCertificateRevocationList { get; set; } = false;

public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> ServerCertificateCustomValidationCallback { get; set; }
X509TrustManagerWithValidationCallback.Helper? _callbackTrustManagerHelper = null;

public Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool>? ServerCertificateCustomValidationCallback
{
get => _callbackTrustManagerHelper?.Callback;
set {
_callbackTrustManagerHelper = value != null ? new X509TrustManagerWithValidationCallback.Helper (value) : null;
}
}

// See: https://developer.android.com/reference/javax/net/ssl/SSLSocket#protocols
public SslProtocols SslProtocols { get; set; } =
Expand Down Expand Up @@ -199,7 +207,7 @@ public int MaxAutomaticRedirections
/// If the website requires authentication, this property will contain data about each scheme supported
/// by the server after the response. Note that unauthorized request will return a valid response - you
/// need to check the status code and and (re)configure AndroidMessageHandler instance accordingly by providing
/// both the credentials and the authentication scheme by setting the <see cref="PreAuthenticationData"/>
/// both the credentials and the authentication scheme by setting the <see cref="PreAuthenticationData"/>
/// property. If AndroidMessageHandler is not able to detect the kind of authentication scheme it will store an
/// instance of <see cref="AuthenticationData"/> with its <see cref="AuthenticationData.Scheme"/> property
/// set to <c>AuthenticationScheme.Unsupported</c> and the application will be responsible for providing an
Expand Down Expand Up @@ -939,7 +947,7 @@ void AppendEncoding (string encoding, ref List <string>? list)

// SSL context must be set up as soon as possible, before adding any content or
// headers. Otherwise Java won't use the socket factory
SetupSSL (httpConnection as HttpsURLConnection);
SetupSSL (httpConnection as HttpsURLConnection, request);
if (request.Content != null)
AddHeaders (httpConnection, request.Content.Headers);
AddHeaders (httpConnection, request.Headers);
Expand Down Expand Up @@ -997,7 +1005,7 @@ void AppendEncoding (string encoding, ref List <string>? list)
internal SSLSocketFactory? ConfigureCustomSSLSocketFactoryInternal (HttpsURLConnection connection)
=> ConfigureCustomSSLSocketFactoryInternal (connection);

void SetupSSL (HttpsURLConnection? httpsConnection)
void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMessage)
{
if (httpsConnection == null)
return;
Expand All @@ -1017,35 +1025,48 @@ void SetupSSL (HttpsURLConnection? httpsConnection)
}
#endif

var keyStore = KeyStore.GetInstance (KeyStore.DefaultType);
keyStore?.Load (null, null);
bool gotCerts = TrustedCerts?.Count > 0;
if (gotCerts) {
for (int i = 0; i < TrustedCerts!.Count; i++) {
Certificate cert = TrustedCerts [i];
if (cert == null)
continue;
keyStore?.SetCertificateEntry ($"ca{i}", cert);
}
}
var keyStore = InitializeKeyStore (out bool gotCerts);
keyStore = ConfigureKeyStore (keyStore);
var kmf = ConfigureKeyManagerFactory (keyStore);
var tmf = ConfigureTrustManagerFactory (keyStore);

if (tmf == null) {
// If there are no certs and no trust manager factory, we can't use a custom manager
// because it will cause all the HTTPS requests to fail because of unverified trust
// chain
if (!gotCerts)
// If there are no trusted certs, no custom trust manager factory or custom certificate validation callback
// there is no point in changing the behavior of the default SSL socket factory
if (!gotCerts && _callbackTrustManagerHelper == null)
return;

tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm);
tmf?.Init (keyStore);
tmf?.Init (gotCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs
}

ITrustManager[]? trustManagers = tmf?.GetTrustManagers ();

if (_callbackTrustManagerHelper != null) {
trustManagers = _callbackTrustManagerHelper.Inject (trustManagers, requestMessage);
}

var context = SSLContext.GetInstance ("TLS");
context?.Init (kmf?.GetKeyManagers (), tmf?.GetTrustManagers (), null);
context?.Init (kmf?.GetKeyManagers (), trustManagers, null);
httpsConnection.SSLSocketFactory = context?.SocketFactory;

KeyStore? InitializeKeyStore (out bool gotCerts)
{
var keyStore = KeyStore.GetInstance (KeyStore.DefaultType);
keyStore?.Load (null, null);
gotCerts = TrustedCerts?.Count > 0;

if (gotCerts) {
for (int i = 0; i < TrustedCerts!.Count; i++) {
Certificate cert = TrustedCerts [i];
if (cert == null)
continue;
keyStore?.SetCertificateEntry ($"ca{i}", cert);
}
}

return keyStore;
}
}

void HandlePreAuthentication (HttpURLConnection httpConnection)
Expand Down Expand Up @@ -1116,4 +1137,4 @@ void SetupRequestBody (HttpURLConnection httpConnection, HttpRequestMessage requ
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

using Javax.Net.Ssl;

using JavaCertificateException = Java.Security.Cert.CertificateException;
using JavaX509Certificate = Java.Security.Cert.X509Certificate;

namespace Xamarin.Android.Net
{
internal sealed class X509TrustManagerWithValidationCallback : Java.Lang.Object, IX509TrustManager
{
internal sealed class Helper
{
public Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> Callback { get; }

public Helper (Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> callback)
{
Callback = callback;
}

public ITrustManager[] Inject (
ITrustManager[]? trustManagers,
HttpRequestMessage requestMessage)
{
IX509TrustManager? x509TrustManager = trustManagers?.OfType<IX509TrustManager> ().FirstOrDefault ();
IEnumerable<ITrustManager> otherTrustManagers = trustManagers?.Where (manager => manager != x509TrustManager) ?? Enumerable.Empty<ITrustManager> ();
var trustManagerWithCallback = new X509TrustManagerWithValidationCallback (x509TrustManager, requestMessage, Callback);
return otherTrustManagers.Prepend (trustManagerWithCallback).ToArray ();
}
}

private readonly IX509TrustManager? _internalTrustManager;
private readonly HttpRequestMessage _request;
private readonly Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> _serverCertificateCustomValidationCallback;

private X509TrustManagerWithValidationCallback (
IX509TrustManager? internalTrustManager,
HttpRequestMessage request,
Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> serverCertificateCustomValidationCallback)
{
_request = request;
_internalTrustManager = internalTrustManager;
_serverCertificateCustomValidationCallback = serverCertificateCustomValidationCallback;
}

public void CheckServerTrusted (JavaX509Certificate[] javaChain, string authType)
{
var sslPolicyErrors = SslPolicyErrors.None;
var certificates = ConvertCertificates (javaChain);

try {
_internalTrustManager?.CheckServerTrusted (javaChain, authType);
} catch (JavaCertificateException) {
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
}

X509Certificate2? certificate = certificates.FirstOrDefault ();
using X509Chain chain = CreateChain (certificates);

if (certificate == null) {
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable;
}

if (!_serverCertificateCustomValidationCallback (_request, certificate, chain, sslPolicyErrors)) {
throw new JavaCertificateException ("The remote certificate was rejected by the provided RemoteCertificateValidationCallback.");
}
}

public void CheckClientTrusted (JavaX509Certificate[] chain, string authType)
=> _internalTrustManager?.CheckClientTrusted (chain, authType);

public JavaX509Certificate[] GetAcceptedIssuers ()
=> _internalTrustManager?.GetAcceptedIssuers () ?? Array.Empty<JavaX509Certificate> ();

private static X509Chain CreateChain (X509Certificate2[] certificates)
{
// the chain initialization is based on dotnet/runtime implementation in System.Net.Security.SecureChannel
var chain = new X509Chain ();

chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;

chain.ChainPolicy.ExtraStore.AddRange (certificates);

return chain;
}

private static X509Certificate2[] ConvertCertificates (JavaX509Certificate[] certificates)
=> certificates.Select (cert => new X509Certificate2 (cert.GetEncoded ()!)).ToArray ();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
-keep class opentk_1_0.platform.android.AndroidGameView { *; <init>(...); }
-keep class opentk_1_0.GameViewBase { *; <init>(...); }
-keep class com.xamarin.java_interop.ManagedPeer { *; <init>(...); }
-keep class xamarin.android.net.X509TrustManagerWithValidationCallback { *; <init>(...); }

-keep class android.runtime.** { <init>(...); }
-keep class assembly_mono_android.android.runtime.** { <init>(...); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,37 @@
"Size": 3032
},
"assemblies/Java.Interop.dll": {
"Size": 55106
"Size": 55099
},
"assemblies/Mono.Android.dll": {
"Size": 88461
"Size": 88852
},
"assemblies/rc.bin": {
"Size": 1083
},
"assemblies/System.Linq.dll": {
"Size": 10120
"Size": 10112
},
"assemblies/System.Private.CoreLib.dll": {
"Size": 519314
"Size": 519255
},
"assemblies/System.Runtime.CompilerServices.Unsafe.dll": {
"Size": 1165
},
"assemblies/System.Runtime.dll": {
"Size": 2374
"Size": 2369
},
"assemblies/UnnamedProject.dll": {
"Size": 3546
"Size": 3543
},
"classes.dex": {
"Size": 345328
"Size": 344840
},
"lib/arm64-v8a/libmonodroid.so": {
"Size": 382304
"Size": 382480
},
"lib/arm64-v8a/libmonosgen-2.0.so": {
"Size": 3192432
"Size": 3176080
},
"lib/arm64-v8a/libSystem.IO.Compression.Native.so": {
"Size": 776216
Expand All @@ -47,7 +47,7 @@
"Size": 150032
},
"lib/arm64-v8a/libxamarin-app.so": {
"Size": 9424
"Size": 9328
},
"META-INF/BNDLTOOL.RSA": {
"Size": 1213
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,43 @@
"Size": 2604
},
"assemblies/Java.Interop.dll": {
"Size": 67956
"Size": 67947
},
"assemblies/Mono.Android.dll": {
"Size": 256630
"Size": 257171
},
"assemblies/mscorlib.dll": {
"Size": 769015
"Size": 769010
},
"assemblies/System.Core.dll": {
"Size": 28199
"Size": 28190
},
"assemblies/System.dll": {
"Size": 9180
"Size": 9178
},
"assemblies/UnnamedProject.dll": {
"Size": 2881
"Size": 2871
},
"classes.dex": {
"Size": 347796
"Size": 349528
},
"lib/arm64-v8a/libmono-btls-shared.so": {
"Size": 1613872
},
"lib/arm64-v8a/libmonodroid.so": {
"Size": 296448
},
"lib/arm64-v8a/libmono-native.so": {
"Size": 750976
},
"lib/arm64-v8a/libmonodroid.so": {
"Size": 296192
},
"lib/arm64-v8a/libmonosgen-2.0.so": {
"Size": 4030448
},
"lib/arm64-v8a/libxa-internal-api.so": {
"Size": 65512
},
"lib/arm64-v8a/libxamarin-app.so": {
"Size": 19960
"Size": 19864
},
"META-INF/ANDROIDD.RSA": {
"Size": 1213
Expand Down Expand Up @@ -74,5 +74,5 @@
"Size": 1724
}
},
"PackageSize": 4011732
"PackageSize": 4015828
}
Loading

0 comments on commit 48540d6

Please sign in to comment.