Skip to content

Instantly share code, notes, and snippets.

@artem-smotrakov
Last active August 19, 2022 06:10
Show Gist options
  • Save artem-smotrakov/bd14e4bde4d7238f7e5ab12c697a86a3 to your computer and use it in GitHub Desktop.
Save artem-smotrakov/bd14e4bde4d7238f7e5ab12c697a86a3 to your computer and use it in GitHub Desktop.
An example of TLS 1.3 client and server on Java. See details in https://blog.gypsyengineer.com/en/security/an-example-of-tls-13-client-and-server-on-java.html
package com.gypsyengineer.tlsbunny.jsse;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
/*
* Don't forget to set the following system properties when you run the class:
*
* javax.net.ssl.keyStore
* javax.net.ssl.keyStorePassword
* javax.net.ssl.trustStore
* javax.net.ssl.trustStorePassword
*
* More details can be found in JSSE docs.
*
* For example:
*
* java -cp classes \
* -Djavax.net.ssl.keyStore=keystore \
* -Djavax.net.ssl.keyStorePassword=passphrase \
* -Djavax.net.ssl.trustStore=keystore \
* -Djavax.net.ssl.trustStorePassword=passphrase \
* com.gypsyengineer.tlsbunny.jsse.TLSv13Test
*
* For testing purposes, you can download the keystore file from
*
* https://github.com/openjdk/jdk/tree/master/test/jdk/javax/net/ssl/etc
*/
public class TLSv13Test {
private static final int delay = 1000; // in millis
private static final String[] protocols = new String[] {"TLSv1.3"};
private static final String[] cipher_suites = new String[] {"TLS_AES_128_GCM_SHA256"};
private static final String message =
"Like most of life's problems, this one can be solved with bending!";
public static void main(String[] args) throws Exception {
try (EchoServer server = EchoServer.create()) {
new Thread(server).start();
Thread.sleep(delay);
try (SSLSocket socket = createSocket("localhost", server.port())) {
InputStream is = new BufferedInputStream(socket.getInputStream());
OutputStream os = new BufferedOutputStream(socket.getOutputStream());
os.write(message.getBytes());
os.flush();
byte[] data = new byte[2048];
int len = is.read(data);
if (len <= 0) {
throw new IOException("no data received");
}
System.out.printf("client received %d bytes: %s%n",
len, new String(data, 0, len));
}
}
}
public static SSLSocket createSocket(String host, int port) throws IOException {
SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault()
.createSocket(host, port);
socket.setEnabledProtocols(protocols);
socket.setEnabledCipherSuites(cipher_suites);
return socket;
}
public static class EchoServer implements Runnable, AutoCloseable {
private static final int FREE_PORT = 0;
private final SSLServerSocket sslServerSocket;
private EchoServer(SSLServerSocket sslServerSocket) {
this.sslServerSocket = sslServerSocket;
}
public int port() {
return sslServerSocket.getLocalPort();
}
@Override
public void close() throws IOException {
if (sslServerSocket != null && !sslServerSocket.isClosed()) {
sslServerSocket.close();
}
}
@Override
public void run() {
System.out.printf("server started on port %d%n", port());
try (SSLSocket socket = (SSLSocket) sslServerSocket.accept()) {
System.out.println("accepted");
InputStream is = new BufferedInputStream(socket.getInputStream());
OutputStream os = new BufferedOutputStream(socket.getOutputStream());
byte[] data = new byte[2048];
int len = is.read(data);
if (len <= 0) {
throw new IOException("no data received");
}
System.out.printf("server received %d bytes: %s%n",
len, new String(data, 0, len));
os.write(data, 0, len);
os.flush();
} catch (Exception e) {
System.out.printf("exception: %s%n", e.getMessage());
}
System.out.println("server stopped");
}
public static EchoServer create() throws IOException {
return create(FREE_PORT);
}
public static EchoServer create(int port) throws IOException {
SSLServerSocket socket = (SSLServerSocket)
SSLServerSocketFactory.getDefault().createServerSocket(port);
socket.setEnabledProtocols(protocols);
socket.setEnabledCipherSuites(cipher_suites);
return new EchoServer(socket);
}
}
}
@vkolomeyko
Copy link

vkolomeyko commented Mar 29, 2019

Hello @artem-smotrakov - thanks for preparing this example!
There are not that many samples on the Web for Java 11/TLS 1.3.

I am trying to run this example without any modification on Oracle JDK 11.0.2 and getting this:

server started on port 63855
accepted
exception: No available authentication scheme
Exception in thread "main" javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
server stopped
	at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:128)
	at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:308)
	at java.base/sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:279)
	at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:181)
	at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:164)
	at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1152)
	at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1063)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:402)
	at java.base/sun.security.ssl.SSLSocketImpl.ensureNegotiated(SSLSocketImpl.java:716)
	at java.base/sun.security.ssl.SSLSocketImpl$AppOutputStream.write(SSLSocketImpl.java:970)
	at java.base/java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:81)
	at java.base/java.io.BufferedOutputStream.flush(BufferedOutputStream.java:142)
	at com.gypsyengineer.tlsbunny.jsse.TLSv13Test.main(TLSv13Test.java:26)

I will dig a bit further to understand the root cause, but wanted to just flag the issue I am experiencing.

@phaugat7
Copy link

@vkolomeyko did you get it fixed? I also got same error.

@artem-smotrakov
Copy link
Author

@vkolomeyko Sorry for the late reply, for some reason I missed your message.

I forgot to mention that the program needs a keystore and a truststore. Otherwise, the server can't find a certificate to authenticate itself, and as a result a SSLHandshakeException is thrown.

@phaugat7 To fix the problem, you need to set javax.net.ssl.keyStore, javax.net.ssl.keyStorePassword, javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword system properties. More details can be found in JSSE docs.

For example:

java -cp classes \
    -Djavax.net.ssl.keyStore=keystore \
    -Djavax.net.ssl.keyStorePassword=passphrase \
    -Djavax.net.ssl.trustStore=keystore \
    -Djavax.net.ssl.trustStorePassword=passphrase \
        com.gypsyengineer.tlsbunny.jsse.TLSv13Test

For testing purposes, you can download the keystore file from https://github.com/openjdk/jdk/tree/master/test/jdk/javax/net/ssl/etc

@vcheruk2
Copy link

vcheruk2 commented Jan 28, 2021

Hi @artem-smotrakov,

Thank you for the example. I am trying to send over 2^14 bytes (Updated the String message & byte[] data) but the program limits it. Is there a specific reason? Or can the code be modified to handle messages over 2^14 bytes?

Thanks.

@artem-smotrakov
Copy link
Author

Hi @vcheruk2

According to the spec, 2^14 is the max length of a TLSPlaintext record:

https://tools.ietf.org/html/rfc8446#section-5.1

If you need to send data that exceeds this limit, you need to split the data. Have you tried that? Do you get any exception?

I think that the default TLS implementation in Java (JSSE) should be able to split the data that exceeds the limit. If it doesn't, then it looks like a bug or a potential enhancement in JSSE.

@dejpec
Copy link

dejpec commented Sep 3, 2021

hi @artem-smotrakov

I tried your example and it worked well. But then I turned on -Djavax.net.debug=all.
Then I see that the TLS handshake for 1.3 fails and the client tries again with TLS 1.2 ...

So the example works but it does not use TLS 1.3. I tried with open jdk 11.0.12.7 and open jdk-14.0.1.7.

Any idea whats going wrong ? When I debug I see that java does not have a cipher and protocol combination which allows to run tls 1.3.

@artem-smotrakov
Copy link
Author

Hi @dejpec

Then I see that the TLS handshake for 1.3 fails and the client tries again with TLS 1.2

That sounds strange. The example code enables only TLS 1.3 via setEnabledProtocols() call. I assume you didn't update the protocols variable. Furthermore, the only enabled cipher suite TLS_AES_128_GCM_SHA256 is available only in TLS 1.3.

Could you please post somewhere the output with -Djavax.net.debug=all?

@dejpec
Copy link

dejpec commented Sep 6, 2021

Thanks artem-smotrakov for looking into. Here is the ouput file

@artem-smotrakov
Copy link
Author

Hi @dejpec

In the logs, I see that TLS 1.3 was negotiated:

ClientHello.java:838|Negotiated protocol version: TLSv1.3
ServerHello.java:987|Negotiated protocol version: TLSv1.3

Also, the handshake messages follow TLS 1.3 spec. For example, see EncryptedExtensions and NewSessionTicket messages that are available only in TLS 1.3.

Please note that ClientHello.client_version and ServerHello.server_version still contain "TLSv1.2". This is expected behavior. You can also notice messages that mention TLSv1.2:

SSLSocketInputRecord.java:213|READ: TLSv1.2 handshake, length = 122

I think it happens because TLSPlaintext.legacy_record_version has to be"TLSv1.2" in TLS 1.3. That's also expected behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment