Sitelet https://howtodoinjava.com/java/java-security/tls12-sslhandshakeexception/

SSLHandshakeException in Java: Fix TLS 1.2 Handshake Errors

SSLHandshakeException means the Java client and the server could not agree on a TLS connection. Fix TLS version mismatches with jdk.tls.client.protocols, untrusted certificates with keytool, and find the rest with javax.net.debug.

Bypass SSL Security

A javax.net.ssl.SSLHandshakeException means the Java client and the server could not agree on a secure connection. Before any HTTPS data is sent, both sides run a short exchange called the TLS handshake. During the handshake, the two sides pick a TLS version and a cipher suite (the set of encryption algorithms), and the client checks the server’s certificate. If any of these steps fails, Java throws SSLHandshakeException and no HTTP request is sent.

We meet the exception when a Java app calls an old server that supports only TLS 1.1, or a test server with a self-signed certificate.

The following example shows the three fixes that solve most cases, one for each common error message.

# 1. TLS version mismatch: "(protocol_version) Received fatal alert" or "No appropriate protocol"
java -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3 -jar app.jar   # client offers TLS 1.3 and 1.2
# in code: SSLContext.getInstance("TLSv1.3")                    # enables [TLSv1.3, TLSv1.2]
#          SSLContext.getInstance("TLSv1.2")                    # enables [TLSv1.2] only

# 2. Certificate not trusted: "PKIX path building failed"
echo | openssl s_client -connect localhost:8444 -servername localhost | openssl x509 > server.crt
cp $JAVA_HOME/lib/security/cacerts mycacerts                   # copy of the JDK trust store (118 entries)
keytool -importcert -noprompt -alias local-server -file server.crt \
        -keystore mycacerts -storepass changeit                 # Certificate was added to keystore
java -Djavax.net.ssl.trustStore=mycacerts \
     -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar   # Status: 200

# 3. Still failing? Print every handshake step
java -Djavax.net.debug=ssl:handshake -jar app.jar

Notice that each fix belongs to one error message, so we read the message first, and we turn on the debug log only when the message does not name the cause.

Next, we see what each error message means and reproduce every error on a local HTTPS server with Java 25. Then we fix TLS version and cipher suite mismatches, untrusted certificates and SNI problems, and configure HttpClient for TLS 1.2 and TLS 1.3.

1. What Does SSLHandshakeException Mean?

HTTPS is HTTP sent over a TLS connection, and TLS (Transport Layer Security) encrypts the data between the client and the server. Before the first HTTP request, the client and the server run the handshake, which has a few steps, and each step can fail in its own way.

  1. The client sends a ClientHello message, which lists the TLS versions and cipher suites the client accepts.
  2. The server picks one TLS version and one cipher suite from the list. If nothing matches, the server sends an alert, a short error message, and closes the connection.
  3. The server sends its certificate, which proves the server’s identity.
  4. The client checks the certificate. A trusted certificate authority (CA) must have signed it, and the certificate must contain the host name from the URL.
Four steps of the TLS handshake from top to bottom, each with the Java error it can produce: 1. ClientHello with TLS versions and cipher suites, failing with No appropriate protocol on the client. 2. Server picks a version and cipher, failing with protocol_version or handshake_failure alerts, or Remote host terminated the handshake when the server closes the connection without an alert. 3. Server sends its certificate, with SNI picking the certificate and unrecognized_name on a wrong host name. 4. Client checks the certificate, failing with PKIX path building failed or No subject alternative names matching.
Each step of the TLS handshake has its own error message. The message tells us which step failed.

When the handshake fails, Java 25 puts the TLS alert name in parentheses at the start of the message, for example (protocol_version). This alert name is the quickest way to find the cause, because each alert points to one handshake step, and the rest of the message tells us the cause.

