"Trust anchor for certification path not found." in a .NET Maui Project trying to contact a local .NET WebApi
Asked Answered
S

6

14

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?

Squamosal answered 9/2, 2022 at 10:2 Comment(0)
V
10

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.

Verbatim answered 20/2, 2022 at 16:29 Comment(8)
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. 😎πŸ₯³ – Betray
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; – Elemental
@Elemental 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. – Betray
@Verbatim Thank you!! This worked perfectly for me in .NET7 rc2. – Cottonseed
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? :) – Pyaemia
Could not resolve 'DangerousAndroidMessageHandler'. Please check your AndroidHttpClientHandlerType setting. Why I get this error when I'm build the app? – Backcourt
@Betray did you ever get this fixed? I'm getting the same error – Cheliform
@RyanLangton Read my answer below & especially the comments beneath it, thanks. – Betray
B
4

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!

Betray answered 1/4, 2022 at 9:56 Comment(5)
did you run into any build errors when setting the csproj property? For some reason i am getting a xamarin build error about it not being able to resolve DangerousAndroidMessageHandler. I have also tried specifying it like "Fullnamespace, AssemblyName" but that did not fix it either :/ – Fleecy
@Dbl: You did read that it's for MAUI & not Xamarin, right? – Betray
yup. i am having this issue on MAUI. So there was no issue for you at all? – Fleecy
Back then: no, not a single one. Nowadays? I don't know, because I'm working for a different employer & MAUI will be done later - rebuilding the whole architecture as a Blazor WASM one is first. But again, all code above was meant as a workaround until github.com/xamarin/xamarin-android/pull/6665 gets merged (finished in April 2022) & since then, there this perfect how-to by Eilon: github.com/dotnet/maui/discussions/8131 – Betray
8131 discussion fixed the issue for me. Thanks! It appears the runtime does not hot swap the HttpClientHandler if it is derived from the android handler type, so the custom type is actually called. Really appreciate your reply a lot. – Fleecy
M
4

In a .NET MAUI and .NET Core 8 application, you can bypass the certificate security check using this code:

// This method must be in a class in a platform project, even if
// the HttpClient object is constructed in a shared project.
public HttpClientHandler GetInsecureHandler()
{
    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;
            };
    return handler;
}

This is how you instantiate your HttpClient:

#if DEBUG
    HttpClientHandler insecureHandler = GetInsecureHandler();
    HttpClient client = new HttpClient(insecureHandler);
#else
    HttpClient client = new HttpClient();
#endif

Full details/Source here: https://learn.microsoft.com/en-us/previous-versions/xamarin/cross-platform/deploy-test/connect-to-local-web-services#bypass-the-certificate-security-check

Malca answered 10/7, 2024 at 10:39 Comment(1)
you truly rock, thanks for sharing! – Chewy
L
2

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"
Lactobacillus answered 1/8, 2022 at 0:38 Comment(0)
W
0

Instead of implementing a class that overrides the certificate verification, emphasizing this solution should not leave the development environment, you might alternatively do this:

Change https to http.

In the client project, change your API URL to HTTP, and add android:usesCleartextTraffic="true" to the AndroidManifest.xml file.

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

Wilkerson answered 4/7, 2022 at 19:55 Comment(1)
Nope, because that doesn't test the HTTPS pipeline, which you're supposed to use exclusively these days, i.e., do your best to prevent any HTTP-non-TLS access. – Betray
B
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?

Berchtesgaden answered 20/12, 2022 at 20:27 Comment(1)
The reasoning behind your answer is just wrong - we're not ignoring all certificates & besides even that, we're doing this in DEBUG only. – Betray

© 2022 - 2025 β€” McMap. All rights reserved.