Install Certificate Authority (CA)

Lars J├Ânsson 2024-06-16

These instructions are based on a guide available at https://roll.urown.net/ca/index.html

Table of Contents

Preparations

Throughout the document, the commands uses the environment variable DOMAIN to point the domain, ROOT_VER to the version of the Root CA and IM_VER to the version of the Intermediate CA.

Set up the variables. Replace example.net with your own domain, and set ROOT_VER and IM_VER to the version of the corresponding CA.

DOMAIN=example.net
ROOT_VER=v1
IM_VER=v1.1

Create base of the CA at ~/CA

cd ~
mkdir CA

Create a safe storage for storage of root and intermediate CA

cd ~/CA
dd if=/dev/zero bs=1M count=100 of=secret-device.img
sudo cryptsetup luksFormat secret-device.img 
sudo cryptsetup open --type luks secret-device.img secret-device
sudo mkfs.ext4 /dev/mapper/secret-device
mkdir safe-storage
sudo mount /dev/mapper/secret-device safe-storage
sudo chown $USER:`id -gn` safe-storage

Create the Root CA

Ensure that applicable steps in Preparations are done.

Directories and files

Create the directory structure in the safe storage and populate it with basic files.

cd ~/CA/safe-storage/
mkdir -p ${DOMAIN}.ca/root-ca-${ROOT_VER}/{certreqs,certs,crl,newcerts,private}
cd ${DOMAIN}.ca/root-ca-${ROOT_VER}/
chmod 700 private
touch root-ca.index
echo 00 > root-ca.crlnum

Set a random serial number for next openssl action.

openssl rand -hex 16 > root-ca.serial

Configuration file

A good starting point is found later in this document (root-ca.cnf) and available for download at:

wget https://roll.urown.net/_downloads/1daede3e61516d4ccdba1eaf26bdb86c/root-ca.cnf

Either download the file and modify it according to the following diff or better download the already modfied file root-ca.cnf:

74c74
< commonName              = example.net Root Certification Authority
---
> commonName              = example.net Root Certification Authority v1
81c81
< nameConstraints         = critical, @name_constraints
---
> #nameConstraints         = critical, @name_constraints
124c124
< caIssuers;URI           = http://ca.example.net/certs/example.net_Root_Certification_Authority.cert.pem
---
> caIssuers;URI           = http://ca.example.net/certs/example.net_Root_Certification_Authority_v1.cert.pem
129c129
< fullname                = URI:http://ca.example.net/crl/example.net_Root_Certification_Authority.crl
---
> fullname                = URI:http://ca.example.net/crl/example.net_Root_Certification_Authority_v1.crl

Replace example.net with your own domain and v1 to the wanted version, in the same file.

sed -i s/example.net/${DOMAIN}/g root-ca.cnf
sed  -i s/v1/${ROOT_VER}/g root-ca.cnf

Activate the configuration

export OPENSSL_CONF=./root-ca.cnf

Generate key and CSR

Create the private key and generate a certificate signing request at the same time.

openssl req -new -out root-ca.req.pem

Protect the private key

chmod 400 private/root-ca.key.pem

Show the CSR

Optionally take peek at the CSR.

openssl req -verify -in root-ca.req.pem -noout -text -reqopt no_version,no_pubkey,no_sigdump -nameopt multiline

Self-Signing of the Root Certificate

Self-sign the request.

openssl rand -hex 16 > root-ca.serial

openssl ca -selfsign -in root-ca.req.pem -out root-ca.cert.pem \
-extensions root-ca_ext \
-startdate `date +%y%m%d000000Z -u -d -1day` \
-enddate `date +%y%m%d000000Z -u -d +10years+1day`

Optionally take peek at the certificate

openssl x509 -in ./root-ca.cert.pem -noout -text \
-certopt no_version,no_pubkey,no_sigdump -nameopt multiline

Verify that the certificate is valid

openssl verify -verbose -CAfile root-ca.cert.pem root-ca.cert.pem

Revocation List (CRL)

Create an empty certificate revication list.

openssl ca -gencrl -out crl/root-ca.crl

Install the Root Certificate

Save the root certificate for later installation and use, and point it out as current.

