67

I am developing an iPhone app. During development, I need to connect to a server that's using a self-signed SSL certificate. I'm pretty certain - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler is my opportunity to write some exception code to allow this. However, I can't find any resources that tell me how to do this. I can see the following error in the log:

NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)

In addition to this, when I NSLog(@"error = %@", error); from within the above delegate method I get:

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.mydevelopmenturl.example which could put your confidential information at risk." UserInfo=0x10cbdbcf0 {NSUnderlyingError=0x112ec9730 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be api.mydevelopmenturl.example which could put your confidential information at risk.", NSErrorFailingURLStringKey=https://api.mydevelopmenturl.example/posts, NSErrorFailingURLKey=https://api.mydevelopmenturl.example/posts, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x112e5a020>, NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be api.mydevelopmenturl.example which could put your confidential information at risk.}

Any ideas on how to resolve this issue? Please post code as I've read the conceptual docs and I don't understand them. Here's an example of one that's beyond me: https://developer.apple.com/library/content/technotes/tn2232/_index.html

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
John Erck
  • 9,478
  • 8
  • 61
  • 71

11 Answers11

124

This works for me:

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:Nil];
...
...
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler{
  if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]){
    if([challenge.protectionSpace.host isEqualToString:@"mydomain.example"]){
      NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
      completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }
  }
}
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
friherd
  • 1,256
  • 1
  • 8
  • 4
27

Apple has a Technical Note 2232 which is quite informative and explains in detail HTTPS server trust evaluation.

In this case error -1202 in the NSURLErrorDomain domain is NSURLErrorServerCertificateUntrusted, which means that server trust evaluation has failed. You might also receive a variety of other errors; Appendix A: Common Server Trust Evaluation Errors lists the most common ones.

From the Technical Note:

In most cases the best way to resolve a server trust evaluation failure is to fix the server. This has two benefits: it offers the best security and it reduces the amount of code you have to write. The remainder of this technote describes how you can diagnose server trust evaluation failures and, if it's not possible to fix the server, how you can customize server trust evaluation to allow your connection to proceed without completely undermining the user's security.

The particular bit that is germane to this question is the section on NSURLSession server trust evaluation:

NSURLSession allows you to customize HTTPS server trust evaluation by implementing the -URLSession:didReceiveChallenge:completionHandler: delegate method. To customize HTTPS server trust evaluation, look for a challenge whose protection space has an authentication method of NSURLAuthenticationMethodServerTrust. For those challenges, resolve them as described below. For other challenges, the ones that you don't care about, call the completion handler block with the NSURLSessionAuthChallengePerformDefaultHandling disposition and a NULL credential.

When dealing with the NSURLAuthenticationMethodServerTrust authentication challenge, you can get the trust object from the challenge's protection space by calling the -serverTrust method. After using the trust object to do your own custom HTTPS server trust evaluation, you must resolve the challenge in one of two ways:

If you want to deny the connection, call the completion handler block with the NSURLSessionAuthChallengeCancelAuthenticationChallenge disposition and a NULL credential.

If you want to allow the connection, create a credential from your trust object (using +[NSURLCredential credentialForTrust:]) and call the completion handler block with that credential and the NSURLSessionAuthChallengeUseCredential disposition.

