7

I'm attempting to make a web request that's failing because of a self signed certificate :

Client = new HttpClient(); 
HttpResponseMessage Response = await Client.GetAsync(Uri)//defined elsewhere 

This throws a trust failure exception.

I tried again using httpclienthandler as suggested here Allowing Untrusted SSL Certificates with HttpClient:

 var handler = new HttpClientHandler();

 handler.ServerCertificateCustomValidationCallback = 
 (
   HttpRequestMessage message, 
   X509Certificate2 cert, 
   X509Chain chain, 
   SslPolicyErrors errors
  ) =>{return true; };//remove if this makes it to production 

  Client = new HttpClient(handler); 

This blows up throwing a system not implemented exception.

Are there any other ways to trust a self signed cert? I've even installed the certificate on the machine making the request but no luck.

SushiHangover
  • 73,120
  • 10
  • 106
  • 165
Kisaragi
  • 2,198
  • 3
  • 16
  • 28

1 Answers1

15

I have seen so many question regarding this I figured I write up as a complete answer and example as I can.

Note: Using WKWebView with self-sign certs, see this answer

HttpClient Implementation

Note: Using badssl.com in this example

Managed (Default)

System.Net.Http.HttpRequestException: An error occurred while sending the request ---> System.Net.WebException: Error: TrustFailure (One or more errors occurred.) ---> System.AggregateException: One or more errors occurred. ---> System.Security.Authentication.AuthenticationException: A call to SSPI failed, see inner exception. ---> Mono.Security.Interface.Tl

The original Mono Managed provider is getting really long in the tooth and only supports TLS1.0, in terms of security & performance I would move to using the NSUrlSession implementation.

CFNetwork (iOS 6+)

Note: As this iOS version is fairly old now and I personally do not target it anymore, so I leave this blank... (unless someone really needs me to lookup my notes for it ;-)

NSUrlSession (iOS 7+)

Xamarin provides a HttpMessageHandler subclass (NSUrlSessionHandler) that is based upon iOS' NSUrlSession.

Using it by itself against a self-signed cert will result in:

System.Net.WebException: An SSL error has occurred and a secure connection to the server cannot be made. ---> Foundation.NSErrorException: Exception of type 'Foundation.NSErrorException' was thrown.

The problem is that a self-sign cert is considered insecure and non-trusted by iOS, thus you have to apply an ATS exception to your app so iOS knows that your app is untrusted in the Info.plist.

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>self-signed.badssl.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

Now that iOS knows that your app is making untrusted calls, a HttpClient request will now result in this error:

System.Net.WebException: The certificate for this server is invalid. You might be connecting to a server that is pretending to be‚ self-signed.badssl.com‚ which could put your confidential information at risk. ---> Foundation.NSErrorException: Exception of type 'Foundation.NSErrorException' was thrown.

This error is due to the fact that even though the ATS exception has been allow, the default NSUrlSession provided by iOS will apply its standard NSUrlAuthenticationChallenge to the certificate and fail since a self-signed cert can never be truly authenticated (even via client pinning) since it does not include a root certificate authority (CA) in its chain that is trusted by iOS.

Thus you need to intercept and bypass the certificate security checking provided by iOS (Yes, a big security alert, flashing red lights, etc...)

But, you can do this via creating a NSUrlSessionDataDelegate subclass that does the bypass.