mkdir -p ~/CA/${DOMAIN}.ca/certs/
rsync -a root-ca.cert.pem ~/CA/${DOMAIN}.ca/certs/root-ca-${ROOT_VER}.cert.pem
rm -f ~/CA/${DOMAIN}.ca/certs/root-ca.cert.pem
ln -rs ~/CA/${DOMAIN}.ca/certs/root-ca-${ROOT_VER}.cert.pem ~/CA/${DOMAIN}.ca/certs/root-ca.cert.pem

Create the Intermediate CA

Ensure that applicable steps in Preparations are done.

Directories and files

Create the directory structure in the safe storage and populate it with basic files.

cd ~/CA/safe-storage/
mkdir -p ${DOMAIN}.ca/intermed-ca-${IM_VER}/{certreqs,certs,crl,newcerts,private}
cd ${DOMAIN}.ca/intermed-ca-${IM_VER}/
chmod 700 private
touch intermed-ca.index
echo 00 > intermed-ca.crlnum

Set a random serial number for next openssl action.

openssl rand -hex 16 > intermed-ca.serial

Configuration file

A good starting point is found later in this document (intermed-ca.cnf) and available for download at:

wget https://roll.urown.net/_downloads/7514c4fc42236f15a393223f7d7c98d9/intermed-ca.cnf

Either download the file and modify it according to the following diff or better download the already modfied file intermed-ca.cnf:

NOTE Setting of xmppAddr and dnsSRV shall probably be removed from intermed-ca.cnf as it seems like newer versions of OpenSSL has native support for XMPP address. They are currently just "commented out".

9a10
> ALTNAME                 =
14,15c15,16
< xmppAddr          = 1.3.6.1.5.5.7.8.5
< dnsSRV            = 1.3.6.1.5.5.7.8.7
---
> #xmppAddr          = 1.3.6.1.5.5.7.8.5
> #dnsSRV            = 1.3.6.1.5.5.7.8.7
32c33
< default_days            = 396 # 1 year + 31 days
---
> default_days            = 730 # Two years
73c74
< default_keyfile         = private/intermed-ca.key
---
> default_keyfile         = private/intermed-ca.key.pem
93c94
< commonName              = example.net Intermediate Certification Authority
---
> commonName              = example.net Intermediate Certification Authority v1.1
106a108,119
> # Server Certificate Extensions with Subject Alternative Name
> [ server_ext_san ]
> keyUsage                = critical, digitalSignature, keyEncipherment
> extendedKeyUsage        = critical, serverAuth, clientAuth
> subjectKeyIdentifier    = hash
> authorityKeyIdentifier  = keyid:always
> issuerAltName           = issuer:copy
> authorityInfoAccess     = @auth_info_access
> crlDistributionPoints   = crl_dist
> subjectAltName          = $ENV::ALTNAME
> 
> #
143c156
< caIssuers;URI           = http://ca.example.net/certs/example.net_Intermediate_Certification_Authority.cert.pem
---
> caIssuers;URI           = http://ca.example.net/certs/example.net_Intermediate_Certification_Authority_v1.1.cert.pem
148c161
< fullname                = URI:http://ca.example.net/crl/example.net_Intermediate_Certification_Authority.crl
---
> fullname                = URI:http://ca.example.net/crl/example.net_Intermediate_Certification_Authority_v1.1.crl

Replace example.net with your own domain and v1.1 to the wanted version, in the same file.

sed -i s/example.net/${DOMAIN}/g intermed-ca.cnf
sed  -i s/v1.1/${IM_VER}/g intermed-ca.cnf

Activate the configuration

export OPENSSL_CONF=./intermed-ca.cnf

Generate key and CSR

Create the private key and generate a certificate signing request at the same time.

openssl req -new -out intermed-ca.req.pem

Protect the private key

chmod 400 private/intermed-ca.key.pem

Show the CSR

Optionally take peek at the CSR.

openssl req -verify -in intermed-ca.req.pem -noout -text -reqopt no_version,no_pubkey,no_sigdump -nameopt multiline

Sign the Intermediate CA with the Root CA

Copy the CSR to the Root CA

cp intermed-ca.req.pem ../root-ca-${ROOT_VER}/certreqs/intermed-ca-${IM_VER}.req.pem

