Bug 158345

Summary: [websocket] does not send client certificate
Product: WebKit Reporter: Adi Stadelmann <adrian.stadelmann>
Component: WebCore Misc.Assignee: Nobody <webkit-unassigned>
Status: NEW ---    
Severity: Normal CC: achristensen, andi, annulen, ap, bugzilla.josh, d.bussink, felix, jli, jung, tobias, triyae, vestbo, webkit-bug-importer, wilander, youennf
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Macintosh   
OS: OS X 10.11   

Description Adi Stadelmann 2016-06-03 06:47:47 PDT
Using a client certificate for a secure websocket connection,

SSL Handshake fails because client certificate was not send.

Tested with latest nightly WebKit-SVN-r201640
OS X 10.11.5 (15F34)

Https works, firefox, chrome and migrosoft edge works too.

Openssh handshake log:
```
verify depth is 9, must return a certificate
Using default temp DH parameters
Using default temp ECDH parameters
ACCEPT
SSL_accept:before/accept initialization
SSL_accept:SSLv3 read client hello A
SSL_accept:SSLv3 write server hello A
SSL_accept:SSLv3 write certificate A
SSL_accept:SSLv3 write certificate request A
SSL_accept:SSLv3 flush data
SSL3 alert write:fatal:handshake failure
SSL_accept:error in SSLv3 read client certificate B
SSL_accept:error in SSLv3 read client certificate B
ERROR
14774:error:140890C7:SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not return a certificate:/BuildRoot/Library/Caches/com.apple.xbs/Sources/OpenSSL098/OpenSSL098-59.40.2/src/ssl/s3_srvr.c:2640:
shutting down SSL
CONNECTION CLOSED
ACCEPT
```
Comment 1 Adi Stadelmann 2016-06-06 00:50:21 PDT
How to reproduce:

// ca
openssl genrsa -des3 -out ca.key 4096
openssl req -new -x509 -days 365 -key ca.key -out ca.crt
// server cert
openssl genrsa -out server.key 4096
openssl x509 -req -sha256 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
// client cert
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
// p12 for import
 openssl pkcs12 -export -clcerts -inkey client.key -in client.crt -out myClientCert.p12


import ca.crt and myClientCert.p12 into keychain, modify both to trust all


debugging with openssl:
sudo openssl s_server -accept 443 -key server.key -cert server.crt -CAfile ca.crt -Verify 9 -state


Connect with (webkit javascript console):
new WebSocket('wss://localhost/test');
Comment 2 andi 2017-01-20 12:48:49 PST
Simular problems here – are there any updates on this issue?
Comment 3 jonas 2018-01-15 23:17:03 PST
Any chance to get that bug fixed soon? No dev attention since 2016... Would be great to see any state change!
Comment 4 bugzilla.josh 2018-05-25 20:56:23 PDT
Also very interested in seeing this fixed. Really cripples secure realtime ios webapps
Comment 5 Radar WebKit Bug Importer 2018-07-11 07:53:23 PDT
<rdar://problem/42071235>
Comment 6 Tor Arne Vestbø 2018-07-12 10:17:33 PDT
Poking a bit a bit at this, it looks like normal loads are fed through WKNetworkSessionDelegate in NetworkSessionCocoa.mm in the Network process, where [WKNetworkSessionDelegate URLSession:task:didReceiveChallenge:completionHandler:] gets a callback with NSURLAuthenticationMethodClientCertificate, which is passed on to the UIProcess via NetworkLoad::didReceiveChallenge() and NetworkProcess::singleton().authenticationManager().didReceiveAuthenticationChallenge(), coming back from the UIProcess via AuthenticationManager::useCredentialForChallenge(), finally ending up calling the completion handler of the original callback.

WebSocket connections are also handled in the Network process, via SocketStreamHandle and SocketStreamHandleImpl in SocketStreamHandleImplCFNet.cpp, which sets up the connection in SocketStreamHandleImpl::createStreams(). There is some logic there for credentials via SocketStreamHandleImpl::getStoredCONNECTProxyCredentials, but nothing for client certificates. Presumably the fix here is to set the kCFStreamPropertySSLSettings property on the CFWriteStreamRef to a dict that includes the cert for the the kCFStreamSSLCertificates key.

