Self-signed certificate for local development (OpenSSL, JavaScript)
Published on May 12, 2024   •   4 min read

Self-signed certificate for local development (OpenSSL, JavaScript)

Jarek CeborskiJarek Ceborski

When developing web apps, it's essential to mirror the production environment as closely as possible. This includes using HTTPS, which allows testing of critical features like authentication, secure cookies, service workers, PWAs, and the Geolocation API to name a few. These features are often restricted to secure contexts, thereby reducing the likelihood of encountering unexpected issues when deploying to production.

In this post, we'll explore how to create a self-signed certificate for local development using two methods: OpenSSL and JavaScript (Node.js). We'll take a pragmatic approach by first generating a Certificate Authority (CA) and then a server certificate. This setup allows you to trust the CA on your system only once and have all certificates signed by it automatically trusted, effectively avoiding annoying browser warnings about untrusted certificates.

Method 1: OpenSSL

Step 1: Create a Certificate Authority (CA)

Create a private key (ca.key) and a self-signed root certificate (ca.crt) for your Certificate Authority. A validity of 10 years is reasonable for our use case.

Bash
openssl req -x509 -newkey rsa:4096 -nodes -keyout ca.key -out ca.crt -days 3650 -subj "/C=US/ST=State/L=Locality/O=Organization/CN=MyRoot"

Step 2: Create server certificate

First create a certificate signing request localhost.csr and server private key localhost.key :

Bash
openssl req -newkey rsa:4096 -nodes -keyout localhost.key -out localhost.csr -subj "/C=US/ST=State/L=Locality/O=Organization/CN=localhost"

then, sign the certificate with your CA, to get localhost.crt certificate, using a validity of 1 year as some browsers may complain about longer periods:

Bash
openssl x509 -req -in localhost.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out localhost.crt -days 365 -extfile <(echo "subjectAltName=DNS:localhost,IP:127.0.0.1")

Modify subjectAltName if you need the certificate to be valid for other domains or IP addresses.

That's it! You now have a CA and a self-signed certificate for localhost. You can use these files in your development environment.

Trust CA on system level (optional)

Example steps for macOS:

  1. Open Keychain Access app
  2. Navigate to System keychain in the sidebar
  3. Drag & drop ca.crt on the list and double click on it
  4. In the new window expand Trust section and choose Always Trust in the first dropdown

Test on minimal HTTPS server

Below is the minimal HTTPS server in Node.js. Run it using node server.js command, then open https://localhost or https://127.0.0.1 in the browser.

server.js
JavaScript / server.js
const https = require('https')
const fs = require('fs')

const sslOptions = {
  key: fs.readFileSync('localhost.key'),
  cert: fs.readFileSync('localhost.crt'),
}

https
  .createServer(sslOptions, (req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' })
    res.end('Hello, HTTPS World!\n')
  })
  .listen(443)

Method 2: JavaScript (Node.js)

A more sophisticated and fully automated process can be implemented using Node.js.

We will use the mkcert package, a user-friendly wrapper over the powerful node-forge (a native implementation of TLS in JavaScript). First initialize a new project with npm init and install mkcert with npm i mkcert.

Step 1: Create a Certificate Authority (CA)

server.js
JavaScript / server.js
import fs from 'fs/promises'
import { createCA, createCert } from 'mkcert'

const caKeyFile = 'ca.key'
const caCertFile = 'ca.cert'
const serverKeyFile = 'server.key'
const serverCertFile = 'server.crt'

async function generateCA() {
  const ca = await createCA({
    organization: 'MyRoot',
    countryCode: 'US',
    state: 'CA',
    locality: 'San Jose',
    validity: 3650,
  })
  await fs.writeFile(caKeyFile, ca.key)
  await fs.writeFile(caCertFile, ca.cert)
  return ca
}

Step 2: Create server certificate

server.js
JavaScript / server.js
async function generateServerCert(ca) {
  const cert = await createCert({
    ca,
    domains: ['127.0.0.1', 'localhost'],
    validity: 365,
  })
  await fs.writeFile(serverKeyFile, cert.key)
  await fs.writeFile(serverCertFile, cert.cert)
  return cert
}

Now using these two functions create a flow of creating CA and server certificate. If the CA was already created, use it to issue and sign the server certificate:

server.js
JavaScript / server.js
async function main() {
  try {
    let ca

    // Check if CA files exist
    const caKeyExists = await fs.access(caKeyFile).then(() => true).catch(() => false)
    const caCertExists = await fs.access(caCertFile).then(() => true).catch(() => false)

    if (caKeyExists && caCertExists) {
      console.log('⏩ Using existing Certificate Authority (CA)')
      const caKey = await fs.readFile(caKeyFile, 'utf8')
      const caCert = await fs.readFile(caCertFile, 'utf8')
      ca = { key: caKey, cert: caCert }
    } else {
      console.log('🛡️ Generating Certificate Authority (CA)...')
      ca = await generateCA()
    }

    await generateServerCert(ca)
    console.log('✅ Server certificate generated successfully')
  } catch (error) {
    console.error('💥 Error generating certificates:', error)
  }
}

main()

Combine these three code snippets into one server.js file and run npm run start to get the CA and server certificate generated.

Conclusion

You now have two methods for creating self-signed certificates for local development using OpenSSL and JavaScript (Node.js). These certificates will allow you to test critical features like authentication, secure cookies, service workers, PWAs, and the Geolocation API in a local environment that closely mirrors production.

For those looking to simplify the process even further, the LocalCan app offers generating these certificates with just one-click, publishing .local domains on a local network and much more. This not only saves time but also reduces the potential for errors, making it an excellent choice for developers at all skill levels.