TLS (formerly known as SSL) applies an encryption layer to TCP communications. This consists of two parts
There are many key exchange algorithms which can be used; there are many secret key algorithms which can be used. See for example Cipher Suites: Ciphers, Algorithms and Negotiating Security Settings for more details.
Be warned: this part of the chapter covers eye-glazing territory. You may wish to skip to the summary section and come back to the rest only when feeling like getting your head messed around.
The most commonly used public/private key system used for TLS is RSA.
(An alternative is ECDSA - see
Comparing ECDSA vs RSA: Everything You Need to Know
).
There are many ways of generating a public/private key pair, some of which
are preferred by the programming language environment (see Java later).
The most commonly used method for RSA keys on Posix systems is
openssl
.
I will use it in this section as a language-independent way of managing TLS
certificates.
It is also available on Windows.
(Other Windows alternatives are given by
Generating self-signed certificates on Windows
).
To generate a 2048-bit RSA key pair and store it in the
file key.pem
,
execute
openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
The command genpkey
replaces the older genrsa
command:
openssl genrsa -out key.pem 2048
.
Amongst other things, it uses the more flexible PKCS #8 format instead of
PKCS #1.
See
PKCS#1 and PKCS#8 format for RSA private key [closed]
for more details.
You keep this file to yourself - giving it to anyone else defeats the purpose of a private key.
The key file is in PEM
format.
The PKCS #8 format is as ASCII text and can be
viewed.
The key file will look like
-----BEGIN PRIVATE KEY-----
<Base 64 encoded data>
-----END PRIVATE KEY-----
and the certificate file will look like
-----BEGIN CERTIFICATE-----
<Base 64 encoded data>
-----END CERTIFICATE-----
The public key can be extracted by
openssl rsa -in key.pem -pubout
and the private key can be viewed in a textual representation by
openssl rsa -in key.pem -text -noout
A more recent file format is PKCS #12, which is a binary format and cannot be 'cut and pasted' like PEM files can.
We all know that CPUs are getting faster, and that GPUs and FPGAs can perform some calculations even faster than most CPUs. This makes it progressively easier to crack any keys. So if you want to keep a key for a long time, you must ensure its key length is long enough.
Mozilla reports that a 2048-bit RSA private key is considered secure for the next 4 years, a 3072-bit RSA key for the next 8 years, while a 4096-bit RSA key will only be okay for the next 15 years.
An X.509 (more accurately, PKIX) certificate associates extra information with an RSA key pair. The primary piece is an identity/subject - usually of a person or a host name.
X.509 certificates are an old format, and some of the elements of that
format are in the process of being replaced. The most significant of these
is the 'Common Name' (CN). For a server, this is usually the DNS hostname, but
the CN is now judged to have an ambiguous meaning.
It should be replaced (or at least augmented) by a 'subjectAltName' (SAN), which has typed
entries such as subjectAltName=DNS:example.com
.
Typically, a certificate will have both.
There are three ways of creating X.509 certificates with subject information
using openssl
:
-subj
parameters on the command line
Browser vendors have a large say in what certificates they will accept - if they don't
like a site's certificate they will make it hard for you to get to that site.
The current version of X.509 is version 3, PKIX, specified by the IETF
RFC 5280
.
Mozilla (creators of Firefox
) spell out in
A Web PKI x509 certificate primer
their expectations of X.509 certificates according to the IETF
specifications and give examples
of creating them using openssl
.
In this chapter I follow those guidelines as some of the programming languages
(particularly Rust) also do so.
The command
openssl req -new -x509 -sha256 -key key.pem -out certificate.crt -days 3650
will create a new X.509 certificate certificate.crt
using interactive
prompting, valid for 10 years.
It will prompt for many tags, the most important being the
CN (Common Name).
The interaction to generate keys for the host name desktop.home.arpa
is
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:VIC
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Home
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:desktop.home.arpa
Email Address []:jan@newmarch.name
The contents of the certificate file can be seen by
openssl x509 -in certificate.crt -text -noout
and are
Certificate:
Data:
...
Issuer: C = AU, ST = VIC, O = Home, CN = desktop.home.arpa, emailAddress = jan@newmarch.name
Validity
Not Before: Jun 21 05:50:58 2020 GMT
Not After : Jun 19 05:50:58 2030 GMT
Subject: C = AU, ST = VIC, O = Home, CN = desktop.home.arpa, emailAddress = jan@newmarch.name
...
X509v3 extensions:
...
The certificate file is also in PEM
format.
It is an ASCII text file and can be
viewed directly (but not informatively).
A key part of PKIX certificates is that they must be able to demonstrate their authenticity. That is, they must be signed by someone and the certificate can be linked to that signature. The certificate generated above is only self-signed. No-one has verified that it is a genuine certificate (apart from me of course, and that is no guarantee :-).
Certificates should be signed by a Certificate Authority (CA) to be trusted.
A recipient of a certificate may or may not trust the CA. Lists of CAs trusted by browsers are pointed to by List of certificate authorities in browsers and mobile platforms [duplicate] although these change all the time.
Wikipedia includes a list of 14 CAs at Certificate authority
Some languages recognise all CAs that the Operating System recognises.
The Go file root_linux.go
tells which files contain the
information for Linux systems:
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6
"/etc/ssl/ca-bundle.pem", // OpenSUSE
"/etc/pki/tls/cacert.pem", // OpenELEC
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
For Debian, see List all available ssl ca certificates The answer from Stéphane Chazelas is to run
awk -v cmd='openssl x509 -noout -subject' '
/BEGIN/{close(cmd)};{print | cmd}' < /etc/ssl/certs/ca-certificates.crt
This gives
subject=CN = ACCVRAIZ1, OU = PKIACCV, O = ACCV, C = ES
subject=C = IT, L = Milan, O = Actalis S.p.A./03358520967, CN = Actalis Authenti
cation Root CA
subject=C = US, O = AffirmTrust, CN = AffirmTrust Commercial
subject=C = US, O = AffirmTrust, CN = AffirmTrust Networking
subject=C = US, O = AffirmTrust, CN = AffirmTrust Premium
subject=C = US, O = AffirmTrust, CN = AffirmTrust Premium ECC
subject=CN = Atos TrustedRoot 2011, O = Atos, C = DE
subject=C = ES, CN = Autoridad de Certificacion Firmaprofesional CIF A62634068
subject=C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
subject=C = NO, O = Buypass AS-983163327, CN = Buypass Class 2 Root CA
subject=C = NO, O = Buypass AS-983163327, CN = Buypass Class 3 Root CA
subject=C = SK, L = Bratislava, O = Disig a.s., CN = CA Disig Root R2
subject=C = CN, O = China Financial Certification Authority, CN = CFCA EV ROOT
subject=C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN
= COMODO Certification Authority
...
which is probably more information than you need!
Looking at the CN field, you can see CAs
Actalis, AffirmTrust, Atos, ... .
Digging up information for other systems doesn't seem quite as easy.
To get a certificate signed by a recognised CA a certificate signing request (not a certificate) must be submitted to them, a validation process must be done by them, maybe some money changes hands (I use ZeroSSL to avoid that), and if successful a certificate is returned.
To prepare for a CA to sign a certificate, a Certificate Signing Request must be
created. This typically ends in .csr
instead of a signed
certificate .crt
. The changes to the earlier certificate
creation request of earlier are minor: drop the -x509
and -days
options and change the filename extension, as in
openssl req -new -sha256 -key key.pem -out certificate.csr -subj '/CN=localhost'
The command to view such a file changes slightly, using the command req
instead of x509
:
openssl req -in certificate.csr -text -noout
One of the features of PKIX is that it considers the possibility that a certificate might somehow become invalid. Events such as the certificate life expiring can be caught from the information in the certificate. But sometimes other events might cause one to become invalid - for example, the private key has been leaked or the indentity of an entity has been found to be false.
In such cases, the certificate needs to be revoked
.
Formerly, this was done using a CRL (Certificate Revocation List).
Now the preferred mechanism (and also required by the Mozilla guidelines)
is to use OCSP (Online Certificate Status Protocol).
This will usually run as a server, with address specified in the
certificate.
The certificate will need to contain entries like
Authority Information Access:
OCSP - URI:http://localhost:8888
and will be generated by openssl
by configuration entries
(see later) such as
authorityInfoAccess = OCSP;URI:http://localhost:8888
We won't need to actually create an OCSP server (it is done by
openssl ocsp ...
) and have it verify certificates.
If you do need to, see
Online Certificate Status Protocol
by Jamie Nguyen.
The .crt
certificate initially created above is self-signed.
Such a certificate will not be trusted by anyone (but me)
but has uses in testing and in setting up local
environments.
It does however require some tweaking to conform to the Mozilla guidelines. These guidelines require the use of X.509 v3 extensions, above the standard X.509 fields. The certificate generated above contains the extension fields
X509v3 extensions:
X509v3 Subject Key Identifier:
...
X509v3 Authority Key Identifier:
...
X509v3 Basic Constraints: critical
CA:TRUE
What is primarily missing is the subjectAltName
. There are other
fields that may be useful, such as extendedKeyUsage
which can be set to values such as serverAuth
to show the
role expected for this certificate. Also, the generated self-signed certificate
assumes that it is for a CA, and this may or may not be true.
There are several routes to creating a satisfactory self-signed certificate.
Really, they are all about playing games with openssl
to get it to add
the X509v3 extensions in the way you want.
-addext
as in
openssl req -new -x509 -sha256 -key key.pem -out certificate.crt
-days 3650 -subj '/CN=localhost'
-addext 'subjectAltName=DNS:localhost'
The only drawback to this method is that it cannot turn off
the CA:TRUE
extension in the certificate -
it should be FALSE
for a leaf certificate
openssl
as described by man 5 x509v3_config
.
Create a file e.g. self.cfg
of
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = localhost
[v3_req]
basicConstraints = CA:FALSE
subjectAltName = DNS:localhost
extendedKeyUsage = serverAuth
authorityInfoAccess = OCSP;URI:http://localhost:8888
Then create the certificate just picking out the v3_req
section:
openssl req -new -x509 -sha256 -key key.pem -out certificate.crt
-days 3650 -subj '/CN=localhost' -config self.cnf
-extensions v3_req
basicConstraints = CA:FALSE
subjectAltName = DNS:localhost
extendedKeyUsage = serverAuth
authorityInfoAccess = OCSP;URI:http://localhost:8888
Then the commands are
# create the CSR file
openssl req -new -key key.pem -days 1096 -extensions v3_ca -batch \
-out certificate.csr -utf8 -subj '/CN=localhost'
# and sign it
openssl x509 -req -sha256 -days 3650 -in certificate.csr \
-signkey key.pem -extfile self.cnf \
-out certificate.pem
These all result in a certificate file with an extensions section such as
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Alternative Name:
DNS:localhost
X509v3 Extended Key Usage:
TLS Web Server Authentication
Authority Information Access:
OCSP - URI:http://localhost:8888
Particularly during development, you may not want to go through the pain of getting a certificate signed by a recognised CA. In that case, you can make your own Certificate Authority. There are three issues involved:
The first is done by first generating a key and
then a certificate. The CN is set to localhost
, but could
be any host name
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/CN=localhost" -days 5000 -out ca.crt
Instead of generating a finished server certificate directly, now we generate a Certificate Signing Request (CSR) file for localhost by
openssl req -new -key server.key -subj "/CN=localhost" -out server.csr
This can then be signed by our new CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 1000
The final step, of getting this CA recognised by each client depends on the client's environment. This may be done by adding to the O/S CA list, which we won't discuss, or programmatically, which we will discuss for each language.
In summary, there are four ways of getting certificates representing
localhost. Each of them will use the same private.key
generated by
openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
self-signed-cert.pem
self-signed.cnf
with contents
basicConstraints = CA:FALSE
subjectAltName = DNS:localhost
extendedKeyUsage = serverAuth
authorityInfoAccess = OCSP;URI:http://localhost:8888
Then create a CSR by
openssl req -new -key key.pem -extensions v3_ca -batch \
-out self-signed-cert.csr -utf8 -subj '/CN=localhost'
Sign this by
openssl x509 -req -sha256 -days 3650 -in self-signed-cert.csr \
-signkey key.pem -extfile self-signed.cnf \
-out self-signed-cert.pem
CA-cert.pem
CA.cnf
with contents
basicConstraints = critical, CA:TRUE
keyUsage = keyCertSign, cRLSign
subjectAltName = DNS:localhost
extendedKeyUsage = serverAuth
authorityInfoAccess = OCSP;URI:http://localhost:8888
Then create a CSR by
openssl req -new -key key.pem -extensions v3_ca -batch \
-out CA.csr -utf8 -subj '/O=NewmarchCA'
Sign this by
openssl x509 -req -sha256 -days 3650 -in CA.csr \
-signkey key.pem -extfile CA.cnf \
-out CA-cert.pem
CA-signed-cert.pem
leaf.cnf
with contents
basicConstraints = critical, CA:FALSE
subjectAltName = DNS:localhost
extendedKeyUsage = serverAuth
authorityInfoAccess = OCSP;URI:http://localhost:8888
openssl req -new -key key.pem -extensions v3_ca \
-batch -out localhost.csr -utf8 -subj '/CN=localhost'
Sign the certificate
openssl x509 -req -sha256 -days 1096 -in localhost.csr \
-CAkey key.pem -CA CA-cert.pem -out CA-signed-cert.pem \
-extfile leaf.cnf -CAcreateserial
The certificates created above
(self-signed-cert.pem
,
CA-cert.pem
and CA-signed-cert.pem
)
will be used in the programs following.
Their relative path will be ../certs/
.
Every TLS client will have a list of root CAs that it trusts. If a certificate is signed by a root CA, then it is trusted too. The root CAs are long-lived. So they typically create a number of shorter-lived non-root CAs and sign them. These non-root CAs can then sign further certificates, possibly forming a chain of signed certificates, with top of the chain being one of the root CAs. To trust any particular certificate, you need to trace from the certificate itself back through the chain until you get to the trusted root CA.
Typically, when you ask a CA to sign a certificate, it will return the certificate signed by an intermediary CA, and also a chain of intermediate certificates back to the root CA. For any client to be able to validate a certificate, it must have both the leaf certificate and also the intermediate certificate chain.
How each system manages these varies. For example, the Apache
TLS server configuration file
(such as sites-enabled/default-ssl.conf
) has an entry for each:
SSLCertificateFile ...
SSLCertificateChainFile ...
To see a certificate chain, run e.g.
openssl s_client -showcerts -connect <host>:443
For example, on my site jan.newmarch.name
,
the chain consists of (at present)
USERTrust RSA Certification Authority
ZeroSSL RSA Domain Secure Site CA
jan.newmarch.name
where the first is the root CA and the second is the
CA I got the certificate signed by.
Nearly all web sites supporting HTTPS have valid certificates. The site Bad SSL has a range of pages with bad certificates such as expired, self-signed, revoked, etc. Good for debugging!
The certificates used in this section created by openssl
were discussed in the section
TL: General
.
They are
Java has long had its own formats for certificates known as Java Key Stores (JKS). Since Java 9 this has using PKCS #12. The Java terminology is
If you are working in Java only and using keytool
alone, the steps are
keytool -genkeypair -keystore mykeystore -alias "..." -keypass "..." -keyalg RSA -keysize 2048
For example,
keytool -genkeypair -keystore mykeystore -alias "localhost" -keypass "abcdefg" -keyalg RSA -keysize 2048
keytool -export -alias "localhost" -keystore mykeystore -file mycertfile.cer
keytool -import -alias "localhost" -keystore mytruststore -file mycertfile.cer
-Djavax.net.debug=ssl
We have created a set of certificates using openssl
already,
which are used by all the languages we discuss.
So we need to convert them into the format required by Java.
To create a Java keystore from PEM certificates, follow the process in
Import private key and certificate into java keystore
, you need to first convert the PEM files to PKCS #12
using openssl
and then import into a keystore using keytool
.
A server wanting to use a keystore with our own CA will need the keystore built from the certificate signed by the CA:
openssl pkcs12 -export -in ../certs/CA-signed-cert.pem \
-inkey ../certs/key.pem -CAfile ../certs/CA-cert.pem \
-name "localhost" -out CA-signed-cert.p12
keytool -importkeystore -deststorepass abcdefg \
-destkeystore CA-signed-cert-keystore.jks \
-srckeystore CA-signed-cert.p12 -srcstoretype PKCS12
Give the keystore a password such as "abcdefg" and trust the resultant certificate.
To build a truststore, we just need the certificate, not the private key. The process is described at ADD A CERTIFICATE TO A TRUSTSTORE USING KEYTOOL .
A client trusting our CA needs a truststore made from the CA's certificate. This becomes here
keytool -import -alias localhost -file ../certs/CA-cert.pem \
-storetype JKS -keystore CA.truststore.jks
HTTP servers offering HTTPS are giving HTTP over TLS. Most will have valid TLS certificates, properly signed by a recognised CA. The client just needs to validate the certificate and can then talk.
Validation is done by the Java SSLSocket
,
without any extra work by the client. The client, talking to an
HTTPS server, will use the HTTP protocol. The simplest
messages is to send an HTTP Head
request,
which returns information about the server.
(HHTP will be discussed in more detail later.)
A simple client talking to
www.verisign.com
is
TLSGetHead.java:
/**
* From https://docs.oracle.com/javase/10/security/sample-code-illustrating-secure-socket-connection-client-and-server.htm
* Copyright © 1993, 2018, Oracle and/or its affiliates. All rights reserved.
*/
import java.net.*;
import java.io.*;
import javax.net.ssl.*;
/*
* This example demostrates how to use a SSLSocket as client to
* send a HTTP request and get response from an HTTPS server.
* It assumes that the client is not behind a firewall
*/
public class TLSGetHead {
public static void main(String[] args) throws Exception {
try {
SSLSocketFactory factory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket socket =
(SSLSocket)factory.createSocket("www.verisign.com", 443);
/*
* send http request
*
* Before any application data is sent or received, the
* SSL socket will do SSL handshaking first to set up
* the security attributes.
*
* SSL handshaking can be initiated by either flushing data
* down the pipe, or by starting the handshaking by hand.
*
* Handshaking is started manually in this example because
* PrintWriter catches all IOExceptions (including
* SSLExceptions), sets an internal error flag, and then
* returns without rethrowing the exception.
*
* Unfortunately, this means any error messages are lost,
* which caused lots of confusion for others using this
* code. The only way to tell there was an error is to call
* PrintWriter.checkError().
*/
socket.startHandshake();
PrintWriter out = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())));
out.print("HEAD / HTTP/1.0\r\n\r\n");
out.flush();
/*
* Make sure there were no surprises
*/
if (out.checkError())
System.out.println(
"SSLSocketClient: java.io.PrintWriter error");
/* read response */
BufferedReader in = new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
System.out.println(inputLine);
in.close();
out.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
When compiled and run by
javac TLSGetHead.java
java TLSGetHead
it produces something like
HTTP/1.1 200 OK
Date: Mon, 15 Jun 2020 10:59:59 GMT
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Transfer-Encoding: chunked
Set-Cookie: locale-name=en_US; Expires=Sun, 13-Sep-2020 10:59:59 GMT; Path=/
Set-Cookie: locale-name=en_US; Expires=Sun, 13-Sep-2020 10:59:59 GMT; Path=/
Set-Cookie: JSESSIONID=BF08615501E1518E02727B6B1DABD54E.brn2; Path=/; Secure; HttpOnly
Strict-Transport-Security: max-age=15768000;
Connection: close
The client uses an SSLSocketFactory
to create
an SSLSocket
.
From then on, the code is essentially the same as the earlier
TCP clients.
The code is TLSEchoClient.java:
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
public class TLSEchoClient{
public static final int MYECHOPORT = 1200;
public static void main(String[] args){
if (args.length != 1) {
System.err.println("Usage: Client address");
System.exit(1);
}
InetAddress address = null;
try {
address = InetAddress.getByName(args[0]);
} catch(UnknownHostException e) {
e.printStackTrace();
System.exit(2);
}
Socket sock = null;
try {
sock = new Socket(address, MYECHOPORT);
} catch(IOException e) {
e.printStackTrace();
System.exit(3);
}
SSLSocketFactory factory =
(SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslSocket = null;
try {
sslSocket =
(SSLSocket) factory.createSocket(sock, args[0], MYECHOPORT, true);
} catch(IOException e) {
e.printStackTrace();
System.exit(3);
}
BufferedReader reader = null;
PrintStream out = null;
try {
reader = new BufferedReader(new InputStreamReader(
sslSocket.getInputStream()));
out = new PrintStream(sslSocket.getOutputStream());
} catch(IOException e) {
e.printStackTrace();
System.exit(6);
}
BufferedReader consoleReader =
new BufferedReader(new InputStreamReader(System.in));
String line = null;
while (true) {
line = null;
try {
System.out.print("Enter line:");
line = consoleReader.readLine();
System.out.println("Read '" + line + "'");
} catch(IOException e) {
e.printStackTrace();
System.exit(6);
}
if (line.equals("BYE"))
break;
try {
// Just send a goodbye message, for testing
out.println(line);
line = reader.readLine();
} catch(IOException e) {
e.printStackTrace();
System.exit(6);
}
System.out.println(line);
}
System.exit(0);
}
} // Client
The difference comes at runtime, where the client has to be told where to pick up the trust keys. Run the client using the truststore
java -Djavax.net.ssl.trustStore=CA.truststore \
-Djavax.net.ssl.trustStorePassword="abcdefg" \
TLSEchoClient localhost
The server similarly needs to create an
SSLSocket
, and then continues like the other TCP servers.
The code is TLSEchoServer.java:
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
public class TLSEchoServer {
public static int MYECHOPORT = 1200;
public static void main(String argv[]) {
try {
SSLServerSocketFactory factory =
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
SSLServerSocket sslSocket =
(SSLServerSocket) factory.createServerSocket(MYECHOPORT);
while (true) {
Socket incoming = sslSocket.accept();
new SocketHandler(incoming).start();
}
} catch(IOException e) {
e.printStackTrace();
System.exit(30);
}
}
}
class SocketHandler extends Thread {
Socket incoming;
SocketHandler(Socket incoming) {
this.incoming = incoming;
}
public void run() {
byte[] data = new byte[1024];
InputStream in;
OutputStream out;
try {
in = incoming.getInputStream();
out = incoming.getOutputStream();
} catch(IOException e) {
System.err.println(e.toString());
return;
}
int numRead = 0;
try {
while (true) {
try {
numRead = in.read(data);
} catch(IOException e) {
System.err.println(e.toString());
return;
}
if (numRead == -1) {
System.out.println("Disconnecting");
return;
}
try {
out.write(data, 0, numRead);
} catch(IOException e) {
System.err.println(e.toString());
return;
}
}
} finally {
System.out.println("Closing all");
try {
in.close();
out.close();
incoming.close();
} catch(IOException e) {
// nothing to do really
}
}
}
}
The server needs to pick up a certificate from the keystore. Run the server using the keystore
java -Djavax.net.ssl.keyStore=CA-signed-cert-keystore.jks \
-Djavax.net.ssl.keyStorePassword="abcdefg" \
TLSEchoServer
For more complex cases see Sample Code Illustrating a Secure Socket Connection Between a Client and a Server
The certificates used in this section were discussed in the section TL: General . They are
A Go client talking to an HTTPS server with a valid certificate is TLSGetHead.go:
/* TLSGetHead
*/
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"os"
)
func main() {
service := "www.verisign.com:443"
conn, err := tls.Dial("tcp", service, nil)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
conn.Close()
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
When run by
go run TLSGetHead.go
it produces essentially the same results as the Java client above.
If the server uses its own CA, then it needs to load its CA signed certificate and set this into its TLS configuration. It can then listen for client connections.
The server is TLSEchoServer.go:
/* TLSEchoServer
*/
package main
import (
"crypto/rand"
"crypto/tls"
"fmt"
"net"
"os"
"time"
)
func main() {
cert, err := tls.LoadX509KeyPair("../certs/CA-signed-cert.pem", "../certs/key.pem")
//cert, err := tls.LoadX509KeyPair("../certs/self-signed-cert.pem", "../certs/key.pem")
checkError(err)
config := tls.Config{Certificates: []tls.Certificate{cert}}
now := time.Now()
config.Time = func() time.Time { return now }
config.Rand = rand.Reader
service := "0.0.0.0:1200"
listener, err := tls.Listen("tcp", service, &config)
checkError(err)
fmt.Println("Listening")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err.Error())
continue
}
fmt.Println("Accepted")
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
var buf [512]byte
for {
fmt.Println("Trying to read")
n, err := conn.Read(buf[0:])
if err != nil {
fmt.Println(err)
return
}
_, err = conn.Write(buf[0:n])
if err != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
The client that talks to this server needs to have the private CA certificate. It reads this and then appends it to a new CertificatePool. If successful, it can then read and write to a server using certificates signed by that CA.
The client is TLSEchoClient.go:
/* TLSEchoClient
*/
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
certPemFile, err := os.Open("../certs/CA-cert.pem")
//certPemFile, err := os.Open("../certs/self-signed-cert.pem")
checkError(err)
pemBytes := make([]byte, 10000) // bigger than the file
_, err = certPemFile.Read(pemBytes)
checkError(err)
certPemFile.Close()
// Create a new certificate pool
certPool := x509.NewCertPool()
// and add our certificate
ok := certPool.AppendCertsFromPEM(pemBytes)
if !ok {
fmt.Println("PEM read failed")
} else {
fmt.Println("PEM read ok")
}
// Dial, using a config with root cert set to ours
conn, err := tls.Dial("tcp", service, &tls.Config{RootCAs: certPool})
checkError(err)
// Now write and read lots
for n := 0; n < 10; n++ {
fmt.Println("Writing...")
conn.Write([]byte("Hello " + string(n+48)))
var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
}
conn.Close()
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
The client and server in the last section had lines to load the appropriate certificates from the private CA. Following these were commented lines to load the self-signed certificates instead. If these are uncommented and the others commented out, they both talk to each other using the self-signed certificates. No other changes are needed.
If you get a certificate request signed by a CA, you will probably get two certificates
If you want to build your own server, you need to concatenate these two files
into a single one for the call to
tls.LoadX509KeyPair()
.
This can be done by e.g.
cat <signed-cert> <intermediate-cert> &tg; combined-cert
That is all that is needed, and go
will do the rest.
(In my case, the signed certificate did not have a newline terminator
so I had to add one using an editor - an advantage of PEM format!).
The certificates used in this section were discussed in the section TL: General . They are
The most popular crate for dealing with TLS seems to be
openssl
.
Using it will require the following to be added to the dependency section
of Cargo.toml
:
openssl = { version = "0.10", features = ["vendored"] }
The openssl crate has a simple example of connecting to a TLS server and getting some information. Slightly adapted to the other examples in this chapter, it is TLSGetHead.rs:
// Based on https://docs.rs/openssl/0.10.29/openssl/ssl/index.html
use openssl::ssl::{SslMethod, SslConnector};
use std::io::{Read, Write};
use std::net::TcpStream;
fn main() {
let connector = SslConnector::builder(SslMethod::tls()).unwrap().build();
let stream = TcpStream::connect("www.verisign.com:443").unwrap();
let mut stream = connector.connect("www.verisign.com", stream).unwrap();
stream.write_all(b"HEAD / HTTP/1.0\r\n\r\n").unwrap();
let mut res = vec![];
stream.read_to_end(&mut res).unwrap();
println!("{}", String::from_utf8_lossy(&res));
}
A client requires a bit more work of splitting the builder out from
creating a connector. The builder then has the function
set_ca_file()
called before building
the connector.
The echo client program talking to a server using
my own CA with certificate file
CA-cert.pem
needs to load the CA-signed certificate
as its CA file. The program is
/TLSEchoClient.rs:
use std::io::{self, Read, Write, BufRead};
use openssl::ssl::{SslMethod, SslConnector};
use std::net::TcpStream;
use std::env;
use std::str;
use std::io::stdout;
fn main() -> std::io::Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage {} hostname", args[0]);
std::process::exit(1);
}
let hostname = &args[1];
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
// turn off all checking - dangerous
//builder.set_verify(openssl::ssl::SslVerifyMode::NONE);
// Working with my own CA
builder.set_ca_file("../../certs/CA-cert.pem")
.expect("Can't find file");
// this works with self-signed cert
//builder.set_ca_file("../../certs/self-signed-cert.pem")
// .expect("Can't find file");
let connector = builder.build();
let stream = TcpStream::connect(hostname.to_string() + &":1200")
.expect("Couldn't connect to the server...");
let mut stream = connector.connect(hostname, stream).unwrap();
print!("Enter line:"); stdout().flush().expect("Flush failed");
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line.unwrap();
println!("Read line '{}'", line);
if &line == "BYE" {
break;
}
stream.write(line.as_bytes())
.expect("Error on write");
let mut echo_bytes: [u8; 2048] = [0; 2048];
stream.read(&mut echo_bytes[..])
.expect("Error on read");
let echo = str::from_utf8(&echo_bytes).unwrap();
println!("Echoed '{}'", echo);
print!("Enter line:"); stdout().flush().expect("Flush failed");
}
Ok(())
} // the stream is closed here
The previous echo client program had lines commented out for a self-signed certificate. Uncomment this and comment out the CA-signed certificate and the client works with a self-signed server.
The server needs to set up an SSL Acceptor with the private key and the certificate file set to the CA signed certificate. It then sets up a TCP listener on the port (here 1200) and then wraps that in the acceptor. From then on it can read and write to the stream.
The server is TLSEchoServer.rs:
// From https://gist.github.com/fortruce/828bcc3499eb291e7e17
//use openssl::ssl::{SslMethod, SslConnector};
use openssl::ssl::{SslMethod, SslAcceptor, SslStream, SslFiletype};
//use std::net::TcpStream;
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::io::Read;
use std::io::Write;
use std::sync::Arc;
fn handle_client(mut stream: SslStream<TcpStream>) {
println!("Connected");
// read 2048 bytes at a time from stream echoing back to stream
loop {
let mut read = [0; 2048];
match stream.read(&mut read) {
Ok(n) => {
if n == 0 {
// connection was closed
break;
}
stream.write(&read[0..n]).unwrap();
}
Err(_err) => {
//panic!(err);
break;
}
}
}
println!("Disconnected");
}
fn main() {
let mut acceptor = SslAcceptor::mozilla_modern(SslMethod::tls()).unwrap();
//let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
acceptor.set_private_key_file("../../certs/key.pem", SslFiletype::PEM).unwrap();
acceptor.set_certificate_file("../../certs/CA-signed-cert.pem", SslFiletype::PEM).unwrap();
//acceptor.set_certificate_chain_file("certs.pem").unwrap();
acceptor.check_private_key().unwrap();
let acceptor = Arc::new(acceptor.build());
let listener = TcpListener::bind("[::]:1200").unwrap();
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let acceptor = acceptor.clone();
thread::spawn(move || {
let stream = acceptor.accept(stream).unwrap();
handle_client(stream);
});
}
Err(_) => {
println!("Error");
}
}
}
}
Openssl has an explicit function call to make on the builder to include an intermediate certificate file:
builder.set_certificate_chain_file("intermediate-cert.pem")
.expect("Can't find file");
Another crate gaining popularity is
rustls
.
It is purely Rust, not relying on the openssl
C library.
Using it will require the following to be added to the dependency section
of Cargo.toml
:
rustls = "0.18"
webpki = "0.21.0"
webpki-roots = "0.20"
custom_error = "1.7.1"
It has two sets of examples. The first at rustls/rustls/examples/ uses standard I/O. The second set at rustls/rustls-mio/examples/ uses the asynchronous mio crate.
The Cargo.toml
file needs the following dependencies:
rustls = "0.18"
webpki = "0.21.0"
webpki-roots = "0.20"
A client using this crate does not have a default set of trusted certificates. The "standard" way of getting such a set is to use the crate wbpki-roots which contains the set of root CA's recognised by Mozilla.
A client using the simpler way to get HEAD information from a server with a Mozilla recognised CA is RustlsGetHead.rs:
use std::sync::Arc;
//use std::sync::{Arc, Mutex};
//use std::process;
//use mio;
use std::net::TcpStream;
use webpki;
use rustls;
//use rustls::Session;
//use std::net::SocketAddr;
//use std::str;
//use std::io;
//use std::fs;
//use std::collections;
//use std::io::{Read, Write, BufReader};
use std::io::{Read, Write};
use std::io::stdout;
fn main() {
let hostname = "www.verisign.com";
let hostnameport = "www.verisign.com:443";
let dns_name = webpki::DNSNameRef::try_from_ascii_str(hostname).unwrap();
let mut sock = TcpStream::connect(hostnameport).unwrap();
let mut config = rustls::ClientConfig::new();
config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
let mut tls_session = rustls::ClientSession::new(&Arc::new(config),
dns_name);
let mut tls = rustls::Stream::new(&mut tls_session, &mut sock);
//let mut tlsclient = TlsClient::new(sock, "www.verisign.com:443", config);
let http_req = "HEAD / HTTP/1.0\r\n\r\n";
tls.write(http_req.as_bytes()).expect("Write fail");
/*
while (!tls_session.wants_write()) {
println!("Waiting to wtite");
}
let num_wrote = tls_session.write_tls(&mut sock).expect("Cant write TLS");
println!("{}", num_wrote.to_string());
*/
/*
while (!tls_session.wants_read()) {
println!("Waiting to read");
}
println!("Ready to read");
tls_session.read_tls(&mut sock).expect("Cant read TLS");
println!("Read tls");
tls_session.process_new_packets().expect("Cant split packets");
let mut buf = Vec::new();
//(&mut io::stdin()
tls_session.read_to_end(&mut buf).unwrap();
println!("{}", String::from_utf8_lossy(&buf));
*/
let mut plaintext = Vec::new();
tls.read_to_end(&mut plaintext).unwrap();
stdout().write_all(&plaintext).unwrap();
}
The client needs to set up a configuration with the CA certificate loaded into the root store. It then creates a session using that configuration and wraps the TCP stream in a Rustls stream. Reads and writes are then done using the stream.
The client is RustLSEchoClient.rs:
// Based on https://github.com/ctz/rustls/blob/main/rustls/examples/simpleclient.rs
use std::sync::Arc;
use std::net::TcpStream;
use std::io::{self, Read, Write, BufRead, stdout, BufReader};
use std::fs::File;
use std::env;
use std::str;
fn main() -> std::io::Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage {} hostname", args[0]);
std::process::exit(1);
}
let hostname = &args[1];
let mut config = rustls::ClientConfig::new();
//add in our root certificate
let mut cert = File::open("../../certs/CA-cert.pem").expect("Cant open file");
let mut cert = BufReader::new(&mut cert);
config.root_store.add_pem_file(&mut cert).expect("Cant add to root store");
let dns_name = webpki::DNSNameRef::try_from_ascii_str(hostname).unwrap();
let mut sess = rustls::ClientSession::new(&Arc::new(config), dns_name);
let mut sock = TcpStream::connect(hostname.to_string() + &":1200").unwrap();
let mut stream = rustls::Stream::new(&mut sess, &mut sock);
print!("Enter line:"); stdout().flush().expect("print err");
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line.unwrap();
println!("Read line: '{}'", line);
if &line == "BYE" {
break;
}
stream.write(line.as_bytes())
.expect("Error on write");
let mut echo_bytes: [u8; 2048] = [0; 2048];
stream.read(&mut echo_bytes[..])
.expect("Error on read");
let echo = str::from_utf8(&echo_bytes).unwrap();
println!("Echoed: '{}'", echo);
print!("Enter line:"); stdout().flush().expect("pritn err");
}
Ok(())
}
Copyright © Jan Newmarch, jan@newmarch.name
" Network Programming using Java, Go, Python, Rust, JavaScript and Julia"
by
Jan Newmarch
is licensed under a
Creative Commons Attribution-ShareAlike 4.0 International License
.
Based on a work at
https://jan.newmarch.name/NetworkProgramming/
.