TLS, or transport layer security, is a protocol used across the globe to encrypt and secure communication over the internet.
In this article, we’ll discuss what TLS is, what benefits it provides, and why you need it. Then we’ll walk through implementing TLS in Java.
What is TLS?
TLS and its predecessor SSL (secure socket layer) are the most commonly used cryptographic protocols for providing encryption, authenticity, and integrity, which enables end-to-end security of data sent between applications over the internet.
The TLS and SSL protocols are located between the application layer and the TCP/IP layer, where they can secure and send application data to the transport layer. Because of their positioning, TLS and SSL can support multiple application layer protocols. Check out this article to learn more about OSI model.
Originally developed by Netscape, SSL 1.0 had a number of security flaws and was never released publicly, but in 1995, SSL 2.0 was introduced, followed by SSL 3.0 in 1996. SSL was then renamed, and TLS 1.0 was released in 1999. The current version, TLS 1.3, was introduced in 2018. To ensure you are protected from known vulnerabilities, it is always recommended to use the latest TLS version. You can learn more about TLS in this article.
The importance of TLS
Historically, internet communication has happened in plain text with minimal security. This means that the data you sent to a server was visible over the wire, leaving it open to man-in-the-middle attacks — which is when a malicious actor inserts themself into a dialogue between a user and an application. This allows them to see and manipulate the data being sent, potentially exposing personal information, such as passwords or banking details.
To solve this type of problem, SSL and TLS were introduced. They provide three primary benefits:
- Integrity Protection: TLS protects against modification of messages by an active wiretapper.
- Authentication: In most modes, TLS provides peer authentication using signed certificates. Servers are usually authenticated, and clients may be authenticated as requested by servers. This authentication happens as part of the TLS handshake.
- Confidentiality: TLS encrypts data being sent between client and server, protecting the confidentiality and privacy of data. This ensures that passive wiretappers won’t see sensitive data shared between the machines.
When SSL or TLS have been implemented, users can be assured that their data is not exposed, and the data they send or receive is not tampered with. This makes TLS implementation a necessity for internet-facing applications, as it enables you to secure your customers’ data.
Core Concepts
Before going into details about how to implement TLS in Java, let’s brush up on a few concepts that will help you understand the low-level details of TLS.
Sockets
A socket is one end of a two-way communication interface between networked programs. The underlying transportation layer (TCP or UDP) identifies the destination application, to which the data will be transmitted, by binding a socket to a port number.
Socket classes are used to represent the connection between client and server applications. The java.net
package provides two classes — Socket
and ServerSocket
— that implement the client side and server side of the connection, respectively.
If you’d like to learn more about sockets, check out this article.
TLS communication happens over secure sockets, which are SSLSocket
and SSLServerSocket
. These classes extend the Socket
and ServerSocket
classes, respectively.
TLS Handshake
An SSL or TLS handshake is a series of actions carried out by the client and server that authenticates them to each other, and establishes the secret keys they’ll use to interact.
The details of a TLS handshake is a very broad topic that extends beyond the scope of this article. If you need a good starting point for understanding TLS handshakes, this article on what happens during a TLS handshake, and this overview of the SSL or TLS handshakes are great places to begin.
Java LTS
Java LTS (long-term support) releases are versions that will receive quarterly security, stability, and performance updates for an extended period of time. Standard (non-LTS) versions only get updates and fixes until the next version is released.
Java 7, 8, 11, and the recently released Java 17 are LTS versions. Because of their extended support windows, it’ss recommended that production environments use LTS versions exclusively.
As with any software, each version has a new set of features and enhancements — you can find the features released in Java 11 here and Java 17 here. Because features are different in each version of Java, please check the documentation for your version if you run into any unexpected issues during TLS implementation.
Implementing TLS in Java
After all the theory, we’ve finally reached the interesting part — implementation. In the subsequent sections of the article, you will learn about the JSSE framework, and how to use JSSE APIs to implement TLS.
JSSE API
Java Secure Socket Extension, or JSSE, is a framework that abstracts the underlying implementation of transport layer security (TLS). By abstracting the complexity of the underlying protocols, JSSE allows programmers to easily implement secure encrypted communications. JSSE uses the TLS protocol to provide secure, encrypted communications between your clients and servers.
There are four main classes provided by JSSE as standard implementation:
- SSLSocketFactory : This class acts as a factory for creating secure sockets.
-
SSLServerSocketFactory : This class is analogous to the
SSLSocketFactory
class, but is used specifically for creating server sockets. -
SSLSocket :
SSLSocket
is an extension ofSocket
that adds a layer of security protections over the underlying network transport protocol, such as TCP and UDP, and provides the benefits of SSL and TLS. -
SSLServerSocket : This class is similar to the
SSLSocket
class. The main difference is thatSSLServerSocket
is used to create sockets at server side, whileSSLSocket
is used to create sockets at client side.
You’ll use these classes to implement client and server-side TLS.
Implementing TLS at the Client Side
In this section, you will work through implementing client-side TLS using SSLSocketFactory
and SSLSocket
.
Create a text file with the name JavaTLSClientExample.class
, then copy-paste the following code into it and save the file:
package io.snyk.programming.tls;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
public class JavaTLSClientExample {
private static final String[] protocols = new String[]{"TLSv1.3"};
private static final String[] cipher_suites = new String[]{"TLS_AES_128_GCM_SHA256"};
public static void main(String[] args) throws Exception {
SSLSocket socket = null;
PrintWriter out = null;
BufferedReader in = null;
try {
// Step : 1
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
// Step : 2
socket = (SSLSocket) factory.createSocket("google.com", 443);
// Step : 3
socket.setEnabledProtocols(protocols);
socket.setEnabledCipherSuites(cipher_suites);
// Step : 4 {optional}
socket.startHandshake();
// Step : 5
out = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())));
out.println("GET / HTTP/1.0");
out.println();
out.flush();
if (out.checkError()) {
System.out.println("SSLSocketClient: java.io.PrintWriter error");
}
// Step : 6
in = new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println(inputLine);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (socket != null) {
socket.close();
}
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
}
}
}
To run the code, open your terminal and use the cd
command to move to the folder where you saved the above file — cd <PathToFile>
.
Compile the file by running javac JavaTLSClientExample.class
, Then, execute the file with the command java JavaTLSClientExample.class
.
Here’s a breakdown of what happens in each step of the example code.
The first step is getting an SSLSocketFactory
, which can then be used to create the socket.
While there are a number of ways of obtaining an SSLSocketFactory
, in this example, you’re getting the default factory by calling the SSLSocketFactory.getDefault()
static method.
Step two is creating an SSL socket. You call the createSocket
method and provide information such as the remote host and remote port. This will create a socket and connect it to the specified remote host at the specified remote port.
Step three is setting up parameters. First, you’re telling the socket what TLS version you want to support. At the time of this writing, TLS v1.3 is the latest TLS version, so that’s the version used in the example above. Additionally, CipherSuite TLS_AES_128_GCM_SHA256
is used to spell out the different sets of parameters to be used at each step of the TLS handshake.
In step four, you initiate a handshake. This is where the TLS handshake is explicitly initiated.
During the handshake, the socket will make use of the protocol and CipherSuite that was set in the previous step.
In step five, the setup is done, and you make an actual request to the server. In this step, textual information is being passed to the socket’s output stream. OutputStreamWriter
converts the character stream to byte stream. BufferedWriter
is used to avoid frequent converter invocations, and PrintWriter
is used to print the formatted representation of objects to the text-output stream. As PrintWriter
does not throw any exceptions, you need to explicitly check for exceptions using the checkError
method.
Finally, in step six, you read the response. After the request is made, the response is read from the inputStream
of the socket, then printed.
Implementing TLS at Server Side
Now you’ll take a look at implementing TLS at the server side using SSLServerSocketFactory
and SSLServerSocket
.
Create a file called JavaTLSServerExample.class
, and paste in the following code:
package io.snyk.programming.tls;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import java.io.*;
public class JavaTLSServerExample {
private static final String[] protocols = new String[]{"TLSv1.3"};
private static final String[] cipher_suites = new String[]{"TLS_AES_128_GCM_SHA256"};
public static void main(String[] args) throws Exception {
SSLServerSocket serverSocket = null;
try {
// Step : 1
SSLServerSocketFactory factory =
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
// Step : 2
serverSocket = (SSLServerSocket) factory.createServerSocket(8980);
// Step : 3
serverSocket.setEnabledProtocols(protocols);
serverSocket.setEnabledCipherSuites(cipher_suites);
// Step : 4
SSLSocket sslSocket = (SSLSocket) serverSocket.accept();
// Step : 5
InputStream inputStream = sslSocket.getInputStream();
InputStreamReader inputStreamReader = new
InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String request = null;
while((request = bufferedReader.readLine()) != null) {
System.out.println(request);
System.out.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
}
}
To run the code, you’ll once again open a terminal in the folder where the file above was saved.
Compile the file by running javac JavaTLSServerExample.class
.
Follow this guide to set up and add a CA certificate to your keystore.
Run java -Djavax.net.ssl.keyStore=<KeystoreFilePath> -Djavax.net.ssl.keyStorePassword=<KeystorePassword> JavaTLSServerExample
to execute the file. Djavax.net.ssl.keyStore
and -Djavax.net.ssl.keyStorePassword
are required to provide the Java runtime with the SSL certificate for the server.
Let’s walk through each step of the code one more time. Steps one to three are similar to steps one to three in the client side TLS example.
In step one, you get a default SSLSocketFactory
.
In step two, you create an SSLServerSocket
and bind it to port 8990.
In step three, you define the protocols and CipherSuites that are supported by your SSLServerSocket
.
In step four, you accept the incoming request. SSLServerSocket.accept()
listens for a connection to be made to this server socket, accepts the connection, and creates a new socket. The method blocks until a connection is made.
In step five, the data is read from the socket’s input stream and printed. InputStreamReader
is used to convert byte stream to character stream, and BufferedReader
is used to avoid frequent converter invocations.
Relationship between JSSE API and SSLSocketFactory
As mentioned above, JSSE is a framework. JSSE provides various classes and implementations that abstract out the complexity of securing the connection between the client and server. JSSE also provides a set of implementation classes that can be used as the default implementations for setting up secure sockets for client and server.
For the client side, SSLSocketFactory
is used to get a default factory, which then is used to get a client side SSLSocket
. At the server side, SSLServerSocketFactory
is used to initiate the SSLServerSocket
.
Conclusion
If you have created a Java application that communicates over the internet, it’s necessary to implement TLS at both client and server side. TLS ensures that the data is safely transferred by providing encryption and integrity, and also helps in authentication of both parties.
JSSE is a framework provided by Java which can help you in implementing TLS for your own application. You can use its classes, such as SSLSocket
and SSLServerSocket
, to create secure sockets.