Ad

Our DNA is written in Swift
Jump

NSURLConnection with Self-Signed Certificates

A year ago I touched upon the question as to how you can prevent NSURLConnection from aborting a HTTPS GET if the certificate is invalid. At that time it seemed like the only method available was a forbidden one: allowsAnyHTTPSCertificateForHost. It’s undocumented, works, but gets your app rejected if Apple finds it when scanning your symbols.

But what should people do who don’t want to shell out hundreds of dollars for a trusted HTTPS certificate just so that they can reap the benefit of encrypting their web traffic and possibly hide user login data from prying eyes? The alternative to those commercial certificates is to produce a Self-Signed one and install it on your web server.

In this article I will demonstrate how to properly and officially deal with self-signed certificates via NSUrlConnection. It just so happens that I have a *.cocoanetics.com on my website, primarily used for protecting SVN communication. If you go to https://www.cocoanetics.com you will see it in this dialog:

Since a Self-Signed certificate does not have a trusted root the standard is to ask the user if he wants to trust the web site temporarily, permanently or not at all. The reason being that encryption only makes sense if you know that the recipient is who he says he is. Any other site can also produce a *.cocoanetics.com certificate for their IP address. Root Certification Authorities (CA) provide security that only a certain IP address can be the holder of a domain name. This is why you see the trust of the certificate be dependent on the trust in the certificate of the CA.

But if you are calling web services of your own you can forego this mechanism. In this article I am documenting how.

Lets first look at how to access our “web service” without the security overhead. In this example I want to access the RSS feed of my blog in a secure fashion.

// our secure service :-)
NSURL *server = [NSURL URLWithString:@"http://www.cocoanetics.com/feed/"];
NSURLRequest *request = [NSURLRequest requestWithURL:server];
 
// use synchronous convenience method
NSURLResponse *response = nil;
NSError *error = nil;
NSData *returnedData = [NSURLConnection sendSynchronousRequest:request
					returningResponse:&response
					error:&error];
if (!returnedData)
{
	NSLog(@"Error retrieving data, %@", [error localizedDescription]);
	return NO;
}
 
// get the correct text encoding
// http://stackoverflow.com/questions/1409537/nsdata-to-nsstring-converstion-problem
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)
					[response textEncodingName]);
NSStringEncoding encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
 
// output
NSString *xml = [[[NSString alloc] initWithData:returnedData encoding:encoding]
					autorelease];
NSLog(@"%@", xml);

We get the xml of my website RSS through this. There is another nifty trick in this, instead of hard coding UTF8 we actually get the appropriate encoding straight from the response. This is a good habbit so you should adopt that in any case.

If we change the HTTP to HTTPS we get the following error logged:

The certificate for this server is invalid. You might be connecting to a server that is pretending to be “www.cocoanetics.com” which could put your confidential information at risk.

We need to change the way we communicate now because the synchronous convenience method does not allow us to set a delegate for NSURLConnection. This forces us to switch to using NSURLConnection asynchronously which is the way that it’s meant to be used anyway.

When working with a delegate the delegate needs to be an instance of an object. Also data might come in chunks and/or there might be several redirects until we have the final data. So let’s put that all into its own class. The following does the same thing, but asynchronously.

WebService.h

@interface WebService : NSObject
{
	NSMutableData *receivedData;
	NSURLConnection *connection;
	NSStringEncoding encoding;
}
 
- (id)initWithURL:(NSURL *)url;
 
@end

WebService.m

#import "WebService.h"
 
@implementation WebService
 
- (id)initWithURL:(NSURL *)url
{
	if (self = [super init])
	{
		NSURLRequest *request = [NSURLRequest requestWithURL:url];
 
		connection = [NSURLConnection connectionWithRequest:request delegate:self];
 
		[connection start];
	}
 
	return self;
}
 
- (void)dealloc
{
	[connection cancel];
	[connection release];
 
	[receivedData release];
 
	[super dealloc];
}
 
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
	// every response could mean a redirect
	[receivedData release], receivedData = nil;
 
	// need to record the received encoding
	// http://stackoverflow.com/questions/1409537/nsdata-to-nsstring-converstion-problem
	CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)
														[response textEncodingName]);
	encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
 
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
	if (!receivedData)
	{
		// no store yet, make one
		receivedData = [[NSMutableData alloc] initWithData:data];
	}
	else
	{
		// append to previous chunks
		[receivedData appendData:data];
	}
}
 
// all worked
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
	NSString *xml = [[[NSString alloc] initWithData:receivedData encoding:encoding] autorelease];
	NSLog(@"%@", xml);
}
 
// and error occured
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
	NSLog(@"Error retrieving data, %@", [error localizedDescription]);
}
 
@end

This gets exactly the same result, with HTTP you get an xml blob logged, with HTTPS you get the error about the certificate. Now for the secret sauce that ghenriksen taught us about.

So we add these two further delegate methods:

// to deal with self-signed certificates
- (BOOL)connection:(NSURLConnection *)connection
		canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
	return [protectionSpace.authenticationMethod
			isEqualToString:NSURLAuthenticationMethodServerTrust];
}
 
