9

I'm new to mobile development and I'm trying to have my .NET Maui app connect to a local ASP.NET Core website (API).

I am currently blocked by this exception:

System.Net.WebException: 'java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.'

I have followed this article https://learn.microsoft.com/en-us/xamarin/cross-platform/deploy-test/connect-to-local-web-services#bypass-the-certificate-security-check

Running dotnet dev-certs https --trust returns A valid HTTPS certificate is already present.

My current code is:

HttpClientHandler handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
  {
     if (cert.Issuer.Equals("CN=localhost"))
          return true;
     return errors == System.Net.Security.SslPolicyErrors.None;
  };

var httpclient = new HttpClient(handler);
var test = await httpclient.PostAsync($"https://10.0.2.2:44393/" + uri, new StringContent(serializedItem, Encoding.UTF8, "application/json"));

But the thing is that i never enter the ServerCertificateCustomValidationCallback.

I also tried

        ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) =>
        {
            return true;
        };

But no luck with that either.

Did something change in .NET MAUI?

Marc
  • 91
  • 1
  • 3

5 Answers5

9

I encountered exactly the same problem when I was trying to get SignalR client to connect my local test server. After digging into the source code, I found that HttpClientHandler actually uses AndroidMessageHandler as its underlying handler.

While AndroidMessageHandler implements a ServerCertificateCustomValidationCallback property, its value is never used when sending requests. This issue is addressed in this pull request.

For now, to disable server certificate verification on Android, you can implement a custom TrustProvider which will bypass any certificate verification:

using Java.Net;
using Java.Security;
using Java.Security.Cert;
using Javax.Net.Ssl;

namespace MyApp.Platforms.Android
{
    internal class DangerousTrustProvider : Provider
    {
        private const string TRUST_PROVIDER_ALG = "DangerousTrustAlgorithm";
        private const string TRUST_PROVIDER_ID = "DangerousTrustProvider";

        public DangerousTrustProvider() : base(TRUST_PROVIDER_ID, 1, string.Empty)
        {
            var key = "TrustManagerFactory." + DangerousTrustManagerFactory.GetAlgorithm();
            var val = Java.Lang.Class.FromType(typeof(DangerousTrustManagerFactory)).Name;
            Put(key, val);
        }

        public static void Register()
        {
            Provider registered = Security.GetProvider(TRUST_PROVIDER_ID);
            if (null == registered)
            {
                Security.InsertProviderAt(new DangerousTrustProvider(), 1);
                Security.SetProperty("ssl.TrustManagerFactory.algorithm", TRUST_PROVIDER_ALG);
            }
        }

        public class DangerousTrustManager : X509ExtendedTrustManager
        {
            public override void CheckClientTrusted(X509Certificate[] chain, string authType, Socket socket) { }

            public override void CheckClientTrusted(X509Certificate[] chain, string authType, SSLEngine engine) { }

            public override void CheckClientTrusted(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 void CheckServerTrusted(X509Certificate[] chain, string authType) { }

            public override X509Certificate[] GetAcceptedIssuers() => Array.Empty<X509Certificate>();
        }

        public class DangerousTrustManagerFactory : TrustManagerFactorySpi
        {
            protected override void EngineInit(IManagerFactoryParameters mgrparams) { }

            protected override void EngineInit(KeyStore keystore) { }

            protected override ITrustManager[] EngineGetTrustManagers() => new ITrustManager[] { new DangerousTrustManager() };

            public static string GetAlgorithm() => TRUST_PROVIDER_ALG;
        }
    }
}

If you also want to disable host name verfication, you'll need to dynamically inherit from AndroidMessageHandler and override its internal GetSSLHostnameVerifier method, to return a dummy IHostNameVerifier:

using Javax.Net.Ssl;
using System.Reflection;
using System.Reflection.Emit;
using Xamarin.Android.Net;

namespace MyApp.Platforms.Android
{
    static class DangerousAndroidMessageHandlerEmitter
    {
        private static Assembly _emittedAssembly = null;

        public static void Register(string handlerTypeName = "DangerousAndroidMessageHandler", string assemblyName = "DangerousAndroidMessageHandler")
        {
            AppDomain.CurrentDomain.AssemblyResolve += (s, e) =>
            {
                if (e.Name == assemblyName)
                {
                    if (_emittedAssembly == null)
                    {
                        _emittedAssembly = Emit(handlerTypeName, assemblyName);
                    }

                    return _emittedAssembly;
                }
                return null;
            };
        }

        private static AssemblyBuilder Emit(string handlerTypeName, string assemblyName)
        {
            var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.Run);
            var module = assembly.DefineDynamicModule(assemblyName);

            DefineDangerousAndroidMessageHandler(module, handlerTypeName);

            return assembly;
        }

        private static void DefineDangerousAndroidMessageHandler(ModuleBuilder module, string handlerTypeName)
        {
            var typeBuilder = module.DefineType(handlerTypeName, TypeAttributes.Public);
            typeBuilder.SetParent(typeof(AndroidMessageHandler));
            typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);

            var methodBuilder = typeBuilder.DefineMethod(
                "GetSSLHostnameVerifier",
                MethodAttributes.Public | MethodAttributes.Virtual,
                typeof(IHostnameVerifier),
                new[] { typeof(HttpsURLConnection) }
            );

