Received fatal alert: handshake_failure
I used to think tracking down non-deterministic bugs was the worst part about being a software engineer. I have come to realize over the years that there exists an even more painful debugging task: figuring out why messages like the following occur.
[Raw read]: length = 5
0000: 15 03 03 00 02
[Raw read]: length = 2
0000: 02 28 .(
main, READ: TLSv1.2 Alert, length = 2
main, RECV TLSv1.2 ALERT: fatal, handshake_failure
main, called closeSocket()
main, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
main, called close()
main, called closeInternal(true)
For the uninitiated, this is the kind of output you should expect to see when you add -Djavax.net.debug=all to your JVM’s command line. Note that this is the most verbose of an error you will get. Pretty helpful right? Unfortunately, the server’s output (OpenLDAP in this case) was not any better.
conn=1234 fd=30 ACCEPT from IP=188.8.131.52:12345 (IP=0.0.0.0:389)
conn=1234 op=0 STARTTLS
conn=1234 op=0 RESULT oid= err=0 text=
conn=1234 fd=30 closed (TLS negotiation failure)
It turns out that this particular issue was caused by the server that my client was attempting to negotiate a TLS session with was configured by a very security-minded co-worker to only accept AES 256-based ciphers. At the time of writing this, the Oracle’s Java Runtime Environment (JRE) comes with AES 256-based ciphers disabled due to various political cryptographic export and import policies.
Due to import control restrictions by the governments of a few countries, the jurisdiction policy files shipped specify that “strong” but limited cryptography may be used. An “unlimited strength” version of these files indicating no restrictions on cryptographic strengths is available for those living in eligible countries (which is most countries). But only the “strong” version can be imported into those countries whose governments mandate restrictions. The JCE framework will enforce the restrictions specified in the installed jurisdiction policy files.
So after spending hours creating new keystores, rebuilding truststores, overriding hostname verifiers, checking if SNI was enabled, setting JVM properties to disable deprecated ciphers, and the rest of what my Google searches returned, all I had to do was drop Oracle’s “Unlimited Strength Jurisdiction Policy Files” into my local JRE installation directory and everything started working.
I keep expecting to become numb to these type of moments, but I still have the same mix of emotions every time; happiness that the problem is solved, frustration at the number of hours dedicated to what turned out to be such a simple fix, and annoyance that something so mainstream has such poor hints to lead an implementer to a solution.