TLS in Go

3 Feb 2020

Another quick post this week while I’m working on other projects.

One of my projects is a backup system and, obviously, I want the communication to be encrypted. Now building your own crypto is almost always a bad idea, and TLS will do what I want so it’s just a case of using it.

For this system, a pinned self-signed certificate makes sense, and can be argued is more secure than just trusting any CA signed cert. However, the documentation for Go doesn’t make it clear how you do this, so here’s what works for me.

As always, this is just what I’ve come up with, there no saying this isn’t wrong, stupid, or going to break if you use it.

There’s also no error checking in this code, so it won’t even compile in Go until you’ve added it. As usual, if there’s an error returned you should be checking it.

Making a key

Now you could do this in OpenSSL, save it to a PEM file and load that, but why not do the whole lot in Go :) .

func GenerateCert() (*ecdsa.PrivateKey, crypto.PublicKey,
        *x509.Certificate) {
    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

    today := time.Now()
	dur := time.Duration(10*365*24) * time.Hour
	expiry := today.Add(dur)

    template := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			Organization: []string{"testcert"},
		},
		NotBefore: today,
		NotAfter:  expiry,

		KeyUsage:              x509.KeyUsageKeyEncipherment |
                                x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

    template.IPAddresses = append(template.IPAddresses, net.IPv4(127, 0, 0, 1))

    publicKey := privateKey.Public()
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template,
        publicKey, privateKey)

    serverCert, err := x509.ParseCertificate(derBytes)

    return privateKey, publicKey, serverCert
}

If you want to save certs and keys to disk, look at the encoding/pem library as it not only stores a []byte blob easily, but lets you have extra header strings along with it.

Testing the TLS stuff

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

func testTLS() {
    // Wrap the certificate into the TLS format
    serverTlsCert := tls.Certificate{
		PrivateKey: privateKey,
		Leaf:       serverCert,
	}
	serverTlsCert.Certificate = append(serverTlsCert.Certificate, serverCert.Raw)

    // Define the server config
    // This limits to TLS1.2 and 1.3
	serverConfig := tls.Config{
		Certificates: []tls.Certificate{serverTlsCert},
		MinVersion:   tls.VersionTLS12,
		MaxVersion:   tls.VersionTLS13,
	}

    // Start a basic server that just responds to a client
    server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter,
                                            r *http.Request) {}))
	server.TLS = &serverConfig
	server.StartTLS()
	defer server.Close()

    // Make a client, that has the self-signed certificate as a CA,
    // so it's the only trusted root.
    clientConfig := tls.Config{
		ServerName: "127.0.0.1",
		MinVersion: tls.VersionTLS12,
		MaxVersion: tls.VersionTLS13,
	}

    // Create a certificate pool and add our cert.
	clientConfig.RootCAs = x509.NewCertPool()
	clientConfig.RootCAs.AddCert(serverCert)
    
    // Create a TLS client, that uses the above config.
	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &clientConfig,
		},
	}

    // Try to connect and get a response
	resp, err := client.Get(server.URL)
	if err != nil {
		log.Fatalf("Failed to get URL: %v", err)
	}
	resp.Body.Close()
}

This post has been taged as Go