/home/alex/dev/ca-tools.sh (1)

From RaySoft
#!/bin/bash -
# ------------------------------------------------------------------------------
# ca-tools.sh
# ===========
#
# Project   Mini CA
# Scope     macOS
# Copyright (C) 2024 by RaySoft, Zurich, Switzerland
# License   GNU General Public License (GPL) 2.0
#           https://www.gnu.org/licenses/gpl2.txt
#
# This script provide the most important functionality to build a CA and sign
# certificates. The whole CA is stored and protected by an encrypted DMG file.
#
# Use the command 'help' to see all commands & subcommands and their description.
#
# WARNING: This script uses functions from the 'nf.sh' and 'os.sh' libraries!
#
# ------------------------------------------------------------------------------

set -o 'noglob' -o 'nounset' -o 'pipefail' # -o 'errexit' -o 'xtrace'

# ------------------------------------------------------------------------------
# Configuration
# -------------

# Path to the global libraries
CA_DE_GLOBAL_LIB="${HOME}/dev/lib"

# Global libraries to be loaded
CA_GLOBAL_LIBS=('nf' 'os')

CA_DE_IMAGES="${HOME}/etc/ca"
CA_FE_CONFIG="${HOME}/etc/ca/openssl.cnf"

CA_CTRY='CH'
CA_LOC='Zurich'
CA_ORG='RaySoft'

CA_USER='alex'
CA_GROUP='users'

# ------------------------------------------------------------------------------
# Binary declaration
# ------------------

CA_X_CAT=('/usr/local/bin/gcat')
CA_X_CP=('/usr/local/bin/gcp')
CA_X_DISKUTIL=('/usr/sbin/diskutil')
CA_X_FIND=('/usr/local/bin/gfind')
CA_X_HDIUTIL=('/usr/bin/hdiutil')
CA_X_MDUTIL=('/usr/bin/mdutil')
CA_X_OPEN=('/usr/bin/open' '-a' 'Finder')
CA_X_OPENSSL=('/usr/local/opt/openssl/bin/openssl')
CA_X_RM=('/usr/local/bin/grm')
CA_X_SUDO=('/usr/bin/sudo')
CA_X_UMASK=('/usr/bin/umask')

# ------------------------------------------------------------------------------
# Public functions
# ----------------

ca::create_image() {
  # This public function creates an encrypted DMG image.
  #
  # Globals:
  #   CA_DE_IMAGES
  #
  # Arguments:
  #   $1: Name of CA

  if [[ "$#" -ne 1 ]]; then
    ca::_usage "Error calling subcommand 'image create': $*!"
  fi

  local ca_name="$1"
  local ca_image_name="CA-${ca_name}"
  local ca_image_file="${CA_DE_IMAGES}/${ca_image_name,,}.dmg"

  nf::print_heading '-' 'ba' "Create DMG image: ${ca_image_name}"

  if ! ca::_test_overwrite "${ca_image_file}"; then
    exit 1
  fi

  ca::_set_env

  ca::_set_passwd 'DMG image'

  if ! echo -n "${passwd}" \
     | "${CA_X_HDIUTIL[@]}" create -encryption 'AES-256' -fs 'APFS' -size '2m' \
       -stdinpass -volname "${ca_image_name}" "${ca_image_file}" 2>'/dev/null'
  then
    nf::carp -e "Error creating image: ${ca_image_file}!"
  fi

  ca::_reset_env

  ca::attach_image "${ca_name}"

  ca::_reset_passwd
}