Go to the Root CA and invoke its configuration.

cd ~/CA/safe-storage/${DOMAIN}.ca/root-ca-${ROOT_VER}/
export OPENSSL_CONF=./root-ca.cnf

Sign the intermediate CSR with the root key with the intermediate extensions

openssl rand -hex 16 > root-ca.serial

openssl ca -in certreqs/intermed-ca-${IM_VER}.req.pem -out certs/intermed-ca-${IM_VER}.cert.pem \
-extensions intermed-ca_ext \
-startdate `date +%y%m%d000000Z -u -d -1day` \
-enddate `date +%y%m%d000000Z -u -d +5years+1day`

Optionally take peek at the certificate.

openssl x509 -in certs/intermed-ca-${IM_VER}.cert.pem -noout -text \
-certopt no_version,no_pubkey,no_sigdump -nameopt multiline

Verify the certificate

openssl verify -verbose -CAfile root-ca.cert.pem certs/intermed-ca-${IM_VER}.cert.pem

Copy the certificate to the Intermediate CA.

cp certs/intermed-ca-${IM_VER}.cert.pem ../intermed-ca-${IM_VER}/intermed-ca.cert.pem

Revocation List

Got to Intermediate CA and invoke its configuration.

cd ~/CA/safe-storage/${DOMAIN}.ca/intermed-ca-${IM_VER}/
export OPENSSL_CONF=./intermed-ca.cnf

Create an empty certificate revication list.

openssl ca -gencrl -out crl/intermed-ca.crl

Install the Intermediate Certificate

Save the intermediate certificate for later installation and use, and point it out as current.

mkdir -p ~/CA/${DOMAIN}.ca/certs/
rsync -a intermed-ca.cert.pem ~/CA/${DOMAIN}.ca/certs/intermed-ca-${IM_VER}.cert.pem
rm -f ~/CA/${DOMAIN}.ca/certs/intermed-ca.cert.pem
ln -rs ~/CA/${DOMAIN}.ca/certs/intermed-ca-${IM_VER}.cert.pem ~/CA/${DOMAIN}.ca/certs/intermed-ca.cert.pem

Install the Intermediate CA as Issuer CA

Copy the Intermediate CA files to the working environment and point it out as Issuer CA.

rsync -a ~/CA/safe-storage/${DOMAIN}.ca/intermed-ca-${IM_VER} ~/CA/${DOMAIN}.ca/
rm -f ~/CA/${DOMAIN}.ca/issuer-ca
ln -rs ~/CA/${DOMAIN}.ca/intermed-ca-${IM_VER} ~/CA/${DOMAIN}.ca/issuer-ca

Sync the previous Issuer CA with its Intermediate CA

The previous Issuer CA will not be used for issuing certificates anymore and now it is a good time to ensure that the content is synced with its Intermediate CA in the secure storage. This can also be done, now and then, when it is active.

Set the environment VER to the version to sync.

VER=v1.1
rsync -a ~/CA/${DOMAIN}.ca/intermed-ca-${VER} ~/CA/safe-storage/${DOMAIN}.ca/intermed-ca-${VER}

Lock up safe storage

A copy of Intemediate CA is available in the working environment and and the Root and Intermediate CAs in the safe storage can be locked up (and stored in a safe place).

cd ~/CA
sudo umount safe-storage
sudo cryptsetup close secret-device

Usage of the Issuer CA

Ensure that the environment variable DOMAIN is set (see Preparations).

The Issuer CA working area is available at:

~/CA/${DOMAIN}.ca/issuer-ca

CSRs to be signed should be stored in:

~/CA/${DOMAIN}.ca/issuer-ca/certreqs

Signed certificates are stored in:

~/CA/${DOMAIN}.ca/issuer-ca/certreqs

