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.