Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A walkthrough for setup and maintain of a private self-signed CA

Running your own self-signed CA

Prepare CA directory

Lets start by initialising the folder where our CA will reside and updating configurations to match your system:

  • Initialise the directory structure:

    export OPENSSL_CONF="$(pwd)/openssl.cnf"
    touch index.txt index.txt.attr
    mkdir config private public
    openssl rand -hex -out serial 5
    
  • Copy openssl-add-san script somewhere in your path, eg: /usr/local/bin

  • Copy CA configuration file onto your system and update the following settings:

    • ca_default > dir - this is the directory where the CA will reside (at this point this should be your current directory).

    • ca_default > certs - this is the directory where openssl ca utility stores backups of all certificates it generates. This is targeted at larger and more complex CA setups. In a low volume / experimental CA this directory quickly fills up with never-used / test certificates. Thus, we point it at the Trash.

    • req_distinguished_name > *_default - un-comment/comment out appropriate items and add meaningful defaults within this section.

    • authority_info_access > URI - add a publicly accessible URI for your ca.cert.pem file

    • crl_distribution_points > fullname - add a publicly accessible URI for your ca.crl.pem file

  • Enable "Folder Actions" on macOS (A similar setup can be achieved on Linux using such tools as fswatch, inotify-tools):

    • open Automator and create a new Folder Action
    • attach this new Folder Action to the folder where your CA resides
    • add "Filter Finder Items" action with the following conditions:
      • Find files where ANY of the following conditions are true
      • Name begins with "serial"
      • Name begins with "index.txt"
    • add "Move Finder Items to Bin" action

    From now on, all new files created within the folder that match the above conditions will be automatically removed. The folder action does not apply to files that already exist in the directory (inc. renamed files).

Create CA certificate

Once the maintenance job (aka Folder Action) is watching over our CA directory, we can proceed to creating a self-signed CA certificate:

  • Generate CA private key:

    openssl ecparam -genkey -name secp384r1 | openssl ec -out private/key.pem  -aes256
    # openssl genpkey -algorithm ED25519 -aes256 -out private/key.pem
    
  • Create a Certificate Signing Request (CSR) for the new CA cert:

    openssl req -new -key private/key.pem -out public/csr.pem
    openssl req -noout -text -verify -in public/csr.pem
    
  • Self-sign the above CA Certificate Signing Request:

    openssl rand -hex -out serial 5
    openssl ca -selfsign -extensions new_ca -startdate 190101000000Z -enddate 380118000000Z -in public/csr.pem -out public/cert.pem
    openssl x509 -noout -text -in ca.cert.pem
    
  • Backup CA private key and the certificate into a PFX bundle (optional):

    openssl pkcs12 -export -inkey private/key.pem -in public/cert.pem -out private/backup.pfx
    
  • Add CA certificate to macOS System key chain:

    sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain public/cert.pem
    

Useful links:

Create a Certificate Revocation List

Once CA certificate is created, we can add a custom Certificate Revocation List and ensure that it works with certificate validation commands.

  • Create a new Certificate Revocation List

    openssl rand -hex -out serial 5
    openssl ca -updatedb
    openssl ca -gencrl -out public/crl.pem
    openssl crl -text -in public/crl.pem
    
  • Link certificates by their serial numbers

    ln -sf cert.pem "public/$(openssl x509 -noout -subject_hash -in public/cert.pem).0"
    ln -sf crl.pem "public/$(openssl crl -noout -hash -in public/crl.pem).r0"
    

NOTE: Don't forget to upload the new CRL to the CRL endpoint defined in the configuration file (see [ crl_distribution_points ]).

Issue a client certificate

Now that we have a functioning CA, we can create client certificates for our users in much the same way as we've created the CA cert.

NOTE: Remember to include your custom OpenSSL configuration file:
export OPENSSL_CONF="path/to/openssl.cnf"

  • Generate user private key:

    openssl ecparam -genkey -name secp384r1 | openssl ec -out key.pem -aes256
    # openssl genpkey -algorithm ED25519 -aes256 -out key.pem
    
  • Create a Certificate Signing Request (CSR) for the client cert:

    openssl req -new -key key.pem -out csr.pem
    openssl req -noout -text -verify -in csr.pem
    
  • Sign the client certificate with the CA:

    openssl rand -hex -out "$(dirname "${OPENSSL_CONF}")/serial" 5
    openssl ca -extensions new_client -in csr.pem -out cert.pem
    

    To provide a custom subjectAltName when signing the certificate, create cert.san (eg: echo "email.0 = me@example.com" > cert.san), and use the alternative form of to the above commands:

    openssl rand -hex -out "${OPENSSL_DIR}/serial" 5
    openssl ca -extfile <( openssl-add-san new_client cert.san ) -in csr.pem -out cert.pem
    

    As always, validate client certificate once it's generated:

    openssl x509 -noout -text -in cert.pem
    
  • Create a certificate distribution bundle (in PFX format) which will encapsulate both client's private key and client's public certificate:

    openssl pkcs12 -export -inkey key.pem -in cert.pem -out client.pfx
    

