#!/usr/bin/bash

# NAME: 'kmodgenca'
# PURPOSE: Helper script to create CA/key pair to sign modules.
# Copyright (c) 2017 Stanislas Leduc <stanislas.leduc@balinor.net>
# Copyright (c) 2018-2019 Nicolas Viéville <nicolas.vieville@uphf.fr>
# Copyright (c) 2024 Rohan Barar <rohan.barar@gmail.com>

################################################################################
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
################################################################################

# EXIT STATUS CODES AND DESCRIPTIONS
# 0 - SUCCESS
# 1 - INSUFFICIENT PRIVILEGES
# 2 - INVALID COMMAND LINE ARGUMENT
# 3 - BROKEN SYMLINKS TO DEFAULT KEY PAIR
# 4 - MISSING CACERT CONFIGURATION TEMPLATE
# 5 - FAILED TO READ CA CERTIFICATE CONFIGURATION TEMPLATE
# 6 - FAILED TO WRITE CA CERTIFICATE CONFIGURATION FILE
# 7 - UNSUCCESSFUL OPENSSL KEY PAIR CREATION COMMAND
# 8 - FAILED TO CREATE KEY PAIR FILES

# ENFORCE STRICT ERROR HANDLING
# - Exit script on error.
# - Ensure pipelines fail on the first error.
set -eo pipefail

# DECLARE CONSTANTS
# Script Information
readonly SCRIPT_NAME="kmodgenca"
readonly SCRIPT_VERSION="0.6.0"

# Directories
readonly AKMODS_DIR="/etc/pki/akmods"
readonly PRIVATE_KEY_DIR="${AKMODS_DIR}/private"
readonly PUBLIC_KEY_DIR="${AKMODS_DIR}/certs"

# Paths
readonly PRIVATE_KEY_PATH="${PRIVATE_KEY_DIR}/private_key.priv"
readonly PUBLIC_KEY_PATH="${PUBLIC_KEY_DIR}/public_key.der"
readonly CACERT_CONFIG_PATH="${AKMODS_DIR}/cacert.config"
readonly RESTORECON_PATH="/usr/sbin/restorecon"

# ANSI
readonly BOLD_RED_TEXT="\e[1;31m"
readonly BOLD_YELLOW_TEXT="\e[1;33m"
readonly BOLD_GREEN_TEXT="\033[1;32m"
readonly BOLD_BLUE_TEXT="\e[1;34m"
readonly BOLD_GREY_TEXT="\e[1;37m"
readonly CLEAR_TEXT="\e[0m"

# DECLARE VARIABLES
# Command Line Argument Flags
FORCE_BUILD=0
AUTOMATIC_BUILD=0
SHOW_HELP=0
SHOW_VER=0
BAD_ARGS=0

# Unique New Key Pair Name (Hostname + UNIX/POSIX Timestamp + Dashless UUID)
cert_hostname="${HOSTNAME}"
KEYNAME="${cert_hostname:0:44}_$(date +%s)_$(uuidgen | awk -F '-' '{print $1}')"

# Other
AUTOMATIC_BUILD_OPTION=""

# FUNCTIONS
function help() {
    echo -e "${BOLD_GREY_TEXT}KMODGENCA HELP${CLEAR_TEXT}"
    echo "Creates a Certificate Authority (CA) and key pair for module signing."
    echo "Private keys are created in:               '${PRIVATE_KEY_DIR}'."
    echo "Public keys (certificates) are created in: '${PUBLIC_KEY_DIR}'."
    echo -e "\nUsage: ${SCRIPT_NAME} [OPTIONS]"
    echo -e "\nOptions:"
    echo "  -a, --auto           Utilise default values for 'cacert.config'."
    echo "  -f, --force          Create CA/key pair even if one already exists."
    echo "  -h, --help           Display this help message."
    echo "  -V, --version        Display script version information."
    echo ""
}

function check_root() {
    # Notify user.
    echo -e "${BOLD_BLUE_TEXT}INFO:${CLEAR_TEXT} CHECKING FOR ELEVATED PRIVILEGES..."

    if [ "$EUID" -ne 0 ]; then
        echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} INSUFFICIENT PRIVILEGES!" >&2
        echo "Please run the command using 'sudo' or as root." >&2
        echo "Quitting." >&2
        exit 1
    fi
}