The upshot of all this is that if you implement the following delegate method, you can override server trust for a particular server:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
    if([challenge.protectionSpace.authenticationMethod
                           isEqualToString:NSURLAuthenticationMethodServerTrust])
    {
        if([challenge.protectionSpace.host
                           isEqualToString:@"domaintooverride.example"])
        {
            NSURLCredential *credential =
                          [NSURLCredential credentialForTrust:
                                          challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
        }
        else
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
}

Note that you have to handle both the case of the host matching the one you want to override and all other cases. If you don't handle the "all other cases" part, the behavior result is undefined.

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
memmons
  • 40,222
  • 21
  • 149
  • 183
  • If you don't call the callback, the session will not continue. It will either wait forever or it will timeout. So I wouldn't say it's undefined as "will not continue" is a definition of what's happening. – Mecki Jul 11 '18 at 18:18
  • Can I use `challenge.proposedCredential` instead of constructing a `URLCredential` from the `serverTrust`? – agirault May 20 '19 at 17:45
  • Could you please check this: https://stackoverflow.com/q/56627757/1364053 – nr5 Jun 17 '19 at 08:49
12

Find a trusted SSL certificate authority online that's offering a free 90 day trial for new certificates. Install the certificate on your server. You now have 90 days to develop your app to a point where you can make a decision as to whether or not it's worth it to pay money to "renew" the certificate. This is the best answer for me since my decision to use the self-signed certificate was financially motivated and 90 days gives me enough time develop my app to a point where I can decide if it's worth it to spend money on an SSL certificate or not. This approach avoids having to deal with the security implications of running a codebase that is tweaked to accept self-signed certificates. Sweet! Yay for bootstrapping!

John Erck
  • 9,478
  • 8
  • 61
  • 71
10

Do yourself a huge favour and don't.

Start by reading the paper The most dangerous code in the world: validating SSL certificates in non-browser software, especially section 10, "Breaking or disabling certificate validation". It specifically calls out a Cocoa-related blog that specifically describes how to do what you ask.

But don't. Disabling SSL certificate checking is like introducing a ticking time bomb into your app. Sometime, someday, it will accidentally be left enabled, and a build will get into the wild. And on that day, your users will be put at serious risk.

Instead you should use a certificate, signed with an intermediate cert that you can install and trust on that specific device, which will allow the SSL validation to succeed without endangering any other device than your own (and only then, temporarily).

Nathan Hughes
  • 94,330
  • 19
  • 181
  • 276
Shaggy Frog
  • 27,575
  • 16
  • 91
  • 128
  • I agree! I am security conscious like yourself. I think your answer adds value to this post, however, I emphasized for *development purposes* knowing that accepting self-signed certs in any other environment is not safe. I figure there may be a way to whitelist ONLY my self-signed cert? Such that, even if, released into a production environment accidentally, wouldn't put users at risk. I'm using a self-signed cert in the first place b/c I don't want to purchase a real one until my app has proven to be worth it (i.e. it doesn't get abandoned). This is for me, not professional projects. – John Erck Oct 22 '13 at 17:14
  • 4
    @JohnErck You can "whitelist" an intermediate certificate on an iOS device, which gets installed at the system level, and then you can sign (chain) a leaf certificate off of it and the leaf will validate successfully. The bonus is you don't need to change a single thing about any of the `NSURL` etc calls -- and don't need to modify your code *at all*. – Shaggy Frog Oct 23 '13 at 02:35
  • 3
    Well, as I write my app I could be willing to disable SSL for testing purposes, because I haven't yet installed a valid and signed SSL certificate in my dev environment. Thus, not answering the question is not answering the question. – Thibault D. May 09 '14 at 09:57
  • @thibaultd Solving the problem is more important than an answer that leads to data insecurity. Read the linked paper. – Shaggy Frog May 10 '14 at 01:09
  • @ShaggyFrog I've been having some issues doing exactly this. I'm trying to use AFNetworking but would also settle for a more basic solution using NSURL classes. Is there any chance you can help me out? I'm basically unable to use a root cert embedded into the app to validate leaf certs sent by our test server. – mitrenegade Sep 05 '15 at 00:13
  • @ShaggyFrog, do you have a link to how one can go about doing what you suggest (intermediate cert). I created a separate question to address this before I saw your comment here - http://stackoverflow.com/questions/35826877/ios-development-with-ssl-rest-api – Marcus Leon Mar 06 '16 at 15:47
  • Could you please check this: https://stackoverflow.com/q/56627757/1364053 – nr5 Jun 17 '19 at 08:49
7

For Swift 3.0 / 4

If you would just like to allow any kind of self-signed certificates, you could use the following approach, to implement an URLSessionDelegate. Apple provides additional information of how to use the URLSessionDelegate for all kinds of authentication methods: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/AuthenticationChallenges.html

