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

android: Cannot detect when HTTPS server needs a client certificate with SocketsHttpHandler #109532

Open
mstefarov opened this issue Nov 5, 2024 · 5 comments
Labels
area-System.Net.Http os-android untriaged New issue has not been triaged by the area owner

Comments

@mstefarov
Copy link

mstefarov commented Nov 5, 2024

Description

I'm building a cross-platform auth library and I am stumped trying to detect a challenge for a client certificate (for PKI-secured HTTPS services). In the past the norm used to be to look for 401/Unauthorized replies, but these days many TLS/SSLv3-secured services use error alerts instead. This happens when the connection is still negotiated so I have no HttpResponseMessage to work with — all I have is an HttpRequestException.

On most platforms, I'm able to detect this. For example:

  • With SocketsHttpHandler on iOS, an InnerException (Interop+AppleCrypto+SslException) gives me a specific libsecurity error code via HResult. I can check if it matches e.g. errSSLPeerBadCert (-9825) and prompt user for a client certificate.
  • With AndroidMessageHandler, an InnerException (Javax.Net.Ssl.SSLException) gives me a detailed message from OpenSSL that includes easily testable strings like "SSLV3_ALERT_BAD_CERTIFICATE".
  • Old Xamarin "Managed" handler on Android used to give me an InnerException with the string "CERTIFICATE_VERIFY_FAILED"
  • Old Xamarin "Native" handler on Android used to give me an Javax.Net.Ssl.SSLHandshakeException with a detailed inner Java.Security.Cert.CertificateException

But with SocketsHttpHandler on Android, the inner exception (Interop+AndroidCrypto+SslException) the HResult is always just SecurityStatusPalErrorCode.InternalError (14). I bet there is more information available to the interop but there does not seem to be any way for me to access it.

Using LocalCertificateSelectionCallback also doesn't help because it just fires once for all HTTPS connections, with no way to know whether a client certificate is required or not.

Reproduction Steps

Use an HttpClient backed by SocketsHttpHandler to make an HTTPS request to a server that requires client certificate authentication:

var client = new HttpClient(new SocketsHttpHandler());
try
{
    await client.GetAsync("https://your-pki-secured-server-here.com/");
}
catch (Exception ex)
{
    // try to figure out if we were asked for client certificate authentication here
}

Unfortunately I do not have a specific public-facing service to share, but it should be possible to set one up for testing.

Expected behavior

SocketsHttpHandler throws an informative exception, from which we can determine that the server needs a client certificate. Perhaps an HttpRequestException with HttpRequestError.UserAuthenticationError would work.

Actual behavior

SocketsHttpHandler throws a generic HttpRequestException with "Unknown" HttpRequestError.

System.Net.Http.HttpRequestException: An error occurred while sending the request.
---> System.IO.IOException: The read operation failed, see inner exception.
---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception.
---> Interop+AndroidCrypto+SslException: Exception of type 'Interop+AndroidCrypto+SslException' was thrown.

The inner-most exception has a generic HResult of 14 ("internal error").

Regression?

Regression only if you compare the behavior to the old "managed" monodroid HttpClient.

Known Workarounds

Using AndroidMessageHandler instead of SocketsHttpHandler.

Configuration

No response

Other information

No response

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Nov 5, 2024
Copy link
Contributor

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

@rzikm
Copy link
Member

rzikm commented Nov 5, 2024

Using LocalCertificateSelectionCallback also doesn't help because it just fires for all HTTPS connections, with no way to know whether a client certificate is required or not.

I am not 100% sure how this behaves on Android, but on desktop platforms, the callback is called up to two times:

  1. before the connection attempt starts
  2. if null was provided during the first invocation and the server specifically asks for a client certificate

You should be able to distinguish between the two cases by looking at the remoteCertificate parameter (it will be null in the (1) case). While not ideal, you might be able to use this as a workaround until a better solution is implemented

Copy link
Contributor

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

@mstefarov
Copy link
Author

mstefarov commented Nov 5, 2024

I modified my repro to add a LocalCertificateSelectionCallback.
bool wasPrompted = false;
var handler = new SocketsHttpHandler();
handler.SslOptions.LocalCertificateSelectionCallback = (sender, host, localCerts, remoteCertificate, issuers) =>
{
    Trace.WriteLine($"{sender}, {host}, {localCerts}, {remoteCertificate}, {issuers}");
    if (remoteCertificate != null)
    {
        wasPrompted = true;
        if (localCerts != null && localCerts.Count > 0)
        {
            return localCerts[0];
        }
    }
    return null;
};
var client = new HttpClient(handler);
try
{
    await client.GetAsync("https://your-pki-secured-server-here.com");
}
catch (Exception ex)
{
    Trace.WriteLine(ex.Message);
}
Trace.WriteLine(wasPrompted.ToString());

I see that on Windows it is indeed called twice, just as you describe. But on Android it is only called once (with a null remoteCertificate).
A bit of additional information:

  • I am building with .NET 8.0.10 targeting net8.0-android34.0 with SupportedOSPlatformVersion set to 26.
  • The service is powered by Apache Tomcat 9.0.19
  • Connection happens via TLS 1.2

@wfurt
Copy link
Member

wfurt commented Nov 6, 2024

As minimum, I think we should surface the native error code @simonrozsival. And ideally figure out why the certificate request is not propagated to .NET

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Net.Http os-android untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

3 participants