How to Implement TLS ft. Golang
When you visit a website you might have noticed that some of them have a padlock icon next to the URL in a web browser’s address bar and the URL begins with https://
instead of http://
. This is an indicator that the website that you are trying to access using that URL is using a secure connection with Transport Layer Security (TLS) encryption.
TLS is an encryption and authentication protocol designed to establish a secure communication channel between applications, typically a client and a server. It ensures that the data exchanged between applications are encrypted and cannot be intercepted by an attacker.
In this blog post, I will be demonstrating how to implement a simple TLS connection between a client and a server using Golang.
Prerequisites
- GoLang: Go is a programming language developed by Google that is designed for building efficient and scalable applications. You will need to have Go installed on your system in order to implement TLS with GoLang.
- OpenSSL: OpenSSL is an open-source implementation of the Secure Sockets Layer (SSL) and TLS cryptographic protocols. It is used to generate a self-signed CA and a certificate for our server.
- Text editor: You will need a text editor to write code with GoLang. There are many text editors to choose from, such as Visual Studio Code, Sublime Text, Atom, or Vim.
- Knowledge of GoLang: You should have a basic understanding of GoLang syntax and concepts. This includes knowledge of variables, functions, packages, and data types.
- Knowledge of HTTP: You should have a basic understanding of the HTTP protocol, which is used to communicate between the client and the server.
- Knowledge of TLS: You should have a basic understanding of the TLS protocol. This includes knowledge of certificates, private keys, and the handshake process.
Implementation
In a production environment, obtaining a certificate from a trusted Certificate Authority (CA) is a necessary step to enable HTTPS. However, for local development purposes, generating a self-signed certificate is a quick and easy way to set up a TLS connection.
When implementing a TLS connection using a self-signed certificate for the server using Golang, there is a good chance of running into certain errors. The reason for this is when using packages like crypto/tls
, it validates the server’s certificate chain and hostname by default. This is to ensure that the server is trusted and not a potential security threat. A work around for this is to set InsecureSkipVerify
to true
in our TLS configuration which will disable the above verification.
Even though the above solution is acceptable for a dev or test environment, I will be signing the server certificate using a self-signed CA in this implementation which allows for the validation of the server’s certificate chain and hostname, without the need to skip the verification process.
Step 1: Generate a self-signed CA
To generate a self-signed CA, we can use the OpenSSL command-line tool. The process of generating a self-signed CA or a self signed certificate involves three steps.
- Generate a private key for the CA or cert.
- Create a Certificate Signing Request (CSR) using the private key
- Generate the self-signed CA or cert using the CSR and the private key.
These three steps can be achieved using three separate openssl commands which I will demonstrate in Step 2 when generating our server certificate. Alternatively, we can combine the above three steps into a single command that generates a self-signed certificate which we can use to generate our self signed CA.
openssl req -new -newkey rsa:2048 -keyout ca.key -x509 -sha256 -days 365 -out ca.crt
This command generates a self-signed X.509 certificate along with a 2048-bit RSA private key and CSR using the private key, all in one step. The resulting private key will be encrypted with a passphrase and will be saved to a file named ca.key
, while the self-signed certificate is saved to a file named ca.crt
which will be valid for 365 days. The certificate is signed using the SHA-256 hash algorithm, which provides strong security for the certificate.
When you run the above command it will prompt you to enter a passphrase for the private key. You will also be prompted to enter certificate data where you can use the default values or values of your own. Alternatively, you can add -nodes
flag to the above command if you do not want the private key to be encrypted with a passphrase.
Step 2: Create a configuration file for the server certificate
Before generating our server certificate, we will require a configuration file in order to specify the Common Name (CN) and Subject Alternative Name (SAN) for our server certificate as localhost
. This is to ensure that the certificate will be accepted by our client when accessing our local development server running on https://localhost
.
The CN field in an SSL certificate is used to specify the primary domain name associated with the certificate. However, this field is considered legacy and has been replaced by the SAN field, which allows for multiple domain names to be specified.
Therefore modern browsers and other clients now require the use of the SAN field to properly validate SSL certificates. If our server certificate uses the CN field instead of the SAN field, it can lead to errors in our go code since we are not setting InsecureSkipVerify
to true
.
In our case the configuration file will look something simple as shown below and we will be saving this into a file named server.cnf
. The keyUsage
and extendedKeyUsage
settings under [v3_ext]
section are not mandatory for this implementation.
[req]
default_md = sha256
prompt = no
req_extensions = v3_ext
distinguished_name = req_distinguished_name
[req_distinguished_name]
CN = localhost
[v3_ext]
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = critical,serverAuth,clientAuth
subjectAltName = DNS:localhost
Step 3: Generate server certificate using the self-signed CA
- First, we need to generate a private key for the server. We can use the following command to generate a 2048-bit RSA key and save it to a file named
server.key
openssl genrsa -out server.key 2048
- Next, we need to create a CSR using the private key. We can use the following command to create a CSR and save it to a file named
server.csr
. We are using-config
flag to specify the path to our configuration file which we created earlier.
openssl req -new -key server.key -out server.csr -config server.cnf
- We can use the below command to view the content of our CSR and verify that we have x509v3 section with the SAN we entered in
server.cnf
file.
openssl req -noout -text -in server.csr
- Now that we have a CSR, we can use it to generate our certificate for the server signed with our self-signed CA. For that we can use the below command which will save our server certificate into a file named
server.crt
. We are using-extfile
flag to specify the path to our configuration file followed by-extensions
flag to specify the section name from the configuration file containing the SAN extension settings.
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 -sha256 -extfile server.cnf -extensions v3_ext
When you run the above command if you encrypted your CA private key with a passphrase in Step 1, you will be prompted to enter that passphrase. Once the command execution is complete you will also see a ca.srl
file, which is used to keep track of the next available serial number for the CA.
Now that we have all the certificates we need, we can proceed to implement our client and server using Golang.
Step 4: Implement the server using Golang
First of all we need to import the required packages.
crypto/tls
package provides support for secure communication over the network using TLS or SSL.log
package provides a simple logging interface.net/http
package provides HTTP client and server implementations.
import (
"crypto/tls"
"log"
"net/http"
)
Next we are going to define some constants for the server. These constants define the server port number and the response message that the server sends to clients.
const (
port = ":8443"
responseBody = "Hello, TLS!"
)
Next we will be loading our server certificate and server private key from the server.crt
and server.key
files. If there is an error while loading the X509 key pair, the program exits with a fatal log message.
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
log.Fatalf("Failed to load X509 key pair: %v", err)
}
It is important to note here that if your server private key is encrypted with a passphrase you will need to decrypt it first before trying to load it using this code or else you may run into some errors.
Next we create our TLS configuration by creating a new tls.Config
struct and setting the Certificates
field to the X509 key pair loaded previously.
config := &tls.Config{
Certificates: []tls.Certificate{cert},
}
Next we are creating a new router using http.NewServeMux()
and register a single handler function handleRequest()
to handle all incoming requests.
router := http.NewServeMux()
router.HandleFunc("/", handleRequest)
Next we create a new http.Server
struct and set the Addr
field to the server port, the Handler
field to the router created previously, and the TLSConfig
field to the TLS configuration created earlier.
server := &http.Server{
Addr: port,
Handler: router,
TLSConfig: config,
}
Next we can start the HTTP server using server.ListenAndServeTLS()
. The log.Printf()
function is used to log a message indicating that the server is listening on the specified port. If there is an error starting the server, the program exits with a fatal log message.
log.Printf("Listening on %s...", port)
err = server.ListenAndServeTLS("", "")
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
Finally, we define the handleRequest()
function we registered earlier, which handles incoming requests by writing an HTTP status code of 200 and the response message defined earlier to the response writer.
func handleRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(responseBody))
}
Your final code should look like below.
package main
import (
"crypto/tls"
"log"
"net/http"
)
const (
port = ":8443"
responseBody = "Hello, TLS!"
)
func main() {
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
log.Fatalf("Failed to load X509 key pair: %v", err)
}
config := &tls.Config{
Certificates: []tls.Certificate{cert},
}
router := http.NewServeMux()
router.HandleFunc("/", handleRequest)
server := &http.Server{
Addr: port,
Handler: router,
TLSConfig: config,
}
log.Printf("Listening on %s...", port)
err = server.ListenAndServeTLS("", "")
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(responseBody))
}
Step 5: Implement a client using Golang
Same as before lets import the necessary packages first. In addition to the packages used for the server there are two additional packages we need.
crypto/x509
package parses X.509-encoded keys and certificates.io/ioutil
package implements some I/O utility functions.
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net/http"
)
Same as before we will define a constant url
that specifies the URL to make the HTTPS request to.
const (
url = "https://localhost:8443"
)
Next we can load the contents of our self-signed CA certificate using the ioutil.ReadFile()
function. If the file cannot be read, the program exits with a fatal error message.
cert, err := ioutil.ReadFile("ca.crt")
if err != nil {
log.Fatalf("Failed to read certificate file: %v", err)
}
Next we create an empty certificate pool using x509.NewCertPool()
and append the self-signed CA certificate to the pool using caCertPool.AppendCertsFromPEM(cert)
.
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(cert)
Next we create a TLS configuration object using &tls.Config{RootCAs: caCertPool}
. This configuration uses the self-signed CA certificate as the root CA to verify the server's TLS certificate.
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
Next we create an HTTP client with the TLS configuration. The Transport
field of the client specifies the HTTP transport to use for making requests. In this case, we create a new transport with the TLSClientConfig
field set to the TLS configuration we created earlier.
tr := &http.Transport{
TLSClientConfig: tlsConfig,
}
client := &http.Client{Transport: tr}
Next we make an HTTPS GET request to the URL specified by the url
constant using the HTTP client created earlier. If an error occurs while making the request, we use the log.Fatalf
function to log the error and exit the program. We also defer the closing of the response body to ensure that it is always closed after we have finished reading it.
resp, err := client.Get(url)
if err != nil {
log.Fatalf("Failed to get response: %v", err)
}
defer resp.Body.Close()
Next we read the contents of the response body using the ioutil.ReadAll
function, which reads all the data from the response body until the end of the stream. If an error occurs while reading the body, we use the log.Fatalf
function to log the error and exit the program.
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed to read response body: %v", err)
}
Finally, we use the log.Printf
function to print the contents of the response body to the console.
log.Printf("Response body: %s\n", body)
Your final code should look like below.
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net/http"
)
const (
url = "https://localhost:8443"
)
func main() {
cert, err := ioutil.ReadFile("ca.crt")
if err != nil {
log.Fatalf("Failed to read certificate file: %v", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(cert)
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
tr := &http.Transport{
TLSClientConfig: tlsConfig,
}
client := &http.Client{Transport: tr}
resp, err := client.Get(url)
if err != nil {
log.Fatalf("Failed to get response: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed to read response body: %v", err)
}
log.Printf("Response body: %s\n", body)
}
Step 6: Test the TLS connection
- First you can start your server using
go run
command and if your server starts successfully, you should be able to seeListening on :8443…
on your console. - Next you can run your client in another terminal using the same command and if our TLS connection is successful, you should be able to see
Response body: Hello, TLS!
on your console.
Wrap Up
Well, that’s it. Happy Coding!!!