Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mobile] SSL certificate hostname not verified when targeting local WebAPI #70434

Closed
shaiscytale opened this issue Jun 8, 2022 · 10 comments
Closed

Comments

@shaiscytale
Copy link

shaiscytale commented Jun 8, 2022

Description

Testing MAUI with Visual Studio 17.3 preview, I just want to reach a test endpoint (local dotnet core web api - running in debug).

Reproduction Steps

the code is pretty simple to reproduce, i just created a base MAUI project and replaced the MainPage.xaml.cs content with the following code :

public partial class MainPage : ContentPage
{
	bool apiReached = false;

	public MainPage()
	{
		InitializeComponent();
	}
    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;
    }
    private HttpClient CreateHttpClient(string authToken)
    {
        HttpClientHandler handler = GetInsecureHandler();
        var httpClient = new HttpClient(handler);
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        if (!string.IsNullOrEmpty(authToken))
        {
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
        }
        return httpClient;
    }

    private async void OnConnectClicked(object sender, EventArgs e)
	{
        //var client = new HttpClient();
        var client = CreateHttpClient("");
        var apiIp = "10.0.2.2";
        var httpPort = 5143;
        var endpoint = $"auth/test";
        var url = $"http://{apiIp}:{httpPort}/api/{endpoint}";
        client.BaseAddress = new Uri(url);

        try
        {
            var response = await client.GetAsync("");
            if (response.IsSuccessStatusCode)
            {
                apiReached = true;
            }
        }
        catch (Exception ex)
        {
        }
    }
}

Note that I followed the docs here

Expected behavior

set the apiReached bool value to true

Actual behavior

I'm stuck with the following exception on the client.GetAsync method :
Javax.Net.Ssl.SSLPeerUnverifiedException: Hostname 10.0.2.2 not verified

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

When I set an url value targetting an external public API like "https://httpbin.org/get", it works like a charm.

I guess the problem comes from the Android Emulator using 10.0.2.2 to reach localhost, when the base dev-certificate only contain a localhost reference, but the InsecureHandler method should bypass this issue, no ?

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jun 8, 2022
@ghost
Copy link

ghost commented Jun 8, 2022

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Testing MAUI with Visual Studio 17.3 preview, I just want to reach a test endpoint (local dotnet core web api - running in debug).

Reproduction Steps

the code is pretty simple to reproduce, i just created a base MAUI project and replaced the MainPage.xaml.cs content with the following code :

public partial class MainPage : ContentPage
{
	bool apiReached = false;

	public MainPage()
	{
		InitializeComponent();
	}
    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;
    }
    private HttpClient CreateHttpClient(string authToken)
    {
        HttpClientHandler handler = GetInsecureHandler();
        var httpClient = new HttpClient(handler);
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        if (!string.IsNullOrEmpty(authToken))
        {
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
        }
        return httpClient;
    }

    private async void OnConnectClicked(object sender, EventArgs e)
	{
        //var client = new HttpClient();
        var client = CreateHttpClient("");
        var apiIp = "10.0.2.2";
        var httpPort = 5143;
        var endpoint = $"auth/test";
        var url = $"http://{apiIp}:{httpPort}/api/{endpoint}";
        client.BaseAddress = new Uri(url);

        try
        {
            var response = await client.GetAsync("");
            if (response.IsSuccessStatusCode)
            {
                apiReached = true;
            }
        }
        catch (Exception ex)
        {
        }
    }
}

Note that I followed the docs here

Expected behavior

set the apiReached bool value to true

Actual behavior

I'm stuck with the following exception on the client.GetAsync method :
Javax.Net.Ssl.SSLPeerUnverifiedException: Hostname 10.0.2.2 not verified

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

When I set an url value targetting an external public API like "https://httpbin.org/get", it works like a charm.

I guess the problem comes from the Android Emulator using 10.0.2.2 to reach localhost, when the base dev-certificate only contain a localhost reference, but the InsecureHandler method should bypass this issue, no ?

Author: shaiscytale
Assignees: -
Labels:

area-System.Net.Http

Milestone: -

@ghost
Copy link

ghost commented Jun 8, 2022

Tagging subscribers to 'arch-android': @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Testing MAUI with Visual Studio 17.3 preview, I just want to reach a test endpoint (local dotnet core web api - running in debug).

Reproduction Steps

the code is pretty simple to reproduce, i just created a base MAUI project and replaced the MainPage.xaml.cs content with the following code :

public partial class MainPage : ContentPage
{
	bool apiReached = false;

	public MainPage()
	{
		InitializeComponent();
	}
    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;
    }
    private HttpClient CreateHttpClient(string authToken)
    {
        HttpClientHandler handler = GetInsecureHandler();
        var httpClient = new HttpClient(handler);
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        if (!string.IsNullOrEmpty(authToken))
        {
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
        }
        return httpClient;
    }

    private async void OnConnectClicked(object sender, EventArgs e)
	{
        //var client = new HttpClient();
        var client = CreateHttpClient("");
        var apiIp = "10.0.2.2";
        var httpPort = 5143;
        var endpoint = $"auth/test";
        var url = $"http://{apiIp}:{httpPort}/api/{endpoint}";
        client.BaseAddress = new Uri(url);

        try
        {
            var response = await client.GetAsync("");
            if (response.IsSuccessStatusCode)
            {
                apiReached = true;
            }
        }
        catch (Exception ex)
        {
        }
    }
}

