iPhone SSL based NSURLConnection with your own root cert
I seem to be struggling with lots of things lately that should be easy, but I just can’t find an easy way to do them. My latest struggle has been using NSURLConnection to connect to an SSL site that is secured using my root certificate. There is no easy reference anywhere for how to do this without loading the root cert into the user’s general key store – which is fiddly and not the best in some circumstances.
In my case I use cacert.org as my root cert and I don’t want to force the end user to trust all sites signed by cacert.org, I just want my SSL connection to work inside my app.
So it turns out there are two methods that you need to use in the NSURLConnection delegate class. Both are referenced in many places, but it’s generally to tell you how to completely bypass any check. You can also use them to check against your own root cert.
First of all, you implement -(BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace so that for server authentication requests the NSURLConnection class knows you can handle them.
Then you implement – (void)connection:(NSURLConnection *)conn didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge to actually handle the check. The code is pretty simple.
As a note – rootCert in the attached code is a reference to a certificate I loaded from a file (DER encoded).
#pragma mark -
#pragma mark Connection Authentication Handling
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
#pragma unused(conn)
NSString * challenge = [protectionSpace authenticationMethod];
if ([challenge compare:NSURLAuthenticationMethodServerTrust] == 0) {
return YES;
}
return NO;
}
/* Look to see if we can handle the challenge */
- (void)connection:(NSURLConnection *)conn didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
#pragma unused(conn)
NSLog(@"didReceiveAuthenticationChallenge %@ %zd", [[challenge protectionSpace] authenticationMethod], (ssize_t) [challenge previousFailureCount]);
NSURLCredential * credential = nil;
NSURLProtectionSpace * protectionSpace;
SecTrustRef trust;
int err;
/* Setup */
protectionSpace = [challenge protectionSpace];
trust = [protectionSpace serverTrust];
credential = [NSURLCredential credentialForTrust:trust];
/* Set up the array of certs we will authenticate against and create cred */
NSArray * certs = [[NSArray alloc] initWithObjects:(id)rootCert,nil];
credential = [NSURLCredential credentialForTrust:trust];
/* Build up the trust anchor using our root cert */
err = SecTrustSetAnchorCertificates(trust, (CFArrayRef) certs);
SecTrustResultType trustResult = 0;
if (err == noErr) {
err = SecTrustEvaluate(trust, &trustResult);
}
[certs release];
BOOL trusted = (err == noErr) && ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified));
// Return based on whether we decided to trust or not
if (trusted)
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
else {
NSLog(@"Trust evaluation failed for service root certificate");
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
Again – I reckon there will be a quicker way to do this and probably some bugs in what I’ve done – feel free to comment and set me on the straight and narrow!
[challenge compare:NSURLAuthenticationMethodServerTrust] ==
0 should be written as [challenge
isEqualToString:NSURLAuthenticationMethodServerTrust]
As a complete newbie in the iPhone dev world, I find
extremely hard to do things like this… that should be
straightforward. Anyway, just wanted to let you know that this
article is great… I want to trust certificates signed by my CA
and it looks like a great starting point. Thanks for sharing this
knowledge!!!
I had to read in a certificate from my bundle, to use to verify. Also, I’m using this code in concurrent threads, and based on comments in Security Framework. I single threaded the security checks. Code also ARCified:
#if ! __has_feature(objc_arc)
#error THIS CODE MUST BE COMPILED WITH ARC ENABLED!
#endif
static CFArrayRef certs;
dispatch_queue_t securityQ;
@implementation MyWebFetcher
+ (void)initialize
{
if(self == [LTJSONFetcher class]) {
// I had a crt certificate, needed a der one, so found this site:
// http://fixunix.com/openssl/537621-re-der-crt-file-conversion.html
// and did this from Terminal: openssl x509 -in crt.crt -outform der -out crt.der
NSString *path = [[NSBundle mainBundle] pathForResource:@”Lot18-CA” ofType:@”der”];
assert(path);
NSData *data = [NSData dataWithContentsOfFile:path];
assert(data);
SecCertificateRef rootcert = SecCertificateCreateWithData(NULL, CFBridgingRetain(data));
const void *array[1] = { rootcert };
certs = CFArrayCreate(NULL, array, 1, &kCFTypeArrayCallBacks);
CFRelease(rootcert); // for completeness, really does not matter
securityQ = dispatch_queue_create(“com.url.security”, DISPATCH_QUEUE_SERIAL);
}
}
#pragma mark –
#pragma mark Connection Authentication Handling
@implementation LTJSONFetcher (NSURLConnectionDelegate)
– (BOOL)connection:(NSURLConnection *)conn canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
#pragma unused(conn)
NSString * challenge = [protectionSpace authenticationMethod];
NSLog(@”canAuthenticateAgainstProtectionSpace challenge %@ isServerTrust=%d”, challenge, [challenge isEqualToString:NSURLAuthenticationMethodServerTrust]);
if ([challenge isEqualToString:NSURLAuthenticationMethodServerTrust]) {
return YES;
}
return NO;
}
/* Look to see if we can handle the challenge */
– (void)connection:(NSURLConnection *)conn didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
#pragma unused(conn)
NSLog(@”didReceiveAuthenticationChallenge %@ FAILURES=%d”, [[challenge protectionSpace] authenticationMethod], (int)[challenge previousFailureCount]);
/* Setup */
NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
assert(protectionSpace);
SecTrustRef trust = [protectionSpace serverTrust];
assert(trust);
CFRetain(trust);
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
#if 0 // moved to initialize for re-use
NSString *path = [[NSBundle mainBundle] pathForResource:@”Lot18-CA” ofType:@”der”];
assert(path);
NSData *data = [NSData dataWithContentsOfFile:path];
assert(data);
SecCertificateRef rootcert = SecCertificateCreateWithData(NULL, CFBridgingRetain(data));
const void *array[1] = { rootcert };
CFArrayRef certs = CFArrayCreate(NULL, array, 1, NULL);
/* Set up the array of certs we will authenticate against and create cred */
#endif
/* Build up the trust anchor using our root cert */
__block int err;
SecTrustResultType trustResult = 0;
SecTrustResultType *trustResultP = &trustResult;
dispatch_sync(securityQ, ^
{
err = SecTrustSetAnchorCertificates(trust, certs);
if (err == noErr) {
err = SecTrustEvaluate(trust,trustResultP);
}
}
);
CFRelease(trust);
BOOL trusted = (err == noErr) && ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified));
// Return based on whether we decided to trust or not
if (trusted) {
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
} else {
NSLog(@”Trust evaluation failed for service root certificate”);
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
@end
I took a slightly modified version of this code and put it on my gist site: https://gist.github.com/2051599
David
PS: don’t think the serial queue is really needed – the class reference is confusing on whether that is only needed in 10.6 or not.