Using Self-signed certificates with iOS5
There have been a few changes to SSL in iOS5 SDK, which caused some snafus to our iPhone app. We have been using ASIHTTPRequest library for accessing our web services that require SSL and we use self-signed certificates in test environment. I noticed that the iPhone app wasn’t able to connect to the server after upgrading to iOS5 SDK. The original code in ASIHTTPRequest.m that accepted self-signed certificates looked like:
NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates, [NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot, [NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain, kCFNull,kCFStreamSSLPeerName, nil]; CFReadStreamSetProperty((CFReadStreamRef)[self readStream], kCFStreamPropertySSLSettings, (CFTypeRef)sslProperties);
However, the iOS5 SDK deprecated kCFStreamPropertySSLPeerCertificates, kCFStreamSSLAllowsExpiredCertificates, kCFStreamSSLAllowsExpiredRoots, kCFStreamSSLAllowsAnyRoot and requires using kCFStreamSSLValidatesCertificateChain to disable certificates, e.g.:
CFReadStreamSetProperty((CFReadStreamRef)[self readStream], kCFStreamPropertySSLSettings, [NSMutableDictionary dictionaryWithObject:(NSString *)kCFBooleanFalse forKey:(NSString *)kCFStreamSSLValidatesCertificateChain]); [[self readStream] setProperty:[NSDictionary dictionaryWithObjectsAndKeys: (id)kCFBooleanFalse, kCFStreamSSLValidatesCertificateChain, nil] forKey:(NSString *)kCFStreamPropertySSLSettings];
Here is a complete example if you need to skip validation without using ASIHTTPRequest:
- (void)start { CFStringRef bodyString = CFSTR("xml...."); CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, bodyString, kCFStringEncodingUTF8, 0); CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("POST"), (CFURLRef) [NSURL URLWithString:@"https://optionshouse.com:443/m?"], kCFHTTPVersion1_1); CFHTTPMessageSetBody(request, bodyData); CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(NULL, request); self.inputStream = (NSInputStream *) readStream; [self.inputStream setProperty:[NSDictionary dictionaryWithObjectsAndKeys:(id) kCFBooleanFalse, kCFStreamSSLValidatesCertificateChain, nil ] forKey:(NSString *) kCFStreamPropertySSLSettings]; [self.inputStream setDelegate:self]; [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [self.inputStream open]; CFRelease(readStream); CFRelease(request); } - (void)stop { [self.inputStream setDelegate:nil]; [self.inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [self.inputStream close]; self.inputStream = nil; } - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { switch (eventCode) { case NSStreamEventOpenCompleted: { NSLog(@"open"); } break; case NSStreamEventHasBytesAvailable: { NSInteger bytesRead; uint8_t junk[1024]; NSLog(@"has bytes"); bytesRead = [self.inputStream read:junk maxLength:sizeof(junk)]; if (bytesRead == 0) { NSLog(@"read end"); [self stop]; } else if (bytesRead < 0) { NSLog(@"read error"); [self stop]; } else { NSString *string = [[[NSString alloc] initWithBytes:junk length:bytesRead encoding:NSUTF8StringEncoding] autorelease]; NSLog(@"Read %@", string); } } break; case NSStreamEventErrorOccurred: { NSError * error = [self.inputStream streamError]; NSLog(@"error %@ / %zd", [error domain], (ssize_t) [error code]); [self stop]; } break; case NSStreamEventEndEncountered: { NSLog(@"end"); [self stop]; } break; default: { assert(NO); [self stop]; } break; } }
Using SSL with old F5 load balancers
Another issue I found with iOS5 was that despite using valid certificate in production environment, the SSL was still not working. Based on Apple's documentation, it turned out that the default version for SSL has been changed to TLS 1.0 and our production environment used F5 load balancer, which required SSLv3. So, I had to explicitly specify SSL version in ASIHTTPRequest.m
const void* keys[] = { kCFStreamSSLLevel }; // kCFStreamSocketSecurityLevelTLSv1_0SSLv3 configures max TLS 1.0, min SSLv3 // (same as default behavior on versions before iOS 5). // kCFStreamSocketSecurityLevelTLSv1_0 configures to use only TLS 1.0. // kCFStreamSocketSecurityLevelTLSv1_1 configures to use only TLS 1.1. // kCFStreamSocketSecurityLevelTLSv1_2 configures to use only TLS 1.2. const void* values[] = { CFSTR("kCFStreamSocketSecurityLevelTLSv1_0SSLv3") }; CFDictionaryRef sslSettingsDict = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFReadStreamSetProperty((CFReadStreamRef)[self readStream], kCFStreamPropertySSLSettings, sslSettingsDict); CFRelease(sslSettingsDict);
With these changes, our iPhone app was working as expected.
Acknowledgement
Many thanks for Apple Engineer "Quinn", who helped resolving the SSL issues.