function parse_arguments() {
    if [ $# -gt 0 ]; then
        while [ "$1" ] ; do
            case "$1" in
            -a|--auto)
                AUTOMATIC_BUILD=1
                shift
                ;;
            -f|--force)
                FORCE_BUILD=1
                shift
                ;;
            -h|--help)
                SHOW_HELP=1
                shift
                ;;
            -V|--version)
                SHOW_VER=1
                shift
                ;;
            -*)
                # Handle combined single-letter options.
                for (( i=1; i<${#1}; i++ )); do
                    case "${1:$i:1}" in
                    a)
                        AUTOMATIC_BUILD=1
                        ;;
                    f)
                        FORCE_BUILD=1
                        ;;
                    h)
                        SHOW_HELP=1
                        ;;
                    V)
                        SHOW_VER=1
                        ;;
                    *)
                        echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} INVALID OPTION '${1:$i:1}' in '${1}'." >&2
                        BAD_ARGS=1
                        ;;
                    esac
                done
                shift
                ;;
            *)
                echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} INVALID OPTION '${1}'." >&2
                BAD_ARGS=1
                shift
                ;;
            esac
        done
    fi

    # Display help message and then exit in the event of invalid argument(s).
    if [[ "$BAD_ARGS" -eq 1 ]]; then
        echo "" >&2
        help >&2
        echo "Quitting." >&2
        exit 2
    fi

    # Display script help information if requested.
    if [[ "$SHOW_HELP" -eq 1 ]]; then
        help
    fi

    # Display script version information if requested.
    if [[ "$SHOW_VER" -eq 1 ]]; then
        echo "${SCRIPT_NAME} v${SCRIPT_VERSION}"
    fi

    # Exit script if version and/or help information requested.
    if [ "$SHOW_VER" -eq 1 ] || [ "$SHOW_HELP" -eq 1 ]; then
        if [ "$AUTOMATIC_BUILD" -eq 1 ]; then
            echo -e "${BOLD_YELLOW_TEXT}WARNING:${CLEAR_TEXT} IGNORING '-a' (--auto)." >&2
        fi
        if [ "$FORCE_BUILD" -eq 1 ]; then
            echo -e "${BOLD_YELLOW_TEXT}WARNING:${CLEAR_TEXT} IGNORING '-f' (--force)." >&2
        fi
        exit 0
    fi

    # Warn user regarding forced builds.
    if [[ "$FORCE_BUILD" -eq 1 ]]; then
        echo -e "${BOLD_YELLOW_TEXT}WARNING:${CLEAR_TEXT} FORCED BUILD SELECTED. KEY PAIR OVERWRITE MAY OCCUR!" >&2
    fi

    # Warn user regarding automatic builds.
    if [[ "$AUTOMATIC_BUILD" -eq 1 ]]; then
        echo -e "${BOLD_YELLOW_TEXT}WARNING:${CLEAR_TEXT} AUTOMATIC BUILD SELECTED. USING DEFAULT VALUES FOR CA/KEY PAIR CREATION." >&2
    fi
}

