NSURLSession with backgroundSessionConfiguration can not work with user certificate - ios

I found a wired thing about NSURLSession when using background session configuration.
We use a self asigned certificate when contact with server, an implement:
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if (self.taskDidReceiveAuthenticationChallenge) {
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
NSLog(#"ServerTrust:%#", task.originalRequest.URL);
} else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) {
if (self.clientCertCredential && [challenge previousFailureCount] == 0) {
credential = self.clientCertCredential;
disposition = NSURLSessionAuthChallengeUseCredential;
NSLog(#"ClientCert:%#", task.originalRequest.URL);
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
When using defaultSessionConfiguration it works perfect, but when I change the session configuration to background session configration, This delegate method will be called in a loop, an none of other delegate method will be called, and this request will never complete.
Here is the console output:
2014-08-11 15:36:01.204 OneBox[1736:a413] ServerTrust:https://demo.mycompany.com/api/v1/files/351/1/9cc69106455d11599a08ed978fbdbe1d/contents
2014-08-11 15:36:01.232 OneBox[1736:1413] ClientCert:https://demo.mycompany.com/api/v1/files/351/1/9cc69106455d11599a08ed978fbdbe1d/contents
2014-08-11 15:36:02.068 OneBox[1736:8c03] ServerTrust:https://demo.mycompany.com/api/v1/files/351/1/9cc69106455d11599a08ed978fbdbe1d/contents
2014-08-11 15:36:02.076 OneBox[1736:1413] ClientCert:https://demo.mycompany.com/api/v1/files/351/1/9cc69106455d11599a08ed978fbdbe1d/contents
2014-08-11 15:36:12.728 OneBox[1736:1413] ServerTrust:https://demo.mycompany.com/api/v1/files/351/1/9cc69106455d11599a08ed978fbdbe1d/contents
2014-08-11 15:36:12.735 OneBox[1736:1413] ClientCert:https://demo.mycompany.com/api/v1/files/351/1/9cc69106455d11599a08ed978fbdbe1d/contents

OK I know this is an old question but I've been banging my head over the walls on this issue also, so hoping this will help somebody, here is my solution to the problem.
To be fair, my "solution" feels more like a convoluted workaround to me than a proper solution, but at least it works.
The short answer is that the key to make it work is to use NSURLProtectionSpace to set a default permanent credential for all sessions. This prevents the delegate from being called when presented with a challenge of type NSURLAuthenticationMethodClientCertificate.
The long answer follows below.
In your code, this will not work :
credential = self.clientCertCredential;
disposition = NSURLSessionAuthChallengeUseCredential;
//
// Redacted for clarity
//
completionHandler(disposition, credential);
Because in a background session, the delegate cannot have access to self.clientCertCredential (God only knows why).
However I found out that the background session will not try to call the delegate if you have previously defined a default credential in an NSURLProtectionSpace.
So scratch all of your else if block and instead do the following :
NSURLProtectionSpace *space = [NSURLProtectionSpace
initWithHost:#"your_address"
port:your_port
protocol:#"https"
realm:#"your_realm"
authenticationMethod:NSURLAuthenticationMethodClientCertificate];
[[NSURLCredentialStorage sharedCredentialStorage]
setDefaultCredential:self.clientCertCredential
forProtectionSpace:space];
If the host, port and realm parameters match exactly those of your server, then when the challenge is presented to the background session, the challenge.protectionSpace will find the default credential automagically.
In order for it to work, this code will need to be executed before you try to make any request with the background session. You can do it whenever you load the client certificate into self.clientCertCredential for instance.
But beware !!! There is one more subtlety here. Whenever you do so, make sure you load the certificate using the persistence option NSURLCredentialPersistencePermanent. Otherwise it will not work.
One last note. Depending on your use case, the drawback of using this hack is that you might find yourself with a bunch of permanently persisted credentials if several NSURLProtectionSpaces. You might then have to do some housekeeping after setting defaultCredential for the NSURLCredentialStorage class. This is beyond the scope of this answer, but the class has some convenience methods such as -removeCredential:forProtectionSpace: which are documented here https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURLCredentialStorage_Class/

Related

NSURLSession at https (kCFErrorDomainCFNetwork Code=310) [duplicate]

I'm fairly new to consuming webservices using SSL channel. After fairly good search I had found a way to perform SSL/HTTPS authentication using NSURLConnection delegate APIs. Following is the code snippet that does the actual authentication thing:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[self printLogToConsole:#"Authenticating...."];
[self printLogToConsole:[NSString stringWithFormat:#"\n%#\n", [challenge description]]];
NSLog(#"\n\nserverTrust: %#\n", [[challenge protectionSpace] serverTrust]);
/* Extract the server certificate for trust validation
*/
NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
assert(protectionSpace);
SecTrustRef trust = [protectionSpace serverTrust];
assert(trust);
CFRetain(trust); // Make sure this thing stays around until we're done with it
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
/* On iOS
* we need to convert it to 'der' certificate. It can be done easily through Terminal as follows:
* $ openssl x509 -in certificate.pem -outform der -out rootcert.der
*/
NSString *path = [[NSBundle mainBundle] pathForResource:#"rootcert" ofType:#"der"];
assert(path);
NSData *data = [NSData dataWithContentsOfFile:path];
assert(data);
/* Set up the array of certificates, we will authenticate against and create credentials */
SecCertificateRef rtCertificate = SecCertificateCreateWithData(NULL, CFBridgingRetain(data));
const void *array[1] = { rtCertificate };
trustedCerts = CFArrayCreate(NULL, array, 1, &kCFTypeArrayCallBacks);
CFRelease(rtCertificate); // for completeness, really does not matter
/* Build up the trust anchor using our root cert */
int err;
SecTrustResultType trustResult = 0;
err = SecTrustSetAnchorCertificates(trust, trustedCerts);
if (err == noErr) {
err = SecTrustEvaluate(trust, &trustResult);
}
CFRelease(trust); // OK, now we're done with it
[self printLogToConsole:[NSString stringWithFormat:#"trustResult: %d\n", trustResult]];
/* http://developer.apple.com/library/mac/#qa/qa1360/_index.html
*/
BOOL trusted = (err == noErr) && ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultConfirm) || (trustResult == kSecTrustResultUnspecified));
// Return based on whether we decided to trust or not
if (trusted) {
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[self printLogToConsole:#"Success! Trust validation successful."];
} else {
[self printLogToConsole:#"Failed! Trust evaluation failed for service root certificate.\n"];
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
But I'm getting following error:
2012-06-11 17:10:12.541 SecureLogin[3424:f803] Error during connection: Error Domain=NSURLErrorDomain Code=-1012 "The operation couldn’t be completed. (NSURLErrorDomain error -1012.)" UserInfo=0x682c790 {NSErrorFailingURLKey=https://staging.esecure.url/authentication/signin/merchants, NSErrorFailingURLStringKey=https://staging.esecure.url/authentication/signin/merchants}
I'm using the same certificate that I got from the server and converted it to 'der' format. I'm building app for iOS 5.x.
I'm not sure whether I'm missing out on something. Let me know of your suggestions.
Thanks.
EDIT
After examining the certificate here how the output looks:
Let me know if there is something wrong.
Thanks.
I cannot tell if your code is valid or not, because I use RestKit for consuming REST interfaces, however the most common problem that results in NSURLErrorDomain Code=-1012 is that the self-signed certificate does not have subject alternative name extension pointing to the web service if address.
To examine your certificate, download the Portecle app, very useful if you need to look inside ssl certificates. Run it and choose Examine->Examine Certificate from the menu and navigate to your certificate. You will see basic information about your certificate, now press the Examine button, then Subject alternative name, and make sure proper ip address of your web service is there. If not, you need to create the certificate again with this information in place.
I did figure out how to resolve this issue.
I ended up comparing the client and server trust certificates, byte-by-byte. Although there could be another way to resolve such issues of self-signed certificate, but for this solution did work.
Here is how I'm doing comparison of the client and server certificates, byte-by-byte, using their CFData objects(you can also reference 'AdvancedURLConnections' example code provided by Apple):
success = NO;
pServerCert = SecTrustGetLeafCertificate(trust);
if (clientCert != NULL) {
CFDataRef clientCertData;
CFDataRef serverCertData;
clientCertData = SecCertificateCopyData(clientCert);
serverCertData = SecCertificateCopyData(pServerCert);
assert(clientCertData != NULL);
assert(serverCertData != NULL);
success = CFEqual(clientCertData, serverCertData);
CFRelease(clientCertData);
CFRelease(serverCertData);
}
if (success) {
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[self printLogToConsole:#"Success! Trust validation successful."];
} else {
[self printLogToConsole:#"Failed! Trust evaluation failed for service root certificate.\n"];
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
Hope this will help someone, who is looking for solution of similar issue,
Thanks.

Switch from unauth to developer authenticated cognito user - AWS iOS SDK

Overall Problem:
I have a problem using a developer authenticated identity with my front end (iOS). I know my backend produces the correct token and identityID but my refresh method never gets called. I've also looked at the sample but I get slightly confused with everything going on.
Flow Explanation:
Currently I have a login screen that has a login button. The user presses the login button, then my api class takes the credentials, encrypts the password and stores it in keychain (commented out for now since it doesn't work on simulator). My DeveloperAuthenticatedIdentityProvider is called my app BusytimeAuthenticated. I have completed all the methods (I'm using AWS lambda and DynamoDB to authenticate users so) I start with unauthenticated access which allows me to access only two methods, login and signup. Then I want to assume my authenticated user which allows me to call my other methods.
my API Code:
[AWSLogger defaultLogger].logLevel = AWSLogLevelVerbose;
id<AWSCognitoIdentityProvider> identityProvider = [[BusytimeAuthenticated alloc] initWithRegionType:AWSRegionUSEast1
identityId:nil
identityPoolId:#"SOMEIDENTITYPOOLID"
logins:#{#"SOMEPROVIDERNAME": #"SOMEUSERNAME"}
providerName:#"SOMEPROVIDERNAME"
];
credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionUSEast1
identityProvider:identityProvider
unauthRoleArn:nil
authRoleArn:nil];
configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1
credentialsProvider:self.credentialsProvider];
AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;
[[credentialsProvider refresh] continueWithBlock:^id(BFTask *task){
[self testAuth];
return nil;
}];
my DeveloperAuthenticatedIdentityProvider code (BusytimeAuthenticated) :
#import "BusytimeAuthenticated.h"
#interface BusytimeAuthenticated()
#property (strong, atomic) NSString *providerName;
#property (strong, atomic) NSString *token;
#end
#implementation BusytimeAuthenticated
#synthesize providerName=_providerName;
#synthesize token=_token;
- (instancetype)initWithRegionType:(AWSRegionType)regionType
identityId:(NSString *)identityId
identityPoolId:(NSString *)identityPoolId
logins:(NSDictionary *)logins
providerName:(NSString *)providerName{
if (self = [super initWithRegionType:regionType identityId:identityId accountId:nil identityPoolId:identityPoolId logins:logins]) {
self.providerName = providerName;
}
return self;
}
// Return the developer provider name which you choose while setting up the
// identity pool in the Amazon Cognito Console
- (BOOL)authenticatedWithProvider {
return [self.logins objectForKey:self.providerName] != nil;
}
// If the app has a valid identityId return it, otherwise get a valid
// identityId from your backend.
- (BFTask *)getIdentityId {
// already cached the identity id, return it
if (self.identityId) {
return [BFTask taskWithResult:nil];
}
// not authenticated with our developer provider
else if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}
// authenticated with our developer provider, use refresh logic to get id/token pair
else {
return [[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
if (!self.identityId) {
return [self refresh];
}
return [BFTask taskWithResult:self.identityId];
}];
}
}
// Use the refresh method to communicate with your backend to get an
// identityId and token.
- (BFTask *)refresh {
if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}else{
// KeychainWrapper *keychain = [[KeychainWrapper alloc]init];
AWSLambdaInvoker *lambdaInvoker = [AWSLambdaInvoker defaultLambdaInvoker];
NSDictionary *parameters = #{#"username" : #"SOMEUSERNAME",
#"password":#"SOMEENCRYPTEDPASS",
#"isError" : #NO};
NSLog(#"Here");
[[lambdaInvoker invokeFunction:#"login" JSONObject:parameters] continueWithBlock:^id(BFTask* task) {
if (task.error) {
NSLog(#"Error: %#", task.error);
}
if (task.exception) {
NSLog(#"Exception: %#", task.exception);
}
if (task.result) {
self.identityId = [task.result objectForKey:#"IdentityId" ];
self.token = [task.result objectForKey:#"Token" ];
// [keychain mySetObject:[task.result objectForKey:#"Token" ] forKey:#"Token"];
// [keychain mySetObject:[task.result objectForKey:#"IdentityId" ] forKey:#"IdentityId"];
NSLog(#"Result: %#", task.result);
}
return [BFTask taskWithResult:self.identityId];
}];
}
return NULL;
}
#end
Summary Problem:
Unfortunately when I test my new priveleges, I see from the error: "Unauth_Role/CognitoIdentityCredentials is not authorized to perform: lambda:InvokeFunction". Clearly I'm not switching properly. I've placed a breakpoint in my refresh method to see if it's getting called. It's not. I'm not quite understanding how I switch properly. Any help with getting this to work is much appreciated.
Note: One big change I did make though is I took out the "DeveloperAuthenticationClient" class because I assumed I could do it without it.
The fundamental problem is that you are trying to call a Lambda function (which requires credentials) to get credentials. Because you are using the "default" client configuration, when your developer authenticated client comes back with a response it is going to override the credentials used to access your Lambda function. Additionally, once that id has been transitioned to authenticated, you won't be able to use it to get credentials in an unauth flow and would need to generate a new unauthenticated id just to authenticate again and then get back to your authenticated id.
I would strongly encourage you to setup API Gateway in front of your Lambda function to remove this circular dependency.
Update based on new information in the question...
A few things here:
1. Avoid code like while(!finished) to wait on an async task to complete. In the best case, this style of busy waiting will consume a CPU/core at 100% while doing nothing useful and adversely affect battery life and will only hurt performance of your app. Instead, use a notification with a block. Since you have already have a AWSTask in this instance, instead of returning nil at the end of the [credentialsProvider refresh] continueWithBlock... just call your [self testAuth] right there and do away with the finished/while code.
2. In your getIdentityId implementation the first if condition checks if there is an identityId and if there is it returns nil. I'm guessing you goal here is to cache the identityId after a successful authentication and return that so that you don't have to call your backend every time getIdentityId is called. If that is the case, pretty sure you want to return identityId instead of nil
3. I don't think this is the cause of your issue but will simplify things: As long as you've configured your identity pool with Auth/UnAuth roles in the console, you don't have to explicitly use them when initializing the AWSCognitoCredentialsProvider.
Once these are resolved if you continue to have problems, please debug the code in more detail and tell us things like the following:
Does the refresh method get called? If so, which parts of your if statement does it enter and what is the result? Does it ever enter the else block and call your backend identity provider? Does it successfully retrieve an identity id and return it?
If you get further but start experiencing a slightly different issue then please mark this question answered and post a separate question instead of continuing to edit this question. This will help keep things clear (this question/answer is getting pretty long and has changed).
Original answer to initial posted question/code... The getIdentity method of the AWSCognitoCredentialsProvider returns an AWSTask (i.e. a BFTask). So you'll need to call something like continueWithBlock in order to actually execute the method. In the first block of code above it looks like you're not doing that.

iOS how to reset NSURLCredential for a given server or make my app forget it ever spoke to that server?

I'm looking for a way to implement being able to switch which NSURLCredential my web app is using to talk to a given server.
Is there a way for me to "reset" NSURLCredential ssl credentials for a given server trust and force the entire handshaking process to repeat using different set of credentials? In other words, I want my web app to forget it ever spoke to this server.
I think the code snippet below is what is retrieving the credential that I previously have evaluated.
- (void)customHTTPProtocol:(CustomHTTPProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSURLCredential * credential = nil;
SecTrustRef trust;
NSURLProtectionSpace* protectionSpace = challenge.protectionSpace;
if ([protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
//this verifies that we are talking to a server that we trust by checking it's certificate
// Extract the SecTrust object from the challenge, apply our trusted anchors to that
// object, and then evaluate the trust. If it's OK, create a credential and use
// that to resolve the authentication challenge. If anything goes wrong, resolve
// the challenge with nil, which continues without a credential, which causes the
// connection to fail.
trust = [protectionSpace serverTrust];
if (trust == NULL) {
assert(NO);
} else {
// Just Ignore Self Signed Certs
SecTrustResultType result;
//This takes the serverTrust object and checkes it against your keychain
SecTrustEvaluate(protectionSpace.serverTrust, &result);
//if we want to ignore invalid server for certificates, we just accept the server
credential = [NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust];
if(result == kSecTrustResultProceed ||
// result == kSecTrustResultConfirm || // Deprecated - Remove?
result == kSecTrustResultUnspecified) {
[challenge.sender useCredential:credential forAuthenticationChallenge: challenge];
}
}
[protocol resolveAuthenticationChallenge:challenge withCredential:credential];
}
//... more ways to resolve challenge
}
I tried various way for achieving this but all in vein.
But 1 simple trick worked.
To force my web page to re-authenticate, I would add below at the end of the request url.
&r=1100910192 **(timestamp)**.
So instead of using the cached authenticated credential it infact re-authenticated.
In other cases just adding a # at the end of the request worked:
[NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://localhost:8080/some.json#"]]
I hope it helped.

iOS - programmatically trusting CA (certificate authority)

I'm trying around with an app that is to securely connect from an iPhone client to a TLS server via sockets/streams for general data-exchange. For that purpose I set up an own CA with the mac keychain-tool and included the certificat in the code bundle.
Now my app should trust any server certificate issued by that CA. (I do not care how other apps treat those certs, I assume that they will not trust it because of the sandbox.)
I have found several similar issues online but seem to have gotten something wrong.
Connecting with the server seems to work fine if I drag & drop the CA certificate into the Simulator and manually accept to trust it.
However, when I try to establish trust for the CA cert programmatically my connection attempts later to the server are refused, despite the code below does not generate errors.
Therefore I must have got the certificate implementation part wrong... Any ideas?
Many thanks in advance!
NSString* certPath = [[NSBundle mainBundle] pathForResource:#"MyTestCA2" ofType:#"cer"]; //cer = CA certificate
NSData* certData = [NSData dataWithContentsOfFile:certPath];
SecCertificateRef cert;
if( [certData length] ) {
cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
if( cert != NULL ) {
CFStringRef certSummary = SecCertificateCopySubjectSummary(cert);
NSString* summaryString = [[NSString alloc] initWithString:(__bridge NSString*)certSummary];
NSLog(#"CERT SUMMARY: %#", summaryString);
certSummary = nil;
} else {
NSLog(#" *** ERROR *** trying to create the SSL certificate from data located at %#, but failed", certPath);
}
}
OSStatus err = noErr;
CFTypeRef result;
NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)kSecClassCertificate, kSecClass,
cert, kSecValueRef,
nil];
err = SecItemAdd((__bridge CFDictionaryRef)dict, &result);
if(err!=noErr) NSLog(#"error while importing");
if (err==errSecDuplicateItem) NSLog(#"Cert already installed");
NSLog(#":%i",(int)err);
assert(err==noErr||err==errSecDuplicateItem); // accept no errors other than duplicate
err = noErr;
SecTrustRef trust;
err = SecTrustCreateWithCertificates(cert, SecPolicyCreateBasicX509() ,&trust);
assert(err==noErr);
err = noErr;
CFMutableArrayRef newAnchorArray = CFArrayCreateMutable(kCFAllocatorDefault,0,&kCFTypeArrayCallBacks);
CFArrayAppendValue(newAnchorArray,cert);
err = SecTrustSetAnchorCertificates(trust, newAnchorArray);
assert(err==noErr);
SecTrustResultType trustResult;
err=SecTrustEvaluate(trust,&trustResult);
assert(err==noErr);
cert=nil;
I did not try to run the partial code, but I have some code (provided below) that I know works. I use it to trust my internal CA.
err=SecTrustEvaluate(trust,&trustResult);
assert(err==noErr);
trustResult is what you are interested in, not the err return from SecTrustEvaluate. err tells you if the API call succeeded/failed; it does not tell you the result of the trust evaluation.
I think you have two strategies here. First is to look for "success" in trustResult with values kSecTrustResultProceed or kSecTrustResultUnspecified. Its "success" because its not "prompt", its not "try to recover" and its not "failure".
The second strategy is "not failure" in trustResult with values kSecTrustResultDeny, kSecTrustResultFatalTrustFailure or kSecTrustResultOtherError. That is, as long as trustResult is not one of those values, then proceed as success. Ignore prompting the user to trust a certificate because they will not understand the prompt and "tap through".
Below is the code I use in NSURLConnection delegate's -didReceiveAuthenticationChallenge:. It expects an ASN.1/DER encoded certificate (named ca-cert.der). It uses Strategy 1 described above. If you use the code in the #ifdef 0, then its using Strategy 2.
I think Apple's Overriding TLS Chain Validation Correctly, Apple's Tech Note TN2232, HTTPS Server Trust Evaluation and Apple's Technical Q&A QA1360, Describing the kSecTrustResultUnspecified error might be useful to you.
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:
(NSURLAuthenticationChallenge *)challenge
{
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]
forAuthenticationChallenge: challenge];
if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust])
{
do
{
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
NSCAssert(serverTrust != nil, #"serverTrust is nil");
if(nil == serverTrust)
break; /* failed */
NSData* caCert = [NSData dataWithContentsOfFile:#"ca-cert.der"];
NSCAssert(caCert != nil, #"caCert is nil");
if(nil == caCert)
break; /* failed */
SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
NSCAssert(caRef != nil, #"caRef is nil");
if(nil == caRef)
break; /* failed */
NSArray* caArray = [NSArray arrayWithObject:(__bridge id)(caRef)];
NSCAssert(caArray != nil, #"caArray is nil");
if(nil == caArray)
break; /* failed */
OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
NSCAssert(errSecSuccess == status, #"SecTrustSetAnchorCertificates failed");
if(!(errSecSuccess == status))
break; /* failed */
SecTrustResultType result = -1;
status = SecTrustEvaluate(serverTrust, &result);
if(!(errSecSuccess == status))
break; /* failed */
NSLog(#"Result: %d", result);
/* https://developer.apple.com/library/ios/technotes/tn2232/_index.html */
/* https://developer.apple.com/library/mac/qa/qa1360/_index.html */
/* kSecTrustResultUnspecified and kSecTrustResultProceed are success */
if(result != kSecTrustResultUnspecified && result != kSecTrustResultProceed)
break; /* failed */
#if 0
/* Treat kSecTrustResultConfirm and kSecTrustResultRecoverableTrustFailure as success */
/* since the user will likely tap-through to see the dancing bunnies */
if(result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure || result == kSecTrustResultOtherError)
break; /* failed to trust cert (good in this case) */
#endif
// The only good exit point
return [[challenge sender] useCredential: [NSURLCredential credentialForTrust: serverTrust]
forAuthenticationChallenge: challenge];
} while(0);
}
// Bad dog
return [[challenge sender] cancelAuthenticationChallenge: challenge];
}
Despite all efforts I have not been able to duplicate with NS/CF-Streams, what jww obviously has managed with NSURLConnection. (As before when drag-dropping the CA-cert on the simulator and accepting it as trusted, the TLS stream works fine, but doing it programmatically does not succeed.)
Therefore I've done a little more search on the topic and actually found an additional post by aeternusrahl that was answered by Rob Napier:Post
...in which the following blog entry from Heath Borders is cited:
Blog
For everyone that has a similar issue as me, I can very much recommend those links – they provide good insight!
Without replicating the entire content of those links: Rob Napier says '...there is, as far as I've ever discovered, only one trusted anchor list in that keychain, shared by everyone. This doesn't help you a lot with socket.io, because it doesn't give you access to its NSURLConnection delegate methods. You'd have to modify socket.io to accept a trust anchor.'
As all authors above have outstanding reputation and I am pretty new to iOS programming, I would not dare to disagree with either one! But since I am having a really hard time consolidating the posts, I would very much appreciate any clarification on preconditions in the posts I might have accidentally skipped.
For me it seems, jww does not consider disabling automatic TLS validation necessary when using own root-certs (see his answer to my comment on his first post). Then, Heath Borders/Rob Napier, seem to suggest that disabling certificate chain validation in ssl settings is necessary for sockets (again if I got it right.)
Basically I see the following possible explanations:
A) jww is referring to NSURLConnections only, whose delegate seems more powerful than the one you're stuck with when working with NS/CF streams
B) the situation has changed since the post from Rob Napier / the blog from Heath Borders
C) I got everything entirely wrong and apologize in advance in that case!
While A) seems somewhat more probable I'm kind of hoping for B)...
I'd be very thankful for additional insight!
PS. I hope placing the above as an answer does not violate any rules... It's certainly not the correct / complete answer, but starting a entirely new question seems neither useful and unfortunately the text is too long for any comment. If there is a better way to include additional information (as the above new links) while listing points that are still unclear, let me know.

iOS and SSL: Unable to validate self-signed server certificate

I'm fairly new to consuming webservices using SSL channel. After fairly good search I had found a way to perform SSL/HTTPS authentication using NSURLConnection delegate APIs. Following is the code snippet that does the actual authentication thing:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[self printLogToConsole:#"Authenticating...."];
[self printLogToConsole:[NSString stringWithFormat:#"\n%#\n", [challenge description]]];
NSLog(#"\n\nserverTrust: %#\n", [[challenge protectionSpace] serverTrust]);
/* Extract the server certificate for trust validation
*/
NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
assert(protectionSpace);
SecTrustRef trust = [protectionSpace serverTrust];
assert(trust);
CFRetain(trust); // Make sure this thing stays around until we're done with it
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
/* On iOS
* we need to convert it to 'der' certificate. It can be done easily through Terminal as follows:
* $ openssl x509 -in certificate.pem -outform der -out rootcert.der
*/
NSString *path = [[NSBundle mainBundle] pathForResource:#"rootcert" ofType:#"der"];
assert(path);
NSData *data = [NSData dataWithContentsOfFile:path];
assert(data);
/* Set up the array of certificates, we will authenticate against and create credentials */
SecCertificateRef rtCertificate = SecCertificateCreateWithData(NULL, CFBridgingRetain(data));
const void *array[1] = { rtCertificate };
trustedCerts = CFArrayCreate(NULL, array, 1, &kCFTypeArrayCallBacks);
CFRelease(rtCertificate); // for completeness, really does not matter
/* Build up the trust anchor using our root cert */
int err;
SecTrustResultType trustResult = 0;
err = SecTrustSetAnchorCertificates(trust, trustedCerts);
if (err == noErr) {
err = SecTrustEvaluate(trust, &trustResult);
}
CFRelease(trust); // OK, now we're done with it
[self printLogToConsole:[NSString stringWithFormat:#"trustResult: %d\n", trustResult]];
/* http://developer.apple.com/library/mac/#qa/qa1360/_index.html
*/
BOOL trusted = (err == noErr) && ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultConfirm) || (trustResult == kSecTrustResultUnspecified));
// Return based on whether we decided to trust or not
if (trusted) {
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[self printLogToConsole:#"Success! Trust validation successful."];
} else {
[self printLogToConsole:#"Failed! Trust evaluation failed for service root certificate.\n"];
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
But I'm getting following error:
2012-06-11 17:10:12.541 SecureLogin[3424:f803] Error during connection: Error Domain=NSURLErrorDomain Code=-1012 "The operation couldn’t be completed. (NSURLErrorDomain error -1012.)" UserInfo=0x682c790 {NSErrorFailingURLKey=https://staging.esecure.url/authentication/signin/merchants, NSErrorFailingURLStringKey=https://staging.esecure.url/authentication/signin/merchants}
I'm using the same certificate that I got from the server and converted it to 'der' format. I'm building app for iOS 5.x.
I'm not sure whether I'm missing out on something. Let me know of your suggestions.
Thanks.
EDIT
After examining the certificate here how the output looks:
Let me know if there is something wrong.
Thanks.
I cannot tell if your code is valid or not, because I use RestKit for consuming REST interfaces, however the most common problem that results in NSURLErrorDomain Code=-1012 is that the self-signed certificate does not have subject alternative name extension pointing to the web service if address.
To examine your certificate, download the Portecle app, very useful if you need to look inside ssl certificates. Run it and choose Examine->Examine Certificate from the menu and navigate to your certificate. You will see basic information about your certificate, now press the Examine button, then Subject alternative name, and make sure proper ip address of your web service is there. If not, you need to create the certificate again with this information in place.
I did figure out how to resolve this issue.
I ended up comparing the client and server trust certificates, byte-by-byte. Although there could be another way to resolve such issues of self-signed certificate, but for this solution did work.
Here is how I'm doing comparison of the client and server certificates, byte-by-byte, using their CFData objects(you can also reference 'AdvancedURLConnections' example code provided by Apple):
success = NO;
pServerCert = SecTrustGetLeafCertificate(trust);
if (clientCert != NULL) {
CFDataRef clientCertData;
CFDataRef serverCertData;
clientCertData = SecCertificateCopyData(clientCert);
serverCertData = SecCertificateCopyData(pServerCert);
assert(clientCertData != NULL);
assert(serverCertData != NULL);
success = CFEqual(clientCertData, serverCertData);
CFRelease(clientCertData);
CFRelease(serverCertData);
}
if (success) {
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[self printLogToConsole:#"Success! Trust validation successful."];
} else {
[self printLogToConsole:#"Failed! Trust evaluation failed for service root certificate.\n"];
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
Hope this will help someone, who is looking for solution of similar issue,
Thanks.

Resources