Skip to main content

Command Palette

Search for a command to run...

Debugging Java SSL/TLS Connections: A Guide to javax.net.debug

Updated
10 min read

Introduction

Understanding SSL/TLS handshakes is crucial for debugging secure connections in Java applications. In this guide, I'll walk you through a real-world example of capturing and analyzing an SSL/TLS handshake using Java's built-in debugging capabilities.

This article covers:

  • How to enable SSL/TLS debugging in Java
  • Real handshake flow with detailed timing
  • Complete working example with HttpURLConnection
  • Step-by-step breakdown of the TLS 1.2 handshake

Enabling SSL/TLS Debug Output

To capture SSL/TLS debugging information, simply set this system property before establishing any connections:

System.setProperty("javax.net.debug", "all");

This enables comprehensive logging of:

  • Cipher suite negotiation
  • Certificate validation
  • Handshake messages
  • Raw bytes sent/received
  • Session information

Real-World TLS 1.2 Handshake Flow

When you connect to an HTTPS endpoint (in our example: https://httpbin.org/get), the following handshake occurs:

ASCII Diagram: Complete TLS 1.2 Handshake

CLIENT (Java App)                           SERVER (httpbin.org)
════════════════════════════════════════════════════════════════

        ┌─────────────────────────────────┐
        │  1. ClientHello                 │
        │  (22:20:06.819 CET)             │
        │  - Ciphers, Extensions          │
        │  - Random value                 │
        └────────────────────┬────────────┘
                             │
                             │ [Raw write]
                             │ TLS 1.2 record
                             ├──────────────────────────────────>
                                                                │
                                           ┌────────────────────┘
                                           │
                                           ▼
                            ┌──────────────────────────────────┐
                            │  2. ServerHello                  │
                            │  (22:20:06.924 CET)              │
                            │  - Selected cipher suite:        │
                            │    TLS_ECDHE_RSA_WITH_AES_128... │
                            │  - Server random                 │
                            │  - Session ID                    │
                            └────────────────┬──────────────────┘
                                             │
                                             │ [Raw read]
        ┌────────────────────────────────────┤
        │                                     │
        ▼                                     ▼
┌──────────────────────────────┐  ┌──────────────────────────────────┐
│  3. Server Certificate       │  │  4. ServerKeyExchange (ECDH)     │
│  (22:20:06.930 CET)          │  │  (22:20:06.950 CET)              │
│  - Certificate chain:        │  │  - ECDH parameters               │
│    └─ httpbin.org            │  │  - Server's ECDH public key      │
│    └─ Amazon RSA 2048 M      │  │  - Signature                     │
│    └─ Root CA                │  │                                  │
└──────────────────────────────┘  └──────────────────────────────────┘
        │                                    │
        └────────────────────┬───────────────┘
                             │
                             ▼
                    ┌──────────────────────┐
                    │  5. ServerHelloDone  │
                    │  (22:20:06.951 CET)  │
                    └────────┬─────────────┘
                             │
                             │ [Raw read]
        ┌────────────────────┘
        │
        ▼
┌───────────────────────────────────────────┐
│  6. ClientKeyExchange (ECDHE)             │
│  (22:20:06.951 CET)                       │
│  - Client's ECDH public key               │
│  - Compute shared secret (PreMasterKey)   │
└────────────────┬────────────────────────────┐
                 │                            │
                 │ [Raw write]                │
                 │ TLS 1.2 handshake         │
                 ├───────────────────────────────────────────────>
                 │                            │
                 ▼                            │
┌────────────────────────────────────────┐   │
│  7. ChangeCipherSpec (Client)          │   │
│  (22:20:06.952 CET)                    │   │
│  - Switch to encrypted traffic         │   │
└────────────────┬─────────────────────────┐ │
                 │                          │ │
                 │ [Raw write]              │ │
                 │ TLS 1.2 change spec      │ │
                 ├──────────────────────────────>
                 │                          │ │
                 ▼                          │ │
┌────────────────────────────────────────┐   │
│  8. Finished (Client)                  │   │
│  (22:20:06.954 CET)                    │   │
│  - MAC of all handshake messages       │   │
│  - Encrypted with session key          │   │
└────────────────┬─────────────────────────┐ │
                 │                          │ │
                 │ [Raw write]              │ │
                 │ TLS 1.2 handshake       │ │
                 ├──────────────────────────────>
                 │                          │ │
                 │                          │ │
                 │                   ┌──────┘ │
                 │                   │        │
                 │                   ▼        ▼
                 │        ┌──────────────────────────────┐
                 │        │  9. NewSessionTicket         │
                 │        │  (22:20:07.056 CET)          │
                 │        │  - For session resumption    │
                 │        └────────┬─────────────────────┘
                 │                 │
                 │                 │ [Raw read]
        ┌────────┴─────────────────┤
        │                          │
        ▼                          ▼
┌──────────────────────┐  ┌────────────────────────────┐
│  10. ChangeCipherSpec│  │  11. Server Finished       │
│  (Server)            │  │  (22:20:07.056 CET)       │
│  (22:20:07.056 CET)  │  │  - MAC of all handshake   │
└──────────────────────┘  │  - Encrypted with key     │
                          └────────┬──────────────────┘
                                   │
                                   │ [Raw read]
                    ┌──────────────┘
                    │
                    ▼
        ┌───────────────────────────────┐
        │  ✓ HANDSHAKE COMPLETE        │
        │  (22:20:07.057 CET)          │
        │                              │
        │  Secure Channel Established  │
        │  - Ready for application     │
        │  - All data encrypted/MAC'd  │
        └───────────────────────────────┘

Handshake Timing & Summary

StepMessage TypeDirectionTimestampDuration
1ClientHelloClient → Server22:20:06.819-
2ServerHelloServer → Client22:20:06.924+105ms
3CertificateServer → Client22:20:06.930+6ms
4ServerKeyExchangeServer → Client22:20:06.950+20ms
5ServerHelloDoneServer → Client22:20:06.951+1ms
6ClientKeyExchangeClient → Server22:20:06.951-
7ChangeCipherSpecClient → Server22:20:06.952+1ms
8FinishedClient → Server22:20:06.954+2ms
9NewSessionTicketServer → Client22:20:07.056+102ms
10ChangeCipherSpecServer → Client22:20:07.056-
11FinishedServer → Client22:20:07.056-
Total Duration22:20:07.057~238ms

Negotiated Security Parameters

  • Protocol Version: TLS 1.2
  • Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xC02F)
  • Key Exchange: ECDHE (Elliptic Curve Diffie-Hellman Ephemeral)
  • Encryption: AES-128-GCM
  • Hash Algorithm: SHA256
  • Server Certificate: httpbin.org (issued by Amazon RSA 2048 M)