ca::attach_image() {
  # This public function attaches an encrypted DMG image. It sets the ownership
  # flag of the image, disables Spotlight and Time Machine and sets the file
  # ownership and permissions.
  #
  # Globals:
  #   CA_DE_IMAGES
  #   CA_USER
  #   CA_GROUP
  #
  # Arguments:
  #   $1: Name of CA

  if [[ "$#" -ne 1 ]]; then
    ca::_usage "Error calling subcommand 'image attach': $*!"
  fi

  local ca_name="$1"
  local ca_image_name="CA-${ca_name}"
  local ca_image_file="${CA_DE_IMAGES}/${ca_image_name,,}.dmg"
  local ca_path="/Volumes/${ca_image_name}"

  if [[ -d "${ca_path}" ]]; then
    return 0
  fi

  nf::print_heading '-' 'ba' "Attach DMG image: ${ca_image_name}"

  if [[ ! -f "${ca_image_file}" ]]; then
    nf::carp -e "Error finding image file: ${ca_image_file}!"
  fi

  os::set_permissions "${CA_USER}" "${CA_GROUP}" 0600 0700 "${ca_image_file}"

  ca::_set_passwd 'DMG image'

  if ! echo -n "${passwd}" \
     | "${CA_X_HDIUTIL[@]}" attach -stdinpass "${ca_image_file}" 2>'/dev/null'
  then
    nf::carp -e 'Error attaching image!'
  fi

  ca::_reset_passwd

  # Enable image's ownership
  if ! "${CA_X_SUDO[@]}" "${CA_X_DISKUTIL[@]}" enableOwnership "${ca_path}" \
       2>'/dev/null'
  then
    nf::carp 'Error enabling ownership!'
  fi

  # Disable Spotlight
  if ! "${CA_X_SUDO[@]}" "${CA_X_MDUTIL[@]}" -d -E -i 'off' "${ca_path}" \
       2>'/dev/null'
  then
    nf::carp 'Error disabling Spotlight!'
  fi

  # Disable Time Machine
  if [[ ! -f "${ca_path}/.com.apple.timemachine.donotpresent" ]]; then
    ca::_set_env "${ca_path}"

    if ! echo -n >"${ca_path}/.com.apple.timemachine.donotpresent" \
         2>'/dev/null'
    then
      nf::carp 'Error disabling Time Machine!'
    fi

    ca::_reset_env
  fi

  os::set_permissions "${CA_USER}" "${CA_GROUP}" 0600 0700 "${ca_path}"

  if ! "${CA_X_OPEN[@]}" "${ca_path}" >'/dev/null' 2>&1; then
    nf::carp "Error opening directory in Finder: ${ca_path}!"
  fi

  if ! cd "${ca_path}" >'/dev/null' 2>&1; then
    nf::carp "Error changing directory: ${ca_path}!"
  fi
}

ca::detach_image() {
  # This public function detaches a DMG image.
  #
  # Arguments:
  #   $1: Name of CA

  if [[ "$#" -ne 1 ]]; then
    ca::_usage "Error calling subcommand 'image detach': $*!"
  fi

  local ca_name="$1"
  local ca_image_name="CA-${ca_name}"
  local ca_path="/Volumes/${ca_image_name}"

  nf::print_heading '-' 'ba' "Detach DMG image: ${ca_image_name}"

  if [[ ! -d "${ca_path}" ]]; then
    nf::carp -e "Error finding CA directory: ${ca_path}!"
  fi

  if [[ "${ca_path}" == "$(pwd)" ]]; then
    if ! cd "${HOME}" >'/dev/null' 2>&1; then
      nf::carp "Error changing directory: ${HOME}!"
    fi
  fi

  if ! "${CA_X_HDIUTIL[@]}" detach "${ca_path}" 2>'/dev/null'; then
    nf::carp -e 'Error detaching image!'
  fi
}

