c# – “Trust anchor for certification path not found.” in a .NET Maui Project trying to contact a local .NET WebApi

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 universal handler – which does implement 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 SslStreameg 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:

MyMauiAppPlatformsAndroidDangerousTrustProvider.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.

MyMauiAppPlatformsAndroidDangerousAndroidMessageHandlerEmitter.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.

MyMauiAppMauiProgram.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 DangerousAndroidMessageHandleran AndroidHttpClientHandlerType property inside the MyMauiApp.csproj file, containing twice the handler’s name, is required.

MyMauiAppPlatformsAndroidMyMauiApp.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 🥳

Leave a Comment