Negotiated Extensions

  • renegotiation_info - Controls renegotiation
  • server_name - SNI (Server Name Indication)
  • ec_point_formats - Supported EC point formats (uncompressed)
  • extended_master_secret - Enhanced security
  • session_ticket - Session resumption capability

Handshake Phase Breakdown

Phase 1: Key Exchange & Authentication

The client and server exchange their capabilities and parameters:

  1. ClientHello → Client offers supported ciphers, TLS versions, and extensions
  2. ServerHello → Server selects cipher, version, and confirms capabilities
  3. Certificate → Server proves identity with certificate chain
  4. ServerKeyExchange → Server sends ECDH parameters for key agreement
  5. ServerHelloDone → Marks end of server's initial messages

Phase 2: Key Derivation

Both sides generate the shared secret:

  1. ClientKeyExchange → Client sends ECDH public key
    • Both sides compute: PreMasterSecret = ECDH(client_key, server_key)
    • Both derive: MasterSecret = PRF(PreMasterSecret, "master secret", client_random || server_random)
    • Both generate session encryption/decryption keys

Phase 3: Cipher Activation & Verification

Both sides verify the handshake and activate encryption:

  1. ChangeCipherSpec (Client) → "Start encrypting from now on"
  2. Finished (Client) → Encrypted MAC of all handshake messages
  3. NewSessionTicket (Server) → Optional ticket for future session resumption
  4. ChangeCipherSpec (Server) → "Start encrypting from now on"
  5. Finished (Server) → Encrypted MAC of all handshake messages

