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