Message after SSLHandshakeException:CauseFix
(protocol_version) Received fatal alert: protocol_versionThe server and the client share no TLS versionUpgrade the server to TLS 1.2 or 1.3, or enable TLS 1.3 on the client (section 3)
No appropriate protocol (protocol is disabled or cipher suites are inappropriate)The client may only use a version that Java has disabled, such as TLS 1.1Use TLS 1.2 or TLS 1.3 (section 3.2)
(certificate_unknown) PKIX path building failed: … unable to find valid certification path to requested targetThe server certificate is self-signed or signed by an unknown CAImport the certificate into a trust store (section 4)
(certificate_unknown) No subject alternative names matching IP address 127.0.0.1 foundThe URL host is not in the certificateCall the host name the certificate was issued for (section 4.3)
(handshake_failure) Received fatal alert: handshake_failureNo shared cipher suite, or the server wants a client certificateEnable modern cipher suites on the server (section 5)
(unrecognized_name) Received fatal alert: unrecognized_nameThe server does not know the host name sent in SNIUse a host name the server serves (section 6)
Remote host terminated the handshakeThe server closed the connection without an alertCheck the server side and the debug log (section 2.2)

2. Reproducing the Errors on a Local Server

We do not need a broken production server to see these errors, because the openssl s_server command starts a small HTTPS server on our machine. Its options limit the TLS versions and cipher suites, so we can create each failure on purpose. Our client runs on Java 25 (Temurin 25.0.4.1), and the server is OpenSSL 3.0.13.

2.1. Starting a Local HTTPS Server

First, we create a self-signed certificate for localhost. A self-signed certificate is signed by its own key, not by a CA, so Java does not trust it by default.

openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 \
  -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"

openssl s_server -accept 8444 -cert server.crt -key server.key -www    # TLS 1.2 and 1.3
openssl s_server -accept 8443 -cert server.crt -key server.key -www \
  -tls1_1 -cipher 'DEFAULT@SECLEVEL=0'                                 # TLS 1.1 only
openssl s_server -accept 8445 -cert server.crt -key server.key -www -tls1_3   # TLS 1.3 only

The -www option makes s_server answer HTTP requests with a status page. The client is the Java 11+ HttpClient, and it prints either the status code or every exception in the cause chain.

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(args[0])).build();
try {
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  System.out.println("Status: " + response.statusCode());
} catch (Exception e) {
  for (Throwable t = e; t != null; t = t.getCause()) System.out.println(t);
}

The first run against the TLS 1.2/1.3 server fails at once, because the certificate is self-signed.

javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

2.2. When the Server Closes the Connection Without an Alert

Sometimes the server sends no alert at all. The server reads the ClientHello and closes the TCP connection. Java 8 reported this case as Remote host closed connection during handshake, whereas Java 25 reports it as Remote host terminated the handshake.

To reproduce the error, we use a tiny Python server that accepts a connection, reads the first message and closes the connection.

c, _ = s.accept(); c.recv(4096); c.close()
javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake
	at java.base/sun.security.ssl.SSLSocketImpl.handleEOF(SSLSocketImpl.java:1716)
	at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1514)
	at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1421)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:455)
Caused by: java.io.EOFException: SSL peer shut down incorrectly
	at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:494)

The message alone does not tell us why the server closed the connection, so we go through the four common reasons.

  • The server or a load balancer in front of the server supports only old TLS versions and drops the ClientHello.
  • A firewall or proxy between the client and the server blocks the TLS traffic.
  • The port does not accept TLS connections at all, for example a plain HTTP port.
  • The server requires a client certificate and closes the connection without an alert.

For example, an app that calls a partner API through a company proxy gets the error when the proxy blocks TLS traffic to that host. The fastest check is to connect with openssl s_client from the same machine, which prints the TLS version and certificate the server offers. Then we compare the result with the ClientHello in the Java debug log (section 5.1).

3. TLS Version Mismatch

Old servers often support only TLS 1.0 or TLS 1.1, and modern Java versions refuse both. The opposite also happens when a server accepts only TLS 1.3 and the client code allows only TLS 1.2.

3.1. Which TLS Versions Does Java 25 Enable?

The JDK lists blocked algorithms and protocols in the file $JAVA_HOME/conf/security/java.security. In Java 25, the property jdk.tls.disabledAlgorithms contains TLSv1 and TLSv1.1, so both are disabled by default.

jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, DTLSv1.0, RC4, DES, \
    MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \
    ECDH, TLS_RSA_*, rsa_pkcs1_sha1 usage HandshakeSignature, \
    ecdsa_sha1 usage HandshakeSignature, dsa_sha1 usage HandshakeSignature