Steps to sign a CSR:

  1. Copy the CSR to the CA

    cp <csr-file> ~/CA/${DOMAIN}.ca/issuer-ca/certreqs
  2. Go the CA

    cd ~/CA/${DOMAIN}.ca/issuer-ca
  1. Invoke the configuration

    export OPENSSL_CONF=./intermed-ca.cnf
  2. Sign the certificate

    • When SAN (Subject Alternative Name) is set in the CSR

      openssl rand -hex 16 > intermed-ca.serial
      
      openssl ca -notext \
      -in certreqs/<function>.csr \
      -out certs/<function>.crt \
      -extensions server_ext
    • When SAN is missing in the CSR

      openssl rand -hex 16 > intermed-ca.serial
      
      ALTNAME=DNS:<hostname-1>,DNS:<hostname-2>,IP:<ip-adress> \
      openssl ca -notext \
      -in certreqs/<function>.csr \
      -out certs/<function>.crt \
      -extensions server_ext_san

      The Common Name (CN) of the CSR is usually used as the SAN and can be retrieved with the following command

      openssl req -noout -subject -in certreqs/<function>.csr | \
      sed -n '/^subject/s/^.*CN\s*=\s*//p'
  3. Copy the certificate to the user

    cp ~/CA/${DOMAIN}.ca/intermed-ca/certs/<cert-file> <user-location>

Configuration files

root-ca.cnf

#
# OpenSSL configuration for the Root Certification Authority.
#

#
# This definition doesn't work if HOME isn't defined.
CA_HOME                 = .
RANDFILE                = $ENV::CA_HOME/private/.rnd

#
# Default Certification Authority
[ ca ]
default_ca              = root_ca

#
# Root Certification Authority
[ root_ca ]
dir                     = $ENV::CA_HOME
certs                   = $dir/certs
serial                  = $dir/root-ca.serial
database                = $dir/root-ca.index
new_certs_dir           = $dir/newcerts
certificate             = $dir/root-ca.cert.pem
private_key             = $dir/private/root-ca.key.pem
default_days            = 1826 # 5 years
crl                     = $dir/root-ca.crl
crl_dir                 = $dir/crl
crlnumber               = $dir/root-ca.crlnum
name_opt                = multiline, align
cert_opt                = no_pubkey
copy_extensions         = copy
crl_extensions          = crl_ext
default_crl_days        = 180
default_md              = sha384
preserve                = no
email_in_dn             = no
policy                  = policy
unique_subject          = no