At this point, client's *.pem files can be deleted.

Issue a server certificate

Last but not least, we can create a certificate for our server. The approach is very similar to what we did before:

NOTE: Remember to include your custom OpenSSL configuration file:
export OPENSSL_CONF="path/to/openssl.cnf"

  • Generate server private key:

    # openssl genpkey -algorithm RSA -out key.pem
    openssl ecparam -genkey -name secp384r1 | openssl ec -out key.pem -aes256
    
  • Create a Certificate Signing Request (CSR) for the server cert:

    openssl req -new -key key.pem -out csr.pem
    openssl req -noout -text -verify -in csr.pem
    
  • Sign the server certificate with the CA:

    openssl rand -hex -out "$(dirname "${OPENSSL_CONF}")/serial" 5
    openssl ca -extensions new_server -in csr.pem -out cert.pem
    

    You provide a custom subjectAltName when signing the certificate by creating a custom cert.san (eg: echo "DNS.0 = localhost" > cert.san), then using the following alternative to the above commands:

    openssl rand -hex -out "$(dirname "${OPENSSL_CONF}")/serial" 5
    openssl ca -extfile <( openssl-add-san new_server cert.san ) -in csr.pem -out cert.pem
    

    As always, validate server certificate once it's generated:

    openssl x509 -noout -text -in cert.pem
    
  • Create a certificate distribution bundle (in PFX format) which will encapsulate both client's private key and client's public certificate:

    openssl pkcs12 -export -inkey key.pem -in cert.pem -out server.pfx
    

Check a certificate against CRL

Any certificate can be validated against the Certificate Revocation List (CRL) maintained by the Certificate Authority (CA) that issued that certificate. In our case, the job is made simpler by including crlDistributionPoints within the generated certificates:

openssl verify -crl_check -CApath path/to/ca_dir path/to/cert.pem

Useful links:

Renew an expired certificate

Keeping the same private key on your root CA allows for all certificates to continue to validate successfully against the new root; all that's required of you is to trust the new root. In fact, keeping the same private key on any certificate allows you to reissue that certificate with a new set of parameters (eg: a new expiry date).

Thus, provided we have access to the original server.key.pem, the certificate of the server can be renewed with:

openssl req -new -key server.key.pem -out server.csr.pem
openssl ca -batch -extensions new_server -in server.csr.pem -out server.cert.pem

Revoke a valid certificate

Currently valid certificates can be revoked manually or via the command line:

openssl ca -revoke /path/to/cert.pem
openssl ca -gencrl -out ca.crl.pem

