Historically, I’ve used my main desktop to store system backups and provide other network services, but this is annoying because I dual-boot it to play video-games, so while I’m playing most services are offline. Additionally, electricity in San Diego is absurdly expensive (thanks SDGE), so the 180+W while idling adds up quite a bit. To fix both problems, I bought an HP Prodesk 600 G3 MT from Human-I-T for very reasonable prices, hoping that the idle power draw stays under 20 Watts. While setting up that server, I decided that I might as well figure out how to set up my own CA for all of the internal services that I’m setting up, since I was getting tired of getting warnings from Firefox about “unsecured” connections due to self-signed certificates.
Most of the information I used to set up my own CA came from the OpenSSL CA documentation, which can be found here: https://openssl-ca.readthedocs.io/en/latest/index.html. However, it is missing the details on how to use Elliptic Curve keys. This post is an attempt to fill in that gap.
The process can be summarized as follows:
- Make a root CA key and certificate
- Make an intermediate CA key and certificate by requesting signing by root CA, and create an intermediate certificate chain
- Make individual server certificates by requesting signing by intermediate CA, and extend the certificate chain
Safeguarding Keys
It is important to protect all private keys, as if they leak anyone with them can sign certificates that pass as valid when checked against your CA certificates. This matters a little less when all of your certs are meant for your local network, but meh, might as well try to do something about it.
What I decided to do was to use LUKS to encrypt some filesystems on disk. I used one to store the CA and intermediate CA, and a second one to store client keys.
# Create the Root CA/Intermediate CA file, and the file for server keys
dd if=/dev/urandom of=ca bs=1M count=100 status=progress
dd if=/dev/urandom of=server bs=1M count=100 status=progress
# it will ask for a password-- remember it or store it somewhere secure/safe/encrypted
cryptsetup luksFormat ca
cryptsetup luksFormat server
# now open the volume, and mount it for use. Opening it will ask for the volume password
cryptsetup open ca ca-keys
cryptsetup open server server-keys
mkdir ca-keys
mkdir server-keys
mount /dev/mapper/ca-keys ca-keys
mount /dev/mapper/server-keys server-keys
It’s probably a good idea to backup the LUKS headers, in case something happens to them:
cryptsetup luksHeaderBackup ca --header-backup-file ca-keys.header.luks
cryptsetup luksHeaderBackup server --header-backup-file server-keys.header.luks
Make Root CA and its Certificate
The general outline of the steps can be found here: https://openssl-ca.readthedocs.io/en/latest/create-the-root-pair.html
cd ca-keys/
mkdir root/
cd root/
mkdir certs crl newcerts private
chmod 700 private
touch index.txt
echo 1000 > serial
At this point a openssl.cnf
file needs to be created that will include default settings. It can be copied from https://openssl-ca.readthedocs.io/en/latest/root-configuration-file.html#root-configuration-file and modified to meet needs. For example, on my local install, I tweaked the dir
and private_key
paths, and changed the defaults for many of the *_default
fields (e.g. countryName_default
to US). I also tweaked the req
default_bits
to 4096.
With that in place, now it’s time to create the key in the ca-keys/root/
directory:
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -pkeyopt ec_param_enc:named_curve -aes-256-cbc -out private/root-ca.key.pem
# This will now ask for a password, set one and keep it safe!
chmod 400 private/root-ca.key.pem
Wait, why is that so messy? Turns out that only some elliptic-curves are really used widely online (e.g. Chrome doesn’t support secp521r1). The two that seem to be the most supported are secp384r1 and secp256r1. So I just arbitrarily-ish picked secp384r1. Also, we are using the -aes-256-cbc
option to enable password protection on the key. There are other options that may work (see the output of openssl enc -list
for tentative options).
OK, now we have the key. Now we need a root certificate (we’re setting the expiry date far into the future, as recommended):
openssl req -config openssl.cnf -new -sha256 -extensions v3_ca -days 7300 -key private/root-ca.key.pem -out csr/ca.csr.pem
# This will request the password for the key, and then will ask for information about the certificate such as its common name, organization, etc.
chmod 444 certs/ca.cert.pem
To verify that the certificate looks ok:
openssl x509 -noout -text -in certs/ca.cert.pem
Make Intermediate CA and its Certificate
OK, now we can work on making the intermediate CA, which is the one that will actually be used to sign all of the certificates for the websites, etc. This should look familiar as it’s similar to what we did with the root certificate:
cd ../
mkdir intermediate
cd intermediate
mkdir certs crl csr newcerts private
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumber
Now, we can copy the base openssl.cnf
for the intermediate certificate from https://openssl-ca.readthedocs.io/en/latest/intermediate-configuration-file.html#intermediate-configuration-file. Once saved, just as with the root openssl.cnf
, variables can be tweaked (such as dir
, *_default
, default_bits
).
Now it is time to create the intermediate CA key:
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -pkeyopt ec_param_enc:named_curve -aes-256-cbc -out private/intermediate.key.pem
# This will now ask for a password, set one and keep it safe!
chmod 400 private/intermediate.key.pem
And now to create and sign the intermediate certificate:
openssl req -config openssl.cnf -new -sha256 -keyprivate/intermediate.key.pem -out csr/intermediate.csr.pem
# This will ask for the intermediate key password, and other information to fill into the certificate
cd ../root
openssl ca -config openssl.cnf -extensions v3_intermediate_ca -days 3650 -notext -md sha256 -in ../intermediate/csr/intermediate.csr.pem -out ../intermediate/certs/intermediate.cert.pem
# This will ask for the root CA password
chmod 444 ../intermediate/certs/intermediate.cert.pem
And to verify the newly created certificate:
openssl x509 -noout -text -in ../intermediate/certs/intermediate.cert.pem
# and check it against the root CA
openssl verify -CAfile ./certs/ca.cert.pem ../intermediate/certs/intermediate.cert.pem
Create the Certificate Chain
This is straight-ish forward (the order matters!). We need to concatenate the certificates of the root and the intermediate together to form the start of the certificate chain (intermediate needs to go first, the root):
# Changing directory to the root of the certs encrypted partition
cd ../
cat ./intermediate/certs/intermediate.cert.pem root/certs/ca.cert.pem > ./intermediate/certs/ca-chain.cert.pem
Great! At this point all of the initial setup is completed. From here on is a matter of creating site specific certificates, and installing the root certificate to all computers that need it.
Creating a Certificate for an Internal Site
Now we need to go into the server-certs
encrypted folder we created earlier, as this is where we will store the certificates and keys for the individual sites as we create them (arguably, the better approach is to have each server create its own private keys and just provide the CSR to the signing server):
cd server-keys
mkdir site1.my.example
# Now create the key, no password
# If password is desired, add `-aes256 -pass stdin`
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -pkeyopt ec_param_enc:named_curve -out site1.my.example/site1.key.pem
chmod 400 site1.my.example/site1.key.pem
With the private key in place (which should probably securely copied to the server for this domain), we now need to prepare the certificate signing request (CSR). We need the openssl.cnf
file from here again (the intermediate CA). subjectAltName
are now effectively required by modern browsers if the certificate is to authenticate a website. Multiple names can be provided, separated by commas (e.g. DNS:site1.my.example,DNS:site1.local
).
openssl req -config openssl.cnf -new -sha256 -key site1.my.example/site1.key.pem -out site1.my.example/site1.csr.pem -addext "subjectAltName = DNS:site1.my.example"
With the CSR in hand, we now need to go back to the intermediate CA to create the certificate:
cd ../ca-keys/intermediate/
openssl ca -config openssl.cnf -extensions server_cert -days 365 -notext -md sha256 -in ../../server-keys/site1.my.example/site1.csr.pem -out ../../server-keys/site1.my.example/site1.cert.pem
And with that we should now have a signed server certificate! The last thing to do is to create the certificate chain with this new certificate:
cd ../../server-keys/site1.my.example/
cat site1.cert.pem ../../ca-keys/intermediate/certs/ca-chain.cert.pem > site1.chain.pem
And finally, with that we should have all of the certificates and chains needed to set up properly signed certificates at home or other locations. The only thing really left to do is to install the root CA certificate on any machines and browsers that should recognize any certificates signed by the intermediate CA as authentic.
Safeguarding Keys, Again: Unmount and Lock
After being done signing and copying certificates and chains, remember to unmount and lock the encrypted storages!
# cd out of server-keys and ca-keys
umount server-keys
umount ca-keys
cryptsetup close ca
cryptsetup close server