Issues with ALPN implementation in JDK 9
Simone Bordet
simone.bordet at gmail.com
Tue Jun 14 13:57:37 UTC 2016
Hi,
I gave a shot at implementing ALPN in JDK 9 in Jetty.
TLDR: I could not find a way to make it work. This email is to discuss
whether I am off road or discuss possible solutions.
Below my feedback.
* Lack of facilities to convert TLS protocol bytes to protocol strings.
This class already exist in JDK, sun.security.ssl.ProtocolVersion, it
would just need to be exposed in javax.net.ssl.
* Lack of facilities to convert TLS cipher bytes to cipher name strings.
As above, sun.security.ssl.CipherSuite exists, needs to be exposed publicly.
Note that for the 2 bullets above, a recent message from Mark Reinhold
to jdk9-dev confirmed that JDK 9 is *not yet* feature complete, so I
hope they can be considered for inclusion.
* Server-side Implementation
I followed the guidelines reported here:
http://mail.openjdk.java.net/pipermail/security-dev/2015-December/013132.html,
namely:
1) Read network bytes after initial connection.
2) Parse network bytes, expecting TLS ClientHello message.
3) Extract from ClientHello the TLS protocol version, the TLS ciphers,
the ALPN protocols.
At this point, I should negotiate the application protocol, and it
must be only one.
Assuming the ClientHello TLS protocol is spoken by both peers, the
server logic can create pairs (cipher, app_proto) for each of the
ciphers in common between client and server, and discard the ciphers
that are not good for any protocol.
At this point, among all the valid pairs, I need to choose a protocol.
Let's make an example; the pairs are:
(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, h3)
(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, h2)
(TLS_WHATEVER, http/1.1)
Here "h3" is the future HTTP/3 protocol which I picked as an example
to show the problem I will encounter.
Because at this point the server logic must choose one protocol only
(so that it can be returned in the ServerHello), it picks h3, which
goes along with a ECDSA cipher, so:
sslParams.setApplicationProtocols(new String[]{"h3"});
sslParams.setCipherSuites(new
String[]{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", ...);
sslEngine.setSSLParameters(sslParams);
At this point, the server logic is good to let SSLEngine do the
unwrap(), and pass the original network bytes to unwrap().
During the unwrap(), the JDK implementation picks a cipher based on
the JDK logic.
In particular, in my case, I had a keystore with a certificate that
was *not* ECDSA.
If, in the snippet above, I set more than one cipher on the
SSLParameters, then perhaps a weaker cipher could be negotiated that
is not good for h3.
Otherwise, if I set only one cipher, there are no ciphers in common
and the TLS handshake is terminated with an error.
Bottom line, no negotiation is possible with this approach.
Next attempt I made was that before calling unwrap(), the server code
opens the keystore, and verifies if the certificate is ECDSA, handles
SNI, etc.
However, this means duplicating all the JDK logic to make sure that
the server logic *before* calling unwrap() is the same of the JDK so
that when unwrap() is called there will be no failures.
I don't think this is maintainable; the JDK is entitled to change the
logic following CVEs, optimizations and what not, and each such change
risks to break existing server code.
I then tried another approach.
In the server code, before calling unwrap(), I would remember the
pairs (cipher, app_proto), but *not* calling
SSLParameters.setApplicationProtocol().
I would then call unwrap(), where the JDK would choose the cipher.
The cipher is chosen in the NEED_TASK step of the unwrap(), so after
the task is run, the cipher chosen by the JDK is now available to the
server logic.
At this point I called again the server logic and, given the exact
cipher, choose the right protocol among the pairs that I have
previously stored.
In the example above, the JDK would have chose the RSA cipher because
the certificate was not ECDSA, and the server logic would have chosen
h2 as the application protocol:
sslParams.setApplicationProtocol(new String[]{"h2"});
Then let the unwrap()/wrap() code to finish the TLS handshake (in
particular, generate the ServerHello).
This approach has the benefit of cipher pre-selection done by the
server logic (it will retain not the intersection of ciphers between
client and server, but a possibly more restricted set that is valid
for the application protocols that are supported - imagine when
http/1.1 is not supported), coupled with JDK logic to interact with
SNI and certificates, coupled with a "late" selection of the
application protocol based on the cipher selected by the JDK logic.
Unfortunately, it does not work.
It does not work because the JDK implementation of
SSLEngine.setSSLParameters() is (more or less):
if (!handshake.started()) {
handshaker.setApplicationProtocols(applicationProtocols);
...
}
By the time the task is run in the NEED_TASK step,
handshaker.started==true, so the application protocols are not copied
into the handshaker and are not used to generate the ServerHello.
Conclusions.
I could not make a reliable ALPN implementation with the current JDK 9.
If I am off road, then that's good news, and I will be all ears for
alternative approaches.
If I am correct, I would like to discuss whether it would be possible
to delay handshaker.started=true to a later time, so that
SSLParameters can be changed just after the NEED_TASK step, so that
server applications will be able to interact with the JDK for what
pertains TLS protocols, ciphers, SNI, etc. without duplicating the
logic.
Comments welcome.
Thanks !
--
Simone Bordet
http://bordet.blogspot.com
---
Finally, no matter how good the architecture and design are,
to deliver bug-free software with optimal performance and reliability,
the implementation technique must be flawless. Victoria Livschitz
More information about the security-dev
mailing list