Published: Nov 19, 2023

Local HTTPS Server

Your connection is not private

Goal

Setup a local HTTPS server that works just like a “real” HTTPS server. Especially regarding certificates.

Also I am not a cryptography or security expert so the terminology used might be incorrect.

My usecase for this is testing and developing PWAs in a local network.

Requirements

  • openssl
  • golang

A quick outline of the steps:

  1. Become a CA (Certificate Authority)
  2. Generate CA signed certificates
  3. Trust the “root certificate”
  4. Use the CA signed certificates to serve a website over HTTPS

1. Certificate Authority

What is a CA

A CA acts as an entity to verify and sign certificates. Well known CAs include IdenTrust, DigiCert or Let’s Encrypt. Their root certificates are very likely already trusted by your machine and that’s why you don’t have to do anything to get a secure HTTPS connection to many servers on the internet.

Becoming a CA

Basically you just need a private key and an X.509 public certificate.

For the private key run:

openssl genrsa -out CA.key 2048

To generate the X.509 public certificate:

openssl req -x509 -new -nodes -key CA.key -sha256 -days 3650 -out CA_public.pem

A few questions will be asked, the only one that matters for our purposes is the Common Name. Pick something easily recognizable.

2. CA-signed certificates

This are the things you (used to) have to pay for. (Thank you Let’s Encrypt for free certificates)

As the “customer” of the CA

First we need another private key that belongs to “us” and not the CA. I am going to name it localhost.key since that’s where it’s going to be used. The hostname would also be a sensible choice.

openssl genrsa -out localhost.key 2048

Now we need to generate a CSR (Certificate Signing Request). As the name kind of implies this will be used by us to request a certificate signed by the CA.

openssl req -new -key localhost.key -out localhost.csr

As the CA

We receive the CSR and the desired domain names (and some money). With that and our CA.key and CA_public.pem we can generate and sign a certificate.

Well actually we also need a small configuration file ssl.conf with following contents:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = hostname.local

Do not forget to insert the hostname of your machine.

Now we can generate the public X.509 certificate:

openssl x509 -req -in localhost.csr -CA CA_public.pem -CAkey CA.key -CAcreateserial -out localhost.crt -sha256 -extfile ssl.conf -days 365

It might be tempting to increase -days 365 but many implementations will reject certificates with a long validity period. Apple, for example, limits it to 398 days on their implementation.

Done

You should now have following files:

  • CA.key
  • CA_public.pem
  • localhost.key
  • localhost.csr
  • localhost.crt

3. Trust

That’s the hard part about being a CA. Getting people/machines to trust you.

The process is different for most operating systems, but the relevant file is CA_public.pem

Since I am currently on macOS/iOS I will only describe the process for those two for now.

macOS (14.0)

  1. Open CA_public.pem in finder
  2. In Keychain Access look for “Default Keychains -> Login -> Certificates”
  3. Double click on your certificate
  4. Expand “Trust”
  5. When using this certificate: Always Trust

iOS (17.1)

  1. Download/Open CA_public.pem in Files
  2. iOS should ask for permission to install the certificate/profile
  3. Check “Settings -> General -> VPN & Device Management” and make sure the profile is trusted/verified
  4. “Settings -> General -> About -> Certificate Trust Settings (at bottom) -> ”
  5. Enable Full Trust For Root Certificates

4. HTTPS Server

Almost done, let’s see if the certificate works as expected. Golang makes it very easy to create a HTTPS server:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.Handle("/", http.FileServer(http.Dir("www")))
	err := http.ListenAndServeTLS("0.0.0.0:8443", "certs/localhost.crt", "certs/localhost.key", nil)
	if err != nil {
		log.Fatal(err)
	}
}

I put all the certificates in a subfolder ./certs and the website in ./www

Now start the server:

go run main.go

And visit https://localhost:8443

The result should be a website served over HTTPS without any security warnings.

Note#1

When accessing the server from another machine you have to use a hostname like devpc.local and it has to be listed in the SAN in ssl.conf when generating the X.509 public certificate.

(If anybody knows a way to create a wildcard certificate for *.local I would be happy to know about it)

Note#2

Trusted root certificates are powerful, so be careful with your CA.key especially once you trusted the root certificate CA_public.pem