How the SSL/TLS Handshake Works: Step-by-Step Guide with Spring Boot & Java Code

4 min read

How the SSL/TLS Handshake Works: Step-by-Step Guide with Spring Boot & Java Code

1. Introduction

The SSL/TLS handshake is the foundation of secure communication between clients and servers. Before any encrypted data can be exchanged, both sides must negotiate protocols, exchange certificates, and derive encryption keys. Understanding this handshake is essential for developers working with HTTPS, secure APIs, or mutual authentication systems.

2. Problem

Many developers use HTTPS without understanding how the SSL/TLS handshake works behind the scenes. This lack of understanding can lead to misconfigurations, insecure deployments, failed client authentication, and confusion when working with certificates, keystores, and truststores.

Without a clear explanation and practical examples, debugging SSL/TLS issues becomes difficult—especially in microservices and enterprise environments where certificate chains, intermediate CAs, and protocol versions vary.

3. Solution

The solution is to break down the SSL/TLS handshake process into clear steps—from the initial Client Hello to server certificate validation and session key derivation—and provide concrete examples for Java and Spring Boot. Additionally, include troubleshooting steps and tools to diagnose failures.

4. Implementation

Below is a detailed implementation: handshake steps, Java/Spring Boot examples, troubleshooting, mutual TLS, and testing/verification commands you can run locally.

4.1 TLS/SSL Handshake Steps

  • Step 1: Client Hello — Client sends supported protocol versions, cipher suites, and a client random.
  • Step 2: Server Hello — Server selects protocol and cipher suite; sends server random.
  • Step 3: Server Certificate — Server sends X.509 certificate chain for identity proof.
  • Step 4: (Optional) Client Certificate Request — In mTLS the server requests the client’s certificate.
  • Step 5: Client Key Exchange — Client generates pre-master secret and encrypts it with the server’s public key (or uses ephemeral Diffie–Hellman).
  • Step 6: Certificate Verification — Client verifies the server certificate using its truststore and performs hostname verification.
  • Step 7: Session Key Derivation — Both sides derive symmetric keys from the pre-master secret + randoms.
  • Step 8: Finished Messages — Both sides send finished messages (encrypted) confirming handshake integrity.
  • Step 9: Encrypted Application Data — Secure data transfer begins using the derived symmetric keys.

4.2 Java / Spring Boot TLS Example (RestTemplate & HttpClient)

This section shows how to configure a Spring Boot application to call an HTTPS server and how the handshake is triggered automatically by the HTTP client.

4.2.1 Create / Import a Truststore

keytool -import -alias server-cert \
  -file server.crt \
  -keystore truststore.jks \
  -storepass changeit

This command imports the server certificate into truststore.jks so the Java client trusts the server certificate chain during the handshake.

4.2.2 Spring Boot server SSL config (application.yml)

server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:server.p12
    key-store-password: changeit
    key-store-type: PKCS12
    key-alias: server

4.2.3 RestTemplate using Apache HttpClient with SSLContext

@Configuration
public class SslConfig {

    @Bean
    public RestTemplate mtlsRestTemplate() throws Exception {
        // Load truststore from classpath
        KeyStore trustStore = KeyStore.getInstance("JKS");
        try (InputStream in = new ClassPathResource("truststore.jks").getInputStream()) {
            trustStore.load(in, "changeit".toCharArray());
        }

        SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(trustStore, null)
                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLContext(sslContext)
                .build();

        return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
    }
}

This RestTemplate will validate the server certificate against truststore.jks during the TLS handshake.

4.3 Troubleshooting & Diagnostics (Common Errors + Commands)

When handshakes fail you need targeted diagnostics. Use the following tools and checks first:

  • Enable Java SSL debug to see the handshake transcript:
java -Djavax.net.debug=all -jar your-app.jar

This produces verbose output showing versions, cipher suites, certificate validation, and failure reasons.

  • Use openssl s_client to inspect server certificates and negotiated protocol/cipher:
openssl s_client -connect secure.example.com:443 -showcerts

Key things to check in the openssl output: certificate chain correctness, certificate expiry, supported TLS versions, and negotiated cipher.

  • Check for common exceptions:
    • PKIX path building failed — missing intermediate CA or untrusted root.
    • no cipher suites in common — client/server share no common ciphers or protocols.
    • Received fatal alert: bad_certificate — server rejected client certificate (mTLS) or client presented wrong certificate.

To diagnose certificate chains use:

keytool -list -v -keystore truststore.jks -storepass changeit

To simulate handshake from the client side (Java) programmatically and inspect the session:

SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
try (SSLSocket socket = (SSLSocket) factory.createSocket("secure.example.com", 443)) {
    socket.startHandshake();
    SSLSession session = socket.getSession();
    System.out.println("Protocol: " + session.getProtocol());
    System.out.println("Cipher: " + session.getCipherSuite());
}

4.4 Mutual TLS (mTLS) Example — Client Keystore + Server Request

If the server requires client certificates (mTLS), the client must present a keystore containing its private key and certificate. Below is how to configure both server and client.

Server: request client cert in application.yml

server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:server.p12
    key-store-password: changeit
    key-store-type: PKCS12
    client-auth: need   # require client certificate

client-auth: need forces the server to request and require a valid client certificate for the TLS handshake.

Client: configure keystore (containing private key + cert)

keytool -importkeystore \
  -srckeystore client.p12 -srcstoretype PKCS12 -srcstorepass changeit \
  -destkeystore client-keystore.jks -deststoretype JKS -deststorepass changeit

Then load key material in the SSLContext so the client can present its certificate during handshake:

KeyStore keyStore = KeyStore.getInstance("JKS");
try (InputStream ksStream = new ClassPathResource("client-keystore.jks").getInputStream()) {
    keyStore.load(ksStream, "changeit".toCharArray());
}

KeyStore trustStore = KeyStore.getInstance("JKS");
try (InputStream ts = new ClassPathResource("truststore.jks").getInputStream()) {
    trustStore.load(ts, "changeit".toCharArray());
}

SSLContext sslContext = SSLContextBuilder.create()
    .loadKeyMaterial(keyStore, "changeit".toCharArray())   // client key + cert
    .loadTrustMaterial(trustStore, null)                   // trust server CA
    .build();

CloseableHttpClient httpClient = HttpClients.custom()
    .setSSLContext(sslContext)
    .build();

RestTemplate rest = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));

With this configuration, the client will send its certificate when the server requests it during the handshake.

4.5 Testing & Verification

Use the following commands and approaches to verify the handshake and cert exchange end-to-end.

4.5.1 Use openssl s_client (server cert only)

openssl s_client -connect secure.example.com:443 -showcerts

Inspect the certificate chain, validity dates, and the negotiated protocol/cipher suite in the command output.

4.5.2 Test mTLS with openssl (client cert)

openssl s_client -connect secure.example.com:8443 \
  -cert client.crt -key client.key -CAfile ca-chain.pem

If mTLS is configured correctly, the handshake completes and you will see the server response or an interactive TLS session.

4.5.3 Test using curl (client cert)

curl -v --cacert ca-chain.pem --cert client.pem --key client-key.pem https://secure.example.com:8443/secure

Use verbose mode to view TLS negotiation details. Replace client.pem and client-key.pem with PEM files or use PKCS#12 via --cert client.p12:password.

4.5.4 Java unit / integration tests

// Example: JUnit test to verify handshake (uses HttpClient with SSLContext)
@Test
public void whenCallSecureEndpoint_thenNoException() {
    // build SSLContext similar to application config
    // create HttpClient with SSLContext and call endpoint
    // assert HTTP 200 or expected response
}

Running tests in a controlled environment (local server with known certs) makes it easy to validate certificate rotation and handshake behavior before production rollout.

5. Conclusion

Understanding the SSL/TLS handshake and how to configure keystores, truststores, and SSLContext is essential for secure client-server communication. You now have:

  • A step-by-step TLS handshake breakdown
  • Concrete Java & Spring Boot examples for one-way TLS and mTLS
  • Troubleshooting commands and common error explanations
  • Testing and verification commands (openssl, curl, Java debug)

🤞 Never miss a story from us, get weekly updates to your inbox!

Leave a Reply

Your email address will not be published. Required fields are marked *