logo
Jiff Slater
🤔 About
✍️ Contact
📚Knowledge
📄Posts
📁Archive
Updated: 7 November 2021

Generating and trusting a local certificate authority

Testing websites locally usually requires your computer to generate and trust a local certificate authority.

We'll create a root CA and then an intermediary CA. The latter will be used to sign the ceritificates. Finally, we'll create the SSL key for use in Apache or Nginx or even Envoy.

One thing that helped me to wrap my head around all these concepts was to hop over to the Firefox certificate store and try to make sense of what's there. It's available in Settings > Privacy & Security > View Certificates.

Generating a CA

In Linux, this isn't too complicated from an invocation perspective. You'll need to prepare information about the root authority ahead of time in a configuration file.

I'll break down how to do this with the three major SSL implementations: OpenSSL, GnuTLS, and Mozilla Network Security Services.

OpenSSL

OpenSSL is of course the primary option here due to its omnipresence in all things cryptography. It's syntax can be a bit unforgiving but that's probably a good thing considering

First we'll write up a certificate configuration file that'll guide OpenSSL on the specifics of the certificate authorities. We'll start with this base file copied directly from the manpage of ca.

[ ca ]
default_ca      = CA_default            # The default ca section

[ ca_default ]
dir            = ./demoCA              # top dir
database       = $dir/index.txt        # index file.
new_certs_dir  = $dir/newcerts         # new certs dir

certificate    = $dir/cacert.pem       # The CA cert
serial         = $dir/serial           # serial no file
#rand_serial    = yes                  # for random serial#'s
private_key    = $dir/private/cakey.pem# CA private key
RANDFILE       = $dir/private/.rand    # random number file

default_days   = 365                   # how long to certify for
default_crl_days= 30                   # how long before next CRL
default_md     = md5                   # md to use

policy         = policy_any            # default policy
email_in_dn    = no                    # Don't add the email into cert DN

name_opt       = ca_default            # Subject name display option
cert_opt       = ca_default            # Certificate display option
copy_extensions = none                 # Don't copy extensions from request

[ policy_any ]
countryName            = supplied
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

Next we'll make a few changes. Of which:

Put the following in a file called root.conf.

[ ca ]
default_ca     = ca_default  # The default ca section

[ ca_default ]
# Directories
dir = ./ca/root  # top dir
certs = $dir/certs
new_certs_dir = $dir/newcerts
crl_dir = $dir/crl

# Files
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand

# Root CA
private_key = $dir/private/ca_key.pem
certificate = $certs/ca_crt.pem

# Certificate revocation lists
crlnumber = $dir/crlnumber
crl = $crl_dir/ca.crl.pem

# Misc.
default_md = sha512 
policy = policy_strict

[ policy_strict ]
countryName            = match
organizationName       = match
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ policy_loose ]
countryName            = optional
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ req ]
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha512
x509_extensions = v3_ca

# This must match the policy list above
[ req_distinguished_name ]
countryName            = Country Name
organizationName       = Organisation Name
organizationalUnitName = Organisation Unit Name
commonName             = Common Name
emailAddress           = Email Address

# Spend 30 minutes and read man x509v3_config
[ v3_ca ]
basicConstraints = critical, CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer

[ v3_sub_ca ]
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer

[ server_cert ]
basicConstraints = CA:false
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid, issuer
extendedKeyUsage = serverAuth

[ usr_cert ]
basicConstraints = CA:false
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid, issuer
extendedKeyUsage = clientAuth

Now we can generate the required keys and directories for the root CA. :

$ mkdir -p ca/root/{certs,crl,newcerts,private}
$ chmod og-rx ca/root/private
$ touch ca/root/index.txt ca/root/serial
$ openssl genpkey -aes256 -algorithm ec -pkeyopt ec_paramgen_curve:P-384 -out ca/root/private/jiffcert-trust-root_key.pem

Next we generate and sign our own certificate because we're the root CA. Note the use of -x509 means we don't need to go through the usual CSR procedure.

$ openssl req -config ca/root/root.conf -new -x509 -sha512 -extensions v3_ca -key private/jiffcert-trust-root_key.pem -out certs/jiffcert-trust-root_crt.pem -days 3650

Next we generate the sub CA key that'll do most of the signing. :

$ mkdir -p ca/sub/{certs,crl,newcerts,private,csr}
$ chmod og-rx ca/sub/private
$ touch ca/sub/index.txt ca/sub/serial
$ openssl genpkey -aes256 -algorithm ec -pkeyopt ec_paramgen_curve:P-384 -out ca/sub/private/jiffcert-trust-sub-d1.pem

Now copy over the config from before and change the key, certificate, and CRL names. :

$ cp ca/root/root.conf ca/sub/sub.conf
$ vim !$
dir = ca/sub
private_key = $dir/private/jiffcert-trust-sub-d1_key.pem
certificate = $certs/jiffcert-trust-sub-d1_crt.pem
crl = $crl_dir/jiffcert-trust-sub-d1_crl.pem
policy = policy_loose

Now we can generate a certificate signing request and then sign it with the root CA. Pay close attention to the change in the referenced configuration file. It's a bit tricky!

$ openssl req -config ca/sub/sub.conf -new -sha512 -key ca/sub/private/jiffcert-trust-sub-d1_key.pem -out ca/sub/csr/jiffcert-trust-sub-d1_csr.pem

$ openssl ca -config ca/root/root.conf -extensions v3_sub_ca -days 3000 -rand_serial -in ca/sub/csr/jiffcert-trust-sub-d1_csr.pem -out ca/sub/certs/jiffcert-trust-sub-d1_crt.pem

Now that the certificate has been signed, we can finally generate a TLS Certificate for your web server - this is called a server certificate. We'll go through all the steps here. :

$ mkdir -p ca/webserver/{certs,crl,newcerts,private,csr}
$ chmod og-rx ca/webserver/private
$ openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-384 -out ca/webserver/private/local.internal_key.pem 
$ openssl req -config ca/sub/sub.conf -addext "subjectAltName = DNS:local.internal" -key ca/webserver/private/local.internal_key.pem -new -sha512 -out ca/webserver/csr/local.internal_csr.pem
$ openssl ca -config ca/sub/sub.conf -extensions server_cert -days 300 -rand_serial -in ca/webserver/csr/local.internal_csr.pem -out ca/webserver/certs/local.internal_cert.pem

Finally, you can add the root cert to your OS trust store so you can develop locally! :

# cp ca/root/certs/jiffcert-trust-root_crt.pem /usr/local/share/ca-certificates/jiffcert-trust-root.crt

If you're using apache you can create a certificate chain by concatenating the subordinate CA and web server certificate together.

$ cat ca/webserver/certs/local.internal_cert.pem ca/sub/certs/jiffcert-trust-sub-d1_crt.pem > ca/webserver/certs/local.internal_chain.pem && chmod 444 !$

And that about wraps it up. A local root certificate authority that you can use to generate certificates for your web servers. This local trust store is crucial for any budding web developer and can also be used for secure communication over local networks.