The SSLContext class creates the TLS connections in Java, and the protocol name passed to SSLContext.getInstance() decides which versions the client offers. The comment on each line shows the result of getDefaultSSLParameters().getProtocols() for that context after init().

SSLContext tls = SSLContext.getInstance("TLS");         // [TLSv1.3, TLSv1.2]
SSLContext tls13 = SSLContext.getInstance("TLSv1.3");   // [TLSv1.3, TLSv1.2]
SSLContext tls12 = SSLContext.getInstance("TLSv1.2");   // [TLSv1.2]   no TLS 1.3!
SSLContext def = SSLContext.getDefault();               // [TLSv1.3, TLSv1.2]

// with -Djdk.tls.client.protocols=TLSv1.2
SSLContext tlsProp = SSLContext.getInstance("TLS");       // [TLSv1.2]
SSLContext tls13Prop = SSLContext.getInstance("TLSv1.3"); // [TLSv1.3, TLSv1.2]  (property ignored)

TLS 1.2 and TLS 1.3 are both enabled by default in Java 25. The system property jdk.tls.client.protocols changes only the “TLS” context and the default context, which the HttpClient and HttpsURLConnection classes use unless we pass our own. The supported list still contains TLSv1.1, TLSv1 and SSLv3, but jdk.tls.disabledAlgorithms blocks them.

3.2. Calling a Server That Supports Only TLS 1.1

Against the TLS 1.1 server on port 8443, the default client fails with a protocol_version alert, because the client offers TLS 1.3 and TLS 1.2 and the server knows neither version.

javax.net.ssl.SSLHandshakeException: (protocol_version) Received fatal alert: protocol_version

When we force the client to TLS 1.1, the error changes, and the client fails before it sends anything, because TLS 1.1 is disabled.

javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

The right fix is to enable TLS 1.2 or TLS 1.3 on the server, because TLS 1.0 and 1.1 are deprecated by RFC 8996.

If we cannot change the server, for example a legacy system in a closed network, we can re-enable TLS 1.1 for one application. We copy the jdk.tls.disabledAlgorithms line into a separate file and remove TLSv1.1 from the copy.

jdk.tls.disabledAlgorithms=SSLv3, TLSv1, DTLSv1.0, RC4, DES, \
    MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \
    ECDH, TLS_RSA_*, rsa_pkcs1_sha1 usage HandshakeSignature, \
    ecdsa_sha1 usage HandshakeSignature, dsa_sha1 usage HandshakeSignature
java -Djava.security.properties=tls11-allowed.security -jar app.jar

The -Djava.security.properties option reads the file and overrides the matching values in java.security for this JVM only. With the file, the TLS 1.1 handshake passed the version check, and the client then failed at the next step, the certificate check (PKIX path building failed).

Re-enabling TLS 1.1 weakens the connection. We use the override only for one known legacy server and remove the override once the server is upgraded.

3.3. Client That Allows Only TLS 1.2 and a TLS 1.3-Only Server

Many older tutorials create the context with SSLContext.getInstance(“TLSv1.2”), which enables TLS 1.2 only, so against the TLS 1.3-only server on port 8445 the handshake fails.

javax.net.ssl.SSLHandshakeException: (protocol_version) Received fatal alert: protocol_version

Adding -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3 gave the same error, because the property does not change an explicit “TLSv1.2” context.

The context names “TLS” and “TLSv1.3” both enable TLS 1.3 and TLS 1.2, and with the default client the same TLS 1.3-only server returned Status: 200. So we use SSLContext.getInstance(“TLS”) or “TLSv1.3” in code, never “TLSv1.2”.

4. Fixing an Untrusted Server Certificate

Java checks every server certificate against a trust store (a file with the CA certificates the JVM trusts). The JDK ships one at $JAVA_HOME/lib/security/cacerts, with 118 public CAs in JDK 25.0.4.1. A self-signed certificate, or a certificate from a company’s internal CA, is not in that file, so the check fails with PKIX path building failed (PKIX is the standard for checking certificate chains).

4.1. Importing the Server Certificate With keytool

The fix is to add the server’s certificate, or better its CA certificate, to a trust store. First, we download the certificate with openssl s_client, and the file is identical to the server.crt we created in section 2.1.