At first implement the delegate method and assign an according delegate:

let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = urlSession.dataTask(with: urlRequest).resume()

Now implement the delegate's method https://developer.apple.com/documentation/foundation/nsurlsessiondelegate/1409308-urlsession?language=objc

func urlSession(_ session: URLSession, 
     didReceive challenge: URLAuthenticationChallenge, 
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

    guard challenge.previousFailureCount == 0 else {
        challenge.sender?.cancel(challenge)
        // Inform the user that the user name and password are incorrect
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    // Within your authentication handler delegate method, you should check to see if the challenge protection space has an authentication type of NSURLAuthenticationMethodServerTrust
    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust
       // and if so, obtain the serverTrust information from that protection space.
       && challenge.protectionSpace.serverTrust != nil
       && challenge.protectionSpace.host == "yourdomain.com" {
        let proposedCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
        completionHandler(URLSession.AuthChallengeDisposition.useCredential, proposedCredential)
    }
}

Still, you could adapt the acceptance of any self-signed cert for your provided domain to match to a very specific one. Make sure you added this certificate before to your build targets bundle. I named it here "cert.cer"

func urlSession(_ session: URLSession, 
     didReceive challenge: URLAuthenticationChallenge, 
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

    guard challenge.previousFailureCount == 0 else {
        challenge.sender?.cancel(challenge)
        // Inform the user that the user name and password are incorrect
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust
       && challenge.protectionSpace.serverTrust != nil
       && challenge.protectionSpace.host == "yourdomain.com" {

        if let trust = challenge.protectionSpace.serverTrust,
           let pem = Bundle.main.url(forResource:"cert", withExtension: "cer"),
           let data = NSData(contentsOf: pem),
           let cert = SecCertificateCreateWithData(nil, data) {
            let certs = [cert]
            SecTrustSetAnchorCertificates(trust, certs as CFArray)
            var result=SecTrustResultType.invalid
            if SecTrustEvaluate(trust,&result)==errSecSuccess {
              if result==SecTrustResultType.proceed || result==SecTrustResultType.unspecified {
                let proposedCredential = URLCredential(trust: trust)
                completionHandler(.useCredential,proposedCredential)
                return
              }
            }

        }
    }
    completionHandler(.performDefaultHandling, nil)
}
Dmitry
  • 713
  • 8
  • 15
Lepidopteron
  • 6,056
  • 5
  • 41
  • 53
  • yesssssssssss, this is the solution that worked for me after weeks of headache – aryaxt Apr 27 '18 at 16:29
  • Please, modify the last code example because basically you load the certificate, then simply ignore it. You can check that it will work fine after adding `certs.removeAll()`. That part needs a call to `SecTrustEvaluate(trust,&result)` – Dmitry Nov 01 '18 at 09:00
  • @Dmitry thank you for your review, please feel free to use the „edit“ functionality of the forum, this way you do get the credebility you deserve and can also better declare in the post itself why this solution would fit fetter :-) – Lepidopteron Nov 01 '18 at 13:52
  • @Lepidopteron I suggested an edit, though always considered direct edit of someone's code as unethical. I added `SecTrustEvaluate(trust,&result)` and default `completionHandler` at the end since it is a requirement to always call it one way or another. – Dmitry Nov 01 '18 at 18:37
  • Doing this returns the `NSURLErrorCancelled` error from the datatask. Is there a way to get `NSURLErrorServerCertificateUntrusted` instead? – agirault May 17 '19 at 20:16
  • @agirault could you open this as a new question? Others might have the same problem and might be looking for this without stumbling across this thread – Lepidopteron May 19 '19 at 06:04
  • Thanks, I made this here: https://stackoverflow.com/questions/56225350/ios-trigger-nsurlerrorservercertificateuntrusted-instead-of-nsurlerrorcancelled Would also love your feedback or @Dmitry's on this related question: https://stackoverflow.com/questions/56224690/ios-difference-between-proposedcredentials-and-urlcredential-for-trust – agirault May 20 '19 at 17:05
  • @agirault Sorry, I don't have any experience with that function, so cannot help. – Dmitry May 21 '19 at 05:55
  • Could you please check this: https://stackoverflow.com/q/56627757/1364053 – nr5 Jun 17 '19 at 08:48
6

Same as friherd's solution but in swift:

func URLSession(session: NSURLSession, task: NSURLSessionTask, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust{
        let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
        completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential,credential);
    }
}
eric
  • 61
  • 1
  • 3
  • Can I use `challenge.proposedCredential` instead of constructing a `URLCredential` from the `serverTrust`? – agirault May 20 '19 at 17:45
3

just need add .cer to SecTrust and it pass on ATS

class NSURLSessionPinningDelegate: NSObject, URLSessionDelegate {

    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {

        if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
            if let trust = challenge.protectionSpace.serverTrust,
               let pem = Bundle.main.path(forResource: "https", ofType: "cer"),
               let data = NSData(contentsOfFile: pem),
               let cert = SecCertificateCreateWithData(nil, data) {
                let certs = [cert]
                SecTrustSetAnchorCertificates(trust, certs as CFArray)

                completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: trust))
                return
            }
        }

        // Pinning failed
        completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
    }
}
yycking
  • 1,017
  • 1
  • 9
  • 14
2

update xcode 9

    var result:(message:String, data:Data?) = (message: "Fail", data: nil)
    var request = URLRequest(url: url)

    let sessionDelegate = SessionDelegate()
    let session = URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: nil)
    let task = session.dataTask(with: request){(data, response, error) in


    }
    task.resume()

the delegate task

    class SessionDelegate:NSObject, URLSessionDelegate
    {

        func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
            if(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
            {
                print(challenge.protectionSpace.host) 
                if(challenge.protectionSpace.host == "111.11.11.11")
                {
                    let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
                   completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential)
                }
            }

        }
    }
luhuiya
  • 2,129
  • 21
  • 20
1

Here is the solution that worked for me. You need to accept the connection in through the connection's delegate including both messages:

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
        [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];

    [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}

Please note that with doing this, you're not checking the trustability of the certificate, so only the SSL encryption of the HTTPS connection is interesting, but the signing authority is not taking into consideration here, which can decrease security.

Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
Maxime T
  • 848
  • 1
  • 9
  • 17
  • @sairam in you delegate. The class that is implementing NSURLConnectionDelegate and that you are using as the delegate on your connection object. Usually it is the same object that handles your connection object. – Maxime T Nov 25 '15 at 15:55
  • 2
    The OP is asking about `NSURLSession` not `NSURLConnection` so this will not be relevant. – levigroker Jan 06 '16 at 22:42
0

This Works fine for me to by pass self-signed :

Delegate : NSURLSessionDelegate

- (void)URLSession:(NSURLSession *)session **task**:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}
Enamul Hassan
  • 5,266
  • 23
  • 39
  • 56
0

Perhaps a better way is to provide the user with the opportunity to accept the certificate confirming (visually) that the URL is accurate for the service being accessed. For example, if the host is entered into some app setting, test at the user's entry and let the user decide right there.

Consider that this "user confirm" tactic is used by Safari, thus condoned by Apple, it would make sense that it would be employed logically for other apps.

Suggest digging into NSErrorRecoveryAttempting (am doing no myself) http://apple.co/22Au1GR

Get the host confirmed, then take the individual URL exclusion route mentioned herewithin. Depending upon the implementation it may also make sense to store the host as an exclusion for future reference.

This seems like something Apple would have implemented by nature in Cocoa but as of yet, I have not found an 'easy button'. Would have liked a "kLetUserDecide" flag on something in NSURL or NSURLSession instead of everyone having to implement the delegate method as well as the NSErrorRecoveryAttempting protocol.

Cerniuk
  • 14,220
  • 2
  • 29
  • 27