SSL Pinning with AFNetworking not working

Hi everyone,

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) :

Code Block objective-c
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 :

Code Block
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!
You should ask this via the support channel for the third-party library you’re using. The underlying API here, NSURLSession, has infrastructure for handling TLS authentication challenges (see Handling an Authentication Challenge). I can help you with that, but AFNetworking layers its own view of this on top of the system’s, and that’s not something I can explain.

Alternatively, you could take a shot at reproducing this using NSURLSession directly. If you can do that, post the details and I can help out with that.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Hi,

As you suggested, I updated my code to use NSURLSession.
I made a very simple api call (again, using placeholder URLs for this message) :

Code Block objc
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
self.urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSString *url = @"https://api.example.com";
[[self.urlSession dataTaskWithURL:[NSURL URLWithString:url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
  if (error == nil) {
    NSLog(@"SUCCESS");
  } else {
    NSLog(@"FAILURE : %@", error.localizedDescription);
  }
}] resume];

And I implemented the delegate like so :

Code Block objc
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
  SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
  SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);
  NSMutableArray *policies = [NSMutableArray array];
  [policies addObject:(bridge_transfer id)SecPolicyCreateSSL(true, (bridge CFStringRef)challenge.protectionSpace.host)];
  SecTrustSetPolicies(serverTrust, (bridge CFArrayRef)policies);
  SecTrustResultType result;
  SecTrustEvaluate(serverTrust, &result);
  BOOL certificateIsValid = (result == kSecTrustResultUnspecified result == kSecTrustResultProceed);
  NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
  NSString *pathToCert = [[NSBundle mainBundle]pathForResource:@"example.com" ofType:@"der"];
  NSData *localCertificate = [NSData dataWithContentsOfFile:pathToCert];
  if ([remoteCertificateData isEqualToData:localCertificate] && certificateIsValid) {
    NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
    completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
  } else {
    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);
  }
}

It appears that the failure comes from line 17 in the snipped above, [remoteCertificateData isEqualToData:localCertificate] returns NO.

This is strange because I downloaded the certificate directly from the server, using the command line :

Code Block
openssl s_client -connect api.example.com:443 </dev/null | openssl x509 -outform DER -out example.com.der

Also, I tried this exact code using the certificate from api.stackexchange.com, downloaded using the same command line above, and it seemed to work properly.

Is there anything I'm doing wrong?
I’m confused by your test program: It’s accessing api.example.com but that host doesn’t exist:

Code Block
% openssl s_client -connect api.example.com:443
getaddrinfo: nodename nor servname provided, or not known
connect:errno=0
%
% host api.example.com
Host api.example.com not found: 3(NXDOMAIN)


I’d expect your request to fail with a transport error without you ever seeing a server trust authentication challenge.

If you can clarify that I’ll take another look.



Oh, one other thing: Your authentication challenge handler should specifically test for the server trust authentication challenge and, if it gets any other challenge, resolve that NSURLSessionAuthChallengePerformDefaultHandling. Right now your program will do very bad things it you get any other form of authentication challenge that gets through the session (rather than task) handler (most notably client identity and NTLM authentication challenges).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
SSL Pinning with AFNetworking not working
 
 
Q