echo | openssl s_client -connect localhost:8444 -servername localhost 2>/dev/null \
  | openssl x509 > fetched.crt

We import the certificate into a copy of cacerts, not into an empty trust store. The javax.net.ssl.trustStore property replaces the JDK trust store completely, so if the trust store holds only our certificate, every public HTTPS site fails with PKIX path building failed. The keytool command adds the certificate.

cp $JAVA_HOME/lib/security/cacerts mycacerts
keytool -importcert -noprompt -alias local-server -file fetched.crt \
        -keystore mycacerts -storepass changeit       # Certificate was added to keystore
keytool -list -keystore mycacerts -storepass changeit # Your keystore contains 119 entries

The default password of cacerts is changeit. Next, we start the application with the new trust store.

Status: 200

We could also import the certificate into $JAVA_HOME/lib/security/cacerts itself, so every application on that JDK trusts the certificate without extra options. But the change is lost when we upgrade the JDK or rebuild the Docker image, which is why a separate trust store file is easier to maintain.

4.2. A Wrong Trust Store Path Gives the Same Error

A typo in the trust store path does not produce a “file not found” message. For example, -Djavax.net.ssl.trustStore=nofile.p12 produces the same PKIX path building failed error as a missing certificate.

When the import “did not work”, we first check the path and print the loaded trust store with -Djavax.net.debug=ssl:handshake.

The debug log then shows lines such as trustStore is: truststore.p12 and Reloaded 1 trust certs.

4.3. Host Name Does Not Match the Certificate

Trusting the certificate is not enough, because the host in the URL must also appear in the certificate’s Subject Alternative Name (SAN) list. Our certificate contains DNS:localhost, so calling the server by IP address fails, even with the trust store.

javax.net.ssl.SSLHandshakeException: (certificate_unknown) No subject alternative names matching IP address 127.0.0.1 found
java.security.cert.CertificateException: No subject alternative names matching IP address 127.0.0.1 found

The fix is to call the server by a name in the certificate, here https://localhost:8444/. Another fix is a new certificate that lists the IP address, for example with -addext “subjectAltName=DNS:localhost,IP:127.0.0.1”.

5. Cipher Suite Mismatch

A cipher suite is a named set of algorithms for one connection. For example, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 uses ECDHE to agree on a key and AES-256-GCM to encrypt the data, and both sides must support at least one common suite.

Java 25 disables every suite whose name starts with TLS_RSA_, because these suites use plain RSA to exchange the key, so if the server key leaks later, an attacker can decrypt recorded traffic. That is why a JDK upgrade can break the calls to an internal server whose cipher list nobody has changed in years.

We start a TLS 1.2 server that offers only two TLS_RSA_ suites.

openssl s_server -accept 8446 -cert server.crt -key server.key -www -tls1_2 \
  -cipher 'AES128-GCM-SHA256:AES256-GCM-SHA384'
javax.net.ssl.SSLHandshakeException: (handshake_failure) Received fatal alert: handshake_failure
error:0A0000C1:SSL routines:tls_post_process_client_hello:no shared cipher

The Java message says only handshake_failure, whereas the server log gives the real reason, no shared cipher.

The fix is to enable ECDHE cipher suites on the server. OpenSSL calls our two suites AES128-GCM-SHA256 and AES256-GCM-SHA384, and Java calls the same suites TLS_RSA_WITH_AES_128_GCM_SHA256 and TLS_RSA_WITH_AES_256_GCM_SHA384.

Removing the TLS_RSA_ entry from a java.security.properties override file, as in section 3.2, also made the call return Status: 200, but we use the override only as a temporary workaround.

5.1. Reading the Handshake Debug Log

When the server log is not available, the JSSE debug log shows what the client offered, and the -Djavax.net.debug=ssl:handshake option prints each handshake message. We use the same debug logging for other SSL issues too. For the cipher test, the log has 426 lines, and only a few of them matter.

Ignore disabled cipher suite: TLS_RSA_WITH_AES_256_GCM_SHA384
Ignore disabled cipher suite: TLS_RSA_WITH_AES_128_GCM_SHA256
...
Produced ClientHello handshake message (
"ClientHello": {
  "client version"      : "TLSv1.2",
  "cipher suites"       : "[TLS_AES_256_GCM_SHA384(0x1302), TLS_AES_128_GCM_SHA256(0x1301), ..., TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(0xC030), ...]",
  "extensions"          : [
    "server_name (0)": {
      type=host_name (0), value=localhost
    },
    "supported_versions (43)": {
      "versions": [TLSv1.3, TLSv1.2]
    },
...
READ: TLSv1.2 alert, length = 2
Received alert message (
"Alert": {
  "level"      : "fatal",
  "description": "handshake_failure"
}

We read the log from top to bottom.

  • Ignore disabled cipher suite lines list the suites that jdk.tls.disabledAlgorithms removed, and the server’s two suites are in that list.
  • The supported_versions extension shows the TLS versions the client offered, so a protocol_version alert means the server needs a version outside this list.
  • The server_name extension is the SNI host name (section 6).
  • The Alert block shows what the server answered.

The “client version” : “TLSv1.2” line is not an error, because TLS 1.3 clients always write TLS 1.2 there for compatibility. The real versions are in supported_versions, as RFC 8446 requires.

6. SNI Host Name Errors

One server often hosts several sites on one IP address, so SNI (Server Name Indication, RFC 6066) tells the server which site the client wants. The client puts the host name into the ClientHello, and the server picks the matching certificate. Java sends the host name from the URL, but it does not send SNI when the URL contains an IP address.

To see an SNI error, we started a server that knows only the name localhost and rejects other names, while the name vm also points to 127.0.0.1 on our machine.

openssl s_server -accept 8448 -cert server.crt -key server.key -www \
  -servername localhost -cert2 server.crt -key2 server.key -servername_fatal
javax.net.ssl.SSLHandshakeException: (unrecognized_name) Received fatal alert: unrecognized_name

The server received vm in SNI and sent the unrecognized_name alert. Turning SNI off with -Djsse.enableSNIExtension=false did not fix the call, because the certificate check failed instead.

javax.net.ssl.SSLHandshakeException: (certificate_unknown) No subject alternative DNS name matching vm found.

The fix is to call the server by the host name it serves, here localhost. SNI errors also happen when a client calls an internal alias or a load balancer address that the server is not configured for.

We do not disable SNI as a fix, because servers behind shared load balancers often need SNI to return the right certificate.

7. HttpClient Example With TLS 1.2 and TLS 1.3

System properties change TLS settings for the whole JVM. When only one client needs a custom trust store, we build the SSLContext in code and pass it to that client.

7.1. Java HttpClient

The HttpClient.Builder accepts an SSLContext and SSLParameters. We load the trust store from section 4, create a “TLS” context, and allow TLS 1.3 and TLS 1.2.

KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(Path.of("truststore.p12"))) {
  trustStore.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);

SSLParameters sslParams = new SSLParameters();
sslParams.setProtocols(new String[] {"TLSv1.3", "TLSv1.2"});

HttpClient client = HttpClient.newBuilder()
    .sslContext(sslContext)
    .sslParameters(sslParams)
    .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String protocol = response.sslSession().get().getProtocol();   // TLSv1.3 (server on 8444)

The trust store truststore.p12 holds only our server certificate, and we created it with keytool -importcert … -keystore truststore.p12 -storepass changeit.

The program prints the negotiated version and cipher suite for two servers, where the server on port 8450 runs s_server with -tls1_2.

$ java TlsClient https://localhost:8444/
Status: 200
Protocol: TLSv1.3
Cipher: TLS_AES_256_GCM_SHA384
$ java TlsClient https://localhost:8450/
Status: 200
Protocol: TLSv1.2
Cipher: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

Some older examples skip init(), but with HttpClient, an uninitialized context fails with java.lang.IllegalStateException: SSLContext is not initialized. So we must call sslContext.init() before we use the context.

7.2. Apache HttpClient 5

Older examples use Apache HttpClient 4 with SSLConnectionSocketFactory and ALLOW_ALL_HOSTNAME_VERIFIER, but both are deprecated, and the verifier turns off the host name check. In Apache HttpClient 5.6.1, a TlsSocketStrategy sets the context and the TLS versions.

SSLContext sslContext = SSLContexts.custom()
    .loadTrustMaterial(new File("mycacerts"), "changeit".toCharArray())
    .build();

var tlsStrategy = ClientTlsStrategyBuilder.create()
    .setSslContext(sslContext)
    .setTlsVersions(TLS.V_1_3, TLS.V_1_2)
    .buildClassic();

var connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
    .setTlsSocketStrategy(tlsStrategy)
    .build();

try (CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build()) {
  int status = client.execute(new HttpGet(url), response -> response.getCode());   // 200
}
<dependency>
  <groupId>org.apache.httpcomponents.client5</groupId>
  <artifactId>httpclient5</artifactId>
  <version>5.6.1</version>
</dependency>

The client returned 200 against the TLS 1.2/1.3 server and the TLS 1.3-only server. Spring’s RestTemplate can use the same Apache client through a RestTemplate with HttpClient configuration.

8. SSLHandshakeException FAQs

8.1. What Does “the trustAnchors parameter must be non-empty” Mean?

The message means Java found the trust store file but read no certificates from it. For example, we get the error when we pass our new truststore.p12 without -Djavax.net.ssl.trustStorePassword.

javax.net.ssl.SSLException: (internal_error) Unhandled exception
java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty

A new PKCS12 file from keytool protects its certificates with the store password, so we always pass the password. The copied mycacerts file works with and without the password.

8.2. Should We Disable Certificate Validation to Fix the Error?

No, not in production. A trust manager that accepts every certificate also accepts an attacker’s certificate, so the encryption no longer protects the data from that attacker.

For a local test against a self-signed server, we can bypass SSL certificate checking, but importing the certificate (section 4.1) is the safe fix and takes two commands.

8.3. Why Do We Get handshake_failure From a Server That Requires a Client Certificate?

The server uses mutual TLS (mTLS), in which the server also asks the client for a certificate, and over TLS 1.2 the missing client certificate shows up only as a generic alert. For example, a bank API that accepts calls only from partner apps asks every caller for a client certificate. We started s_server with -Verify 1 and called the server without a client certificate. Over TLS 1.3, the message named the problem, certificate_required, whereas over TLS 1.2 the same problem appeared as a generic handshake_failure.

$ java Client https://localhost:8449/
javax.net.ssl.SSLHandshakeException: (certificate_required) Received fatal alert: certificate_required
$ java -Djdk.tls.client.protocols=TLSv1.2 Client https://localhost:8449/
javax.net.ssl.SSLHandshakeException: (handshake_failure) Received fatal alert: handshake_failure

The fix is a key store with the client’s private key and certificate, which we pass with -Djavax.net.ssl.keyStore and -Djavax.net.ssl.keyStorePassword. A key store holds our own keys, while a trust store holds the certificates we trust.

8.4. How Do We Configure HTTPS in a Spring Boot Server?

We give the server a certificate and a key store, because the server side of the handshake needs them too. For an embedded Tomcat, we configure HTTPS in Spring Boot with the *server.ssl.** properties.

9. Conclusion

SSLHandshakeException means one of the handshake steps failed, and the message tells us which step failed.

The messages protocol_version and No appropriate protocol mean a TLS version mismatch. Java 25 enables only TLS 1.2 and TLS 1.3, so we upgrade the server or use SSLContext.getInstance(“TLS”) instead of “TLSv1.2”.

PKIX path building failed means the certificate is not trusted, and importing the certificate with keytool -importcert into a copy of cacerts fixes the error. In most cases, the handshake_failure alert means either no shared cipher suite or a missing client certificate.

For Remote host terminated the handshake, we check the server side. For every case, -Djavax.net.debug=ssl:handshake shows what the client offered and what the server answered.

10. References

Happy Learning !!

Leave a Comment

  1. Hello Lokesh,

    Thanks for posting this article.

    I have the same issue while redeploying JEE application on Payara5. Could you please advise – I assume that the certificate (.crt) file that need to go into the JKS store is the .crt for the domain. I am bit confused that you have specified “maven.cer” file!

  2. Hi Lokesh,

    I am using rest assured to connect to external api- which is twilio. I have added twilio certs to location /Java/jdk8/jre/lib/security/cacerts. I am still getting ssl hand shake error.

    javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
    Caused by: java.io.EOFException: SSL peer shut down incorrectly

    Can you help?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.