Go - Developing an mTLS HTTP Client
Using the Go programming language to develop a mutual TLS HTTP client to secure API endpoints.
Table of Contents
↑What is mutual TLS (mTLS)?
Mutual TLS, or mTLS for short, is a method for authentication between a client and a server. mTLS ensures that both parties at each end of a network connection are who they claim to be by verifying that they both have the correct private key. The information within their respective TLS certificates provides additional verification.
mTLS is often used in a Zero Trust security framework to verify users, devices, and servers within an organization. It can also help keep APIs secure.
In this article we’ll learn how to write a HTTP client in Go, which presents the certificates to an API endpoint which enforces mTLS.
↑Creating a Certificate Signing Request
Before I could interact with the endpoint secured by mTLS, I first had to create a certificate signing request. To do this I used openssl to generate a private key. Then used the private key to generate a CSR (certificate signing request).
1openssl genrsa -out qe1-preproduction.key 2048
2
3openssl req \
4 -key qe1-preproduction.key \
5 -new -out qe1-preproduction.csr \
6 -subj "/C=GB/ST=Cumbria/L=Carlisle/O=Signal Zero/CN=qe1.signalzero.co.uk"
I then sent the CSR file to the endpoint supplier, who created a TLS certificate. They installed the certificate on their servers and then sent me a copy of the certificate which I needed to present to the endpoint, along with the private key to establish an mTLS connection.
↑Creating a Go HTTP Client with mTLS support
We’re going to develop a function called mutualTLSWebClient which returns two values. The first value being the response from the API endpoint, with the second being an error if an error is detected.
We define the variables we’ll need throughout the function and set a sensible timeout for the endpoint we’re calling.
After the variable definitions, we’re going to initialise the certificate pool. This function returns two values, the first being a certificate pool, the second being the TLS certificate.
1func mutualTLSWebClient() (resp string, err error) {
2 var clientTimeout = 10 * time.Second
3 var response *http.Response
4 var responseBody []byte
5 var caCertPool *x509.CertPool
6 var tlsCertificate tls.Certificate
7
8 caCertPool, tlsCertificate, err = initializeCertificatePool()
9 if err != nil {
10 return ``, err
11 }
12
13 ...
14}
The initializeCertificatePool function is shown below in its entirety. The function starts by loading the PEM certificate file from disk and appends the cert into a new certificate pool. It then uses the LoadX509KeyPair function to load both the PEM certificate along with the private key to create a TLS certificate pair. If no errors were detected then the function returns both the certificate pool and the TLS certificate.
1func initializeCertificatePool() (*x509.CertPool, tls.Certificate, error) {
2 var err error
3 var caCertPool *x509.CertPool
4 var pemFile []byte
5 var tlsCertificate tls.Certificate
6
7 pemFile, err = ioutil.ReadFile("qe1-preproduction.pem")
8 if err != nil {
9 return nil, tls.Certificate{}, err
10 }
11
12 caCertPool = x509.NewCertPool()
13 caCertPool.AppendCertsFromPEM(pemFile)
14
15 tlsCertificate, err = tls.LoadX509KeyPair(
16 "qe1-preproduction.pem",
17 "qe1-preproduction.key")
18
19 if err != nil {
20 return nil, tls.Certificate{}, err
21 }
22
23 return caCertPool, tlsCertificate, nil
24}
Once the certificates are loaded, then we can continue with the rest of the mutualTLSWebClient function.
We create a HTTP client object and set all the timeouts to our pre-defined value. We pass our certificate pool into the RootCAs property and our TLS certificate into the Certificates collection.
The TLS certificate was self-signed, and not signed by a recognized certificate authority. This does’t necessarily reduce the security, but it does mean we have to set the InsecureSkipVerify property to true otherwise we’ll get an error message like x509: certificate signed by unknown authority.
1 ...
2
3 client := &http.Client{
4 Timeout: clientTimeout,
5
6 Transport: &http.Transport{
7 TLSClientConfig: &tls.Config{
8 RootCAs: caCertPool,
9 Certificates: []tls.Certificate{tlsCertificate},
10 InsecureSkipVerify: true,
11 },
12
13 IdleConnTimeout: clientTimeout,
14 ResponseHeaderTimeout: clientTimeout,
15 },
16 }
17
18 response, err = client.Get("https://qe1.signalzero.co.uk/v1/health")
19 if err != nil {
20 return ``, err
21 }
22
23 if response.StatusCode != 200 {
24 return ``, errors.New(
25 strconv.Itoa(response.StatusCode) +
26 ` ` + response.Status)
27 }
28
29 defer response.Body.Close()
30
31 responseBody, err = ioutil.ReadAll(response.Body)
32 if err != nil {
33 return ``, err
34 }
35
36 return string(responseBody), nil
37}
Once the HTTP client object has been defined, then we can perform a GET, POST or DELETE request against our API endpoint. We check that the response StatusCode is a valid 200 response. Any value other than a 200 will result in the error being returned.
Assuming the HTTP status code was 200 then we read the complete contents of the response body and return the value as a string to the calling function.