Skip to content

Transport Layer Security (TLS)

Introduction

Transport Layer Security (TLS) is a cryptographic protocol that provides end-to-end encryption, authentication, and data integrity for communication over computer networks. It is the backbone of secure internet communication, protecting everything from HTTPS web traffic to email, VoIP, and API calls. Understanding TLS is essential for any developer building distributed systems, microservices, or any application that transmits sensitive data.

Core Concepts

What TLS Provides

TLS delivers three fundamental security properties:

  • Confidentiality: Data is encrypted so that only the intended recipient can read it.
  • Authentication: The identity of the server (and optionally the client) is verified using digital certificates.
  • Integrity: Messages are protected against tampering using Message Authentication Codes (MACs).

TLS vs SSL

SSL (Secure Sockets Layer) was the predecessor to TLS. SSL 3.0 was deprecated in 2015 due to the POODLE vulnerability. TLS 1.0 and 1.1 have also been deprecated. Modern systems should use TLS 1.2 or TLS 1.3.

VersionStatusYearKey Feature
SSL 2.0Deprecated1995First widely adopted
SSL 3.0Deprecated1996POODLE vulnerability
TLS 1.0Deprecated1999Upgrade from SSL
TLS 1.1Deprecated2006IV protection
TLS 1.2Active2008AEAD ciphers, SHA-256
TLS 1.3Active (Preferred)2018Fewer round trips, no legacy ciphers

The TLS Handshake (TLS 1.2)

The TLS 1.2 handshake is a multi-step process that establishes a secure session between client and server. It requires two round trips before application data can flow.

The TLS 1.3 Handshake

TLS 1.3 dramatically simplifies and accelerates the handshake, reducing it to a single round trip. It also eliminates insecure legacy algorithms (RSA key exchange, CBC mode ciphers, SHA-1, etc.).

Cipher Suites

A cipher suite defines the combination of algorithms used during a TLS session. In TLS 1.2, a suite specifies four components:

Example cipher suite string: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

  • ECDHE: Elliptic Curve Diffie-Hellman Ephemeral key exchange
  • RSA: Server authentication via RSA certificate
  • AES_256_GCM: 256-bit AES in Galois/Counter Mode for encryption
  • SHA384: SHA-384 for HMAC

In TLS 1.3, only five cipher suites remain, all using AEAD encryption:

  • TLS_AES_256_GCM_SHA384
  • TLS_AES_128_GCM_SHA256
  • TLS_CHACHA20_POLY1305_SHA256
  • TLS_AES_128_CCM_SHA256
  • TLS_AES_128_CCM_8_SHA256

Certificate Chain of Trust

TLS authentication relies on a chain of X.509 certificates leading back to a trusted Certificate Authority (CA).

The client verifies each certificate in the chain by:

  1. Checking the digital signature against the issuer's public key
  2. Verifying the certificate has not expired
  3. Checking Certificate Revocation Lists (CRL) or OCSP
  4. Confirming the leaf certificate matches the requested domain (Subject Alternative Name)

Forward Secrecy

Forward secrecy (also called Perfect Forward Secrecy, PFS) ensures that if the server's long-term private key is compromised, past session keys cannot be recovered. This is achieved by using ephemeral Diffie-Hellman key exchange (DHE or ECDHE), where unique session keys are generated per connection and never stored.

Implementation: TLS in Java

Creating an HTTPS Server with SSLContext

Java's javax.net.ssl package provides comprehensive TLS support. Here is a complete example of configuring an SSL server socket:

java
import javax.net.ssl.*;
import java.io.*;
import java.security.KeyStore;

public class TlsServer {

    public static void main(String[] args) throws Exception {
        // Load the server keystore containing the private key and certificate
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream("server-keystore.p12")) {
            keyStore.load(fis, "changeit".toCharArray());
        }

        // Initialize KeyManagerFactory with the keystore
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm()
        );
        kmf.init(keyStore, "changeit".toCharArray());

        // Create and initialize SSLContext for TLS 1.3
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(kmf.getKeyManagers(), null, null);

        // Create SSL server socket
        SSLServerSocketFactory ssf = sslContext.getServerSocketFactory();
        try (SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(8443)) {

            // Enforce TLS 1.2 and 1.3 only
            serverSocket.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"});

            System.out.println("TLS Server listening on port 8443...");

            while (true) {
                try (SSLSocket clientSocket = (SSLSocket) serverSocket.accept()) {
                    handleClient(clientSocket);
                } catch (IOException e) {
                    System.err.println("Client connection error: " + e.getMessage());
                }
            }
        }
    }

    private static void handleClient(SSLSocket socket) throws IOException {
        // Log the negotiated TLS session details
        SSLSession session = socket.getSession();
        System.out.println("Protocol: " + session.getProtocol());
        System.out.println("Cipher Suite: " + session.getCipherSuite());
        System.out.println("Peer Host: " + session.getPeerHost());

        BufferedReader reader = new BufferedReader(
            new InputStreamReader(socket.getInputStream())
        );
        PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);

        String line = reader.readLine();
        System.out.println("Received: " + line);
        writer.println("HTTP/1.1 200 OK\r\n\r\nHello over TLS!");
    }
}

Creating an HTTPS Client with Custom Trust Store

java
import javax.net.ssl.*;
import java.io.*;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

public class TlsClient {

    public static void main(String[] args) throws Exception {
        // Option 1: Use custom trust store for self-signed or private CA certs
        SSLContext sslContext = createCustomSslContext("ca-certificate.pem");

        SSLSocketFactory sf = sslContext.getSocketFactory();
        try (SSLSocket socket = (SSLSocket) sf.createSocket("localhost", 8443)) {

            // Enforce strong protocols
            socket.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"});

            // Start handshake explicitly (optional — happens on first I/O)
            socket.startHandshake();

            // Verify the session
            SSLSession session = socket.getSession();
            System.out.println("Connected with: " + session.getProtocol());
            System.out.println("Cipher: " + session.getCipherSuite());

            // Send request
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            writer.println("GET / HTTP/1.1");

            // Read response
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream())
            );
            String response;
            while ((response = reader.readLine()) != null) {
                System.out.println(response);
            }

        } catch (SSLHandshakeException e) {
            System.err.println("TLS handshake failed: " + e.getMessage());
            System.err.println("Possible causes: certificate untrusted, expired, or hostname mismatch");
        }
    }

    private static SSLContext createCustomSslContext(String caCertPath) throws Exception {
        // Load the CA certificate
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate caCert;
        try (FileInputStream fis = new FileInputStream(caCertPath)) {
            caCert = (X509Certificate) cf.generateCertificate(fis);
        }

        // Create a trust store containing the CA cert
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(null, null);
        trustStore.setCertificateEntry("custom-ca", caCert);

        // Initialize TrustManagerFactory
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm()
        );
        tmf.init(trustStore);

        // Create SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(null, tmf.getTrustManagers(), null);
        return sslContext;
    }
}

Using Java HttpClient with TLS (Java 11+)

Modern Java provides a cleaner HTTP client API with built-in TLS support:

java
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class ModernTlsClient {

    public static void main(String[] args) throws Exception {
        // Create SSLContext (uses default trust store with system CAs)
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(null, null, null);

        // Configure SSL parameters
        SSLParameters sslParams = new SSLParameters();
        sslParams.setProtocols(new String[]{"TLSv1.3", "TLSv1.2"});
        // Enable hostname verification (default in HttpClient)
        sslParams.setEndpointIdentificationAlgorithm("HTTPS");

        HttpClient client = HttpClient.newBuilder()
            .sslContext(sslContext)
            .sslParameters(sslParams)
            .connectTimeout(Duration.ofSeconds(10))
            .version(HttpClient.Version.HTTP_2)
            .build();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.example.com/data"))
            .header("Accept", "application/json")
            .GET()
            .build();

        try {
            HttpResponse<String> response = client.send(
                request, HttpResponse.BodyHandlers.ofString()
            );

            System.out.println("Status: " + response.statusCode());
            System.out.println("Body: " + response.body());

            // Inspect the SSL session
            response.sslSession().ifPresent(session -> {
                System.out.println("TLS Protocol: " + session.getProtocol());
                System.out.println("Cipher Suite: " + session.getCipherSuite());
            });

        } catch (javax.net.ssl.SSLHandshakeException e) {
            System.err.println("Certificate verification failed: " + e.getMessage());
        } catch (java.net.ConnectException e) {
            System.err.println("Connection refused: " + e.getMessage());
        }
    }
}

Implementation: Mutual TLS (mTLS)

Mutual TLS extends standard TLS by requiring both the client and server to present certificates. This is widely used in service-to-service communication, zero-trust architectures, and API security.

java
import javax.net.ssl.*;
import java.io.*;
import java.security.KeyStore;

public class MutualTlsServer {

    public static void main(String[] args) throws Exception {
        // Server's own keystore (its certificate + private key)
        KeyStore serverKeyStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream("server-keystore.p12")) {
            serverKeyStore.load(fis, "server-pass".toCharArray());
        }

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm()
        );
        kmf.init(serverKeyStore, "server-pass".toCharArray());

        // Trust store containing the Client CA certificate
        KeyStore trustStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream("client-ca-truststore.p12")) {
            trustStore.load(fis, "trust-pass".toCharArray());
        }

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm()
        );
        tmf.init(trustStore);

        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

        SSLServerSocketFactory ssf = sslContext.getServerSocketFactory();
        try (SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(8443)) {

            // CRITICAL: require client certificate
            serverSocket.setNeedClientAuth(true);
            serverSocket.setEnabledProtocols(new String[]{"TLSv1.3"});

            System.out.println("mTLS Server listening on port 8443...");

            while (true) {
                try (SSLSocket client = (SSLSocket) serverSocket.accept()) {
                    SSLSession session = client.getSession();
                    System.out.println("Client DN: " +
                        session.getPeerCertificates()[0].toString());
                    System.out.println("Protocol: " + session.getProtocol());

                    PrintWriter out = new PrintWriter(client.getOutputStream(), true);
                    out.println("Mutual TLS authenticated successfully!");
                } catch (SSLHandshakeException e) {
                    System.err.println("Client certificate rejected: " + e.getMessage());
                }
            }
        }
    }
}

