SSL Pinning with AFNetworking doesn't work - ios

I'm trying to add SSL pinning to my app, with a self-signed certificate, but I can't seem to get it to work.
I have tried everything I could find on the internet with no success, and not being an expert at how SSL works doesn't help.
I'm using objective-c with the latest version of AFNetworking.
I made a very simple piece of code to test my API calls (I'm using a placeholder URL for this post) :
NSString *url = #"https://api.example.net/webservice";
NSString *cerPath = [[NSBundle mainBundle] pathForResource:#"example.net" ofType:#"der"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:url]];
manager.requestSerializer = [AFJSONRequestSerializer new];
manager.responseSerializer = [AFJSONResponseSerializer new];
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
[policy setAllowInvalidCertificates:YES];
[policy setValidatesDomainName:NO];
policy.pinnedCertificates = [NSSet setWithObject:certData];
manager.securityPolicy = policy;
[manager POST:url parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(#"SUCCESS");
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(#"FAILURE : %#", error.localizedDescription);
}];
Every time I try executing this code, I get a failure with the following error :
Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “api.example.net” which could put your confidential information at risk."
I tried using different formats for my certificate (.der, .cer, ...), but I still always get the same error.
I tried using NSAllowsArbitraryLoads in my info.plist but nothing changes.
To make sure I'm using working code, I also downloaded the example project from a Ray Wenderlich tutorial, but my own certificate is still invalid (in the tutorial they use the stackexchange certificate, this one works).
I have been researching this issue for days and haven't found a solution yet.
The same certificate works perfectly on our Android app, as well as Postman.
Is this because I use a self-signed certificate and iOS doesn't like it?
Is there anything obvious I missed in my code or in my app configuration?
Is there something specific to implement server-side to make sure it works with iOS?
Do I have to export my certificate in a very specific format?
Any information is welcome.
Thanks!

I'm looking at an old project where I used self signed certificates without a problem. These are just comments that may help - I make them here because I have more space and can format them better.
The der version worked.
In Info.plist you need something like the following.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>server1.local</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>server2.local</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>server3.local</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
Note your error message - it is complaining about the server certificate, so maybe the problem is in the DNS or server certificate. Both needs to be correct and the name you use e.g. server1.local must match the DNS name of the server as well as the CN of the certificate for iOS to work.
I added both the CA and the server certificates to the chain in iOS.
I trust this will help you.
FWIW my implementation did not use AFNetworking but I used SecTrustSetAnchorCertificates inside URLSession:didReceiveChallenge:completionHandler: in the NSURLSessionDelegate that I used to support a normal NSURLRequest.
Here is that piece of code.
// Look to see if we can handle the challenge
- ( void ) URLSession:( NSURLSession * ) session
didReceiveChallenge:( NSURLAuthenticationChallenge * ) challenge
completionHandler:( void ( ^ ) ( NSURLSessionAuthChallengeDisposition, NSURLCredential * ) ) completionHandler
{
#ifdef DEBUG
NSLog( #"didReceiveChallenge %# %zd", challenge.protectionSpace.authenticationMethod, ( ssize_t ) challenge.previousFailureCount );
#endif
NSURLCredential * credential = nil;
NSURLProtectionSpace * protectionSpace;
SecTrustRef trust;
int err;
// Setup
protectionSpace = challenge.protectionSpace;
trust = protectionSpace.serverTrust;
credential = [NSURLCredential credentialForTrust:trust];
if ( protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust )
{
// Build up the trust anchor using server certificates
err = SecTrustSetAnchorCertificates ( trust, ( CFArrayRef ) fhWebSupportDelegate.serverCertificates );
SecTrustResultType trustResult = 0;
if ( err == noErr )
{
SecTrustSetAnchorCertificatesOnly ( trust, true );
err = SecTrustEvaluate ( trust, & trustResult );
#ifdef DEBUG
NSLog ( #"Trust result %lu", ( unsigned long ) trustResult );
#endif
}
BOOL trusted =
( err == noErr ) &&
( ( trustResult == kSecTrustResultProceed ) || ( trustResult == kSecTrustResultUnspecified ) || ( trustResult == kSecTrustResultRecoverableTrustFailure ) );
// Return based on whether we decided to trust or not
if ( trusted )
{
#ifdef DEBUG
NSLog ( #"Trust evaluation succeeded" );
#endif
if ( completionHandler )
{
completionHandler ( NSURLSessionAuthChallengeUseCredential, credential );
}
}
else
{
#ifdef DEBUG
NSLog ( #"Trust evaluation failed" );
#endif
if ( completionHandler )
{
completionHandler ( NSURLSessionAuthChallengeCancelAuthenticationChallenge, credential );
}
}
}
else if ( completionHandler )
{
completionHandler ( NSURLSessionAuthChallengePerformDefaultHandling, nil );
}
}
Here fhWebSupportDelegate.serverCertificates returns an array with the CA as well as the server certificate. Also I was extremely lenient in when I granted trust to the server as can be seen in the code.

Related

NSURLSessionAuthChallengeUseCredential does not help. How to make iOS trust my server?

My server uses self signed SSL certificates. And iOS does not want to accept them no matter what I do. This is my code:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
NSURLCredential *credential))completionHandler
{
NSString* authenticationMethod = challenge.protectionSpace.authenticationMethod;
if (![authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}
SecTrustRef trust = challenge.protectionSpace.serverTrust;
CFIndex count = SecTrustGetCertificateCount(trust);
CFMutableArrayRef originalCertificates = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
for (CFIndex i = 0; i < count; i++)
{
SecCertificateRef certRef = SecTrustGetCertificateAtIndex(trust, i);
CFArrayAppendValue(originalCertificates, certRef);
CFStringRef certSummary = SecCertificateCopySubjectSummary(certRef);
NSLog(#"CERT %ld %#", i, certSummary);
}
//SecPolicyRef policyRef = SecPolicyCreateSSL(true, CFSTR("192.168.50.80"));
SecPolicyRef policyRef = SecPolicyCreateBasicX509();
SecTrustRef newTrust;
OSStatus status = SecTrustCreateWithCertificates(originalCertificates, policyRef, & newTrust);
assert(status == noErr);
NSString* path = [[NSBundle mainBundle] pathForResource:#"no1bcCA" ofType:#"der"];
NSData* data = [NSData dataWithContentsOfFile:path];
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) data);
assert(cert);
NSString* rootPath = [[NSBundle mainBundle] pathForResource:#"no1bcRootCA" ofType:#"der"];
NSData* rootData = [NSData dataWithContentsOfFile:rootPath];
SecCertificateRef rootCert = SecCertificateCreateWithData(NULL, (CFDataRef) rootData);
assert(rootCert);
SecTrustSetAnchorCertificates(newTrust, (CFArrayRef)#[(__bridge id)rootCert, (__bridge id)cert]);
SecTrustSetAnchorCertificatesOnly(newTrust, NO);
SecTrustResultType trustResult;
SecTrustEvaluate(newTrust, &trustResult);
if (trustResult == kSecTrustResultUnspecified || trustResult == kSecTrustResultProceed)
{
NSURLCredential* credential = [NSURLCredential credentialForTrust:newTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
else
{
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
so trustResult is kSecTrustResultUnspecified but in the completion handler of my NSURLSessionDataTask I still receive the following error:
Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x6000003040b0>, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9802, NSErrorPeerCertificateChainKey=(
"<cert(0x7f81ef80ca00) s: sems.no1bc.local i: no1bcCA>",
"<cert(0x7f81ef80d400) s: no1bcCA i: no1bcRootCA>",
"<cert(0x7f81ef82b800) s: no1bcRootCA i: no1bcRootCA>"
), NSUnderlyingError=0x604000255030 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x6000003040b0>, _kCFNetworkCFStreamSSLErrorOriginalValue=-9802, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9802, kCFStreamPropertySSLPeerCertificates=(
"<cert(0x7f81ef80ca00) s: sems.no1bc.local i: no1bcCA>",
"<cert(0x7f81ef80d400) s: no1bcCA i: no1bcRootCA>",
"<cert(0x7f81ef82b800) s: no1bcRootCA i: no1bcRootCA>"
)}}, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://192.168.50.80/pgpuniversaldesktop, NSErrorFailingURLStringKey=https://192.168.50.80/pgpuniversaldesktop, NSErrorClientCertificateStateKey=0}
I love the recovery suggestion. It says
Would you like to connect to the server anyway?
Yes, I would, but how? What do I do?
Apart from all that I also tried to play with ATS, this is what I put into the plist file:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>192.168.50.80</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSRequiresCertificateTransparency</key>
<false/>
</dict>
</dict>
</dict>
but it never helps. So, I explicitly tell iOS: "Trust this server, trust it", but it doesn't. What may be the reason? How do I force the system to trust the server? And how am I supposed to connect to the server anyway?
It is funny that it works without problems if I run this code from a Mac app. But doesn't work on iOS
In iOS 11, apparently you need to also set NSExceptionRequiresForwardSecrecy to false for that domain. Otherwise, App Transport Security won't even let your custom cert reach your custom authentication code. This is arguably a bug.
For more info, see this thread in Apple's developer forums.
It turned out that the server ran some outdated TLS. Mac was able to deal with it whereas iOS not, so it never allowed to connect to the server no matter what. The solution was to fix the server

iOS Objective C HTTPS request failing

I've searched extensively and have made the necessary changes (so i think) to conform to Appl'es ATS restrictions.
Private key 2048 bits or greater
openssl rsa -in privkey.pem -text -noout
Private-Key: (2048 bit)
Running ssl v1.2 on nginx
ssl verified at v1.2
And have even run the make nscurl utility to check the connection, all tests passed.
I also can verify that the server is functioning properly by making a GET on https from the browser and having everything work properly.
My though was that maybe the subdomain is causing an issue, so i updated the info.plist file to the following
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>boramash.com</key> (also tried gateway.boramash.com)
<dict>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
With what I believe to be everything working, I get the following errors.
2016-01-25 15:59:17.345 StripePlayground[2999:84984]
NSURLSession/NSURLConnection HTTP load failed
(kCFStreamErrorDomainSSL, -9802) 2016-01-25 15:59:17.348
StripePlayground[2999:84989] (null) 2016-01-25 15:59:17.348
StripePlayground[2999:84989] Error Domain=NSURLErrorDomain Code=-1200
"An SSL error has occurred and a secure connection to the server
cannot be made."
UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=, NSLocalizedRecoverySuggestion=Would you like to
connect to the server anyway?, _kCFStreamErrorDomainKey=3,
_kCFStreamErrorCodeKey=-9802, NSErrorPeerCertificateChainKey={type = immutable, count = 1, values = (
0 : )}, NSUnderlyingError=0x7fd97252e580 {Error
Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)"
UserInfo={_kCFStreamPropertySSLClientCertificateState=0,
kCFStreamPropertySSLPeerTrust=,
_kCFNetworkCFStreamSSLErrorOriginalValue=-9802, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9802, kCFStreamPropertySSLPeerCertificates={type = immutable, count = 1, values = ( 0 :
)}}}, NSLocalizedDescription=An SSL error has occurred
and a secure connection to the server cannot be made.,
NSErrorFailingURLKey=https://gateway.boramash.com/stripe-add-customer,
NSErrorFailingURLStringKey=
prependingtext_for_stack_overflowhttps://gateway.boramash.com/stripe-add-customer,
NSErrorClientCertificateStateKey=0}
Also here is my request making code, pretty basic.
NSString *myrequest = #"https://gateway.boramash.com/stripe-add-customer";
// NSURL *newcustomerURL = [NSURL URLWithString:#"http//45.55.154.107:5050/create-customer"];
NSURL *newcustomerURL = [NSURL URLWithString: myrequest];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: newcustomerURL];
//request.HTTPBody = [[NSString stringWithFormat:#"customer_id=%#&first_name=%#&last_name=%#", testID, firstName, lastName] dataUsingEncoding: NSUTF8StringEncoding ];
request.HTTPMethod = #"GET";
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse *_Nullable response, NSError * _Nullable error) {
//print the result here - new customer has been created!
NSString *myresponse = [NSString stringWithFormat:#"%#", response];
NSString *myerror = [NSString stringWithFormat:#"%#", error];
NSLog(#"%#", myresponse);
NSLog(#"%#", myerror);
}] resume];
Any advice would be much appreciated!
TL;DR: For some reason, your server is not (always?) sending the intermediate certificate. Check your server configuration, and the certificate/intermediate certificate format (check for errors in your logs, and check that the server was properly restarted).
You can check on the command line with openssl s_client -connect gateway.boramash.com:443.
It currently returns:
depth=0 CN = gateway.boramash.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = gateway.boramash.com
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
0 s:/CN=gateway.boramash.com
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X1
...
Verify return code: 21 (unable to verify the first certificate)
Which means it can't find a certificate to validate the signature on the certificate.
You want it to return:
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X1
verify return:1
depth=0 CN = gateway.boramash.com
verify return:1
---
Certificate chain
0 s:/CN=gateway.boramash.com
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X1
...
Verify return code: 0 (ok)
(this was obtained by downloading the intermediate certificate and feeding it to openssl with -CAfile lets-encrypt-x1-cross-signed.pem).
You can also verify that the intermediate certificate is indeed not sent by adding -showcerts.
The weird part is that it indeed works (for me) in Safari, though it doesn't work in Firefox. Not quite sure what makes the difference (maybe the intermediate cert was cached from another request to a properly configured server using a certificate from the same CA), but double-check your server configuration (and the format of your certificate file) until openssl likes it, and iOS should like it too.
The issue isn't ATS, the issue is that you are receiving an invalid SSL certificate when you make the GET request to https://gateway.boramash.com/...
To get past this without replacing the certificate on the backend, you will need to implement the following delegate method:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler;
Here is an example:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
NSString *host = challenge.protectionSpace.host;
NSArray *acceptedDomains = #[#".boramash.com$"];
BOOL accept = NO;
for (NSString *pattern in acceptedDomains)
{
NSRange range = [host rangeOfString:pattern options:NSCaseInsensitiveSearch|NSRegularExpressionSearch];
if (range.location != NSNotFound)
{
accept = YES;
break;
}
}
if (accept)
{
NSLog(#"%#", [NSString stringWithFormat:#"WARNING: accepting an invalid certificate from host: %#", host]);
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
else
{
NSLog(#"%#", [NSString stringWithFormat:#"WARNING: discarding an invalid certificate from host: %#", host]);
}
}
}
Try adding NSExceptionAllowsInsecureHTTPLoads and setting that the true

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.

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