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.
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:
- xamarin-macios/src/Foundation/NSUrlSessionHandler.cs
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: