8 Replies
      Latest reply on Jul 26, 2018 5:12 AM by eskimo
      rgm Level 1 Level 1 (10 points)



        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
                completionHandler(.useCredential, URLCredential(trust: ps.serverTrust!))
                completionHandler(.performDefaultHandling, nil)

        Thanks in advance,