ca::create_ca() {
  # This public function creates a CA based of a private key and a self signed
  # certificate.
  #
  # Globals:
  #   CA_DE_IMAGES
  #   CA_FE_CONFIG
  #   CA_CTRY
  #   CA_LOC
  #   CA_ORG
  #
  # Arguments:
  #   $1: Name of CA

  if [[ "$#" -ne 1 ]]; then
    ca::_usage "Error calling subcommand 'ca create': $*!"
  fi

  local ca_name="$1"
  local ca_path="/Volumes/CA-${ca_name}"
  local cnf_file="${ca_path}/openssl.cnf"
  local key_file="${ca_path}/private/ca.key.pem"
  local cert_file="${ca_path}/certs/ca.cert.pem"
  local cert_subject="/C=${CA_CTRY}/L=${CA_LOC}/O=${CA_ORG}"
        cert_subject+="/CN=${ca_name} Root CA"

  nf::print_heading '-' 'ba' "Create CA: ${ca_name}"

  ca::attach_image "${ca_name}"

  if [[ ! -f "${cnf_file}" ]]; then
    if ! "${CA_X_CP[@]}" "${CA_FE_CONFIG}" "${cnf_file}" 2>'/dev/null'; then
      nf::carp -e "Error copying file: ${CA_FE_CONFIG}!"
    fi
  fi

  if ! ca::_test_overwrite "${key_file}" "${cert_file}"; then
    exit 1
  fi

  ca::_set_env "${ca_path}"

  if ! os::mkdir_ifn "${ca_path}/"{'certs','crl','newcerts','private','status'}
  then
    exit 1
  fi

  if [[ ! -f "${ca_path}/status/index" ]]; then
    if ! echo -n >"${ca_path}/status/index" 2>'/dev/null'; then
      nf::carp -e "Error creating file: ${ca_path}/status/index!"
    fi
  fi

  for item in 'crl/crlnumber' 'status/serial'; do
    if [[ ! -f "${ca_path}/${item}" ]]; then
      if ! echo 1000 >"${ca_path}/${item}" 2>'/dev/null'; then
        nf::carp -e "Error creating file: ${ca_path}/${item}!"
      fi
    fi
  done

  ca::_set_passwd 'CA private key'

  if ! "${CA_X_OPENSSL[@]}" genpkey \
       -algorithm 'RSA' -pkeyopt 'rsa_keygen_bits:4096' -aes-256-cbc \
       -out "${key_file}" -pass 'env:passwd' 2>'/dev/null'
  then
    nf::carp -e "Error creating CA private key: ${key_file}!"
  fi

  if ! "${CA_X_OPENSSL[@]}" req -new -x509 -config "${cnf_file}" \
       -extensions 'v3_ca' -subj "${cert_subject}" -days 3650 \
       -key "${key_file}" -passin 'env:passwd' -out "${cert_file}" 2>'/dev/null'
  then
    nf::carp -e "Error creating CA certificate: ${cert_file}!"
  fi

  ca::_reset_env

  "${CA_X_OPENSSL[@]}" x509 -text -in "${cert_file}" -noout

  ca::create_crl "${ca_name}"

  ca::_reset_passwd
}

ca::create_server_csr() {
  # This public function creates a private key and a Certificate Signing Request
  # (CSR) which can be used for a server (e.g. HTTPS).
  #
  # Arguments:
  #   $1: Name of CA
  #   $2: FQHN of machine

  if [[ "$#" -ne 2 ]]; then
    ca::_usage "Error calling subcommand 'server_csr create': $*!"
  fi

  local ca_name="$1"; shift
  local srv_fqdn="$1"
  local ca_path="/Volumes/CA-${ca_name}"
  local cnf_file="${ca_path}/openssl.cnf"
  local key_file="${ca_path}/private/${srv_fqdn}.key.pem"
  local csr_file="${ca_path}/certs/${srv_fqdn}.csr.pem"

  nf::print_heading '-' 'ba' "Creating server CSR: ${srv_fqdn}"

  ca::attach_image "${ca_name}"

  if [[ ! -f "${cnf_file}" ]]; then
    nf::carp -e "Error finding OpenSSL configuration file: ${cnf_file}!"
  fi

  if ! ca::_test_overwrite "${key_file}" "${csr_file}"; then
    exit 1
  fi

  ca::_set_env "${ca_path}"

  ca::_set_passwd 'server private key'

  if ! "${CA_X_OPENSSL[@]}" genpkey \
       -algorithm 'RSA' -pkeyopt 'rsa_keygen_bits:2048' -aes-256-cbc \
       -out "${key_file}" -pass 'env:passwd' 2>'/dev/null'
  then
    nf::carp -e "Error creating server private key: ${key_file}!"
  fi

  if ! "${CA_X_OPENSSL[@]}" req -new -config "${cnf_file}" \
       -subj "/CN=${srv_fqdn}" -key "${key_file}" -passin 'env:passwd' \
       -out "${csr_file}" 2>'/dev/null'
  then
    nf::carp -e "Error creating server CSR: ${csr_file}!"
  fi

  ca::_reset_passwd

  ca::_reset_env

  "${CA_X_OPENSSL[@]}" req -text -in "${csr_file}" -noout
}