- (void)connection:(NSURLConnection *)connection
		didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
	if ([challenge.protectionSpace.authenticationMethod
		 isEqualToString:NSURLAuthenticationMethodServerTrust])
	{
		// we only trust our own domain
		if ([challenge.protectionSpace.host isEqualToString:@"www.cocoanetics.com"])
		{
			NSURLCredential *credential =
				[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
			[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
		}
	}
 
	[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

When encountering a protected space the NSURLConnection asks the delegate if he is able to provide an authentication for this protection space. We respond YES if its a question of trusting the server, NSURLAuthenticationMethodServerTrust.

Then an authentication challenge is formed for this protection space to which we respond with an appropriate token symbolizing “yes we trust this server”. But of course only our own.

The only piece missing here to make this a usable web service wrapper – consider this your homework – is to create a delegate protocol for our WebService class which informs the outside world if an error has occurred (WebService:self didFailWithError:error) or of success (WebService:self didFinishWithString:string). Optionally you might want to be able to set the trusted host dynamically, like by retrieving the [url host] and storing it in an instance variable for later comparison.

The same mechanism could be used if you wanted to ask the user like Safari does. In this case you’d have to hold onto the NSURLAuthenticationChallenge instance until the user has chosen his response and then you either don’t create the server trust credential, or create it and save the server name temporarily or permanently in an internal list of trusted hosts.


Categories: Recipes

31 Comments »

  1. I’m using this for accessing to some NAS devices running self signed certificates …
    I’m looking for a way to use server with untrusted certificate with MPMoviePlayerViewController, but I didn’t find any solution yet (apart from declaring the certificate in the phone using iphone configuration tool).

  2. There’s no way to do that directly. You could download the file like demonstrated and play it locally.

  3. Thanks for your answer !
    That’s what I was afraid off … Some answer by Apple staff members on the apple dev forum are saying that the only way is to add certificate in the iPhone/iPad … Downloading the whole file is not an option in my case …

  4. So why don’t you get a proper SSL certificate for your media server?

  5. Because the problem is not for me, it is for people that will get my application from the appstore and use it …
    Video will be restricted to non SSL connection …

  6. If the server has a proper SSL certificate coming from a proper CA then you have no problem.

  7. Hello Drops

    I am using a HTTPs connection for my project. I am testing my code in the iOS simulator. I have copied the .der file into the Resource bundle and used SecItemAdd to install it.

    When I use NSURL connection now, i still get error.

    Is this a correct approach?

    Thank You

  8. To my knowledge you cannot programmatically install certificates like this. You have to install them with the iPhone administration tool

  9. Hi,

    Thanks for your sharing.. It is more valuable for the new beginners like me…

    Is it possible to try this sample code with the localhost. I want to try this one with the local site running in my mac system ? I configured the HTTPS in my local apache server and I am trying to do this one but I am not getting anything .

  10. If it does not work with https://localhost:443 then your apache config might be to blame.

  11. Thanks once agian… I will try it first and let you know.. Thanks once again…

  12. Hi.. In this example you tried the asynchronous connection but i am using synchronous connection to receive the data it is possible to call these same callback events for synchronous connection also? My code is below:

    NSData *responseData = [NSURLConnection sendSynchronousRequest:requestForSynchronousConn returningResponse:&response error:&error];

  13. Sorry.. I confused i think I am talking about your previous scenario but using NSURLConnection is the only way achieve this we didn’t have any other solution

  14. Hi, I tried your method and received below error:

    Error retrieving data, The certificate for this server is invalid. You might be connecting to a server that is pretending to be “localhost” which could put your confidential information at risk.

  15. Synchronous requests don’t call authentication call-backs.

  16. Hi ….. I was just trying out the same thing but getting stuck at one place. If you could help me out

  17. But all you’ve done here is told NSURLConnection to ignore the cert, thereby bypassing identification. You might as well not have a cert at all, from the standpoint of verifying the server identity — as you pointed out at the top, anyone can create a self-signed cert with your domain name in it and your code shown here will accept it as valid.

    In other words, you’re using SSL purely for encryption, not for identification. And it’s only encryption from an attacker who can’t mess with your DNS. If they can point http://www.cocoanetics.com to their server, which is pretty easy to do if they’ve compromised a wireless base station, they mount a man-in-the-middle attack to forward data to and from your real server and watch all the traffic in cleartext.

    If you actually wanted to secure and identify the connection, you’d copy your real certificate’s public key into the app and compare it against the public key of the cert being presented by the server.

  18. FYI, alternate sample code that actually does validate against the specific known cert is at: http://blog.wingsofhermes.org/?p=58

  19. If you need to quickly create a free self-signed certificate you can use http://MakeCert.com

  20. This is a great post and explains the issue concerning self-signed certificates very well. Thank you.

    However, if you use Apple’s reachability to check whether the https server is reachable either through a WiFi network or WWAN and if that server’s ssl certificate is self-signed, the reachability status returns not reachable. This happens even before starting the asynchronous NSURLconnection. Have you seen this issue? Please post your comments on this topic.