public class SelfSignedSessionDataDelegate : NSUrlSessionDataDelegate, INSUrlSessionDelegate
{
    const string host = "self-signed.badssl.com";
    public override void DidReceiveChallenge(NSUrlSession session, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
    {
        switch (challenge.ProtectionSpace.Host)
        {
            case host:
                using (var cred = NSUrlCredential.FromTrust(challenge.ProtectionSpace.ServerSecTrust))
                {
                    completionHandler.Invoke(NSUrlSessionAuthChallengeDisposition.UseCredential, cred);
                }
                break;
            default:
                completionHandler.Invoke(NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, null);
                break;
        }
    }
}

Now you need to apply that NSUrlSessionDataDelegate to a NSUrlSession and use that new session in the creation of your NSUrlSessionHandler that will be provided in the constructor of the HttpClient.

var url = "https://self-signed.badssl.com";
using (var selfSignedDelegate = new SelfSignedSessionDataDelegate())
using (var session = NSUrlSession.FromConfiguration(NSUrlSession.SharedSession.Configuration, (INSUrlSessionDelegate)selfSignedDelegate, NSOperationQueue.MainQueue))
using (var handler = new NSUrlSessionHandler(session))
using (var httpClient = new HttpClient(handler))
using (var response = await httpClient.GetAsync(url))
using (var content = response.Content)
{
    var result = await content.ReadAsStringAsync();
    Console.WriteLine(result);
}

Note: Example only, normally you would create a single Delegate, NSUrlSession, HttpClient, NSUrlSessionHandler and re-use it for all your requests (i.e. Singleton pattern)

Your request now works:

<html>
   <head>
    <title>self-signed.badssl.com</title>
  </head>
  <body><div id="content"><h1 style="font-size: 12vw;">
    self-signed.<br>badssl.com
    </h1></div>
  </body>
</html>

Note: The option to supply your own custom NSUrlSession to Xamarin's NSUrlSessionHandler is really new (Nov. 2017) and not currently in a release build (alpha, beta or stable), but of course, source is available at:

Using NSUrlSession instead of HttpClient:

You can also directly use a NSUrlSession instead of HttpClient against a self-signed cert.

var url = "https://self-signed.badssl.com";
using (var selfSignedDelegate = new SelfSignedSessionDataDelegate())
using (var session = NSUrlSession.FromConfiguration(NSUrlSession.SharedSession.Configuration, (INSUrlSessionDelegate)selfSignedDelegate, NSOperationQueue.MainQueue))
{
    var request = await session.CreateDataTaskAsync(new NSUrl(url));
    var cSharpString = NSString.FromData(request.Data, NSStringEncoding.UTF8).ToString(); 
    Console.WriteLine(cSharpString);
}

Note: Example only, normally you would create a single Delegate and NSUrlSession and re-use it for all your requests, i.e. Singleton pattern

Real Solution? Use Free Secure Certificates:

IHMO, avoid self-signed certs all together, even in a development environment and use one of the free certificate services and avoid all the headaches of applying ATS exceptions, custom code to intercept/bypass iOS security, etc... and make your app web services actually secure.

I personally use Let’s Encrypt:

SushiHangover
  • 73,120
  • 10
  • 106
  • 165
  • 1
    Thank you or this in depth write up! This has been very beneficial for me! – Kisaragi Dec 06 '17 at 20:03
  • Thanks for the write up! Literally the only thing I can manage to find on this. But, I have some issues: The source being available for this is awesome, but how are we supposed to use it in an actual application? For us, we're not trying to bypass a self-signed cert, but rather do certificate pinning. – Thomas F. Mar 06 '18 at 15:49
  • @ThomasF. Are you trying to use a `WKWebView` via a client pinned cert? – SushiHangover Mar 06 '18 at 15:59
  • Nah, and actually I'm not sure if this stuff applies to `Xamarin.Forms` *(I just noticed this was tagged Xamarin.iOS not Forms)*. I'm trying to implement a web API that's being called via `HttpClient` but using `NSUrlSessionHandler` and pin the API certificate.This example works great up to the point where I can't actually make `NSUrlSessionHandler` use the session and custom `NSUrlSessionDataDelegate DidReceiveChallenge` implementation :/ – Thomas F. Mar 06 '18 at 16:06
  • @ThomasF. Does it apply to Forms, yes, as Forms is *just* the UI, not the actual platform app. You can apply the `NSUrlSessionHandler` via the build settings within your `Xamarin.iOS` project and thus all `HttpClient` instances will have the `NSUrlSessionHandler` applied "auto-magically". You can then pin against it via the `ServerCertificateValidationCallback`, take a look at : https://thomasbandt.com/certificate-and-public-key-pinning-with-xamarin – SushiHangover Mar 06 '18 at 16:14
  • Ah ok, see I thought that `ServerCertificateValidationCallback` was only managed, so it wouldn't be used if you picked the native handler. Thanks, that clears some things up! – Thomas F. Mar 06 '18 at 16:18
  • @ThomasF. The `ServerCertificateValidationCallback` will work in this case since you are not using self-signed certs, if you run into problems post a new question. Happy Coding – SushiHangover Mar 06 '18 at 16:23
  • 1
    Unless I'm misunderstanding something, there seems to be a new property `NSUrlSessionHandler.TrustOverrideForUrl` that may be more convenient than creating a delegate, but achieves the same goal. – Mike Asdf Jul 12 '21 at 19:21
  • Your 'real solution' doesn't account for anyone calling HTTPS servers on their local network. For example, I'm trying to call a JSON-RPC service on the OpenWrt box that routes my Internet connection, and LE are great but I can't ask them to validate a cert on an HTTPS server that isn't open to the Internet (as far as I'm aware.) I don't want any old device connected to my network (e.g. Google Chromecast) to be able to see the traffic in cleartext. So... the real solution in this case IS to use a self-signed certificate. – abagonhishead Nov 13 '22 at 20:03