ca::sign_server_csr() {
  # This public function signs a Certificate Signing Request (CSR) using the
  # CA's private key.
  #
  # Globals:
  #   CA_CTRY
  #   CA_LOC
  #   CA_ORG
  #
  # Arguments:
  #   $1: Name of CA
  #   $2: FQHN of machine
  #   $@: SANs of machine (optional)

  if [[ "$#" -lt 2 ]]; then
    ca::_usage "Error calling subcommand 'server_csr sign': $*!"
  fi

  local ca_name="$1"; shift
  local srv_fqdn="$1"; shift
  local sans_array=("$@")
  local sans_string="DNS:${srv_fqdn}"
  local ca_path="/Volumes/CA-${ca_name}"
  local cnf_file="${ca_path}/openssl.cnf"
  local csr_file="${ca_path}/certs/${srv_fqdn}.csr.pem"
  local cert_file="${ca_path}/certs/${srv_fqdn}.cert.pem"
  local cert_subject="/C=${CA_CTRY}/L=${CA_LOC}/O=${CA_ORG}"
        cert_subject+="/CN=${srv_fqdn}"

  nf::print_heading '-' 'ba' "Signing server CSR: ${srv_fqdn}"

  ca::attach_image "${ca_name}"

  if [[ ! -f "${cnf_file}" ]]; then
    nf::carp -e "Error finding OpenSSL configuration file: ${cnf_file}!"
  fi

  if [[ ! -f "${csr_file}" ]]; then
    nf::carp -e "Error finding CSR file: ${csr_file}!"
  fi

  if ! ca::_test_overwrite "${cert_file}"; then
    exit 1
  fi

  for item in "${sans_array[@]}"; do
    if [[ ! "${item}" =~ ^(DNS|IP): ]]; then
      nf::carp -e "Error parsing SAN: ${item}!"
    fi

    sans_string+=",${item}"
  done

  ca::_set_env "${ca_path}" "${sans_string}"

  ca::_set_passwd 'CA private key'

  if ! "${CA_X_OPENSSL[@]}" ca -batch -notext -config "${cnf_file}" \
       -extensions 'server_cert' -subj "${cert_subject}" -rand_serial \
       -passin 'env:passwd' -in "${csr_file}" -out "${cert_file}" 2>'/dev/null'
  then
    nf::carp -e 'Error signing CSR!'
  fi

  ca::_reset_passwd

  ca::_reset_env

  if [[ -f "${csr_file}" ]]; then
    if ! "${CA_X_RM[@]}" "${csr_file}" 2>'/dev/null'; then
      nf::carp 'Error deleting CSR!'
    fi
  fi

  "${CA_X_OPENSSL[@]}" x509 -text -in "${cert_file}" -noout
}

ca::revoke_cert() {
  # This public function revokes a certificate.
  #
  # Arguments:
  #   $1: Name of CA
  #   $2: Name of certificate

  if [[ "$#" -ne 2 ]]; then
    ca::_usage "Error calling subcommand 'cert revoke': $*!"
  fi

  local ca_name="$1"; shift
  local cert_name="$1"
  local ca_path="/Volumes/CA-${ca_name}"
  local cnf_file="${ca_path}/openssl.cnf"
  local cert_file="${ca_path}/certs/${cert_name}.cert.pem"

  nf::print_heading '-' 'ba' "Revoke the certificate: ${cert_name}"

  ca::attach_image "${ca_name}"

  if [[ ! -f "${cnf_file}" ]]; then
    nf::carp -e "Error finding OpenSSL configuration file: ${cnf_file}!"
  fi

  read -n 1 -p 'Press Ctrl-C to exit or any key to proceed.' -r -s
  echo; echo

  ca::_set_env "${ca_path}"

  ca::_set_passwd 'CA private key'

  if ! "${CA_X_OPENSSL[@]}" ca -revoke "${cert_file}" -notext \
       -config "${cnf_file}" -passin 'env:passwd' 2>'/dev/null'
  then
    nf::carp -e 'Error revoking certificate!'
  fi

  ca::_reset_env

  if ! "${CA_X_FIND[@]}" "${ca_path}/"{'certs','newcerts','private'} \
       -name "*${cert_name}.*.pem" -delete -printf "Delete %p\\n" \
       2>'/dev/null'
  then
    nf::carp "Error deleting revoked certificate: ${cert_file}!"
  fi

  ca::create_crl "${ca_name}"

  ca::_reset_passwd
}