#
# Distinguished Name Policy for CAs
[ policy ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = supplied
organizationalUnitName  = optional
commonName              = supplied

#
# Root CA Request Options
[ req ]
default_bits            = 4096
default_keyfile         = private/root-ca.key.pem
encrypt_key             = yes
default_md              = sha384
string_mask             = utf8only
utf8                    = yes
prompt                  = no
req_extensions          = root-ca_req_ext
distinguished_name      = distinguished_name
subjectAltName          = @subject_alt_name

#
# Root CA Request Extensions
[ root-ca_req_ext ]
subjectKeyIdentifier    = hash
subjectAltName          = @subject_alt_name

#
# Distinguished Name (DN)
[ distinguished_name ]
organizationName        = example.net
commonName              = example.net Root Certification Authority

#
# Root CA Certificate Extensions
[ root-ca_ext ]
basicConstraints        = critical, CA:true
keyUsage                = critical, keyCertSign, cRLSign
nameConstraints         = critical, @name_constraints
subjectKeyIdentifier    = hash
subjectAltName          = @subject_alt_name
authorityKeyIdentifier  = keyid:always
issuerAltName           = issuer:copy

#
# Intermediate CA Certificate Extensions
[ intermed-ca_ext ]
basicConstraints        = critical, CA:true, pathlen:0
keyUsage                = critical, keyCertSign, cRLSign
subjectKeyIdentifier    = hash
subjectAltName          = @subject_alt_name
authorityKeyIdentifier  = keyid:always
issuerAltName           = issuer:copy
authorityInfoAccess     = @auth_info_access
crlDistributionPoints   = crl_dist

#
# CRL Certificate Extensions
[ crl_ext ]
authorityKeyIdentifier  = keyid:always
issuerAltName           = issuer:copy

#
# Certificate Authorities Alternative Names
[ subject_alt_name ]
URI                     = http://ca.example.net/
email                   = certmaster@example.net

#
# Name Constraints
[ name_constraints ]
permitted;DNS.1         = example.net
permitted;DNS.2         = example.org
permitted;DNS.3         = lan
permitted;DNS.4         = onion
permitted;email.1       = example.net
permitted;email.2       = example.org

#
# Certificate download addresses for the root CA
[ auth_info_access ]
caIssuers;URI           = http://ca.example.net/certs/example.net_Root_Certification_Authority.cert.pem

#
# CRL Download address for the root CA
[ crl_dist ]
fullname                = URI:http://ca.example.net/crl/example.net_Root_Certification_Authority.crl

# EOF

intermed-ca.cnf

#
# OpenSSL configuration for the Intermediate Certification Authority.
#

#
# This definition doesn't work if HOME isn't defined.
CA_HOME                 = .
RANDFILE                = $ENV::CA_HOME/private/.rnd
oid_section             = new_oids

#
# XMPP address Support
[ new_oids ]
xmppAddr          = 1.3.6.1.5.5.7.8.5
dnsSRV            = 1.3.6.1.5.5.7.8.7

#
# Default Certification Authority
[ ca ]
default_ca              = intermed_ca

#
# Intermediate Certification Authority
[ intermed_ca ]
dir                     = $ENV::CA_HOME
certs                   = $dir/certs
serial                  = $dir/intermed-ca.serial
database                = $dir/intermed-ca.index
new_certs_dir           = $dir/newcerts
certificate             = $dir/intermed-ca.cert.pem
private_key             = $dir/private/intermed-ca.key.pem
default_days            = 396 # 1 year + 31 days
crl                     = $dir/crl/intermed-ca.crl
crl_dir                 = $dir/crl
crlnumber               = $dir/intermed-ca.crlnum
name_opt                = multiline, align
cert_opt                = no_pubkey
copy_extensions         = copy
crl_extensions          = crl_ext
default_crl_days        = 30
default_md              = sha384
preserve                = no
email_in_dn             = no
policy                  = policy
unique_subject          = no

#
# Distinguished Name Policy
[ policy ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied

#
# Distinguished Name Policy for Personal Certificates
[ user_policy ]
countryName             = supplied
stateOrProvinceName     = optional
localityName            = supplied
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = supplied
#xmppAddr               = optional # Added to SubjAltName by req

#
# Intermediate CA request options
[ req ]
default_bits            = 3072
default_keyfile         = private/intermed-ca.key
encrypt_key             = yes
default_md              = sha384
string_mask             = utf8only
utf8                    = yes
prompt                  = no
req_extensions          = req_ext
distinguished_name      = distinguished_name
subjectAltName          = subject_alt_name

#
# Intermediate CA Request Extensions
[ req_ext ]
subjectKeyIdentifier    = hash
subjectAltName          = @subject_alt_name

#
# Distinguished Name (DN)
[ distinguished_name ]
organizationName        = example.net
commonName              = example.net Intermediate Certification Authority

#
# Server Certificate Extensions
[ server_ext ]
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = critical, serverAuth, clientAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always
issuerAltName           = issuer:copy
authorityInfoAccess     = @auth_info_access
crlDistributionPoints   = crl_dist

#
# Client Certificate Extensions
[ client_ext ]
keyUsage                = critical, digitalSignature
extendedKeyUsage        = critical, clientAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always
issuerAltName           = issuer:copy
authorityInfoAccess     = @auth_info_access
crlDistributionPoints   = crl_dist

#
# User Certificate Extensions
[ user_ext ]
keyUsage                = critical, digitalSignature
extendedKeyUsage        = critical, clientAuth, emailProtection
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always
issuerAltName           = issuer:copy
authorityInfoAccess     = @auth_info_access
crlDistributionPoints   = crl_dist

#
# CRL Certificate Extensions
[ crl_ext ]
authorityKeyIdentifier  = keyid:always
issuerAltName           = issuer:copy

#
# Certificate Authorities Alternative Names
[ subject_alt_name ]
URI                     = http://ca.example.net/
email                   = certmaster@example.net

#
# Certificate download addresses for the intermediate CA
[ auth_info_access ]
caIssuers;URI           = http://ca.example.net/certs/example.net_Intermediate_Certification_Authority.cert.pem

#
# CRL Download address for the intermediate CA
[ crl_dist ]
fullname                = URI:http://ca.example.net/crl/example.net_Intermediate_Certification_Authority.crl

# EOF