go · golang · https

HTTPS and Go

Setting up a HTTP server is one the first things you’ll learn as a Go programmer.

It’s pretty easy. Just type

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there!")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

and you’re up and running on port 8080. Visit https://127.0.0.1:8080 in your browser and you’ll be presented with “Hi there!”.

But what if you’d like to serve encrypted traffic via HTTPS? At first glance, it’s just as easy. That’s what the aptly named ListenAndServeTLS is for. Just replace http.ListenAndServe(":8080", nil) with

http.ListenAndServeTLS(":8081", "cert.pem", "key.pem", nil)

and you’re done. Well, almost. That function takes two more arguments: “cert.pem” is the path to your server certificate in PEM format, “key.pem” is the path to your server private key in PEM format.

Acquiring the Server Certificate and Private Key

1. Using OpenSSL

You could generate those files using OpenSSL. OpenSSL comes with Mac OS X and Linux. If you are on Windows, you’ll have to install the binaries.

Luckily, generating the server certificate and private key with OpenSSL takes just one command:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem

You’ll be asked to fill in some information. The most critical part is the Common Name (e.g. server FQDN or YOUR name) field. Here you’ll have to put in your server address (e.g. myblog.com, or 127.0.0.1:8081 if you’d like to access your server on your local machine with port 8081).

After that, you’ll be presented with the “cert.pem” and “key.pem” files in the directory you ran the OpenSSL command in. Note that these files are what’s called a self-signed certificate, which means that you can use them to encrypt the traffic to and from your server, but a browser will not trust the connection out of the box.

You’ll see that as soon as you start your HTTP server with

http.ListenAndServeTLS(":8081", "cert.pem", "key.pem", nil)

pointed to the certificates you just generated. Visit https://127.0.0.1:8081 in your browser and you’ll be presented with a security warning.

It will tell you that the server certificate wasn’t signed by a trusted authority. In Firefox, click “I Understand the Risks” and add an exception to continue to your site. In Chrome, click “Advanced” and then proceed to your site.

2. Using Go

Alternatively, you can generate the certificate and key files directly in your Go code. Go comes with an example program that shows you how to do that. It’s called generate_cert.go.

For convenience, I bundled it into a self-contained library called httpscerts. Using httpscerts, we can modify the program to automatically generate the needed certificate:

package main

import (
    "fmt"
    "github.com/kabukky/httpscerts"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there!")
}

func main() {
    // Check if the cert files are available.
    err := httpscerts.Check("cert.pem", "key.pem")
    // If they are not available, generate new ones.
    if err != nil {
        err = httpscerts.Generate("cert.pem", "key.pem", "127.0.0.1:8081")
        if err != nil {
            log.Fatal("Error: Couldn't create https certs.")
        }
    }
    http.HandleFunc("/", handler)
    http.ListenAndServeTLS(":8081", "cert.pem", "key.pem", nil)
}

Of course, your browser will prompt you with the same warning as above as this is still a self-signed certificate.

3. Using StartSSL

A self-signed certificate is fine for testing purposes. But once you are running a production server, you want a certificate signed by an authority that’s trusted by browsers and operating systems.

Unfortunately, that’ll cost you at most places. Comodo for example charges $100 for a certificate that’s valid for one year. The Internet Security Research Group is working on fixing that, but at the time of writing that certificate authority isn’t available.

Therefore, the only alternative is StartSSL. StartSSL will issue a certificate for your domain that’s valid for one year, free of charge. You will still have to pay them to revoke the certificate should something like Heartbleed happen again, but for now it’s your only shot at a free and widely trusted server certificate.

Sign up with StartSSL and generate the server certificate and private key for your domain. Read the instructions carefully or consult one of the many tutorials out there that show you how to use StartSSL.

In the following, I will assume you saved the server certificate as “cert.pem” and the private key as “key.pem”.

Your server private key might be protected by a passphrase. Check by opening “key.pem” in a text editor. If it is indeed encrypted, you’ll see something like this at the top:

Proc-Type: 4,ENCRYPTED

To remove the passphrase from your private key, use this OpenSSL command:

openssl rsa -in key.pem -out key_unencrypted.pem

Finally, you’ll need to append the StartSSL Intermediate CA and the StartSSL Root CA to “cert.pem”.

Download the “Class 1 Intermediate Server CA” and “StartCom Root CA (PEM encoded)” from the StartSSL Tool Box (Log In > Tool Box > StartCom CA Certificates) and place the files into the same folder as your “cert.pem”. Using Linux or Mac OS X, run:

cat cert.pem sub.class1.server.ca.pem ca.pem > cert_combined.pem

Using Windows, run:

type cert.pem sub.class1.server.ca.pem ca.pem > cert_combined.pem

Now take “cert_combined.pem” and “key_unencrypted.pem” and use them with Go. If you want to, you can rename them to “cert.pem” and “key.pem”.

Handling HTTP Connections

Your HTTPS server is running just fine now. Using the StartSSL certificate, you can point your browser to https://yourdomain.com and it’ll load the page without any warning.

But what about http://yourdomain.com? Well, since there is no HTTP server running any more, it won’t load. There are two ways to handle this.

1. Serve the same Content on both HTTP and HTTPS

This is the simplest option and easily achievable:

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there!")
}

func main() {
    http.HandleFunc("/", handler)
    // Start the HTTPS server in a goroutine
    go http.ListenAndServeTLS(":8081", "cert.pem", "key.pem", nil)
    // Start the HTTP server
    http.ListenAndServe(":8080", nil)
}

2. Redirect from HTTP to HTTPS

This is the most sensible option if you want to encrypt all your connections. To achieve this, you’ll need a function to redirect from HTTP to HTTPS:

package main

import (
    "fmt"
    "net/http"
)

func redirectToHttps(w http.ResponseWriter, r *http.Request) {
    // Redirect the incoming HTTP request. Note that "127.0.0.1:8081" will only work if you are accessing the server from your local machine.
    http.Redirect(w, r, "https://127.0.0.1:8081"+r.RequestURI, http.StatusMovedPermanently)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there!")
}

func main() {
    http.HandleFunc("/", handler)
    // Start the HTTPS server in a goroutine
    go http.ListenAndServeTLS(":8081", "cert.pem", "key.pem", nil)
    // Start the HTTP server and redirect all incoming connections to HTTPS
    http.ListenAndServe(":8080", http.HandlerFunc(redirectToHttps))
}

Or, using two separate ServeMux for the HTTP and HTTPS servers, you could redirect only specific paths (e.g. /admin/) to HTTPS:

package main

import (
    "fmt"
    "net/http"
)

func redirectToHttps(w http.ResponseWriter, r *http.Request) {
    // Redirect the incoming HTTP request. Note that "127.0.0.1:8081" will only work if you are accessing the server from your local machine.
    http.Redirect(w, r, "https://127.0.0.1:8081"+r.RequestURI, http.StatusMovedPermanently)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there!")
}

func adminHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi admin!")
}

func main() {
    // Create a new ServeMux for HTTP connections
    httpMux := http.NewServeMux()
    // Create a new ServeMux for HTTPS connections
    httpsMux := http.NewServeMux()
    // Redirect /admin/ paths to HTTPS
    httpMux.Handle("/admin/", http.HandlerFunc(redirectToHttps))
    // Accept everything else
    httpMux.Handle("/", http.HandlerFunc(homeHandler))
    // Accept everything on HTTPS
    httpsMux.Handle("/", http.HandlerFunc(homeHandler))
    httpsMux.Handle("/admin/", http.HandlerFunc(adminHandler))
    // Start the HTTPS server in a goroutine
    go http.ListenAndServeTLS(":8081", "cert.pem", "key.pem", httpsMux)
    // Start the HTTP server and redirect all incoming connections to HTTPS
    http.ListenAndServe(":8080", httpMux)
}

And that’s it. Have fun experimenting with HTTPS and Go!

Published:
comments powered by Disqus