            var generator = methodBuilder.GetILGenerator();
            generator.Emit(OpCodes.Call, typeof(DangerousHostNameVerifier).GetMethod("Create"));
            generator.Emit(OpCodes.Ret);

            typeBuilder.CreateType();
        }
    }

    public class DangerousHostNameVerifier : Java.Lang.Object, IHostnameVerifier
    {
        public bool Verify(string hostname, ISSLSession session)
        {
            return true;
        }

        public static IHostnameVerifier Create() => new DangerousHostNameVerifier();
    }
}

Call DangerousAndroidMessageHandlerEmitter.Register and DangerousTrustProvider in your MauiProgram:

#if ANDROID && DEBUG
Platforms.Android.DangerousAndroidMessageHandlerEmitter.Register();
Platforms.Android.DangerousTrustProvider.Register();
#endif

One last step, you need to tell Xamarin to use your dynamically generated DangerousAndroidMessageHandler. You should be able to do so by setting AndroidHttpClientHandlerType to fully-qualified name of the handler type in your csproj file:

<PropertyGroup>
    <AndroidHttpClientHandlerType>DangerousAndroidMessageHandler, DangerousAndroidMessageHandler</AndroidHttpClientHandlerType>
</PropertyGroup>

Or set Android runtime environment variable XA_HTTP_CLIENT_HANDLER_TYPE to the name of the handler:

XA_HTTP_CLIENT_HANDLER_TYPE=DangerousAndroidMessageHandler, DangerousAndroidMessageHandler

The above workaround will also work for ClientWebSocket and anything else using SslStream. Which means you can connect to your test SignalR server with the WebSocket transport (which is what I was trying to achieve).

Just remember, DO THIS ONLY IN DEBUG BUILDS.

noelex
  • 300
  • 2
  • 8
  • 1
    Using latest MAUI (VS 17.2 Preview 2.1 as of today), I kept getting `[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, changing `TrustManagerFactory` (see `var key` & `Security.SetProperty` lines above) to `DangerousTrustManagerFactory` solved that. – Yoda Mar 31 '22 at 07:59
  • DangerousTrustProvider has a heap of missing dependancies, couldnt spot them in NUGET, where do we get these usings from? using Java.Net; using Java.Security; using Java.Security.Cert; using Javax.Net.Ssl; – devonuto Apr 13 '22 at 12:42
  • 1
    @devonuto You need to create a .NET MAUI project & select `net6.0-android` as compile target from the toolbar menu drop-down. Then, MAUI / Visual Studio will properly resolve them, no need to install any NuGet package - see my own answer for a full how-to. – Yoda Apr 16 '22 at 13:07
  • @noelex Thank you!! This worked perfectly for me in .NET7 rc2. – gcadmes Oct 25 '22 at 15:21
  • Thank you so much! this helped me a lot for debugging my chromecast app to a physical device on my network. Do you have a solution for iOS? :) – vhugo Jul 05 '23 at 17:09
2

Intro

Since some "super-clever" SO reviewers thought it would - quote -

defaces the post in order to promote a product or service, or is deliberately destructive

if nolex's answer gets edited to

  • fix a bug causing her/his solution to fail in latest MAUI (using VS version 17.2 Preview 2.1)
  • remove unnecessary / obsolete stuff from her/his code to
  • simplify it by using C# syntax sugar available since at least C# 10.0, if not already 9.0

I'm posting the updated code as a separate answer.

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:

  1. As I enabled Nullable for the whole MAUI project you'll see ? suffixes on strings & the like.
  2. 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!

Now go, chase that (app) dream of yours

Yoda
  • 539
  • 1
  • 9
  • 18
1

In MainApplication.cs for the Android platform:

#if DEBUG
[Application(AllowBackup = false, Debuggable = true, UsesCleartextTraffic = true)]
#else
[Application]
#endif
public class MainApplication : MauiApplication

In ASP.NET Core API Program.cs:

#if !DEBUG
app.UseHttpsRedirection();
#endif

In MauiProgram.cs:

    #if DEBUG
        private static readonly string Base = "http://192.168.0.15";
        private static readonly string ApiBaseUrl = $"{Base}:5010/";
    #else
        private static readonly string ApiBaseUrl = "https://YOUR_APP_SERVICE.azurewebsites.net/";
    #endif

...

builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(ApiBaseUrl) });

In ASP.NET Core API launchSettings.json:

"applicationUrl": "https://*:5011;http://*:5010"
unsane
  • 81
  • 1
  • 6
0

If we're forced to implement classes that override the certificate verification, with emphasis on this never leaving the development environment, might as well do the bad thing... with fewer lines of code.

Just change https to http.

In the client project change the URL of your API to http and add android:usesCleartextTraffic="true" in AndroidManifest.xml.

In your server project comment out line app.UseHttpsRedirection();

This is terrible and I hope it will be fixed soon.

Coop
  • 74
  • 6
0

Alternative to ignoring all certificates is to install certificate on your dev device yourself, it will also workaround MAUI/Xamarin issue with ServerCertificateCustomValidationCallback for android SSL connections. For iOS it works out of the box, for Android you need to allow app to use user certificates as described here: How to install trusted CA certificate on Android device?

Bohdan
  • 96
  • 3