December 15, 2019

Easy your way to understanding to TLS in Go

Earlier before I started using TLS(or HTTPS), I used to think that it was a complicated process and I had to learn alot of math and complex algorithms. I was wrong. That was already done for me and all I had to do was figure out how to place the pieces together. Like my every learning experience, it all begun with a Google search. But the articles I found were either too high level for my understanding or they did not offer what I wanted. So I turned to what every sane developer would do, the docs. While the docs were realativley easy to comprehend, a few concepts needed further reading. In this article, I will start with the basics as we proceed to somewhat high level topics.

Simple Analogy

Everybody (minus the millennials) has sent at least one letter,right? How does it work? A letter is dropped inside a letter-box and only the owner of the box who has the key can retrive the letter. While this analogy is an oversimplication of the true scenario, it does present a few concepts that resonant with TLS. For starters, anybody who knows the address of the letter-box can drop a letter. The letter-box serves as a means of identification. Once the letter has been dropped, it is impossible (unless brute force is applied) to fetch back the letter. The owner however who holds a key,which is secret, can open the letter-box and retrieve the letters.

Real world situation

If you are a frequent Github or Gitlab user, you may have encountered a situation where for you to access resources,you have to ssh. For that secure connection to work,you have to identify yourself by providing a public key then using your private key to validate your authority. Sounds familiar? In this case, the public key is like the letter-box. Anybody and everybody can know or have it while the private key is the key to the letter-box. And as such,it has to secret.

Step back. Way back

To hide information, we encrypt data while decrypt to retrive the data. This is the basic of data security. The math involved in hiding such data is way beyond the scope of this article. There are two types of encryption;

Symmetric Encryption

Here, we use only a single key to convert data into a cipher text and vice verse. Even at first glance this does not look truly safe. What if the key is stolen? I think you get the idea now.

Asymmetric Encryption

Here,we use two keys. One key is used to encrypt data, while the other one is used to decrypt it. Allow me to explain further with an example.

Assume we have data of type string;

 var data string = "This is the string data"

Our goal is to encrypt-decrypt this data. Now we create two keys, Black Key and White Key. In this scenario, we assume to encrypt the data using Black Key. Once the cipher text has been created, the only way to convert is back is decrypt it using the White Key. The gist here is that a key can not be used for both encryption and decryption. The choose of which key to use for encryption and decryption is up to you.

Asymetric encryption is the basis of what we have all come to know as public-private key combination.

Generating Keys

Go has an awesome package rsa that we use to generate the public and private keys.


    package main

    import (
       "crypto/rand"
        "crypto/rsa"
    )

    func main(){
       privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
        if err != nil {
            //handle error
        }
       publicKey := &privateKey.PublicKey
    }

From the code above we have generated a private and public key that we can to encrypt and decrypt data. Note the rand from crypto/rand package. This is better for this purpose.

Generate Certificate and key

For any TLS connection to work, we need to add a cerficate. But what is a certicate? A certifcate is just a public key with informartion embebbed. In a moment, you will understand what I mean;

To begin, we need to create a certificate template. We will be using crypto/x509 package. If you have never done this, before, the number of fields that the Certificate struct has can be quite intimidating at first. Lets get down to business


    var privateKey *rsa.PrivateKey
    var ioR io.Reader

    func init() {
        ioR = rand.Reader
        privateKey, _ = rsa.GenerateKey(ioR, 2048)
    }

    func certTemp() (*x509.Certificate, error) {
        max := new(big.Int).Lsh(big.NewInt(1), 128)
        serialNumber, err := rand.Int(ioR, max)
        if err != nil {
            return nil, err
        }

        tmpl := x509.Certificate{
            SerialNumber: serialNumber,
            Version:      int(time.Now().UnixNano()),
            Issuer: pkix.Name{Organization: []string{"This is my company yoh"},
                Country: []string{"Kenya"}, Locality: []string{"Nairobi"}},
            Subject:               pkix.Name{CommonName: "Learning TLS in Go gently"},
            SignatureAlgorithm:    x509.SHA256WithRSAPSS,
            NotBefore:             time.Now(),
            NotAfter:              time.Now().Add((356 * 24 * 20) * time.Hour), //20 years
            BasicConstraintsValid: true,
            IsCA:        true,
            KeyUsage:    x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
            ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
            DNSNames:    []string{"localhost"},
        }
        return &tmpl, nil
    }


Here we have just created a helper function certTemp() that returns a certificate template and an error. I strongly advise you to visit the docs which has tremendous detail that is invaluable.

To proceed we will now create the certificate using the template


    rootCertTmpl, err := certTemp()
	if err != nil {
		log.Errorf("Failed to create template %v", err)
		return
	}

	certBytes, err := x509.CreateCertificate(ioR, rootCertTmpl, rootCertTmpl, &privateKey.PublicKey, privateKey)
	if err != nil {
		log.Errorf("Failed to create cert %v", err)
		return
	}

The CreateCertificate function creates a self-signed certificate. Certificates have to be signed by the private key of the parent certificate. By parent certificate I mean by an external certificate authority (CA). In this case however, we will be acting as our own CA hence we will privide our template as the parent thus generating a self-signed certificate.

Now we need to actually output the certificate bytes into a file called cert.pem and encode it so that we can use it later in our TLS connection


    certOut, err := os.Create("path/to/dir", "cert.pem"))
	if err != nil {
		log.Errorf("Failed to open cert.pem for writing: %s", err)
		return
	}
	defer certOut.Close()

	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})

We do the same for the key


    keyOut, err := os.OpenFile("path/to/dir", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		log.Errorf("Failed to open cert.pem for writing: %s", err)
		return
	}
	defer keyOut.Close()
	pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})

Again I strongly advise you to visit the docs which will explaing in great detail the encoding and different types of private key marshalling.

Putting it all together, our build function should look like this


    func BuildCert(){
        rootCertTmpl, err := certTemp()
        if err != nil {
            log.Errorf("Failed to create template %v", err)
            return
        }

        certBytes, err := x509.CreateCertificate(ioR, rootCertTmpl, rootCertTmpl, &privateKey.PublicKey, privateKey)
        if err != nil {
            log.Errorf("Failed to create cert %v", err)
            return
        }

        certOut, err := os.Create("path/to/dir", "cert.pem"))
        if err != nil {
            log.Errorf("Failed to open cert.pem for writing: %s", err)
            return
        }
        defer certOut.Close()

        pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})

        keyOut, err := os.OpenFile("path/to/dir", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
        if err != nil {
            log.Errorf("Failed to open cert.pem for writing: %s", err)
            return
        }
        defer keyOut.Close()
        pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})

    }

Putting it on the server

Now that our certificate and key are generated, the only this left to do is to configure our server to accept TLS connections


    import (
        "crypto/tls"
        "net/http"
    )

    func serverConnection() {
        serverCert, err := tls.LoadX509KeyPair("path/to/cert.pem", "path/to/key.pem")
        if err != nil {
            //handle
        }
        config := tls.Config{
            Certificates: []tls.Certificate{serverCert},
        }

        s := http.Server{
            Addr:      "localhost",
            TLSConfig: &config,
        }

        s.ListenAndServeTLS("path/to/cert.pem", "path/to/key.pem")
    }

On connection however, the client will reject the certificate hence we need a way to make sure the client trusts the certificate.


    import (
	"crypto/tls"
	"crypto/x509"
	"io/ioutil"
	"net/http"
)

    func clientConnection() {
        cert, err := tls.LoadX509KeyPair("path/to/cert.pem", "path/to/key.pem")
        if err != nil {
            //handle
        }

        clientCertPem, err := ioutil.ReadFile("path/to/cert.pem")
        if err != nil {
            //handle
        }

        certPool := x509.NewCertPool()
        certPool.AppendCertsFromPEM(clientCertPem)

        config := &tls.Config{
            RootCAs:      certPool,
            Certificates: []tls.Certificate{cert},
        }

        client := &http.Client{
            Transport: &http.Transport{
                TLSClientConfig: config,
            },
        }
    }


When that in place now the client can be able to validate the certifacte provided by the server during the handshake.

Conclusion

As evidenced, TLS is more about managing certificates rather than knowing the complex math invlolved in generating and signing certificates.

I hope this was gentle enough to get you interested in furthuring your know-how about TLS in Go.

Au revoir.

Share

© David Dexter 2022