TLS in Go
3 Feb 2020Another 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()
}