client-side cert getting swallowed by WKNavigationDelegate?

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

Accepted Reply

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"

Replies

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"

Well, shoot. Ok. Thanks. Can stop scratching my head, anyway.


I'll try out the workaround and head for radar.

Hello,

Does this bug is fixed now ?


Arnaud

Hello,


I also have this issue.


Thank you,

Ionut

Does this bug is fixed now ?

No. If this bug is causing you signficant amounts of grief, I encourage you to file your own bug report about it. It will probably be closed as a dup but you will at least be able to explain, in your own words, the impact that 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"

Hi Quinn,


Please can you confirm if this issue is still present in iOS 11? Potentially a whole client project hangs on whether this issue still exists or not.


Many thanks,


Andrew

Please can you confirm if this issue is still present in iOS 11?

Indeed it is.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

There’s finally been some good news on this front. See this thread for details.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"