To revoke a certificate manually (useful for when you don't have access to the actual certificate file):

  • Open index.txt in your editor of choice
    • Find the line representing the certificate you wish to revoke
    • Replace whatever letter the line starts with with R (for Revoked)
    • Add revocation time stamp (yymmddhhmmssZ) between certificate expiration timestamp and certificate serial number.
  • Update the CRL: openssl ca -gencrl -out ca.crl.pem

NOTE: Don't forget to upload the new CRL to the CRL endpoint defined in the configuration file (see [ crl_distribution_points ]).

Useful links:

--

Sources:

#!/usr/bin/env bash
: ${1?-Missing section name}
: ${2?-Missing SAN file }
: ${OPENSSL_CONF:="../openssl.cnf"}
sed -e "1,/\[ ${1} ]\$/d" -e '/^\s*$/,$d' ${OPENSSL_CONF}
echo 'subjectAltName = @subject_alt_name'
echo
sed -n '/^\[ authority_info_access ]$/,/^\s*$/p' ${OPENSSL_CONF}
sed -n '/^\[ crl_distribution_points ]$/,/^\s*$/p' ${OPENSSL_CONF}
echo '[ subject_alt_name ]'
cat ${2}
[ ca ]
default_ca = ca_default
[ ca_default ]
dir = __YOUR_CA_DIR__
certs = /Users/__YOUR_MAC_USERNAME__/.Trash
# Directories and file locations
database = ${dir}/index.txt
serial = ${dir}/serial
new_certs_dir = ${certs}
# CA private key and certificates
private_key = ${dir}/private/key.pem
certificate = ${dir}/public/cert.pem
# CRL certificate and extensions
crlnumber = ${serial}
default_crl_days = 365
crl_extensions = new_crl
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha512
# Format certificate details during signing
name_opt = ca_default
cert_opt = ca_default
# and the rest ...
default_days = 365
copy_extensions = copy
email_in_dn = no
policy = policy
preserve = no
unique_subject = no
# https://support.apple.com/en-us/HT210176
# default_startdate = 190101000000Z
# https://en.wikipedia.org/wiki/Year_2038_problem
# default_enddate = 380118000000Z
[ req ]
default_bits = 4096
string_mask = utf8only
utf8 = yes
distinguished_name = req_distinguished_name
x509_extensions = new_req
req_extensions = new_req
[ policy ]
countryName = supplied
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req_distinguished_name ]
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = __YOUR_NAME__
commonName_max = 64
emailAddress = Email Address
emailAddress_default = __YOUR_EMAIL__
emailAddress_max = 64
0.organizationName = Organization Name (e.g. company)
0.organizationName_default = Root CA
0.organizationName_max = 64
organizationalUnitName = Organizational Unit Name (freeform)
organizationalUnitName_max = 64
localityName = Locality Name (e.g. city)
localityName_max = 64
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_max = 64
countryName = Country Name (2 letter code)
countryName_default = __YOUR_COUNTRY___
countryName_min = 2
countryName_max = 2
# Key Usage
# ---------
#
# cRLSign - subject public key is used for verifying signatures on
# certificate revocation lists (aka CRL).
# keyCertSign - subject public key is used for verifying signatures on
# public key certificates. For this to work the CA bit in
# the basic constraints extension MUST also be enabled.
#
# General usage
# -------------
#
# digitalSignature - used for entity authentication and data origin
# authentication with integrity.
# nonRepudiation - same as 'digitalSignature' but also asserts to those
# verifying the signature that you can't easily deny it
# was you who signed it. (aka contentCommitment)
# keyEncipherment - used by the subject to encrypt a symmetric key which is
# then transferred to the target, decrypted, and used to
# encrypt and decrypt data sent between the two entities.
# dataEncipherment - used by the subject to encrypt and decrypt actual
# application data. This is not used in TLS but might be
# relevant to S/MIME, VPN, signing of documents, etc.
# keyAgreement - enables the subject to use a key agreement protocol to
# establish a symmetric key with a target that may then be
# used to encrypt and decrypt transferred data.
# encipherOnly - when enabled AND 'keyAgreement' is also set, subject
# public key may be used ONLY for enciphering data while
# performing key agreement.
# decipherOnly - when enabled AND 'keyAgreement' is also set, subject
# public key may be used ONLY for deciphering data while
# performing key agreement.
#
# Extended usage
# --------------
#
# serverAuth - Web/VPN Server authentication, distinguishes a server
# which clients can authenticate against.
# Req: digitalSignature or nonRepudiation, and/or
# keyEncipherment or keyAgreement
# clientAuth - Web/VPN Client authentication, distinguishing a client
# as a client only.
# Req: digitalSignature or nonRepudiation, and/or
# keyAgreement
# codeSigning - does exactly what it says on the tin.
# Req: digitalSignature or nonRepudiation, and/or
# keyEncipherment or keyAgreement
# emailProtection - Email Protection via S/MIME, allows you to send and
# receive encrypted emails.
# Req: digitalSignature or nonRepudiation, and/or
# keyEncipherment or keyAgreement
# timeStamping - Trusted Timestamping... self explanatory
# Req: digitalSignature or nonRepudiation
# OCSPSigning - Modern alternative to CRL which run as a daemon process
# over HTTPS connection. (Requires permanent connection.)
[ new_crl ]
authorityKeyIdentifier = keyid,issuer:always
basicConstraints = critical, CA:false
issuerAltName = issuer:copy
[ new_req ]
subjectAltName = email:move
[ new_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, cRLSign, keyCertSign
extendedKeyUsage = critical, OCSPSigning
authorityInfoAccess = @authority_info_access
crlDistributionPoints = crl_distribution_points
[ new_client ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
basicConstraints = critical, CA:false
keyUsage = critical, digitalSignature, nonRepudiation, dataEncipherment, keyEncipherment
extendedKeyUsage = critical, clientAuth, codeSigning, emailProtection, timeStamping
issuerAltName = issuer:copy
authorityInfoAccess = @authority_info_access
crlDistributionPoints = crl_distribution_points
[ new_server ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
basicConstraints = critical, CA:false
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment
extendedKeyUsage = critical, serverAuth, clientAuth, timeStamping
subjectAltName = @subject_alt_name
issuerAltName = issuer:copy
authorityInfoAccess = @authority_info_access
crlDistributionPoints = crl_distribution_points
[ authority_info_access ]
caIssuers;URI = __YOUR_CERTIFIACATE_URL__
[ crl_distribution_points ]
fullname = URI:__YOUR_CRL_URL__
[ subject_alt_name ]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment