2 Replies
      Latest reply: Dec 16, 2016 10:42 AM by rgm RSS
      rgm Level 1 Level 1 (10 points)

        Hi--

         

        I'm having trouble getting a client-side TLS certificate to work with WKWebView on iOS 9/10.

        Is there some mechanism after I've supplied an (apparently valid) `URLCredential` to `completionHandler(.useCredential, credential)` in `WKNavigationDelegate#webview(_:didReceive:completionHandler:)` that would cause the webview not to forward the credential?

        As far as I'm able to see with my (limited) debugger-fu, that's what appears to be taking place. So please take my diagnosis with a grain of salt.

        What I've tried:

        • I'm running against a test server in a Vagrant, terminating TLS at an nginx process, which is set up with a self-signed server cert. It is set with `ssl_verify_client on;` and the error log level is set to debug, so I can watch the TLS handshaking in the nginx logs.
        • I've generated my own CA, and the CA cert file is set in nginx under `ssl_client_certificate` so it can verify client certs.
        • I've made up a client key / certificate, signed it with the CA's key, and packaged this into a PKCS#12 (.p12) file.
        • Importing the .p12 file into my Mac's keychain lets desktop Safari and Chrome connect fine. `curl` connects OK with supplying the p12 via the command line. Tailing the nginx error log (I think) I can see the handshakes take place and the successful client certificate verification go by.
        • I've just embedded this .p12 into my app's bundle for now.
        • When I load the identity with the code below, everything seems to be ok, in that `err` is `errSecSuccess` and the `URLIdentity` created is, as far as I can tell at the debugger, populated. I linked in the class at https://developer.apple.com/library/content/samplecode/AdvancedURLConnections/Listings/Credentials_m.html#//apple_ref/doc/uid/DTS40009558-Credentials_m-DontLinkElementID_19 and used `- (void)_printIdentity:(SecIdentityRef)identity attributes:(NSDictionary *)attrs;` to print out the identity... nothing blew up on the asserts and I got OK looking summaries.
        • But, if I supply this credential with `completionHandler(.useCredential, credential)`, the webview shows "400 Bad Request // No required SSL certificate was sent." Tailing the nginx debug log I don't see any evidence that the certificate was ever supplied... it's not that it's rejecting it, it's that it never seemed to get it at all.
        • The webview populates with 200 and the right content if I restart nginx with `ssl_client_verify on` commented out of its configuration.
        • Turning off ATS doesn't seem to make a difference.
        • Installing my CA root certificate with a `.mobileconfig` doesn't seem to matter either, but this would seem to make sense based on my read of https://forums.developer.apple.com/message/194812#194812
        func webCredentialFromBundle(host: String, passphrase: String) -> URLCredential? {
        
            // expand this, just return const for now
            func hostToString(host: String) -> String { return "vagrant_client_cert" }
        
            func bundledCertData(_ filename: String) -> Data? {
                guard let url = Bundle.main.url(forResource: filename, withExtension: "p12")
                else { return nil }
        
                let data = try? Data(contentsOf: url)
                return data
            }
        
            guard let certData = bundledCertData(hostToString(host)) else { return nil }
        
            let importOption: NSDictionary = [kSecImportExportPassphrase as NSString: passphrase]
        
            var cfitems: CFArray?
            let err = SecPKCS12Import(certData as CFData, importOption, &cfitems)
        
            if err != errSecSuccess {
                print("security error \(err) in loading client-side cert")
                return nil
            }
        
            var credential: URLCredential? = nil
            guard let first = (cfitems! as NSArray).firstObject as? [String: AnyObject]
            else {
                print("empty p12 file")
                return nil
            }
        
            let identity = first[kSecImportItemIdentity as String] as! SecIdentity
            // do we need these at all??
            // let trust = first[kSecImportItemTrust as String] as! SecTrust
            // let certificates = first[kSecImportItemCertChain as String] as! [Any]
        
            credential = URLCredential(identity: identity,
                                       certificates: nil,
                                       persistence: .forSession)
        
            return credential
        }
        
        // excerpted from WKNavigationDelegate
        
        func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge,
                     completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
            let ps = challenge.protectionSpace
            if ps.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
                print("NSURLAuthenticationMethodClientCertificate for \(ps.host)")
                let passphrase = "Some passphrase"
                if let credential = webCredentialFromBundle(host: ps.host, passphrase: passphrase) {
                    completionHandler(.useCredential, credential)
                } else {
                    print("couldn't find client-side cert, cancelling auth challenge")
                    completionHandler(.cancelAuthenticationChallenge, nil)
                }
            } else {
                print("got auth challenge \(method) \(challenge) \(challenge.proposedCredential)")
                #if DEBUG
                // turn off server cert check for simulator / debug builds
                print("WARNING - IGNORING SERVER CERTIFICATE CHECK")
                completionHandler(.useCredential, URLCredential(trust: ps.serverTrust!))
                #else
                completionHandler(.performDefaultHandling, nil)
                #endif
            }
        }
        
        
         

        Thanks in advance,

        Ryan

        • Re: client-side cert getting swallowed by WKNavigationDelegate?
          eskimo Apple Staff Apple Staff (6,310 points)

          I'm having trouble getting a client-side TLS certificate to work with WKWebView on iOS 9/10.

          This is, alas, a known bug (r. 22659960).  WKWebView’s authentication challenge support works in general but does not work for client identity authentication challenges (NSURLAuthenticationMethodClientCertificate).

          It’s possible to work around this, but the workaround has some serious drawbacks.  The approach is to revert to UIWebView and then use a custom NSURLProtocol subclass to catch the network requests done by that web view.  At that point you can re-issue the network requests, and you’re in control of those network requests so you can respond to the client identity authentication challenge.

          The main drawback of this approach is that it requires you to use UIWebView, which is a step backwards relative to WKWebView.

          If you want to try this workaround, a good place to start is the CustomHTTPProtocol sample code.

          Finally, while this is a known issue, you should feel free to file your own bug report about it, explaining the impact this issue is having on your app.

          Share and Enjoy

          Quinn “The Eskimo!”
          Apple Developer Relations, Developer Technical Support, Core OS/Hardware
          let myEmail = "eskimo" + "1" + "@apple.com"