The issue
As nolex already pointed out in his answer, the HttpClientHandler
actually uses AndroidMessageHandler
as its underlying handler - which does implemented the known ServerCertificateCustomValidationCallback
.
However, its value is never used when sending requests which you can easily verify yourself by searching the linked source code file for another occurrence of that property.
There's even a pull request waiting for (further) approval & merge since February 11th this year to solve this. But even after the latest resolve just 17 days ago as of today, it's still not merged. Plus, 5 checks are failing now - again.
The only workaround - for the time being that is
If you desire (or even require) to run your (debug) server build on the same machine your Android Emulator runs on & a secure connection between them is required, there's only way for you: overwrite Android's default TrustManager
with your own DangerousTrustManager
. This allows your app to bypass any certificate verification, hence the prefix Dangerous
. π
I can't stress that enough, so again: do not use this workaround's code beyond locally running debug builds. Not on testing environments. Not on staging environments. Seriously!
Though, there's also a goodie here: this workaround allows any connection attempt using SslStream
, e. g. ClientWebSocket
, to succeed. Therefore, your local SignalR server's WebSocket
transport will work as well!
Notes regarding code below:
- As I enabled
Nullable
for the whole MAUI project you'll see ?
suffixes on string
s & the like.
- I can't stand horizontal code scrolling anywhere, hence excessive usage of line breaks.
Alright, let's get into it:
MyMauiApp\Platforms\Android\DangerousTrustProvider.cs
:
#if DEBUG // Ensure this never leaves debug stages.
using Java.Net;
using Java.Security;
using Java.Security.Cert;
using Javax.Net.Ssl;
namespace MyMauiApp.Platforms.Android;
internal class DangerousTrustProvider : Provider
{
private const string DANGEROUS_ALGORITHM = nameof(DANGEROUS_ALGORITHM);
// NOTE: Empty ctor, i. e. without Put(), works for me as well,
// but I'll keep it for the sake of completeness.
public DangerousTrustProvider()
: base(nameof(DangerousTrustProvider), 1, "Dangerous debug TrustProvider") =>
Put(
$"{nameof(DangerousTrustManagerFactory)}.{DANGEROUS_ALGORITHM}",
Java.Lang.Class.FromType(typeof(DangerousTrustManagerFactory)).Name);
public static void Register()
{
if (Security.GetProvider(nameof(DangerousTrustProvider)) is null)
{
Security.InsertProviderAt(new DangerousTrustProvider(), 1);
Security.SetProperty(
$"ssl.{nameof(DangerousTrustManagerFactory)}.algorithm", DANGEROUS_ALGORITHM);
}
}
public class DangerousTrustManager : X509ExtendedTrustManager
{
public override void CheckClientTrusted(X509Certificate[]? chain, string? authType) { }
public override void CheckClientTrusted(X509Certificate[]? chain, string? authType,
Socket? socket) { }
public override void CheckClientTrusted(X509Certificate[]? chain, string? authType,
SSLEngine? engine) { }
public override void CheckServerTrusted(X509Certificate[]? chain, string? authType) { }
public override void CheckServerTrusted(X509Certificate[]? chain, string? authType,
Socket? socket) { }
public override void CheckServerTrusted(X509Certificate[]? chain, string? authType,
SSLEngine? engine) { }
public override X509Certificate[] GetAcceptedIssuers() =>
Array.Empty<X509Certificate>();
}
public class DangerousTrustManagerFactory : TrustManagerFactorySpi
{
protected override ITrustManager[] EngineGetTrustManagers() =>
new[] { new DangerousTrustManager() };
protected override void EngineInit(IManagerFactoryParameters? parameters) { }
protected override void EngineInit(KeyStore? store) { }
}
}
#endif
Since Android performs additional hostname verification, dynamically inheriting AndroidMessageHandler
in order to override its internal GetSSLHostnameVerifier
method by returning a dummy IHostNameVerifier
is required, too.
MyMauiApp\Platforms\Android\DangerousAndroidMessageHandlerEmitter.cs
:
#if DEBUG // Ensure this never leaves debug stages.
using System.Reflection;
using System.Reflection.Emit;
using Javax.Net.Ssl;
using Xamarin.Android.Net;
namespace MyMauiApp.Platforms.Android;
internal static class DangerousAndroidMessageHandlerEmitter
{
private const string NAME = "DangerousAndroidMessageHandler";
private static Assembly? EmittedAssembly { get; set; } = null;
public static void Register(string handlerName = NAME, string assemblyName = NAME) =>
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
assemblyName.Equals(args.Name)
? (EmittedAssembly ??= Emit(handlerName, assemblyName))
: null;
private static AssemblyBuilder Emit(string handlerName, string assemblyName)
{
var assembly = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName(assemblyName), AssemblyBuilderAccess.Run);
var builder = assembly.DefineDynamicModule(assemblyName)
.DefineType(handlerName, TypeAttributes.Public);
builder.SetParent(typeof(AndroidMessageHandler));
builder.DefineDefaultConstructor(MethodAttributes.Public);
var generator = builder.DefineMethod(
"GetSSLHostnameVerifier",
MethodAttributes.Public | MethodAttributes.Virtual,
typeof(IHostnameVerifier),
new[] { typeof(HttpsURLConnection) })
.GetILGenerator();
generator.Emit(
OpCodes.Call,
typeof(DangerousHostNameVerifier)
.GetMethod(nameof(DangerousHostNameVerifier.Create))!);
generator.Emit(OpCodes.Ret);
builder.CreateType();
return assembly;
}
public class DangerousHostNameVerifier : Java.Lang.Object, IHostnameVerifier
{
public bool Verify(string? hostname, ISSLSession? session) => true;
public static IHostnameVerifier Create() => new DangerousHostNameVerifier();
}
}
#endif
As a second last step, the newly created types need to be registered for Android MAUI debug builds.
MyMauiApp\MauiProgram.cs
:
namespace MyMauiApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>()
.ConfigureFonts(fonts => fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"));
builder.Services.AddTransient(provider => new HttpClient
{
BaseAddress = new Uri($@"https://{(DeviceInfo.DeviceType == DeviceType.Virtual
? "10.0.2.2" : "localhost")}:5001/"),
Timeout = TimeSpan.FromSeconds(10)
});
#if ANDROID && DEBUG
Platforms.Android.DangerousAndroidMessageHandlerEmitter.Register();
Platforms.Android.DangerousTrustProvider.Register();
#endif
return builder.Build();
}
}
Finally, for MAUI / Xamarin to really use the dynamically generated DangerousAndroidMessageHandler
, an AndroidHttpClientHandlerType
property inside the MyMauiApp.csproj
file, containing twice the handler's name, is required.
MyMauiApp\Platforms\Android\MyMauiApp.csproj
:
<PropertyGroup>
<AndroidHttpClientHandlerType>DangerousAndroidMessageHandler, DangerousAndroidMessageHandler</AndroidHttpClientHandlerType>
</PropertyGroup>
Alternatively, setting the Android runtime environment variable XA_HTTP_CLIENT_HANDLER_TYPE
to the same value works as well:
XA_HTTP_CLIENT_HANDLER_TYPE=DangerousAndroidMessageHandler, DangerousAndroidMessageHandler
Outro
Until the official fix arrives, remember: for the sake of this world's security, do not use this in production!
[X509Util] Error creating trust manager (crc64e2862aff4a97f0b0.DangerousTrustProvider_DangerousTrustManager): java.lang.IllegalArgumentException: Required method checkServerTrusted(X509Certificate[], String, String, String) missing [X509Util] Could not find suitable trust manager
which is weird as there's no such method with 3 string parameters documented - anywhere! Turns out, changingTrustManagerFactory
(seevar key
&Security.SetProperty
lines above) toDangerousTrustManagerFactory
solved that. ππ₯³ β Betray