Implementation: Certificate Pinning

Certificate pinning prevents man-in-the-middle attacks by binding a known certificate or public key to a specific host, rather than trusting any certificate signed by a trusted CA.

java
import javax.net.ssl.*;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class CertificatePinningClient {

    // SHA-256 hash of the expected server certificate's public key
    private static final String EXPECTED_PIN =
        "sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=";

    public static void main(String[] args) throws Exception {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");

        TrustManager[] pinningTrustManagers = new TrustManager[]{
            new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType)
                        throws java.security.cert.CertificateException {
                    if (chain == null || chain.length == 0) {
                        throw new java.security.cert.CertificateException(
                            "No server certificates provided");
                    }

                    // Compute the SHA-256 pin of the leaf certificate's public key
                    try {
                        byte[] publicKeyBytes = chain[0].getPublicKey().getEncoded();
                        MessageDigest digest = MessageDigest.getInstance("SHA-256");
                        byte[] hash = digest.digest(publicKeyBytes);
                        String pin = "sha256/" + Base64.getEncoder().encodeToString(hash);

                        System.out.println("Server certificate pin: " + pin);

                        if (!EXPECTED_PIN.equals(pin)) {
                            throw new java.security.cert.CertificateException(
                                "Certificate pin mismatch! Expected: " + EXPECTED_PIN +
                                " but got: " + pin
                            );
                        }

                        System.out.println("Certificate pin verified successfully!");
                    } catch (java.security.NoSuchAlgorithmException e) {
                        throw new java.security.cert.CertificateException(
                            "Failed to compute pin", e);
                    }
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }
        };

        sslContext.init(null, pinningTrustManagers, null);

        HttpClient client = HttpClient.newBuilder()
            .sslContext(sslContext)
            .build();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.example.com/secure"))
            .GET()
            .build();

        HttpResponse<String> response = client.send(
            request, HttpResponse.BodyHandlers.ofString()
        );
        System.out.println("Response: " + response.body());
    }
}

TLS in AWS Architecture

AWS provides TLS termination and management at multiple layers:

TLS Attacks and Mitigations

Understanding common attacks is crucial for proper TLS configuration:

TLS Session Lifecycle

Best Practices

  1. Enforce TLS 1.2+ minimum: Disable all SSL versions and TLS 1.0/1.1 in server configurations; TLS 1.3 should be preferred wherever possible.

  2. Use forward-secret cipher suites: Always prefer ECDHE-based key exchange over static RSA key exchange to ensure past sessions remain secure even if keys are later compromised.

  3. Enable HSTS (HTTP Strict Transport Security): Set the Strict-Transport-Security header to prevent protocol downgrade attacks and cookie hijacking.

  4. Automate certificate management: Use services like AWS Certificate Manager (ACM) or Let's Encrypt with auto-renewal to prevent certificate expiration outages.

  5. Implement certificate pinning carefully: Pin to intermediate CA certificates rather than leaf certificates to allow rotation; maintain a backup pin to avoid bricking clients.

  6. Use OCSP Stapling: Reduce latency and improve privacy by having the server fetch and staple OCSP responses rather than requiring clients to contact the CA.

  7. Separate key stores and trust stores: Never mix private keys (KeyStore) with trusted certificates (TrustStore); follow the principle of least privilege.

  8. Monitor certificate transparency logs: Subscribe to CT log notifications for your domains to detect unauthorized certificate issuance.

  9. Rotate credentials regularly: Rotate TLS private keys and certificates on a schedule (e.g., every 90 days for Let's Encrypt, annually for enterprise CAs).

  10. Use mTLS for service-to-service communication: In microservice architectures and zero-trust networks, mutual TLS provides strong bidirectional identity verification without relying on network-level trust.

  11. Test your configuration: Regularly scan your endpoints with tools like SSL Labs (ssllabs.com) or testssl.sh to catch misconfigurations and weak cipher suites.

  • Cryptography: Foundational encryption, hashing, and key exchange algorithms that underpin TLS.
  • OAuth: Token-based authentication that rides on top of TLS-secured channels.
  • REST HTTP Verbs and Status Codes: HTTPS APIs that depend on TLS for transport security.
  • AWS SSO: Identity federation that relies on TLS-protected SAML/OIDC flows.