ca::create_crl() {
  # This public function creates a Certificate Revocation List (CRL).
  #
  # Arguments:
  #   $1: Name of CA

  if [[ "$#" -ne 1 ]]; then
    ca::_usage "Error calling subcommand 'crl create': $*!"
  fi

  local ca_name="$1"
  local ca_path="/Volumes/CA-${ca_name}"
  local cnf_file="${ca_path}/openssl.cnf"
  local crl_file="${ca_path}/crl/ca.crl.pem"

  nf::print_heading '-' 'ba' "Creating CRL: ${ca_name}"

  ca::attach_image "${ca_name}"

  if [[ ! -f "${cnf_file}" ]]; then
    nf::carp -e "Error finding OpenSSL configuration file: ${cnf_file}!"
  fi

  if [[ -f "${crl_file}" ]]; then
    if ! "${CA_X_RM[@]}" "${crl_file}" 2>'/dev/null'; then
      nf::carp "Error deleting old CRL: ${crl_file}!"
    fi
  fi

  ca::_set_env "${ca_path}"

  ca::_set_passwd 'CA private key'

  if ! "${CA_X_OPENSSL[@]}" ca -gencrl -notext -config "${cnf_file}" \
       -crldays 365 -passin 'env:passwd' -out "${crl_file}" 2>'/dev/null'
  then
    nf::carp -e 'Error creating CRL!'
  fi

  ca::_reset_passwd

  ca::_reset_env

  "${CA_X_OPENSSL[@]}" crl -text -in "${crl_file}" -noout
}

# ------------------------------------------------------------------------------
# Private functions
# -----------------

ca::_usage() {
  # This private function prints the 'usage' message and ends the script with a
  # error.
  #
  # Arguments:
  #   $@: Problem message

  if [[ "$#" -eq 0 ]]; then
    nf::carp -e 'Error calling function!'
  fi

  local exe="${BASH_SOURCE[0]##*/}"

  "${CA_X_CAT[@]}" <<-EOF

		$*

		Usage:
		  ${exe} image ( create | attach | detach ) <ca>
		  ${exe} ca create <ca>
		  ${exe} server_csr ( create <ca> <fqdn> | sign <ca> <fqdn> [DNS|IP:<san> ...] )
		  ${exe} cert revoke <ca> <fqdn>
		  ${exe} crl create <ca>
		  ${exe} help
	EOF

  exit 1
} 1>&2

ca::_help() {
  # This private function prints the 'help' message.

  if [[ "$#" -ne 0 ]]; then
    nf::carp -e "Error calling function: $*!"
  fi

  local exe="${BASH_SOURCE[0]##*/}"

  "${CA_X_CAT[@]}" <<-EOF

		${exe} image ( create | attach | detach ) <ca>
		  create    Creates an encrypted DMG image.
		  attach    Attaches the DMG image to the file system.
		  detach    Detaches the DMG image from the file system.

		${exe} ca create <ca>
		  create    Creates a CA private key and a certificate.

		${exe} server_csr ( create <ca> <fqdn> | sign <ca> <fqdn> [DNS|IP:<san> ...] )
		  create    Creates a private key and a Certificate Signing Request (CSR)
		            for a server.
		  sign      Signs a Certificate Signing Request (CSR) for a server.

		${exe} cert revoke <ca> <fqdn>
		  revoke    Revokes a certifikate.

		${exe} crl create <ca>
		  create    Creates a Certificate Revocation List (CRL).

		${exe} help
		  Shows this help message.
	EOF
}

ca::_set_env() {
  # This private function sets the environment.
  #
  # Globals:
  #   CA_CTRY
  #   CA_LOC
  #   CA_ORG
  #
  # Arguments:
  #   $1: Path to CA (optional)
  #   $2: SANs string (optional)

  declare -g -x CA_{CTRY,LOC,ORG} ca_path="${1-}" cert_sans="${2-}" \
                sys_umask="$("${CA_X_UMASK[@]}")"

  "${CA_X_UMASK[@]}" 0077
}

ca::_reset_env() {
  # This private function resets the environment.
  #
  # Globals:
  #   CA_CTRY
  #   CA_LOC
  #   CA_ORG

  "${CA_X_UMASK[@]}" "${sys_umask}"

  declare +g +x CA_{CTRY,LOC,ORG} ca_path cert_sans

  unset -v sys_umask
}

ca::_set_passwd() {
  # This private function asks for a password and sets the environment.
  #
  # Arguments:
  #   $1: Purpose

  if [[ "$#" -ne 1 ]]; then
    nf::carp -e "Error calling function: $*!"
  fi

  if [[ -z "${passwd-}" ]]; then
    read -p "Enter the '$1' password: " -r -s passwd
    echo; echo

    declare -g -x passwd
  fi
}

ca::_reset_passwd() {
  # This private function resets the environment.

  unset -v passwd
}

ca::_test_overwrite() {
  # This private function tests if a file already exists and prints a
  # message.

  for item in "$@"; do
    if [[ -f "${item}" ]]; then
      nf::carp "File must not be overwritten: ${item}!"
      return 1
    fi
  done
}

