Debugging Java SSL/TLS Connections: A Guide to javax.net.debug
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
| Step | Message Type | Direction | Timestamp | Duration |
| 1 | ClientHello | Client → Server | 22:20:06.819 | - |
| 2 | ServerHello | Server → Client | 22:20:06.924 | +105ms |
| 3 | Certificate | Server → Client | 22:20:06.930 | +6ms |
| 4 | ServerKeyExchange | Server → Client | 22:20:06.950 | +20ms |
| 5 | ServerHelloDone | Server → Client | 22:20:06.951 | +1ms |
| 6 | ClientKeyExchange | Client → Server | 22:20:06.951 | - |
| 7 | ChangeCipherSpec | Client → Server | 22:20:06.952 | +1ms |
| 8 | Finished | Client → Server | 22:20:06.954 | +2ms |
| 9 | NewSessionTicket | Server → Client | 22:20:07.056 | +102ms |
| 10 | ChangeCipherSpec | Server → Client | 22:20:07.056 | - |
| 11 | Finished | Server → Client | 22:20:07.056 | - |
| Total Duration | 22: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:
- ClientHello → Client offers supported ciphers, TLS versions, and extensions
- ServerHello → Server selects cipher, version, and confirms capabilities
- Certificate → Server proves identity with certificate chain
- ServerKeyExchange → Server sends ECDH parameters for key agreement
- ServerHelloDone → Marks end of server's initial messages
Phase 2: Key Derivation
Both sides generate the shared secret:
- 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
- Both sides compute:
Phase 3: Cipher Activation & Verification
Both sides verify the handshake and activate encryption:
- ChangeCipherSpec (Client) → "Start encrypting from now on"
- Finished (Client) → Encrypted MAC of all handshake messages
- NewSessionTicket (Server) → Optional ticket for future session resumption
- ChangeCipherSpec (Server) → "Start encrypting from now on"
- 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
Clone or download the project:
cd ssl-debugBuild the project:
./gradlew buildRun with SSL debugging enabled:
./gradlew runCapture 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:
- End-Entity Certificate: httpbin.org (issued by Amazon RSA 2048 M CA)
- Intermediate CA: Amazon RSA 2048 M
- 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
- Java JSSE Documentation
- RFC 5246 - TLS 1.2
- OWASP Transport Layer Protection
- httpbin.org - HTTP Request/Response Service
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!