The question is how to plumb a previously accepted cert on the UI side to the cert on the network process side. The normal load works via a callback, while CFStream seems to require the property to be set up front. There doesn't seem to be any way to peek into the cert negotiation and only ask for the cert and set the property at that point.

Alex, any thoughts?

One thing to note, is that the original bugreport described the reproducer as just connecting via new WebSocket('wss://localhost/test');, but this is not supported in Chrome either. You need to do a normal page load first, and then do websocket requests after accepting the cert in the UI. Running the reproducer with s_server -HTTP should make that easy to test by first loading a file on disk and then doing the JS bit within the loaded page.

For reference, I couldn't get Chrome or Safari to accept the self-signed server cert without a SAN. Here's what I used instead:

openssl req -x509 -nodes -new -out server.crt -keyout server.key -subj /CN=localhost -reqexts SAN -extensions SAN -config <(cat /etc/ssl/openssl.cnf && printf '[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1') -sha256 -days 3650
Comment 7 Alex Christensen 2018-07-12 10:49:50 PDT
I think the best way to solve this would be to re-implement WebSockets using NSURLSessionStreamTask instead of CFReadStreamRef/CFWriteStreamRef.  That's a substantial amount of work with high regression risk.
Comment 8 Tor Arne Vestbø 2018-07-12 11:04:56 PDT
Yeah, I suspected something like that :)

Pending someone doing that work (who know's what they are doing, to lessen the risk), is there any way to plumb the two worlds together within the current architecture? 

There are workarounds on the server side, at least when doing reverse proxying with ngnix [1], but they are not pretty, so it would be nice to have an upstream fix for macOS/iOS Safari.

[1] https://blog.christophermullins.com/2017/04/30/securing-homeassistant-with-client-certificates/
Comment 9 Tor Arne Vestbø 2018-07-12 11:06:33 PDT
Presumably this issue also affects iOS apps using WKWebView (assuming the app has imported the client cert into the app's keychain)?
Comment 10 tobias 2020-04-11 03:06:56 PDT
Is there any timeline for a bugfix, the bug is open for almost 4 years now?
Comment 11 youenn fablet 2020-04-11 07:30:30 PDT
There is ongoing work to use NSURLSession WebSocket API instead of the internal WebSocket implementation. This should hopefully fix this issue.

An early version is available in Safari Tech Preview on recent MacOSX: Develop -> Experimental Features -> "NSURLSession WebSocket".
Comment 12 tobias 2020-04-12 04:57:26 PDT
Awesome, thanks for the tip.
The experimental feature can even be enabled in the already released Safari 13.1 and in iOS 13.4.1.
Activating seems to work fine for the sites I tested.
Looking forward to the feature being integrated.
Comment 13 Alexey Proskuryakov 2020-04-12 17:21:43 PDT
Please feel encouraged to file bugs about any new issues that you may encounter with the experimental implementation. As Alex said back in 2018, there is a high regression risk.
Comment 14 Andreas Jung 2020-05-24 05:31:23 PDT
Tested client cert websocket with the NSURLSession WebSocket in iOS 13.5
It works once per browser start - after I hit refresh it will no longer connect. Restart browser app - it works again until refresh.

(Blazor Webassembly / SignalR with Websocket only)
Comment 15 Dirkjan Bussink 2020-07-14 05:11:46 PDT
> There is ongoing work to use NSURLSession WebSocket API instead of the internal WebSocket implementation. This should hopefully fix this issue.

We found that if this feature is enabled on the Big Sur or iOS 14 betas, it breaks websockets in a problematic way. In these new betas, support was added for per message deflate on a websocket connection but the implementation for that is broken it looks like.

https://github.com/dbussink/big-sur-websocket-bug contains a standalone Swift reproduction for this plus some network captures of the broken behavior. Tl;dr, if a websocket server doesn't support compression, the client enables it nonetheless which is a protocol violation.

If the server does accept compression, the client still ends up in an invalid state on messages sent to the server after the first one (slightly different behavior if context takeover is enabled vs. not). 

I've also reported this at https://feedbackassistant.apple.com/feedback/7970295 but not sure if there's a better channel for it then.