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