SSL Pinning with AFNetworking doesn't work

513 Views Asked by At

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!

1

There are 1 best solutions below

1
skaak On

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.