Note that I followed the docs here

Expected behavior

set the apiReached bool value to true

Actual behavior

I'm stuck with the following exception on the client.GetAsync method :
Javax.Net.Ssl.SSLPeerUnverifiedException: Hostname 10.0.2.2 not verified

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

When I set an url value targetting an external public API like "https://httpbin.org/get", it works like a charm.

I guess the problem comes from the Android Emulator using 10.0.2.2 to reach localhost, when the base dev-certificate only contain a localhost reference, but the InsecureHandler method should bypass this issue, no ?

Author: shaiscytale
Assignees: -
Labels:

area-System.Net.Http, os-android, untriaged

Milestone: -

@rzikm
Copy link
Member

rzikm commented Jun 8, 2022

I find it strange that it even tries to verify the/a certificate since you are using HTTP and not HTTPS.

@simonrozsival
Copy link
Member

@shaiscytale this happens when there is a mismatch between the hostname (10.0.2.2) and the CN of the certificate (localhost). That check is independent of the server certificate validation. To bypass that validation, you need to extend AndroidMessageHandler and override the GetSSLHostnameVerifier function and return your custom verifier. In your case you could use something like:

internal sealed class CustomAndroidMessageHandler : AndroidMessageHandler
{
    protected override IHostnameVerifier GetSSLHostnameVerifier(HttpsURLConnection connection)
        => new CustomHostnameVerifier();

    private sealed class CustomHostnameVerifier : Java.Lang.Object, IHostnameVerifier
    {
        public bool Verify(string hostname, ISSLSession session)
            => HttpsURLConnection.DefaultHostnameVerifier.Verify(hostname, session)
                || (hostname == "10.0.2.2" && session.PeerPrincipal.Name == "CN=localhost");
    }
}

@joekrauss
Copy link

@simonrozsival I tried the solution you provided and I get another error "trust anchor for certification path not found". When I debug the solution the verify method in the CustomHostnameVerifier doesn't get called. I've tried finding any solution online and nothing seems to get past the self-signed certificates or the ability to use HTTP / clear text fix.

@steveisok steveisok removed the untriaged New issue has not been triaged by the area owner label Jun 9, 2022
@Eilon
Copy link
Member

Eilon commented Jun 15, 2022

I created a little helper class that I started using that works on Android and Windows to connect to "local" SSL: https://gist.github.com/Eilon/49e3c5216abfa3eba81e453d45cba2d4

And here's how you can use it to call from an Android emulator to a Windows-hosted ASP.NET Core app:

        var devSslHelper = new DevHttpsConnectionHelper(sslPort: 7155);
        var http = devSslHelper.HttpClient;
        var responseText = await http.GetStringAsync(devSslHelper.DevServerRootUrl + "/someApi");

It seems that to make it work on Android, there are two places in the handler that need to make the SSL checks go through:

  1. You need a ServerCertificateCustomValidationCallback that bypasses cert checks for localhost
  2. You need an IHostnameVerifier that reports that the 10.0.2.2 host is valid (that's how Android talks to the Windows host)

@Eilon
Copy link
Member

Eilon commented Jun 16, 2022

I started a discussion topic on how to connect from Android emulators to a local ASP.NET Web API running on Windows: dotnet/maui#8131

Please check that out and let us know if you have any feedback on any of the solutions presented.

@RorroRojas3
Copy link

  internal sealed class CustomAndroidMessageHandler : AndroidMessageHandler
  {
      protected override IHostnameVerifier GetSSLHostnameVerifier(HttpsURLConnection connection)
          => new CustomHostnameVerifier();
  
      private sealed class CustomHostnameVerifier : Java.Lang.Object, IHostnameVerifier
      {
          public bool Verify(string hostname, ISSLSession session)
              => HttpsURLConnection.DefaultHostnameVerifier.Verify(hostname, session)
                  || (hostname == "10.0.2.2" && session.PeerPrincipal.Name == "CN=localhost");
      }
  }

I can confirm that this fixed the "Hostname 10.0.2.2 not verified" for .NET Maui Blazor Hybrid for me

@EdCharbeneau
Copy link

EdCharbeneau commented Jul 13, 2022

I created a little helper class that I started using that works on Android and Windows to connect to "local" SSL: https://gist.github.com/Eilon/49e3c5216abfa3eba81e453d45cba2d4

And here's how you can use it to call from an Android emulator to a Windows-hosted ASP.NET Core app:

        var devSslHelper = new DevHttpsConnectionHelper(sslPort: 7155);
        var http = devSslHelper.HttpClient;
        var responseText = await http.GetStringAsync(devSslHelper.DevServerRootUrl + "/someApi");

It seems that to make it work on Android, there are two places in the handler that need to make the SSL checks go through:

  1. You need a ServerCertificateCustomValidationCallback that bypasses cert checks for localhost
  2. You need an IHostnameVerifier that reports that the 10.0.2.2 host is valid (that's how Android talks to the Windows host)

I added some bits to help with setting up DI. LMK what you think. @Eilon

https://gist.github.com/EdCharbeneau/ed3d44d8298319c201f276de7a0580f1

@Eilon
Copy link
Member

Eilon commented Jul 14, 2022

@EdCharbeneau very nice!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants