Generating SSL Certificates for Development

How to get the πŸ”’ locally

Β·

6 min read

Why I'm Writing This

If you've ever wanted to create an HTTPS server for local development, you know that it's really frustrating to understand OpenSSL, the crossed out πŸ”’ sign, and an endless stream of terminology.

After a lot of research both in and out of work, I compiled a list of steps that will help make it easier to get the πŸ”’ sign on localhost - truly satisfying, isn't it πŸ™‚

Jared being Jared

Let's Do This

Here's all the steps I followed to create an SSL πŸ”’ certificate for local development purposes:

  1. Generate a Certificate Authority

    We import the generated rootCACert.pem file to the browser and add it to our Trusted Root Certificates

  2. Create an RSA Private Key

    This is what we use to "sign" a new certificate signing request

  3. Create a Certificate Signing Request

    Done using the Private Key

  4. Create a Config File

    Sets up DNS stuff for localhost

  5. Generate a Public Certificate

    Signed with Private Key and by Certificate Authority

  6. Forward Secrecy

    From NodeJS documentation using Diffie-Hellman key-agreement protocol

IMPORTANT NOTE: if you're prompted for a Common Name while issuing a Certificate, enter localhost. For development purposes, I set the Common Name of the Root CA to my name (cuz why not).

As we want to use TLS v1.3 - the most secure version - we need to install Node 12.x or higher. These versions automatically enforce TLSv1.3, so it's easier to work with. I chose to use the current version of Node, i.e., version 16.10.0.

Here's how you can update your node version

  • Check version using node --version
  • Switch version using nvm use 16.10.0
  • Install it using nvm i 16.10.0). This ensures TLS v1.3 is enforced (better security).

All OpenSSL-related tasks will occur in a folder titled certs in this repo. Run the following commands to set it up:

mkdir certs
cd certs

The procedure below highlights how we can generate 4096-bit RSA private keys, however, 2048-bit keys are very secure too and take slightly less time to verify during the TLS handshake. Now, let's proceed with the steps to generate a Root Certifying Authority (CA) and Certificate.

Generate a Certificate Authority

A Certifying Authority is basically someone that says "yep, I issued this certificate - you can trust me." For developmental purposes, we can install this certificate (the rootCACert.pem file) generated from the commands below in our browser's Trusted Root Certificates. Here's how we issue the certificate:

Step 1: Generate a Private Key

openssl genrsa -out rootCAKey.pem 4096

Step 2: Generate a Self-Signed Root Certificate Authority

openssl req -x509 -sha256 -new -nodes -key rootCAKey.pem -days 3650 -out rootCACert.pem

This certificate, however, (obviously) won't be available on everyone else's devices and will still produce the Not Trusted message when visiting https://your-website.com.

Create a Private Key

This is the private key we'll use to sign our public certificate - it's unique to our server. This step and the next two steps can be replicated for any number of servers you own.

openssl genrsa -out server-key.pem 4096

Create a Certificate Signing Request

We create the CSR and send it over to the Root CA to be signed.

Here's a random analogy - the Root CA has the same authority that your doctor has when they sign a note saying you were sick the day of your calc test - everyone trusts the doc πŸ˜‚

openssl req -new -key server-key.pem -out server-csr.pem

Create a Config File

This file can help "resolve" the DNS to localhost. I was facing the issue before where it said it couldn't verify the 'Common Name' of the certificate. I tried using 127.0.0.1, localhost, localhost:3001, https://localhost:3001, and so on, however, that didn't resolve it.

The following solution helps get around that.

This will create a new file v3.ext and open a text editor

touch v3.ext
nano v3.ext

Add the following contents to the file

authorityKeyIdentifier=keyid,issuer
# CA:FALSE means the certificate we generate using this file 
# will not be an intermediate certifying authority
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost

You can also add an IP address to alt_names in the following way

# Uncomment below to include local IP
IP.1 = 127.0.0.1
IP.2 = 0.0.0.0

Exit the file by clicking Ctrl+X followed by y to save the changes.

Generate a Public Certificate

Using the CSR, Root CA, and the .ext file generated above, we'll finally create a public certificate.

openssl x509 -req -in server-csr.pem -CA rootCACert.pem -CAkey rootCAKey.pem -CAcreateserial -out server-cer.pem -days 500 -sha256 -extfile v3.ext

This is what the server sends to the client when they request the https site. The certificate has been signed by the Root CA and this can be verified by the client after doing some crypto magic since they have the Root CA's certificate (as we did in the first step)

Verify Certificate Contents

The command below can be used to parse the certificate's contents just to check that it's "normal" and has everything; to simply it, if you see confusing Hackerman style stuff, it worked.

openssl x509 -in server-cer.pem -text -noout

Forward Secrecy

Intense security stuff that's quite good. I highly recommend watching this video by Hussein Nasser for a great explanation of TLS1.2 vs TLS1.3

Perfect forward secrecy was optional up to TLSv1.2, but it is not optional for TLSv1.3, because all TLSv1.3 cipher suites use ECDHE - from the NodeJS TLS Documentation

openssl dhparam -outform PEM -out dhparam.pem 2048

Setting Up a Basic HTTPS Server

Assuming you followed the same directory structure from above, you can either clone my https-demo repo or copy the following code to run a basic HTTPS server.

const https = require('https')
const fs = require('fs')

const options = {
    ca: fs.readFileSync('./certs/rootCACert.pem'),
    key: fs.readFileSync('./certs/server-key.pem'),
    cert: fs.readFileSync('./certs/server-cer.pem'),
    // I believe these are generated anyway by Node if it uses TLS v1.3
    dhparam: fs.readFileSync('./certs/dhparam.pem'),
    ecdhCurve: 'auto',
    honorCipherOrder: true,
}

https.createServer(options, function (req, res) {
    res.writeHead(200)
    res.end('tls v1.3')
}).listen(4555, "0.0.0.0", () => {
    console.info(`Listening`)
})

This can, of course, be configured with Express and other frameworks to generate dynamic web pages.

Takeaways

Rami Malek doing his thing

I've struggled with this for an exTREMELY long time now... it was only when I took some time away from the actual computer and confusing OpenSSL documentation (I swear, it's me - not them) that I understood how all this actually works.

Learning about what TLS actually is, the TLS handshake, TLSv1.2 vs TLSv1.3 - all this let me scratch the surface and learn more about how the web works. It's truly interesting stuff... watching the video I referred to earlier (this one if you're lazy) helped me visually understand things. I find that this type of mixed-learning style really helps, especially in back-end stuff since you can't "see" the changes the same way you do in front-end development.

Β