ca::_trap() {
  # This private function defines the commands which are executed when the
  # corresponding handler is activated.

  ca::_reset_env
  ca::_reset_passwd

  echo

  exit 1
}

# ------------------------------------------------------------------------------
# Main
# ----

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  # Test if library path is available
  if [[ ! -d "${CA_DE_GLOBAL_LIB}" ]]; then
    echo "Error finding directory: ${CA_DE_GLOBAL_LIB}"
    exit 1
  fi

  # Load libraries
  for lib in "${CA_GLOBAL_LIBS[@]}"; do
    lib="${CA_DE_GLOBAL_LIB}/${lib}.sh"

    if [[ ! -f "${lib}" ]]; then
      echo "Error finding library: ${lib}!"
      exit 1
    fi

    if ! source "${lib}"; then
      exit 1
    fi
  done

  # Test the defined variables
  if ! os::test_env 'CA'; then
    exit 1
  fi

  if [[ ! "$("${CA_X_OPENSSL[@]}" version 2>'/dev/null')" =~ ^OpenSSL ]]; then
    nf::carp -e 'Error finding OpenSSL!'
  fi

  if ! trap 'ca::_trap' SIG{INT,QUIT,TERM} >'/dev/null' 2>&1; then
    nf::carp -e 'Error setting up trap!'
  fi

  nf::print_heading '=' 'b' 'Mini CA Tools'

  command="${1-}"; shift
  subcommand="${1-}"; shift

  case "${command}" in
    '') ca::_usage 'Error finding command!' ;;
    'image')
      case "${subcommand}" in
        '') ca::_usage "Error finding '${command}' subcommand!" ;;
        'create') ca::create_image "$@" ;;
        'attach') ca::attach_image "$@" ;;
        'detach') ca::detach_image "$@" ;;
        *) ca::_usage "Error validating '${command}' subcommand: ${subcommand}!" ;;
      esac ;;
    'ca')
      case "${subcommand}" in
        '') ca::_usage "Error finding '${command}' subcommand!" ;;
        'create') ca::create_ca "$@" ;;
        *) ca::_usage "Error validating '${command}' subcommand: ${subcommand}!" ;;
      esac ;;
    'server_csr')
      case "${subcommand}" in
        '') ca::_usage "Error finding '${command}' subcommand!" ;;
        'create') ca::create_server_csr "$@" ;;
        'sign') ca::sign_server_csr "$@" ;;
        *) ca::_usage "Error validating '${command}' subcommand: ${subcommand}!" ;;
      esac ;;
    'cert')
      case "${subcommand}" in
        '') ca::_usage "Error finding '${command}' subcommand!" ;;
        'revoke') ca::revoke_cert "$@" ;;
        *) ca::_usage "Error validating '${command}' subcommand: ${subcommand}!" ;;
      esac ;;
    'crl')
      case "${subcommand}" in
        '') ca::_usage "Error finding '${command}' subcommand!" ;;
        'create') ca::create_crl "$@" ;;
        *) ca::_usage "Error validating '${command}' subcommand: ${subcommand}!" ;;
      esac ;;
    'help') ca::_help "$@" ;;
    *) ca::_usage "Error validating command: ${command}!" ;;
  esac

  exit 0
fi

# ------------------------------------------------------------------------------

Usage

NOTE:
This script uses functions from the libraries ~/dev/lib/nf.sh & ~/dev/lib/os.sh.
Provide the configuration template

Copy the openssl.cnf template to the path which is defined in the variable CA_FE_CONFIG.

Show help
~/dev/ca-tools.sh help
Create an image and a CA certificate
ca_name='RaySoft'

~/dev/ca-tools.sh image create "${ca_name}"

~/dev/ca-tools.sh ca create "${ca_name}"
Create a CSR and sign it
ca_name='RaySoft'
srv_name='neon.raysoft.loc'
san_names=('DNS:mx1.raysoft.loc' 'DNS:ns1.raysoft.loc' 'DNS:ntp1.raysoft.loc')

~/dev/ca-tools.sh server_csr create "${ca_name}" "${srv_name}"

~/dev/ca-tools.sh server_csr sign "${ca_name}" "${srv_name}" "${san_names[@]}"
Detach image
ca_name='RaySoft'

~/dev/ca-tools.sh image detach "${ca_name}"