Phase 4: Secure Communication

Application data is now encrypted using the derived session keys.


Complete Working Example

Here's the complete Java code that produces the above handshake:

App.java - Main Application

/*
 * This source file was generated by the Gradle 'init' task
 */
package ssl.debug;

import ssl.debug.adapter.BasicHttpsClient;

public class App {

    BasicHttpsClient basicHttpsClient = new BasicHttpsClient();

    public String getGreeting() throws Exception {
        return basicHttpsClient.get("https://httpbin.org/get");
    }

    public String getSslInfo() throws Exception {
        return basicHttpsClient.getSSLInfo("https://httpbin.org/get");
    }

    public static void main(String[] args) throws Exception {
        System.setProperty("javax.net.debug", "all");
        App app = new App();
        System.out.println(app.getGreeting());
        System.out.println(app.getSslInfo());
    }
}

BasicHttpsClient.java - HTTPS Client Library

package ssl.debug.adapter;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

/**
 * A simple HTTPS client for making secure HTTP requests.
 * Useful for demonstrating SSL/TLS handshake and debugging.
 */
public class BasicHttpsClient {

    private String userAgent = "Mozilla/5.0 (Java SSL Debug)";
    private int connectionTimeout = 10000; // 10 seconds
    private int readTimeout = 10000; // 10 seconds

    /**
     * Performs a GET request to the specified URL
     * 
     * @param urlString The HTTPS URL to connect to
     * @return The response body as a string
     * @throws Exception if connection fails
     */
    public String get(String urlString) throws Exception {
        return get(urlString, null);
    }

    /**
     * Performs a GET request with custom headers
     * 
     * @param urlString The HTTPS URL to connect to
     * @param headers Custom headers to send (can be null)
     * @return The response body as a string
     * @throws Exception if connection fails
     */
    public String get(String urlString, Map<String, String> headers) throws Exception {
        HttpURLConnection connection = null;
        try {
            URL url = new URI(urlString).toURL();
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(connectionTimeout);
            connection.setReadTimeout(readTimeout);
            connection.setRequestProperty("User-Agent", userAgent);

            // Add custom headers if provided
            if (headers != null) {
                headers.forEach(connection::setRequestProperty);
            }

            int responseCode = connection.getResponseCode();
            String responseBody = readResponse(connection);

            System.out.println("Status Code: " + responseCode);
            System.out.println("Response Headers:");
            printHeaders(connection.getHeaderFields());

            return responseBody;

        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

    /**
     * Gets SSL/TLS information from the connection
     * 
     * @param urlString The HTTPS URL to connect to
     * @return SSL information as a formatted string
     * @throws Exception if connection fails
     */
    public String getSSLInfo(String urlString) throws Exception {
        HttpURLConnection connection = null;
        try {
            URL url = new URI(urlString).toURL();
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("HEAD");
            connection.setConnectTimeout(connectionTimeout);
            connection.setReadTimeout(readTimeout);

            // Trigger connection to perform SSL handshake
            connection.getResponseCode();

            StringBuilder info = new StringBuilder();
            info.append("=== SSL/TLS Information ===\n");
            info.append("URL: ").append(urlString).append("\n");
            info.append("Status Code: ").append(connection.getResponseCode()).append("\n");
            info.append("Content Type: ").append(connection.getContentType()).append("\n");

            // Get cipher suite if available (only on HTTPS)
            if (connection instanceof javax.net.ssl.HttpsURLConnection httpsConnection) {
                httpsConnection.connect();
                info.append("Cipher Suite: ").append(httpsConnection.getCipherSuite()).append("\n");
                var sslSession = httpsConnection.getSSLSession();
                if (sslSession.isPresent()) {
                    info.append("Protocol: ").append(sslSession.get().getProtocol()).append("\n");
                }
            }

            return info.toString();

        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

    /**
     * Reads the response body from the connection
     */
    private String readResponse(HttpURLConnection connection) throws Exception {
        StringBuilder response = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line).append("\n");
            }
        }
        return response.toString();
    }

    /**
     * Prints response headers
     */
    private void printHeaders(Map<String, List<String>> headers) {
        headers.forEach((key, values) -> {
            if (key != null) { // null key is the status line
                values.forEach(value -> System.out.println("  " + key + ": " + value));
            }
        });
    }

    /**
     * Sets the connection timeout
     */
    public void setConnectionTimeout(int timeout) {
        this.connectionTimeout = timeout;
    }

    /**
     * Sets the read timeout
     */
    public void setReadTimeout(int timeout) {
        this.readTimeout = timeout;
    }

    /**
     * Sets the User-Agent header
     */
    public void setUserAgent(String userAgent) {
        this.userAgent = userAgent;
    }
}

How to Run the Example

Prerequisites