function check_broken_key_pair() {
    # Check for broken non-selected key pairs.
    local unmatched_public_key_paths=()
    local unmatched_private_key_paths=()

    # Store paths of public and private keys.
    local public_key_paths=()
    local private_key_paths=()
    # Note: Requires superuser permissions (i.e., sudo).
    mapfile -t public_key_paths < <(find "$PUBLIC_KEY_DIR" -maxdepth 1 -name "*.der")
    mapfile -t private_key_paths < <(find "$PRIVATE_KEY_DIR" -maxdepth 1 -name "*.priv")

    # Find public/private keys without corresponding private/public keys.
    local key_file_path
    for key_file_path in "${public_key_paths[@]}"; do
        # Skip symlink.
        if [[ "$key_file_path" == "$PUBLIC_KEY_PATH" ]]; then
            continue
        fi

        # Remove file extension.
        local public_key_name
        public_key_name="$(basename "$key_file_path")"
        public_key_name="${public_key_name%.*}"

        # Check if the corresponding private key exists.
        local found=0
        for private_key_path in "${private_key_paths[@]}"; do
            if [[ "$private_key_path" == "${PRIVATE_KEY_DIR}/${public_key_name}.priv" ]]; then
                found=1
                break
            fi
        done

        # Store public key file name (with extension) if unpaired.
        if [[ "$found" -eq 0 ]]; then
            unmatched_public_key_paths+=("$key_file_path")
        fi
    done

    for key_file_path in "${private_key_paths[@]}"; do
        # Skip symlink.
        if [[ "$key_file_path" == "$PRIVATE_KEY_PATH" ]]; then
            continue
        fi

        # Remove file extension.
        local private_key_name
        private_key_name="$(basename "$key_file_path")"
        private_key_name="${private_key_name%.*}"

        # Check if the corresponding public key exists.
        local found=0
        for public_key_path in "${public_key_paths[@]}"; do
            if [[ "$public_key_path" == "${PUBLIC_KEY_DIR}/${private_key_name}.der" ]]; then
                found=1
                break
            fi
        done

        # Store private key file name (with extension) if unpaired.
        if [[ "$found" -eq 0 ]]; then
            unmatched_private_key_paths+=("$key_file_path")
        fi
    done

    # Check if isolated keys were detected.
    if [[ ${#unmatched_private_key_paths[@]} -gt 0 || ${#unmatched_public_key_paths[@]} -gt 0 ]]; then
        echo -e "${BOLD_YELLOW_TEXT}WARNING:${CLEAR_TEXT} SOME KEY PAIRS ARE BROKEN!" >&2

        # Notify user regarding isolated public keys.
        if [[ ${#unmatched_public_key_paths[@]} -gt 0 ]]; then
            echo "Isolated Public Keys:" >&2
            local isolated_pub_key_path
            for isolated_pub_key_path in "${unmatched_public_key_paths[@]}"; do
                echo "    ${isolated_pub_key_path}" >&2
            done
            echo "" >&2
        fi

        # Notify user regarding isolated private keys.
        if [[ ${#unmatched_private_key_paths[@]} -gt 0 ]]; then
            echo "Isolated Private Keys:" >&2
            local isolated_pri_key_path
            for isolated_pri_key_path in "${unmatched_private_key_paths[@]}"; do
                echo "    ${isolated_pri_key_path}" >&2
            done
            echo "" >&2
        fi
    fi

    # Terminate the script when:
    #   1. A certificate (public key) OR private key exists (but not both), AND
    #   2. A forced rebuild was not requested (i.e., 'FORCE_BUILD' is NOT '1')

    # Check for broken symlinks to the currently selected pair of keys.
    # Note: Requires superuser permissions (i.e. sudo).
    # shellcheck disable=SC2155
    local pub_key_exists=$(readlink -e "$PUBLIC_KEY_PATH" &>/dev/null && echo 1 || echo 0)

    # Note: Requires superuser permissions (i.e. sudo).
    # shellcheck disable=SC2155
    local pri_key_exists=$(readlink -e "$PRIVATE_KEY_PATH" &>/dev/null && echo 1 || echo 0)

    if [[ "$pub_key_exists" -ne "$pri_key_exists" && "$FORCE_BUILD" -eq 0 ]]; then
        # Notify user.
        echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} BROKEN SYMLINK(S) TO THE DEFAULT KEY PAIR!" >&2
        echo "Valid symlinks to a public and private key must exist." >&2
        echo "" >&2

        # Dynamic status output with colours.
        echo -e "${PUBLIC_KEY_PATH}: $( [[ $pub_key_exists -eq 1 ]] && echo -e "${BOLD_GREEN_TEXT}WORKING${CLEAR_TEXT}" || echo -e "${BOLD_RED_TEXT}BROKEN${CLEAR_TEXT}" )" >&2
        echo -e "${PRIVATE_KEY_PATH}: $( [[ $pri_key_exists -eq 1 ]] && echo -e "${BOLD_GREEN_TEXT}WORKING${CLEAR_TEXT}" || echo -e "${BOLD_RED_TEXT}BROKEN${CLEAR_TEXT}" )" >&2
        echo "" >&2
        echo "Quitting." >&2

        # Exit script.
        exit 3
    fi
}

function check_existing_key_pair() {
    # Notify user.
    echo -e "${BOLD_BLUE_TEXT}INFO:${CLEAR_TEXT} CHECKING FOR AN EXISTING KEY PAIR..."

    # Terminate the script when:
    #   1. Both a certificate (public key) and private key already exist, AND
    #   2. A forced rebuild was not requested (i.e., 'FORCE_BUILD' is NOT '1')

    # Note: This approach will return '1' in the event of a broken symlink.
    # Note: Requires superuser permissions (i.e. sudo).
    if readlink -e "$PUBLIC_KEY_PATH" &>/dev/null && \
       readlink -e "$PRIVATE_KEY_PATH" &>/dev/null && \
       [ "$FORCE_BUILD" -eq 0 ]; then

        # Notify user.
        echo -e "${BOLD_YELLOW_TEXT}WARNING:${CLEAR_TEXT} EXISTING KEY PAIR." >&2
        echo "Please specify argument '--force' to overwrite the existing key pair." >&2
        echo "Quitting." >&2

        # Exit script.
        exit 0
    fi
}

function set_key_pair_name() {
    if [ "$AUTOMATIC_BUILD" -eq 0 ]; then
        while true; do
            local key_pair_file_name=""
            local valid_name=1

            # Request key pair name from user.
            # shellcheck disable=SC2162
            read -p "Key Pair Name: " key_pair_file_name

            # Check for empty string.
            if [[ -z $(echo "$key_pair_file_name" | xargs) ]]; then
                valid_name=0
                echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} NAME MUST NOT BE EMPTY.\n" >&2
            fi

            # Ensure name is not '.' or '..'.
            if [[ $(echo "$key_pair_file_name" | xargs) == "." ]] || [[ $(echo "$key_pair_file_name" | xargs) == ".." ]]; then
                valid_name=0
                echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} NAME MUST NOT BE '.' OR '..'.\n" >&2
            fi

            # Ensure name is not longer than 255 characters.
            if [ "$(echo "$key_pair_file_name" | xargs | awk '{print length}')" -gt 255 ]; then
                valid_name=0
                echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} NAME MUST NOT BE LONGER THAN 255 CHARACTERS.\n" >&2
            fi

            # Ensure name only contains valid characters.
            # - Letters (A-Z) (a-z)
            # - Numbers (0-9)
            # - Special
            #   - Period ('.')
            #   - Underscore ('_')
            #   - Hyphen ('-')
            if ! [[ $(echo "$key_pair_file_name" | xargs) =~ ^[0-9a-zA-Z._-]+$ ]]; then
                # Avoid triggering on an empty string.
                if [[ -n $(echo "$key_pair_file_name" | xargs) ]]; then
                    valid_name=0

                    # Inform user of illegal characters within provided name.
                    local illegal_chars
                    illegal_chars=$(echo "$key_pair_file_name" | awk -F '' '{for(i=1;i<=NF;i++) if ($i !~ /^[0-9a-zA-Z._-]$/) print $i}' | sort -u | tr -d '\n')
                    echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} NAME MUST NOT CONTAIN ILLEGAL CHARACTERS." >&2
                    echo -e "Illegal characters in provided name:" >&2
                    for (( i=0; i<${#illegal_chars}; i++ )); do
                        echo "-    '${illegal_chars:i:1}'" >&2
                    done
                    echo -e "\nPlease ensure the name only contains letters, numbers, periods, underscores and hyphens.\n" >&2
                fi
            fi

            # Ensure key pair with same name does not exist.
            if [ -f "${PUBLIC_KEY_DIR}/${key_pair_file_name}.der" ] || [ -f "${PRIVATE_KEY_DIR}/${key_pair_file_name}.priv" ]; then
                valid_name=0
                echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} EXISTING KEY PAIR WITH SAME NAME.\n" >&2
            fi

            # Break the loop if a valid name was provided.
            if [ "$valid_name" -eq 1 ]; then
                break
            fi
        done

        # Update global key pair name variable.
        KEYNAME="$key_pair_file_name"
    else
        # Handle the extremely unlikely occurrence of a key pair name conflict with an existing key pair.
        while [ -f "${PUBLIC_KEY_DIR}/${KEYNAME}.der" ]; do
            KEYNAME="${cert_hostname:0:44}_$(date +%s)_$(uuidgen | awk -F '-' '{print $1}')"
        done
    fi
}

function create_cacert_config() {
    # Notify user.
    echo -e "${BOLD_BLUE_TEXT}INFO:${CLEAR_TEXT} UPDATING CACERT CONFIGURATION FILE AT '${CACERT_CONFIG_PATH}'..."

    # Check if the cacert configuration template exists.
    if [[ -f "${CACERT_CONFIG_PATH}.in" ]]; then
        local sed_output=""
        local sed_exit_status=0

        if [ "$AUTOMATIC_BUILD" -eq 1 ]; then
            # Set '-batch' argument.
            AUTOMATIC_BUILD_OPTION="-batch"

            local cert_country_code=$(locale country_ab2)
            if [[ -z ${cert_country_code} ]]; then
                echo -e "${BOLD_YELLOW_TEXT}WARNING:${CLEAR_TEXT} COULD NOT DETECT COUNTRY CODE FROM LOCALE; USING FALLBACK VALUE: US" >&2
                cert_country_code=US
            fi

            # Utilise default values if 'AUTOMATIC_BUILD' is equal to '1'.
            # - Set OpenSSL field values.
            # - Comment default and min/max values.
            sed_output=$(sed -e "s#\(0.organizationName *= \).*#\1${cert_hostname}#" \
                -e "s#\(organizationalUnitName *= \).*#\1${cert_hostname}#" \
                -e "s#\(emailAddress *= \).*#\1akmods@${cert_hostname}#" \
                -e "s#\(localityName *= \).*#\1None#" \
                -e "s#\(stateOrProvinceName *= \).*#\1None#" \
                -e "s#\(countryName *= \).*#\1${cert_country_code}#" \
                -e "s#\(commonName *= \).*#\1${KEYNAME}#" \
                -e "s/^[^#]*_default *= /#&/" \
                -e "s/^[^#]*_min/#&/" \
                -e "s/^[^#]*_max/#&/" "${CACERT_CONFIG_PATH}.in")
            sed_exit_status=$?
        else
            # Request user enter values manually if 'AUTOMATIC_BUILD' is equal to '0'.
            # Request OpenSSL prompt user for values later.
            sed_output=$(sed -e "s#\(prompt *= \).*#\1yes#" "${CACERT_CONFIG_PATH}.in")
            sed_exit_status=$?
        fi

        # Check if 'sed' command failed.
        if [ "$sed_exit_status" -ne 0 ]; then
            echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} FAILED TO READ CACERT CONFIGURATION TEMPLATE at '${CACERT_CONFIG_PATH}.in'." >&2
            echo "Quitting." >&2
            exit 5
        else
            # Note: Requires superuser permissions (i.e. sudo).
            if ! echo "$sed_output" > "$CACERT_CONFIG_PATH"; then
                echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} FAILED TO WRITE CACERT CONFIGURATION FILE to '${CACERT_CONFIG_PATH}'." >&2
                echo "Quitting." >&2
                exit 6
            fi
        fi
    else
        echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} MISSING CACERT CONFIGURATION TEMPLATE!" >&2
        echo "Failed to locate the CAcert configuration template at '${CACERT_CONFIG_PATH}.in'." >&2
        echo "Quitting." >&2
        exit 4
    fi
}

function create_new_key_pair() {
    # Notify user.
    echo -e "${BOLD_BLUE_TEXT}INFO:${CLEAR_TEXT} CREATING NEW KEY PAIR..."

    # Prepare an OpenSSL command to generate the key pair.
    local key_pair_generation_command=(
        "openssl req"                                     # Request new certificate
        "-x509"                                           # X.509 certificate type
        "-new"                                            # New key pair
        "-nodes"                                          # No DES
        "-utf8"                                           # UTF-8 encoding
        "-sha256"                                         # SHA-256 hash algorithm
        "-days" "3650"                                    # 10 year cert validity
        "${AUTOMATIC_BUILD_OPTION}"                       # Empty or "-batch"
        "-config" "${CACERT_CONFIG_PATH}"                 # Configuration file path
        "-outform" "DER"                                  # DER output format
        "-out" "${PUBLIC_KEY_DIR}/${KEYNAME}.der"         # Public key output path
        "-keyout" "${PRIVATE_KEY_DIR}/${KEYNAME}.priv"    # Private key output path
    )

    # Execute the key pair generation command within the 'akmods' group context.
    # Ensure 'rw-rwx---' permissions.
    # Note: Requires superuser permissions (i.e. sudo).
    if sg akmods -c "umask 037 && ${key_pair_generation_command[*]}"; then
        # Check if both a public and a private key file were created.
        if [[ ! -f "${PUBLIC_KEY_DIR}/${KEYNAME}.der" || ! -f "${PRIVATE_KEY_DIR}/${KEYNAME}.priv" ]]; then
            echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} KEY PAIR CREATION FAILED!" >&2
            echo "The OpenSSL key pair generation command ran, but key files were not created." >&2
            echo "Quitting." >&2
            exit 8
        fi
    else
        echo -e "${BOLD_RED_TEXT}ERROR:${CLEAR_TEXT} KEY PAIR CREATION FAILED!" >&2
        echo "The OpenSSL key pair generation command did not complete successfully." >&2
        echo "Quitting." >&2
        exit 7
    fi
}

function set_key_permissions() {
    # Notify user.
    echo -e "${BOLD_BLUE_TEXT}INFO:${CLEAR_TEXT} SETTING KEY PAIR PERMISSIONS..."

    # Ensure that akmods group can read keys.
    # Note: Requires superuser permissions (i.e. sudo).
    chmod g+r "${PUBLIC_KEY_DIR}/${KEYNAME}.der"
    chmod g+r "${PRIVATE_KEY_DIR}/${KEYNAME}.priv"

    # Sanitise permissions.
    # Note: Requires superuser permissions (i.e. sudo).
    if [[ -x "$RESTORECON_PATH" ]] ; then
        $RESTORECON_PATH "${PUBLIC_KEY_DIR}/${KEYNAME}.der"
        $RESTORECON_PATH "${PRIVATE_KEY_DIR}/${KEYNAME}.priv"
    fi
}

function update_key_symlinks() {
    # Notify user.
    echo -e "${BOLD_BLUE_TEXT}INFO:${CLEAR_TEXT} UPDATING KEY PAIR SYMLINKS..."

    # Note: Requires superuser permissions (i.e. sudo).
    ln -nsf "${PUBLIC_KEY_DIR}/${KEYNAME}.der" "$PUBLIC_KEY_PATH"
    ln -nsf "${PRIVATE_KEY_DIR}/${KEYNAME}.priv" "$PRIVATE_KEY_PATH"
    chown -h root:akmods "$PUBLIC_KEY_PATH"
    chown -h root:akmods "$PRIVATE_KEY_PATH"
}

# SCRIPT MAINLINE
# Parse any supplied arguments.
parse_arguments "$@"

# Check for elevated privileges.
check_root

# Check for broken key pairs.
check_broken_key_pair

# Check for existing key pair.
check_existing_key_pair

# Set key pair name.
set_key_pair_name

# Create 'cacert.config' using template file 'cacert.config.in'.
create_cacert_config

# Create new key pair.
create_new_key_pair

# Set permissions and sanitise keys.
set_key_permissions

# Update symlink to use new key pair.
update_key_symlinks

# Print completion messages.
echo -e "\n${BOLD_GREEN_TEXT}SUCCESS!${CLEAR_TEXT}"
echo "Public Key (Certificate) created at:    ${PUBLIC_KEY_DIR}/${KEYNAME}.der"
echo "Private Key created at:                 ${PRIVATE_KEY_DIR}/${KEYNAME}.priv"
echo -e "\nSymlinks:"
echo "${KEYNAME}.der  -> ${PUBLIC_KEY_PATH}"
echo "${KEYNAME}.priv -> ${PRIVATE_KEY_PATH}"

# Exit script.
exit 0
