Build with Naz : TLS (Transport Layer Security) in Rust with tokio, rustls, CFSSL

- Introduction
- TLS primer
- Rust and TLS primer
- YouTube videos for this article
- First, create the certificates by running gen-certs.fish
- Second, write and run the code
- Build with Naz video series on developerlife.com YouTube channel
Introduction #
This repo contains code for a simple server and client program written in Rust that
communicate over TLS using the tokio and rustls libraries.
- TLS is used to secure the communication between the server and client.
- It is an added layer on top of the TCP connection.
TLS primer #
TLS is a cryptographic protocol designed to provide secure communication over a computer network. It ensures:
- Confidentiality: Data is encrypted so that only the intended recipient can read it.
- Integrity: Data cannot be altered without detection.
- Authentication: The identities of the parties involved can be verified.
It consists of both symmetric and asymmetric encryption algorithms. Here’s a brief overview of both.
Symmetric Encryption
- Definition: Uses the same key for both encryption and decryption.
- Examples: AES (Advanced Encryption Standard), DES (Data Encryption Standard).
- Benefits:
- Faster than asymmetric encryption.
- Suitable for encrypting large amounts of data.
- Drawbacks:
- Key distribution can be a challenge; both parties must securely share the key. So sharing the key between both parties can either happen out of band, or using some other mechanism (like asymmetric encryption).
Asymmetric Encryption
- Definition: Uses a pair of keys (public and private) for encryption and decryption.
- Examples: RSA, ECC (Elliptic Curve Cryptography).
- Benefits:
- Solves the key distribution problem; the public key can be shared openly.
- Provides authentication through digital signatures.
- Drawbacks:
- Slower than symmetric encryption.
- Not suitable for encrypting large amounts of data directly.
TLS uses a combination of both symmetric and asymmetric encryption. It uses asymmetric encryption to establish a secure connection and symmetric encryption to encrypt the data transferred over the connection.
Additionally the following are required to make the communication secure between the client and server:
- The client needs to have the CA certificate in case you are using self-signed certificates.
- The server needs to have both the server certificate and the private key.
Here’s an overview of how TLS works:
- Handshake - The client and server perform a handshake to establish a secure
connection. During this process:
- The client and server agree on the TLS version and cipher suites to use.
- The server presents its digital certificate, which contains its public key.
- The client verifies the server’s certificate against trusted Certificate Authorities (CAs).
- The client generates a random session key, encrypts it with the server’s public key, and sends it to the server.
- Session Key - Once the server receives the encrypted session key, it decrypts it using its private key. Both parties now have the same session key, which is used for symmetric encryption of the data transmitted during the session.
- Data Transmission - All data sent between the client and server is encrypted using the session key, ensuring confidentiality and integrity.
Rust and TLS primer #
Now that we know more about TLS, how do we access it in Rust? Rust has 2 main implementations for TLS:
-
rustls: A modern, safe, and fast TLS library written in Rust. This does not have any dependencies on OpenSSL, or any C code, or any OS specific code. It is a pure Rust implementation. -
native-tls: A thin wrapper around the platform’s native TLS implementation. It uses OpenSSL on Unix-like systems and SChannel on Windows.
YouTube videos for this article #
If you like to learn via video, please watch the companion video on the developerlife.com YouTube channel where I live code the entire program from scratch. You can follow along there, step by step if you like, in addition to this article and repo.
The code in the video and this tutorial are all in this GitHub repo.
First, create the certificates by running gen-certs.fish #
All the scripts and certificate related files are in the certs folder:
- The main script is
gen-certs.fish. It generates the CA and server certificates. It also uses the script below to get the CFSSL binaries. - The
get-cfssl-binaries.fishscript downloads the CFSSL binaries if needed. If they are already downloaded, it does nothing.
Tools used by the scripts (CFSSL) #
- The CFSSL tool is used to generate the certificates.
- Learn more about the tool in this blog post.
- You can get the prebuilt binaries here.
- This video goes over the process of setting up TLS with CFSSL.
Configuration files deep dive #
There are 3 JSON files that are used to generate the certificates:
`ca-config.json`: The configuration for the CA.
- The main node is
signingwhich has theprofilesnode. You can have multiple profiles. In this case, I create a single profile namedserver, which is a name I just made up.- The node named
server, which is a made up name of a profile, is used to generate the server certificate. This is a name that I created, it is not a reserved keyword, it has no special meaning. It is used in thecfssl gencert ... -profile=server server-csr.jsoncommand and used to tie all the generated files together.- The
expirynode sets the expiration date for a certificate. I changed it 10 years or87600h. - The
usagesnode sets the key usage for the certificate. I set it tosigning,key encipherment,server auth, andclient auth.
- The
- Here’s an example:
{ "signing": { "default": { "expiry": "87600h" }, "profiles": { "server": { "expiry": "87600h", "usages": ["signing", "key encipherment", "server auth"] } } } }
- The node named
`server-csr.json`: The configuration for the server certificate. This is related to the
server profile above. The CA will sign the server certificate using the server
profile.
- The
CNnode is the Common Name for this certificate. I set it toserver. This has no special meaning. It is set to ensure that thecfssl gencert -ca ca.pem ...commands to generate the certificates work and can find the information related to theserver, which matches the profile name. - The
keynode sets the key size and type. I set it to2048bits andrsa. This is important. - The
hostsnode sets the DNS names and IP addresses for the certificate. This is really important. The client will use aServerNamein Rust code to connect to the server. That name must match whatever is in thehostsarray. You can just add another name there which can be parsed as a DNS name or an IP address. In my case, I havelocalhostandr3bl.com(which is just made up). However, in the Rust client code to connect to the server, I can create aServerNameusing either"localhost"or"r3bl.com". - Here’s an example:
{ "CN": "server", "hosts": ["localhost", "r3bl.com"], "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "US", "ST": "Texas", "L": "Austin" } ] }
`ca-csr.json`: The Certificate Signing Request (CSR) for the CA.
- The
CNnode is the Common Name for the CA. I set it toca. This has no special meaning. It is just to make sure that thecfssl gencert -initca ca-csr.jsoncommands to generate the certificates work and can find the information related to the CA. - The
keynode sets the key size and type. I set it to2048bits andrsa. This is important. - Here’s an example:
{ "CN": "ca", "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "US", "ST": "Texas", "L": "Austin" } ] }
Each of these files are modified from some default values to the desired values. They all started life using the following commands:
./cfssl print-defaults config > ca-config.json./cfssl print-defaults csr > ca-csr.json./cfssl print-defaults csr > server-csr.json
Run the scripts and generate the certificates #
Run the following commands to generate the certificates in the certs/generated folder:
cd certs
./gen-certs.fish
Running this script will generate the following files:
- Generate root certificate (CA) and sign it. The
castring in the filenames comes from thecfssl gencert ... | cfssljson -bare cacommand. If you change the stringcain the command, it will change the filenames that are produced.
| File | Description |
|---|---|
ca.csr |
Certificate signing request |
ca-key.pem |
Private key |
ca.pem |
Public key; used in the Rust client code |
- Generate server certificate (and private key) and sign it with the CA. The
serverstring in the filenames comes from thecfssl gencert ... | cfssljson -bare servercommand. If you change the stringserverin the command, it will change the filenames that are produced.
| File | Description |
|---|---|
server.csr |
Certificate signing request |
server-key.pem |
Private key; used in the Rust server code |
server.pem |
Public key; used in the Rust server code |
Examine the generated certificates #
- Look in the
certs/generated/folder to see the generated certificates. You can examine them using theopensslcommand:
openssl x509 -noout -text -in generated/ca.pem
Look for the following lines which confirm that this is a CA certificate, and some other
configuration properties provided in the ca-config.json file:
| Field | Description |
|---|---|
Issuer: C=US, ST=TX, L=Austin, CN=ca |
The CA’s own details, from ca-config.json |
Not After: ... |
Expiration date |
Public-Key: (2048 bit) |
Key size and type from ca-csr.json |
CA:TRUE |
This is a CA (root certificate) |
- Look in the
certs/generatedfolder to see the server certificates. You can examine them using theopensslcommand:
openssl x509 -noout -text -in generated/server.pem
Look for the following lines which confirm that this is a server certificate, and some
other configuration properties provided in the server-csr.json file:
| Field | Description |
|---|---|
Issuer: C=US, ST=Texas, L=Austin, CN=ca |
Issued by the CA above |
Subject: C=US, ST=Texas, L=Austin, CN=server |
The server’s own details |
Not After : ... |
Expiration date |
CA:FALSE |
Not a root certificate |
TLS Web Server Authentication |
Extended Key Usage for server authentication |
DNS:localhost, IP Address:127.0.0.1 |
This is from server-csr.json. The Rust client code uses this in ServerName to make a TLS connection |
- Finally verify the server certificate against the CA certificate:
openssl verify -CAfile generated/ca.pem generated/server.pem
If the certificate is valid, you will see the following output: generated/server.pem: OK
Second, write and run the code #
Once the certificates are generated, the next step is to write the server and client code. Here’s the mental model for doing this.
-
Client code
-
Certificate concerns:
- The client code will need to load the root certificate store, inside of which will
reside the CA (certificate authority) certificate chain, that we have generated (the
ca.pemfile). - The client will also need to know the server’s hostname, which is used to verify the
server’s certificate. This has to match the
hostsentry in theserver-csr.jsonconfig file. This entry has to be in the form of aServerNamein the Rust code, which is a DNS or IP address parsable format.
- The client code will need to load the root certificate store, inside of which will
reside the CA (certificate authority) certificate chain, that we have generated (the
-
Code concerns:
- The certificate and key files above is used to generate a
ClientConfigstruct, from therustlscrate. It is then used to create aTlsConnectorstruct. - The unsecure connection of type
TcpStreamwill be created as per usual usingTcpStream::connect(). However, this will then be wrapped in aTlsConnectorwhich will make it a secure connection. The reader and writer halves are split from thisTlsStreamstruct. And the reader and writer halves are used as per usual.
- The certificate and key files above is used to generate a
-
-
Server code
-
Certificate concerns:
- The server code will need to load the server’s certificate and private key, which we
have generated (the
server.pemandserver-key.pemfiles).- This server certificate is signed by the CA certificate. Since we are using
self-signed certificates, only the client will need to load the CA certificate to
verify the server certificate. And not the server.
- This is because the server is self-signed and doesn’t need to verify any incoming certificates.
- If we weren’t using self-signed certificates, the client would just have to load the root certificate store that’s available publicly (like Mozilla root certificates).
- The server will not need to load the root certificate store, inside of which will
reside the CA certificate chain, that we have generated (the
ca.pemfile).
- This server certificate is signed by the CA certificate. Since we are using
self-signed certificates, only the client will need to load the CA certificate to
verify the server certificate. And not the server.
- The server code will need to load the server’s certificate and private key, which we
have generated (the
-
Code concerns:
- The certificate and key files above are used to generate a
ServerConfigstruct, from therustlscrate. It is then used to create aTlsAcceptorstruct. - The server will create a
TcpListenerand accept incoming connections. Each connection will be wrapped in aTlsAcceptorwhich will make it a secure connection. The reader and writer halves are split from thisTlsStreamstruct. And the reader and writer halves are used as per usual.
- The certificate and key files above are used to generate a
-
Here’s some more information about mapping the Rust code to the TLS files:
For details on the actual, code, here are some files from the tls repo:
Here are the files for the TLS configuration and certificate generation:
Build with Naz video series on developerlife.com YouTube channel #
You can watch a video series on building this crate with Naz on the developerlife.com YouTube channel.
- YT channel
- Playlists
👀 Watch Rust 🦀 live coding videos on our YouTube Channel.
📦 Install our useful Rust command line apps usingcargo install r3bl-cmdr(they are from the r3bl-open-core project):
- 🐱
giti: run interactive git commands with confidence in your terminal- 🦜
edi: edit Markdown with style in your terminalgiti in action
edi in action