  • Java 21+ (or JDK 17+ minimum)
  • Gradle 8.x

Steps

  1. Clone or download the project:

    cd ssl-debug
    
  2. Build the project:

    ./gradlew build
    
  3. Run with SSL debugging enabled:

    ./gradlew run
    
  4. Capture output to file:

    ./gradlew run > all.log 2>&1
    

The application will:

  • Enable javax.net.debug=all
  • Make a GET request to https://httpbin.org/get
  • Log all TLS handshake messages
  • Display SSL/TLS information (cipher suite, protocol version, etc.)

Key Insights from the Handshake

Certificate Chain Validation

The SSL/TLS debug output shows the server provided three certificates:

  1. End-Entity Certificate: httpbin.org (issued by Amazon RSA 2048 M CA)
  2. Intermediate CA: Amazon RSA 2048 M
  3. Root CA: Amazon Root CA (trusted in system trust store)

The JVM validates this chain by:

  • Loading 152 trusted CA certificates from cacerts
  • Verifying each certificate's signature against its issuer
  • Checking validity dates
  • Confirming key usage extensions

Cipher Suite Selection

The server chose TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 because it's:

  • Secure: Modern encryption standard
  • Forward-Secure: Uses ephemeral ECDH keys (PFS)
  • Authenticated: RSA signature verification
  • Authenticated Encryption: AES-GCM prevents tampering

Key Exchange Details

  • Algorithm: ECDHE (Elliptic Curve Diffie-Hellman Ephemeral)
  • Benefits:
    • Perfect Forward Secrecy (PFS) - past sessions are safe even if long-term keys are compromised
    • Faster than traditional DHE
    • Smaller key sizes with equivalent security

Common SSL/TLS Debugging Scenarios

Debug Level Options

// Full debug (as shown in this guide)
System.setProperty("javax.net.debug", "all");

// Specific areas only
System.setProperty("javax.net.debug", "ssl");           // SSL records
System.setProperty("javax.net.debug", "ssl:handshake"); // Handshake only
System.setProperty("javax.net.debug", "ssl:trustmanager"); // Certificate validation
System.setProperty("javax.net.debug", "ssl:record");    // Record layer details

Troubleshooting Common Issues

Issue: PKIX path building failed

  • Cause: Certificate not in trust store
  • Solution: Import certificate or use custom TrustManager

Issue: Unsupported or unrecognized SSL message

  • Cause: Connecting to non-HTTPS endpoint
  • Solution: Verify URL starts with https://

Issue: Excessive network latency

  • Cause: Certificate revocation checks (OCSP, CRL)
  • Solution: Disable revocation checks if acceptable for your use case

Conclusion

Understanding TLS/TLS handshakes is essential for:

  • Debugging: Identifying connection issues quickly
  • Security: Understanding what's being negotiated
  • Performance: Analyzing handshake overhead
  • Compliance: Verifying secure cipher suites are used

The javax.net.debug system property provides deep visibility into Java's SSL/TLS layer, making it invaluable for developers working with secure connections.

Remember: The SSL/TLS logs contain sensitive handshake information. Only enable debugging in development environments, never in production without careful security considerations.


Resources


Get the Code

The source code for this project is available on GitHub:

📚 github.com/mnafshin/ssl-debug

Feel free to fork, clone, and experiment with the examples to deepen your understanding of SSL/TLS handshakes!