#!/usr/bin/env bash
# vim: foldmarker={{{,}}}:foldmethod=marker

# pki-realm: client-side PKI management
# Copyright (C) 2016-2024 Maciej Delmanowski <drybjed@gmail.com>
# Copyright (C) 2016-2017 Robin Schneider <ypid@riseup.net>
# Copyright (C) 2016-2024 DebOps <https://debops.org/>
# SPDX-License-Identifier: GPL-3.0-only

set -o nounset -o pipefail -o errexit

umask 022

script="${0}"
script_name="$(basename "${script}")"
pid="$$"

state=()

log_message () {
    # Display a message if script is used interactively, otherwise send it to syslog
    local msg="${1:-}"

    if [ -n "${msg}" ] ; then
        if tty -s > /dev/null 2>&1 ; then
            echo "${script_name}: ${msg}" 1>&2
        elif type logger > /dev/null 2>&1 ; then
            logger -t "${script_name}[${pid}]" "${msg}"
        fi
    fi
}

clean_up () {
    # Clean up pidfile
    local pidfile="${1}"

    if [ -n "${pidfile}" ] && [ -r "${pidfile}" ] ; then
        rm -f "${pidfile}"
    fi
    exit 0
}

wait_for_pid () {
    # Wait for the specified process to exit
    local pidfile="${1}"

    if [ -n "${pidfile}" ] && [ -r "${pidfile}" ] ; then
        local wait_pid
        wait_pid="$(< "${pidfile}")"
        while kill -0 "${wait_pid}" > /dev/null 2>&1 ; do
            log_message "Waiting for PID ${wait_pid} to finish"
            sleep $(( ( RANDOM % 30 ) + 5 ))
        done
        if [ -f "${pidfile}" ] ; then
            rm -f "${pidfile}"
        fi
    fi
}

create_lock () {
    local pidfile="${1}"

    # Try and lock the script operation
    echo "${pid}" > "${pidfile}"
    sleep 0.5

    # Exclusive lock failed
    if [ "$(cat "${pidfile}")" != "${pid}" ] ; then
        log_message "PKI management started by PID $(< "${pidfile}")"
        exit 0
    fi

    # Exclusive lock succeeded
    # shellcheck disable=SC2064
    trap "clean_up ${pidfile}" EXIT
}

# Inlined in the following scripts: pki-authority, pki-realm {{{
version () {
    # Normalize version numbers for comparison.
    echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'
}

key_exists () {
    # Check if an associative array has a specified key present
    # shellcheck disable=SC2086
    eval '[ ${'$1'[$2]+test_of_existence} ]'
}

get_absolute_path () {
    # Get an absolute path to a file
    if type python3 > /dev/null ; then
        python3 -c 'import sys, os.path; print(os.path.abspath(sys.argv[1]))' "${1}"
    else
        python -c 'import sys, os.path; print(os.path.abspath(sys.argv[1]))' "${1}"
    fi
}

get_relative_path () {
    # Get a relative path to a file
    if type python3 > /dev/null ; then
        python3 -c 'import sys, os.path; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "${1}" "${2:-$PWD}"
    else
        python -c 'import sys, os.path; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "${1}" "${2:-$PWD}"
    fi
}

get_dnsdomainname () {
    # Wrap dnsdomainname if it's not present (on MacOS X)

    if type dnsdomainname > /dev/null 2>&1 ; then
        dnsdomainname
    else
        local fqdn
        fqdn="$(hostname -f)"
        if [[ ${fqdn} == *.* ]] ; then
            echo "${fqdn#*.}"
        else
            echo ""
        fi
    fi
}

chmod_idempotent () {
    # chmod does a `fchmodat` (after a `stat`) even if it does not need to
    # change anything as of 8.23-4.
    # This behavior is not ideal for this script as it would update the ctime.

    local new_mode_in_octal="${1##0}"
    local file_path="${2}"

    if [ "$(stat -c %a "${file_path}")" != "${new_mode_in_octal}" ] ; then
        chmod "${new_mode_in_octal}" "${file_path}"
    fi
}

chgrp_idempotent () {
    # chgrp does a `fchownat` (after a `newfstatat`) even if it does not need
    # to change anything as of 8.23-4.
    # This behavior is not ideal for this script as it would update the ctime.

    local new_group_name="${1}"
    local file_path="${2}"

    if [ "$(stat -c %G "${file_path}")" != "${new_group_name}" ] ; then
        chgrp "${new_group_name}" "${file_path}"
    fi
}
# }}}

initialize_environment () {

    declare -gA config

    config["pki_authority_preference"]="external/acme/internal/selfsigned"

    config["acme_user"]="${PKI_ACME_USER:-pki-acme}"
    config["acme_group"]="${PKI_ACME_GROUP:-pki-acme}"

    config["private_group"]="${PKI_PRIVATE_GROUP:-ssl-cert}"

    config["acme_challenge_dir"]="/srv/www/sites/acme/public/.well-known/acme-challenge"

    config["acme_client"]="acme_tiny"

    local -A acme_client_script_map
    acme_client_script_map=(
        ["acme_tiny"]="$(command -v acme-tiny || true)"
    )

    config["acme_client_script"]="${acme_client_script_map[${config['acme_client']}]}"

    config["acme_ca"]="le-staging-v2"

    local -A acme_ca_api_map
    acme_ca_api_map=(
        ["le-live"]="https://acme-v01.api.letsencrypt.org"
        ["le-live-v2"]="https://acme-v02.api.letsencrypt.org"
        ["le-staging"]="https://acme-staging.api.letsencrypt.org"
        ["le-staging-v2"]="https://acme-staging-v02.api.letsencrypt.org"
    )
    config["acme_ca_api"]="${acme_ca_api_map[${config['acme_ca']}]}"

    config["acme_contacts"]=""

    config["acme_expiration_days"]="30"

    # shellcheck disable=SC2154
    config["acme_expiration_seconds"]="$(( 60 * 60 * 24 * config['acme_expiration_days'] ))"

    config["acme_root_ca_path"]="/usr/share/ca-certificates"
    config["acme_root_ca_file"]="mozilla/ISRG_Root_X1.crt"

    config["pki_default_ca_bundle"]="/etc/ssl/certs/ca-certificates.crt"
    config["pki_default_subject"]="CN=$(hostname -f)"
    config["pki_default_fqdn"]="$(hostname -f)"
    config["pki_default_domain"]="$(get_dnsdomainname)"
    config["pki_default_subdomains"]="_wildcard_"

    config["pki_dhparam"]="false"
    config["dhparam_file"]=""
    config["realm_key_size"]="2048"

    config["internal_expiration_days"]="14"
    # shellcheck disable=SC2154
    config["internal_expiration_seconds"]="$(( 60 * 60 * 24 * config['internal_expiration_days'] ))"

    config["selfsigned_sign_days"]="365"
    # shellcheck disable=SC2154
    config["selfsigned_sign_seconds"]="$(( 60 * 60 * 24 * config['selfsigned_sign_days'] ))"

    config["selfsigned_expiration_days"]="14"
    # shellcheck disable=SC2154
    config["selfsigned_expiration_seconds"]="$(( 60 * 60 * 24 * config['selfsigned_expiration_days'] ))"

    config["subject"]=""
    config["domains"]=""
    config["subdomains"]=""
    config["subject_alt_names"]=""

    config["acme_default_subject"]="CN=$(get_dnsdomainname)"
    config["acme_default_domain"]="$(get_dnsdomainname)"
    config["acme_default_subdomains"]="www/ftp/mail/smtp/imap"

    config["acme_type"]=""
    config["acme_subject"]=""
    config["acme_domains"]=""
    config["acme_subdomains"]=""
    config["acme_alt_names"]=""

    if [ "$EUID" -eq 0 ] ; then

        config["lock_dir"]="/run"

        config["pki_root"]="${PKI_ROOT:-/etc/pki}"
        config["pki_library"]="${PKI_LIBRARY:-gnutls}"
        config["pki_acme"]="${PKI_ACME:-true}"
        config["pki_acme_library"]="${PKI_ACME_LIBRARY:-openssl}"
        config["pki_internal"]="${PKI_INTERNAL:-true}"

        config["pki_realms"]="${config['pki_root']}/realms"
        config["pki_hooks"]="${config['pki_root']}/hooks"

        config["public_dir_group"]="root"
        config["public_file_group"]="root"

        config["realm_dir_group"]="root"
        config["realm_file_group"]="root"

        if getent group "${config['private_group']}" > /dev/null ; then
            config["private_dir_group"]="${config['private_group']}"
            config["private_file_group"]="${config['private_group']}"
        else
            config["private_dir_group"]="root"
            config["private_file_group"]="root"
        fi

        if getent group "${config['acme_group']}" > /dev/null ; then
            config["acme_dir_group"]="${config['acme_group']}"
            config["acme_file_group"]="${config['acme_group']}"
        else
            config["acme_dir_group"]="root"
            config["acme_file_group"]="root"
        fi

        config["public_dir_mode"]="755"
        config["acme_dir_mode"]="775"
        config["private_dir_mode"]="750"
        config["realm_dir_mode"]="700"

        config["public_file_mode"]="644"
        config["acme_file_mode"]="640"
        config["private_file_mode"]="640"
        config["realm_file_mode"]="400"

    else

        config["lock_dir"]="${XDG_RUNTIME_DIR:-/tmp}"

        config["pki_root"]="${PKI_ROOT:-${XDG_CONFIG_HOME:-${HOME}/.config}/pki}"
        config["pki_library"]="${PKI_LIBRARY:-gnutls}"
        config["pki_acme"]="${PKI_ACME:-true}"
        config["pki_acme_library"]="${PKI_ACME_LIBRARY:-openssl}"
        config["pki_internal"]="${PKI_INTERNAL:-true}"

        config["pki_realms"]="${config['pki_root']}/realms"
        config["pki_hooks"]="${config['pki_root']}/hooks"

        config["public_dir_group"]="$(id -g)"
        config["public_file_group"]="$(id -g)"

        config["private_dir_group"]="$(id -g)"
        config["private_file_group"]="$(id -g)"

        config["realm_dir_group"]="$(id -g)"
        config["realm_file_group"]="$(id -g)"

        config["acme_dir_group"]="$(id -g)"
        config["acme_file_group"]="$(id -g)"

        config["public_dir_mode"]="755"
        config["acme_dir_mode"]="775"
        config["private_dir_mode"]="700"
        config["realm_dir_mode"]="700"

        config["public_file_mode"]="644"
        config["acme_file_mode"]="640"
        config["private_file_mode"]="600"
        config["realm_file_mode"]="400"

    fi

    config["private_dir_acl_groups_x"]=""
    config["private_file_acl_groups_r"]=""

    mkdir -p "${config['pki_root']}"
    chmod_idempotent "${config['public_dir_mode']}" "${config['pki_root']}"
}

check_openssl_key_req_match () {
    local check_key="${1}"
    local check_req="${2}"

    local key_modulus
    local req_modulus

    key_modulus="$(openssl rsa -noout -modulus -in "${check_key}" | base64)"
    req_modulus="$(openssl req -noout -modulus -in "${check_req}" | base64)"

    if [ "${key_modulus}" = "${req_modulus}" ] ; then
        return 0
    else
        return 1
    fi
}

check_openssl_key_crt_match () {
    local check_key="${1}"
    local check_crt="${2}"

    if [ -r "${check_key}" ] && [ -r "${check_crt}" ] ; then
        local key_modulus
        local crt_modulus

        key_modulus="$(openssl rsa  -noout -modulus -in "${check_key}" | base64)"
        crt_modulus="$(openssl x509 -noout -modulus -in "${check_crt}" | base64)"

        if [ "${key_modulus}" = "${crt_modulus}" ] ; then
            return 0
        else
            return 1
        fi
    else
        return 1
    fi
}

update_file_signature () {
    local check_file="${1}"

    if [ -n "${check_file}" ] && [ -r "${check_file}" ] ; then
        if [ -r "${check_file}.sig" ] ; then
            set +e
            md5sum --quiet --status -c "${check_file}.sig"
            local rc=$?
            set -e
            if [ ${rc} -ne 0 ] ; then
                rm -rf "${check_file}.sig"
                md5sum "${check_file}" > "${check_file}.sig"
            fi
        else
            md5sum "${check_file}" > "${check_file}.sig"
        fi
    fi
}

check_file_signature () {
    local check_file="${1}"

    if [ -n "${check_file}" ] && [ -r "${check_file}" ] ; then
        if [ -r "${check_file}.sig" ] ; then
            set +e
            md5sum --quiet --status -c "${check_file}.sig"
            local rc=$?
            set -e
            return "${rc}"
        fi
    fi
    return 1
}

enter_realm () {

    local realm="${1}"
    local config_file="${2:-config/realm.conf}"

    mkdir -p "${config['pki_hooks']}"
    chmod_idempotent "${config['public_dir_mode']}" "${config['pki_hooks']}"

    if [ ! -d "${config['pki_realms']}/${realm}" ] ; then
        mkdir -p "${config['pki_realms']}/${realm}"
        chmod "${config['public_dir_mode']}" "${config['pki_realms']}/${realm}"
        state+=('new-realm')
    fi

    cd "${config['pki_realms']}/${realm}"
    local rc=$?

    if [ -r "${config_file}" ] ; then

        # shellcheck source=/dev/null
        . "${config_file}"
    fi

    return "${rc}"
}

run_pki_hooks () {

    if [ -n "${state:-}" ] && [ -d "${config['pki_hooks']}" ] ; then

        export PKI_SCRIPT_REALM="${config['name']}"
        export PKI_SCRIPT_FQDN="${config['pki_default_fqdn']}"
        export PKI_SCRIPT_SUBJECT="${config['subject']:-${config['pki_default_subject']}}"
        export PKI_SCRIPT_DOMAINS="${config['domains']:-${config['pki_default_domain']}}"
        export PKI_SCRIPT_SUBDOMAINS="${config['subdomains']:-${config['pki_default_subdomains']}}"
        export PKI_SCRIPT_PRIVATE_KEY
        PKI_SCRIPT_PRIVATE_KEY="$(get_absolute_path private/key.pem)"

        export PKI_SCRIPT_DEFAULT_CRT
        export PKI_SCRIPT_DEFAULT_KEY
        export PKI_SCRIPT_DEFAULT_PEM

        PKI_SCRIPT_DEFAULT_CRT="$(get_absolute_path default.crt)"
        PKI_SCRIPT_DEFAULT_KEY="$(get_absolute_path default.key)"
        PKI_SCRIPT_DEFAULT_PEM="$(get_absolute_path default.pem)"

        local current_state
        current_state=$(printf ",%s" "${state[@]:-}")
        current_state="${current_state:1}"
        export PKI_SCRIPT_STATE="${current_state}"

        cd "${config['pki_hooks']}" || exit 1
        ( run-parts . )
        cd - > /dev/null 2>&1

    fi
}

run_external_script () {

    if [ -r external/script ] ; then

        if [ ! -r private/realm_key.pem ] ; then
            "generate_${config['pki_library']}_rsa_realm_key" private/realm_key.pem
# FIXME: Redundancy! Why?
            if [ ! -r private/key.pem ] ; then
                install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key.pem.tmp
                if type setfacl > /dev/null 2>&1 ; then
                    local private_file_acl_groups_r
                    read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                    if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                        for acl_group in "${private_file_acl_groups_r[@]}" ; do
                            if getent group "${acl_group}" > /dev/null ; then
                                setfacl -m "g:${acl_group}:r" private/key.pem.tmp
                            fi
                        done
                    fi
                fi
                cat private/realm_key.pem >> private/key.pem.tmp
                mv private/key.pem.tmp private/key.pem
            fi
        fi

        export PKI_SCRIPT_REALM="${config['name']}"
        export PKI_SCRIPT_FQDN="${config['pki_default_fqdn']}"
        export PKI_SCRIPT_SUBJECT="${config['subject']:-${config['pki_default_subject']}}"
        export PKI_SCRIPT_DOMAINS="${config['domains']:-${config['pki_default_domain']}}"
        export PKI_SCRIPT_SUBDOMAINS="${config['subdomains']:-${config['pki_default_subdomains']}}"

# FIXME: Redundancy! Why?
        export PKI_SCRIPT_PRIVATE_KEY
        PKI_SCRIPT_PRIVATE_KEY="$(get_absolute_path private/key.pem)"

        export PKI_SCRIPT_DEFAULT_CRT
        export PKI_SCRIPT_DEFAULT_KEY
        export PKI_SCRIPT_DEFAULT_PEM

        PKI_SCRIPT_DEFAULT_CRT="$(get_absolute_path default.crt)"
        PKI_SCRIPT_DEFAULT_KEY="$(get_absolute_path default.key)"
        PKI_SCRIPT_DEFAULT_PEM="$(get_absolute_path default.pem)"

        local current_state
        current_state=$(printf ",%s" "${state[@]:-}")
        current_state="${current_state:1}"
        export PKI_SCRIPT_STATE="${current_state}"

        test -x external/script || chmod ug+x external/script

        cd external || exit 1
        ( ./script )
        cd - > /dev/null 2>&1
    fi
}

update_symlink () {

    local symlink_target="${1}"

    if [ -n "${symlink_target}" ] ; then

        shift 1

        for target in "${@}" ; do
            if  [ -r "${target}" ] ; then
                if [ -L "${symlink_target}" ] && [ "$(readlink "${symlink_target}")" == "${target}" ] ; then
                    break
                else
                    ln -sf "${target}" "${symlink_target}"
                fi
                break
            fi
        done

        if [ ! -e "${symlink_target}" ] ; then
            rm -f "${symlink_target}"
        fi

    fi
}

create_openssl_config () {

    local config_file="${1:-config/openssl.conf}"
    local config_type="${2:-internal}"
    local req_dn="${3}"
    local req_domains="${4}"
    local req_subdomains="${5:-}"
    local req_san="${6:-}"

    if [ -n "${config_file}" ] && [ -n "${req_dn}" ] ; then

        cat << EOF > "${config_file}"
# Configuration file generated by pki-realm

[ req ]
default_md         = sha256
default_bits       = ${config['realm_key_size']}
default_keyfile    = private/key.pem
prompt             = no
encrypt_key        = no
distinguished_name = req_dn
req_extensions     = ext_req
utf8               = yes
string_mask        = utf8only
EOF

        if [ -n "${PKI_SESSION_TOKEN:-}" ] && [ "${config_type}" = "internal" ] ; then
            cat << EOF >> "${config_file}"
attributes         = req_attributes

[ req_attributes ]
challengePassword  = \${ENV::PKI_SESSION_TOKEN}
EOF
        fi

        # shellcheck disable=SC2129
        cat << EOF >> "${config_file}"

[ req_dn ]
EOF

        echo "${req_dn}" | tr "/" "\\n" | grep --invert-match '^\s*$' | sed \
            -e 's/^[Cc]=/countryName=/' \
            -e 's/^[Ss][Tt]=/stateOrProvinceName=/' \
            -e 's/^[Ll]=/localityName=/' \
            -e 's/^[Oo]=/organizationName=/' \
            -e 's/^[Oo][Uu]=/organizationalUnitName=/' \
            -e 's/^[Cc][Nn]=/commonName=/' >> "${config_file}"

        cat << EOF >> "${config_file}"

[ ext_req ]
basicConstraints   = CA:FALSE
keyUsage           = digitalSignature, keyEncipherment
extendedKeyUsage   = serverAuth, clientAuth
EOF
        if [ -n "${req_domains}" ] || [ -n "${req_subdomains}" ] || [ -n "${req_san}" ] ; then
            cat << EOF >> "${config_file}"
subjectAltName     = @ext_req_san

[ ext_req_san ]
EOF

            local domains
            read -r -a domains <<< "$(echo "${req_domains}" | tr "/" " ")"
            local subdomains
            read -r -a subdomains <<< "$(echo "${req_subdomains}" | tr "/" " ")"

            local san_ip
            read -r -a san_ip <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^ip://pi'    | xargs )"
            local san_dns
            read -r -a san_dns <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^dns://pi'   | xargs )"
            local san_uri
            read -r -a san_uri <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^uri://pi'   | xargs )"
            local san_email
            read -r -a san_email <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^email://pi' | xargs )"

            local all_dns
            read -r -a all_dns <<< "${domains[@]:-}"

            if [ ${#subdomains[@]} -gt 0 ] ; then
                for i in "${!domains[@]}" ; do
                    for j in "${!subdomains[@]}" ; do
                        if [ "${subdomains[${j}]}" == "_wildcard_" ] ; then
                            all_dns+=( "*.${domains[${i}]}" )
                        else
                            all_dns+=( "${subdomains[${j}]}.${domains[${i}]}" )
                        fi
                    done
                done
            fi

            # FIXME: Not sure how to handle appending to the array in this case
            # shellcheck disable=SC2206
            all_dns+=( ${san_dns[@]:-} )

            for i in "${!all_dns[@]}" ; do
                printf "%-18s = %s\\n" "DNS.${i}" "${all_dns[${i}]}" >> "${config_file}"
            done

            for i in "${!san_ip[@]}" ; do
                printf "%-18s = %s\\n" "IP.${i}" "${san_ip[${i}]}" >> "${config_file}"
            done

            for i in "${!san_uri[@]}" ; do
                printf "%-18s = %s\\n" "URI.${i}" "${san_uri[${i}]}" >> "${config_file}"
            done

            for i in "${!san_email[@]}" ; do
                printf "%-18s = %s\\n" "email.${i}" "${san_email[${i}]}" >> "${config_file}"
            done

        fi

    fi
}

create_gnutls_config () {

    local config_file="${1:-config/gnutls.conf}"
    local config_type="${2:-internal}"
    local req_dn="${3}"
    local req_domains="${4}"
    local req_subdomains="${5:-}"
    local req_san="${6:-}"

    if [ -n "${config_file}" ] && [ -n "${req_dn}" ] ; then

        cat << EOF > "${config_file}"
# Configuration file generated by pki-realm

signing_key
encryption_key
tls_www_client
tls_www_server

EOF

        if [ -n "${PKI_SESSION_TOKEN:-}" ] && [ "${config_type}" = "internal" ] ; then
            cat << EOF >> "${config_file}"
# This password is meaningless outside of current session
challenge_password  = ${PKI_SESSION_TOKEN:-}

EOF
        elif [ "${config_type}" = "selfsigned" ] ; then
            cat << EOF >> "${config_file}"
expiration_days = ${config['selfsigned_sign_days']}
EOF
        fi

        echo "${req_dn}" | tr "/" "\\n" | grep --invert-match '^\s*$' | sed \
            -e 's/^[Cc]=/country = "/' \
            -e 's/^[Ss][Tt]=/state = "/' \
            -e 's/^[Ll]=/locality = "/' \
            -e 's/^[Oo]=/organization = "/' \
            -e 's/^[Oo][Uu]=/unit = "/' \
            -e 's/^[Cc][Nn]=/cn = "/' \
            -e 's/$/"/' >> "${config_file}"

        local domains
        read -r -a domains <<< "$(echo "${req_domains}" | tr "/" " ")"
        local subdomains
        read -r -a subdomains <<< "$(echo "${req_subdomains}" | tr "/" " ")"

        local san_ip
        read -r -a san_ip <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^ip://pi'    | xargs )"
        local san_dns
        read -r -a san_dns <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^dns://pi'   | xargs )"
        local san_uri
        read -r -a san_uri <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^uri://pi'   | xargs )"
        local san_email
        read -r -a san_email <<< "$( echo "${req_san}" | tr "|" "\\n" | sed --quiet -e 's/^email://pi' | xargs )"

        local all_dns
        read -r -a all_dns <<< "${domains[@]:-}"

        if [ ${#subdomains[@]} -gt 0 ] ; then
            for i in "${!domains[@]}" ; do
                for j in "${!subdomains[@]}" ; do
                    if [ "${subdomains[${j}]}" == "_wildcard_" ] ; then
                        all_dns+=( "*.${domains[${i}]}" )
                    else
                        all_dns+=( "${subdomains[${j}]}.${domains[${i}]}" )
                    fi
                done
            done
        fi

        # FIXME: Not sure how to handle appending to the array in this case
        # shellcheck disable=SC2206
        all_dns+=( ${san_dns[@]:-} )

        for i in "${!all_dns[@]}" ; do
            printf 'dns_name = "%s"\n' "${all_dns[${i}]}" >> "${config_file}"
        done

        for i in "${!san_ip[@]}" ; do
            printf 'ip_address = "%s"\n' "${san_ip[${i}]}" >> "${config_file}"
        done

        if [ "$(version "$(certtool --version | head -n 1 | awk '{print $NF}')")" -ge "$(version 3.0.20)" ] ; then
            for i in "${!san_uri[@]}" ; do
                printf 'uri = "%s"\n' "${san_uri[${i}]}" >> "${config_file}"
            done
        fi

        for i in "${!san_email[@]}" ; do
            printf 'email = "%s"\n' "${san_email[${i}]}" >> "${config_file}"
        done

    fi
}

save_realm_config () {

    local config_file="${1:-config/realm.conf}"

    if [ -r "${config_file}" ] ; then
        if grep -q -E "^#\\s+Configuration\\s+file\\s+generated\\s+by\\s+pki-realm$" "${config_file}" ; then
            rm -f "${config_file}"
        fi
    fi

    if [ ! -r "${config_file}" ] ; then
        cat << EOF > "${config_file}"
# Configuration file generated by pki-realm

EOF
        for key in "${!config[@]}" ; do
            echo "config['${key}']='${config[${key}]}'" >> "${config_file}"
        done
    fi
}

generate_openssl_request () {

    local req_config="${1}"
    local req_out="${2}"

    if [ -n "${req_config}" ] && [ -n "${req_out}" ] ; then
        if [ -r "${req_config}" ] && [ ! -r "${req_out}" ] ; then

            local req_keyfile
            req_keyfile=$(grep -E '^default_keyfile\s+=\s+' "${req_config}" | awk '{print $3}')
            if [ -n "${req_keyfile}" ] ; then
                openssl req -new -key "${req_keyfile}" -config "${req_config}" -out "${req_out}.tmp"
            else
                openssl req -new -config "${req_config}" -out "${req_out}.tmp"
            fi
            test -r "${req_out}.tmp" && mv "${req_out}.tmp" "${req_out}"

        fi
    fi

}

generate_gnutls_request () {

    local req_config="${1}"
    local req_out="${2}"

    if [ -n "${req_config}" ] && [ -n "${req_out}" ] ; then
        if [ -r "${req_config}" ] && [ ! -r "${req_out}" ] ; then

            certtool --generate-request --template "${req_config}" \
                     --load-privkey private/key.pem --outfile "${req_out}.tmp"
            # Remove text output from the request
            test -r "${req_out}.tmp" && sed -n -i '/-----BEGIN NEW CERTIFICATE REQUEST-----/,$ p' "${req_out}.tmp"
            test -r "${req_out}.tmp" && mv "${req_out}.tmp" "${req_out}"

        fi
    fi

}

selfsign_openssl_request () {

    local sign_req="selfsigned/request.pem"
    local sign_out="selfsigned/cert.pem"
    local root_out="selfsigned/root.pem"

    if [ -n "${sign_req}" ] && [ -n "${sign_out}" ] ; then
        if [ -r "${sign_req}" ] && [ ! -r "${sign_out}" ] ; then

            openssl x509 -in "${sign_req}" -out "${sign_out}.tmp" \
                    -req -signkey private/key.pem -days "${config['selfsigned_sign_days']}"
            test -r "${sign_out}.tmp" && mv "${sign_out}.tmp" "${sign_out}"
            if [ -r "${sign_out}" ] && [ ! -r "${root_out}" ] ; then
                cd "$(dirname "${root_out}")" || exit 1
                ln -s "$(basename ${sign_out})" "$(basename "${root_out}")"
                cd - > /dev/null
            fi

        fi
    fi

}

selfsign_gnutls_request () {

    local sign_req="selfsigned/request.pem"
    local sign_out="selfsigned/cert.pem"
    local root_out="selfsigned/root.pem"

    if [ -n "${sign_req}" ] && [ -n "${sign_out}" ] ; then
        if [ -r "${sign_req}" ] && [ ! -r "${sign_out}" ] ; then

            certtool --generate-self-signed --load-privkey private/key.pem \
                     --load-request "${sign_req}" --outfile "${sign_out}.tmp" \
                     --template selfsigned/gnutls.conf
            test -r "${sign_out}.tmp" && mv "${sign_out}.tmp" "${sign_out}"
            if [ -r "${sign_out}" ] && [ ! -r "${root_out}" ] ; then
                cd "$(dirname "${root_out}")" || exit 1
                ln -s "$(basename ${sign_out})" "$(basename "${root_out}")"
                cd - > /dev/null
            fi

        fi
    fi

}

generate_openssl_rsa_realm_key () {

    local key_file="${1:-private/realm_key.pem}"
    local key_size="${2:-${config['realm_key_size']}}"
    local key_group="${3:-${config['realm_file_group']}}"

    test -r "${key_file}" || openssl genrsa -out "${key_file}.tmp" "${key_size}"

    if [ -r "${key_file}.tmp" ] ; then
        chmod "${config['realm_file_mode']}" "${key_file}.tmp"
        chgrp "${key_group}" "${key_file}.tmp"
        mv "${key_file}.tmp" "${key_file}"
    fi

    chmod_idempotent "${config['realm_file_mode']}" "${key_file}"
    chgrp_idempotent "${key_group}" "${key_file}"

}

generate_gnutls_rsa_realm_key () {

    local key_file="${1:-private/realm_key.pem}"
    local key_size="${2:-${config['realm_key_size']}}"
    local key_group="${3:-${config['realm_file_group']}}"

    if ! [ -r "${key_file}" ] ; then
        if [ "$(version "$(certtool --version | head -n 1 | awk '{print $NF}')")" -lt "$(version 3.1.0)" ] ; then
            certtool --generate-privkey --outfile "${key_file}.tmp" --bits "${key_size}"
        else
            certtool --generate-privkey --rsa --outfile "${key_file}.tmp" --bits "${key_size}"
        fi
    fi

    if [ -r "${key_file}.tmp" ] ; then
        sed -n -i '/-----BEGIN RSA PRIVATE KEY-----/,$ p' "${key_file}.tmp"
        chmod "${config['realm_file_mode']}" "${key_file}.tmp"
        chgrp "${key_group}" "${key_file}.tmp"
        mv "${key_file}.tmp" "${key_file}"
    fi

    chmod_idempotent "${config['realm_file_mode']}" "${key_file}"
    chgrp_idempotent "${key_group}" "${key_file}"

}

generate_openssl_rsa_private_key () {

    local key_file="${1:-private/key.pem}"
    local key_size="${2:-${config['realm_key_size']}}"
    local key_group="${3:-${config['private_file_group']}}"
    local key_acl="${4:-true}"

    test -r "${key_file}" || openssl genrsa -out "${key_file}.tmp" "${key_size}"

    if [ -r "${key_file}.tmp" ] ; then
        chmod "${config['private_file_mode']}" "${key_file}.tmp"
        chgrp "${key_group}" "${key_file}.tmp"
        if [ "${key_acl}" = "true" ] ; then
            if type setfacl > /dev/null 2>&1 ; then
                local private_file_acl_groups_r
                read -r private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                    for acl_group in "${private_file_acl_groups_r[@]}" ; do
                        if getent group "${acl_group}" > /dev/null ; then
                            setfacl -m "g:${acl_group}:r" "${key_file}.tmp"
                        fi
                    done
                fi
            fi
        fi
        mv "${key_file}.tmp" "${key_file}"
    fi

    chmod_idempotent "${config['private_file_mode']}" "${key_file}"
    chgrp_idempotent "${key_group}" "${key_file}"

}

generate_gnutls_rsa_private_key () {

    local key_file="${1:-private/key.pem}"
    local key_size="${2:-${config['realm_key_size']}}"
    local key_group="${3:-${config['private_file_group']}}"
    local key_acl="${4:-true}"

    if ! [ -r "${key_file}" ] ; then
        if [ "$(version "$(certtool --version | head -n 1 | awk '{print $NF}')")" -lt "$(version 3.1.0)" ] ; then
            certtool --generate-privkey --outfile "${key_file}.tmp" --bits "${key_size}"
        else
            certtool --generate-privkey --rsa --outfile "${key_file}.tmp" --bits "${key_size}"
        fi
    fi

    if [ -r "${key_file}.tmp" ] ; then
        sed -n -i '/-----BEGIN RSA PRIVATE KEY-----/,$ p' "${key_file}.tmp"
        chmod "${config['private_file_mode']}" "${key_file}.tmp"
        chgrp "${key_group}" "${key_file}.tmp"
        if [ "${key_acl}" = "true" ] ; then
            if type setfacl > /dev/null 2>&1 ; then
                local private_file_acl_groups_r
                read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                    for acl_group in "${private_file_acl_groups_r[@]}" ; do
                        if getent group "${acl_group}" > /dev/null ; then
                            setfacl -m "g:${acl_group}:r" "${key_file}.tmp"
                        fi
                    done
                fi
            fi
        fi
        mv "${key_file}.tmp" "${key_file}"
    fi

    chmod_idempotent "${config['private_file_mode']}" "${key_file}"
    chgrp_idempotent "${key_group}" "${key_file}"

}

create_public_directories () {

    local dir_group="${1}"
    if [ -n "${dir_group}" ] ; then
        shift
    else
        return 1
    fi

    local directories
    read -r -a directories <<< "${@}"

    for directory in "${directories[@]}" ; do
        mkdir -p "${directory}"
        chmod_idempotent "${config['public_dir_mode']}" "${directory}"
        chgrp_idempotent "${dir_group}" "${directory}"
    done

}

create_private_directories () {

    local dir_group="${1}"
    if [ -n "${dir_group}" ] ; then
        shift
    else
        return 1
    fi

    local directories
    read -r -a directories <<< "${@}"

    for directory in "${directories[@]}" ; do
        mkdir -p "${directory}"
        chmod_idempotent "${config['private_dir_mode']}" "${directory}"
        chgrp_idempotent "${dir_group}" "${directory}"
        if type setfacl > /dev/null 2>&1 ; then
            local private_dir_acl_groups_x
            read -r -a private_dir_acl_groups_x <<< "$(echo "${config['private_dir_acl_groups_x']}" | tr "/" " ")"
            if [ ${#private_dir_acl_groups_x[@]} -ne 0 ] ; then
                for acl_group in "${private_dir_acl_groups_x[@]}" ; do
                    if getent group "${acl_group}" > /dev/null ; then
                        setfacl -m "g:${acl_group}:x" "${directory}"
                    fi
                done
            fi
        fi
    done

}

create_private_noacl_directories () {

    local dir_group="${1}"
    if [ -n "${dir_group}" ] ; then
        shift
    else
        return 1
    fi

    local directories
    read -r -a directories <<< "${@}"

    for directory in "${directories[@]}" ; do
        mkdir -p "${directory}"
        chmod_idempotent "${config['private_dir_mode']}" "${directory}"
        chgrp_idempotent "${dir_group}" "${directory}"
    done

}

check_expiration_time () {

    local cert_file="${1}"
    local seconds="${2}"

    if [ -n "${cert_file}" ] && [ -r "${cert_file}" ] ; then

        current_time="$(date +%s)"
        cert_enddate="$(openssl x509 -in "${cert_file}" -noout -enddate | cut -d= -f2)"
        cert_expires=$(date --date="${cert_enddate}" +%s)

        if [ -n "${seconds}" ] ; then
            cert_expires="$(( cert_expires - seconds ))"
        fi

        if (( (cert_expires - current_time) >= 0 )) ; then
            return 0
        else
            return 1
        fi

    else
        return 1
    fi
}

convert_der_to_pem () {

    local input_file="${1}"

    if [ -n "${input_file}" ] && [ -r "${input_file}" ] ; then
        if ! openssl x509 -inform PEM -in "${input_file}" -noout 2>/dev/null ; then
            openssl x509 -inform DER -in "${input_file}" -outform PEM -out "${input_file}.tmp"
            mv "${input_file}.tmp" "${input_file}"
        fi
    fi
}

request_acme_tiny_certificate () {

    # Import per-realm environment variables
    # shellcheck disable=SC1091
    source config/environment

    # Rotate acme/error.log if it was modified more than two days ago. This
    # allows another renewal attempt, but still prevents hitting rate limits.
    # Let's Encrypt limits duplicate certificate requests to five per week
    # (https://letsencrypt.org/docs/rate-limits/).
    if [[ -e acme/error.log ]] && [[ $(find acme/error.log -mtime +1) ]]; then
        mv acme/error.log "acme/error-$(date +%s).log"
    fi

    if [ -x "${config['acme_client_script']}" ] && [ -d "${config['acme_challenge_dir']}" ] && [ ! -e acme/error.log ] && [ -r acme/request.pem ] ; then

        if ! ( check_expiration_time acme/cert.pem "${config['acme_expiration_seconds']}" && check_openssl_key_crt_match private/key.pem acme/cert.pem ) ; then

            local acme_tiny_request_params="--account-key acme/account_key.pem \
                    --directory-url \"${config['acme_ca_api']}\" --csr acme/request.pem \
                    --acme-dir \"${config['acme_challenge_dir']}\""

            if [ -n "${config['acme_contacts']}" ]; then
                acme_tiny_request_params+=(" --contact \"${config['acme_contacts']//,/\" \"}\"")
            fi

            if [ "$EUID" -eq 0 ] ; then

                set +e
                su --shell /bin/bash -c "umask 0022 ; source config/environment ; ${config['acme_client_script']} ${acme_tiny_request_params[*]} > acme/cert.pem.tmp 2>acme/error.log" "${config['acme_user']}"
                set -e

            else

                set +e
                "${config['acme_client_script']}" "${acme_tiny_request_params[*]}" > acme/cert.pem.tmp 2>acme/error.log
                set -e

            fi

            if [ -r acme/cert.pem.tmp ] && [ -s acme/cert.pem.tmp ] ; then

                # Let's Encrypt provides certificates with attached
                # intermediates but we don't want that, PKI realms handle the
                # intermediates themselves. Extract the first certificate from
                # the generated file and swap it in, so the rest of the script
                # works.
                openssl x509 -in acme/cert.pem.tmp -out acme/first-cert.pem.tmp
                mv acme/first-cert.pem.tmp acme/cert.pem.tmp

                if [ -r acme/cert.pem ] ; then
                    mv acme/cert.pem acme/cert.pem.old
                fi
                mv acme/cert.pem.tmp acme/cert.pem

                local acme_intermediate_uri
                acme_intermediate_uri="$(openssl x509 -in acme/cert.pem -text -noout -certopt ca_default,no_validity,no_serial | grep -E '^\s+CA\s+Issuers\s+-\s+URI:' | awk '{print $4}' | sed -e 's/^URI://')"

                if [ ! -r acme/intermediate.pem ] ; then

                    if [ -n "${acme_intermediate_uri}" ] ; then
                        curl -s -L -o acme/intermediate.pem.tmp "${acme_intermediate_uri}"
                        convert_der_to_pem acme/intermediate.pem.tmp
                        mv acme/intermediate.pem.tmp acme/intermediate.pem
                        if [ ! -r acme/intermediate_uri.txt ] ; then
                            echo "${acme_intermediate_uri}" > acme/intermediate_uri.txt
                        fi
                    fi

                elif [ -r acme/intermediate.pem ] ; then

                    if [ -n "${acme_intermediate_uri}" ] && [ -r acme/intermediate_uri.txt ] ; then
                        if [ "${acme_intermediate_uri}" != "$(<acme/intermediate_uri.txt)" ] ; then
                            curl -s -L -o acme/intermediate.pem.tmp "${acme_intermediate_uri}"
                            if ! diff -q -N acme/intermediate.pem acme/intermediate.pem.tmp > /dev/null ; then
                                rm -f acme/intermediate.pem acme/intermediate_uri.txt
                                convert_der_to_pem acme/intermediate.pem.tmp
                                mv acme/intermediate.pem.tmp acme/intermediate.pem
                                if [ ! -r acme/intermediate_uri.txt ] ; then
                                    echo "${acme_intermediate_uri}" > acme/intermediate_uri.txt
                                fi
                            else
                                rm -f acme/intermediate.pem.tmp acme/intermediate_uri.txt
                                if [ ! -r acme/intermediate_uri.txt ] ; then
                                    echo "${acme_intermediate_uri}" > acme/intermediate_uri.txt
                                fi
                            fi
                        fi
                    fi

                fi

                if [ ! -r acme/root.pem ] ; then
                    if [ -n "${config['acme_root_ca_path']}" ] && [ -n "${config['acme_root_ca_file']}" ] ; then
                        if [ -r "${config['acme_root_ca_path']}/${config['acme_root_ca_file']}" ] && grep -q "${config['acme_root_ca_file']}" /etc/ca-certificates.conf ; then
                            ln -s "${config['acme_root_ca_path']}/${config['acme_root_ca_file']}" acme/root.pem
                        fi
                    fi
                fi

                if [ -r acme/error.log ] ; then
                    rm -f acme/error.log
                fi

            elif [ -r acme/cert.pem.tmp ] && [ ! -s acme/cert.pem.tmp ] ; then
                rm -f acme/cert.pem.tmp
            fi

        fi

    fi

}

request_acme_dns_certificate() {
    # We expect the API secrets to be in ./private/dns-*-credentials.key
    local credentials=/etc/pki/realms/${config['name']}/private/${config['acme_type']}-credentials.key
    local pkirealm=/etc/pki/realms/${config['name']}
    local email=""

    if [ -n "${config['acme_contacts']}" ]; then
        email="-m${config['acme_contacts']}"
    fi

    local all_dns
    local req_subjects=${config['subject']:-${config['pki_default_subject']:-}}
    # shellcheck disable=SC2001
    req_subjects=$(echo "${req_subjects}" | sed 's/^[Cc][Nn]=//')

    local req_domains="${config['domains']:-${config['pki_default_domain']}}"
    local req_subdomains="${config['subdomains']:-${config['pki_default_subdomains']:-}}"

    read -r -a all_dns <<< "$(echo "${req_subjects}" | tr "/" " ")"

    local domains
    read -r -a domains <<< "$(echo "${req_domains}" | tr "/" " ")"

    # shellcheck disable=SC2206
    all_dns+=( ${domains[*]:-} )

    local subdomains
    read -r -a subdomains <<< "$(echo "${req_subdomains}" | tr "/" " ")"

    if [ ${#subdomains[@]} -gt 0 ] ; then
        for i in "${!domains[@]}" ; do
            for j in "${!subdomains[@]}" ; do
                if [ "${subdomains[${j}]}" == "_wildcard_" ] ; then
                    all_dns+=( "*.${domains[${i}]}" )
                else
                    all_dns+=( "${subdomains[${j}]}.${domains[${i}]}" )
                fi
            done
        done
    fi

    # remove redundant domains
    for i in "${all_dns[@]}"; do
        for j in "${all_dns[@]}"; do
            if test "${i}" == "${j}"; then
                true
            elif test "*.${i#*\.}" == "${j}"; then
                # i is part of j. j is wildcard
                all_dns=( "${all_dns[@]/$i}" )
            fi
        done
    done

    local all_dns_str
    # remove duplicate lines without sorting
    all_dns_str=$(printf '%s\n' "${all_dns[@]}" | awk '!x[$0]++' | xargs)
    # shellcheck disable=SC2086
    all_dns_str=$(echo " "${all_dns_str} | sed 's/ / -d/g')

    if test "${config['acme_ca']}" == 'le-live-v2'; then
      # shellcheck disable=SC2086
      certbot certonly -n --authenticator ${config['acme_type']} --expand --agree-tos "${email}" --${config['acme_type']}-credentials ${credentials} ${all_dns_str}
    elif test "${config['acme_ca']}" == 'le-staging-v2'; then
      # shellcheck disable=SC2086
      certbot certonly --test-cert -n --authenticator ${config['acme_type']} --expand --agree-tos "${email}" --${config['acme_type']}-credentials ${credentials} ${all_dns_str}
    fi

    local folder
    local letsencrypt
    folder=$(printf '%s\n' "${all_dns[@]}" | xargs | cut -d' ' -f1)
    letsencrypt="/etc/letsencrypt/live/${folder}"
    if [ -f "${letsencrypt}/privkey.pem" ]; then
        ln -vsfT "${letsencrypt}/privkey.pem"   "${pkirealm}/private/key.pem"
        ln -vsfT "${letsencrypt}/cert.pem"      "${pkirealm}/acme/cert.pem"
        ln -vsfT "${letsencrypt}/chain.pem"     "${pkirealm}/acme/intermediate.pem"
    else
      echo "error: could not symlink let's encrypt certificates into pki realm" >&2
    fi
}

request_acme_manual_certificate() {
    # We expect the API secrets to be in ./private/dns-*-credentials.key
    local pkirealm=/etc/pki/realms/${config['name']}
    local email=""

    if [ -n "${config['acme_contacts']}" ]; then
        email="-m${config['acme_contacts']}"
    fi

    local all_dns
    local req_subjects=${config['subject']:-${config['pki_default_subject']:-}}
    # shellcheck disable=SC2001
    req_subjects=$(echo "${req_subjects}" | sed 's/^[Cc][Nn]=//')

    local req_domains="${config['domains']:-${config['pki_default_domain']}}"
    local req_subdomains="${config['subdomains']:-${config['pki_default_subdomains']:-}}"

    read -r -a all_dns <<< "$(echo "${req_subjects}" | tr "/" " ")"

    local domains
    read -r -a domains <<< "$(echo "${req_domains}" | tr "/" " ")"

    # shellcheck disable=SC2206
    all_dns+=( ${domains[*]:-} )

    local subdomains
    read -r -a subdomains <<< "$(echo "${req_subdomains}" | tr "/" " ")"

    if [ ${#subdomains[@]} -gt 0 ] ; then
        for i in "${!domains[@]}" ; do
            for j in "${!subdomains[@]}" ; do
                if [ "${subdomains[${j}]}" == "_wildcard_" ] ; then
                    all_dns+=( "*.${domains[${i}]}" )
                else
                    all_dns+=( "${subdomains[${j}]}.${domains[${i}]}" )
                fi
            done
        done
    fi

    # remove redundant domains
    for i in "${all_dns[@]}"; do
        for j in "${all_dns[@]}"; do
            if test "${i}" == "${j}"; then
                true
            elif test "*.${i#*\.}" == "${j}"; then
                # i is part of j. j is wildcard
                all_dns=( "${all_dns[@]/$i}" )
            fi
        done
    done

    local all_dns_str
    # remove duplicate lines without sorting
    all_dns_str=$(printf '%s\n' "${all_dns[@]}" | awk '!x[$0]++' | xargs)
    # shellcheck disable=SC2086
    all_dns_str=$(echo " "${all_dns_str} | sed 's/ / -d/g')

    if test "${config['acme_ca']}" == 'le-live-v2'; then
      # shellcheck disable=SC2086
      certbot certonly -n --authenticator ${config['acme_type']} --preferred-challenges=dns --expand --agree-tos "${email}" ${all_dns_str}
    elif test "${config['acme_ca']}" == 'le-staging-v2'; then
      # shellcheck disable=SC2086
      certbot certonly --test-cert -n --authenticator ${config['acme_type']} --preferred-challenges=dns --expand --agree-tos "${email}" ${all_dns_str}
    fi

    local folder
    local letsencrypt
    folder=$(printf '%s\n' "${all_dns[@]}" | xargs | cut -d' ' -f1)
    letsencrypt="/etc/letsencrypt/live/${folder}"
    if [ -f "${letsencrypt}/privkey.pem" ]; then
        ln -vsfT "${letsencrypt}/privkey.pem"   "${pkirealm}/private/key.pem"
        ln -vsfT "${letsencrypt}/cert.pem"      "${pkirealm}/acme/cert.pem"
        ln -vsfT "${letsencrypt}/chain.pem"     "${pkirealm}/acme/intermediate.pem"
    else
      echo "error: could not symlink let's encrypt certificates into pki realm" >&2
    fi
}

request_acme_certificate () {
    if [ "${config['pki_acme']}" = "true" ]; then
        if [ "${config['acme_type']}" == "acme-tiny" ] || [ "${config['acme_type']}" == "" ]; then
          request_acme_tiny_certificate
        fi
        if [[ "${config['acme_type']}" =~ "dns-" ]]; then
          request_acme_dns_certificate
        fi
        if [[ "${config['acme_type']}" == "manual" ]]; then
          request_acme_manual_certificate
        fi

    fi
}

check_public_certificate () {

    if [ -r public/cert.pem ] ; then
        if ! check_file_signature public/cert.pem ; then
            state+=("changed-certificate")
            state+=("file-change")
        fi
    fi

}

check_files () {

    local name="${config['name']}"
    local library="${config['pki_library']}"
    local acme_library="${config['pki_acme_library']}"

    if [ "${config['pki_internal']}" == "true" ] ; then

        if [ ! -r private/realm_key.pem ] ; then
            "generate_${library}_rsa_realm_key" private/realm_key.pem
            if [ ! -r private/key.pem ] ; then
                install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key.pem.tmp
                if type setfacl > /dev/null 2>&1 ; then
                    local private_file_acl_groups_r
                    read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                    if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                        for acl_group in "${private_file_acl_groups_r[@]}" ; do
                            if getent group "${acl_group}" > /dev/null ; then
                                setfacl -m "g:${acl_group}:r" private/key.pem.tmp
                            fi
                        done
                    fi
                fi
                cat private/realm_key.pem >> private/key.pem.tmp
                mv private/key.pem.tmp private/key.pem
                state+=('new-private-key')
            fi
        fi

        if [ -r private/key.pem ] ; then
            if type setfacl > /dev/null 2>&1 ; then
                local private_file_acl_groups_r
                read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                    if ! getfacl -sc private/key.pem | grep -Eq '^group' ; then
                        for acl_group in "${private_file_acl_groups_r[@]}" ; do
                            if getent group "${acl_group}" > /dev/null ; then
                                setfacl -m "g:${acl_group}:r" private/key.pem
                            fi
                        done
                    fi
                fi
            fi
        fi

        if [ -n "${PKI_SESSION_TOKEN:-}" ] && [ -r internal/cert.pem ] ; then
            if ! ( check_expiration_time internal/cert.pem "${config['internal_expiration_seconds']}" && check_openssl_key_crt_match private/key.pem internal/cert.pem ) ; then
                rm -f "internal/${library}.conf" internal/request.pem
            fi
        fi

        if [ -n "${PKI_SESSION_TOKEN:-}" ] && [ ! -r public/cert.pem ] ; then
            if ! ( check_expiration_time internal/cert.pem "${config['internal_expiration_seconds']}" && check_openssl_key_crt_match private/key.pem internal/cert.pem ) ; then
                rm -f "internal/${library}.conf" internal/request.pem
            fi
        fi

        if [ ! -r "internal/${library}.conf" ] && [ ! -r internal/request.pem ] ; then

            if [[ ${name} != *.* && ${name} != *@* ]] ; then

                "create_${library}_config" "internal/${library}.conf" internal "${config['subject']:-${config['pki_default_subject']}}" "${config['domains']:-${config['pki_default_domain']}}" "${config['subdomains']:-${config['pki_default_subdomains']}}" "${config['subject_alt_names']:-}"

            elif [[ ${name} == *.* && ${name} != *@* ]] ; then

                "create_${library}_config" "internal/${library}.conf" internal "${config['subject']:-cn=${name}}" "${config['domains']:-${name:-${config['pki_default_domain']}}}" "${config['subdomains']:-${config['pki_default_subdomains']}}" "${config['subject_alt_names']:-}"

            fi

            "generate_${library}_request" "internal/${library}.conf" internal/request.pem
            state+=('new-internal-request')
        fi

        if [ -r "internal/${library}.conf" ] && [ -r internal/request.pem ] ; then
            if ! check_openssl_key_req_match private/key.pem internal/request.pem ; then

                rm -f internal/request.pem
                "generate_${library}_request" "internal/${library}.conf" internal/request.pem
                state+=('new-internal-request')

            fi
        fi

    elif [ "${config['pki_internal']}" == "false" ] ; then

        if [ ! -r private/realm_key.pem ] ; then
            "generate_${library}_rsa_realm_key" private/realm_key.pem
            if [ ! -r private/key.pem ] ; then
                install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key.pem.tmp
                if type setfacl > /dev/null 2>&1 ; then
                    local private_file_acl_groups_r
                    read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                    if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                        for acl_group in "${private_file_acl_groups_r[@]}" ; do
                            if getent group "${acl_group}" > /dev/null ; then
                                setfacl -m "g:${acl_group}:r" private/key.pem.tmp
                            fi
                        done
                    fi
                fi
                cat private/realm_key.pem >> private/key.pem.tmp
                mv private/key.pem.tmp private/key.pem
                state+=('new-private-key')
            fi
        fi

        if [ -r private/key.pem ] ; then
            if type setfacl > /dev/null 2>&1 ; then
                local private_file_acl_groups_r
                read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                    if ! getfacl -sc private/key.pem | grep -Eq '^group' ; then
                        for acl_group in "${private_file_acl_groups_r[@]}" ; do
                            if getent group "${acl_group}" > /dev/null ; then
                                setfacl -m "g:${acl_group}:r" private/key.pem
                            fi
                        done
                    fi
                fi
            fi
        fi

        if [ -n "${PKI_SESSION_TOKEN:-}" ] && [ -r selfsigned/cert.pem ] ; then
            if ! ( check_expiration_time selfsigned/cert.pem "${config['selfsigned_expiration_seconds']}" && check_openssl_key_crt_match private/key.pem selfsigned/cert.pem ) ; then
                rm -f "selfsigned/${library}.conf" selfsigned/request.pem
            fi
        fi

        if [ -n "${PKI_SESSION_TOKEN:-}" ] && [ ! -r public/cert.pem ] ; then
            if ! ( check_expiration_time selfsigned/cert.pem "${config['selfsigned_expiration_seconds']}" && check_openssl_key_crt_match private/key.pem selfsigned/cert.pem ) ; then
                rm -f "selfsigned/${library}.conf" selfsigned/request.pem
            fi
        fi

        if [ ! -r "selfsigned/${library}.conf" ] && [ ! -r selfsigned/request.pem ] ; then

            if [[ ${name} != *.* && ${name} != *@* ]] ; then

                "create_${library}_config" "selfsigned/${library}.conf" selfsigned \
                    "${config['subject']:-${config['pki_default_subject']}}" \
                    "${config['domains']:-${config['pki_default_domain']}}" \
                    "${config['subdomains']:-${config['pki_default_subdomains']}}" \
                    "${config['subject_alt_names']:-}"

            elif [[ ${name} == *.* && ${name} != *@* ]] ; then

                "create_${library}_config" "selfsigned/${library}.conf" selfsigned \
                    "${config['subject']:-cn=${name}}" \
                    "${config['domains']:-${name:-${config['pki_default_domain']}}}" \
                    "${config['subdomains']:-${config['pki_default_subdomains']}}" \
                    "${config['subject_alt_names']:-}"

            fi

            "generate_${library}_request" "selfsigned/${library}.conf" selfsigned/request.pem
            "selfsign_${library}_request"
            state+=('new-selfsigned-certificate')
        fi

        if [ -r "selfsigned/${library}.conf" ] && [ -r selfsigned/request.pem ] ; then
            if ! check_openssl_key_req_match private/key.pem selfsigned/request.pem ; then

                rm -f selfsigned/request.pem
                "generate_${library}_request" "selfsigned/${library}.conf" selfsigned/request.pem
                "selfsign_${library}_request"
                state+=('new-selfsigned-certificate')

            fi
        fi

    fi

    if [ "${config['pki_acme']}" = "true" ] ; then

        if [ ! -r private/realm_key.pem ] ; then
            "generate_${library}_rsa_realm_key" private/realm_key.pem
            if [ ! -r private/key.pem ] ; then
                install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key.pem.tmp
                if type setfacl > /dev/null 2>&1 ; then
                    local private_file_acl_groups_r
                    read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                    if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                        for acl_group in "${private_file_acl_groups_r[@]}" ; do
                            if getent group "${acl_group}" > /dev/null ; then
                                setfacl -m "g:${acl_group}:r" private/key.pem.tmp
                            fi
                        done
                    fi
                fi
                cat private/realm_key.pem >> private/key.pem.tmp
                mv private/key.pem.tmp private/key.pem
                state+=('new-private-key')
            fi
        fi

        if [ -r acme/cert.pem ] && [ -r "acme/${acme_library}.conf" ] && [ -r acme/request.pem ] ; then
            if ! check_expiration_time acme/cert.pem "${config['acme_expiration_seconds']}" ; then
                rm -f "acme/${acme_library}.conf" acme/request.pem
            fi
        fi

        if [ ! -r "acme/${acme_library}.conf" ] && [ ! -r acme/request.pem ] ; then

            if [[ ${name} != *.* && ${name} != *@* ]] ; then

                "create_${acme_library}_config" "acme/${acme_library}.conf" acme "${config['acme_subject']:-${config['acme_default_subject']}}" "${config['acme_domains']:-${config['acme_default_domain']}}" "${config['acme_subdomains']:-${config['acme_default_subdomains']}}" "${config['acme_alt_names']:-}"

            elif [[ ${name} == *.* && ${name} != *@* ]] ; then

                "create_${acme_library}_config" "acme/${acme_library}.conf" acme "${config['acme_subject']:-cn=${name}}" "${config['acme_domains']:-${name:-${config['acme_default_domain']}}}" "${config['acme_subdomains']:-${config['acme_default_subdomains']}}" "${config['acme_alt_names']:-}"

            fi

            "generate_${library}_rsa_private_key" acme/account_key.pem 4096 "${config['acme_file_group']}" false
            "generate_${acme_library}_request" "acme/${acme_library}.conf" acme/request.pem
            chgrp "${config['acme_file_group']}" acme/request.pem
            chmod "${config['acme_file_mode']}" acme/request.pem
            chmod "${config['acme_dir_mode']}" acme
            state+=('new-acme-request')
        fi

        if [ -r "acme/${acme_library}.conf" ] && [ -r acme/request.pem ] ; then
            if ! check_openssl_key_req_match private/key.pem acme/request.pem ; then

                rm -f acme/request.pem
                "generate_${acme_library}_request" "acme/${acme_library}.conf" acme/request.pem
                chgrp "${config['acme_file_group']}" acme/request.pem
                chmod "${config['acme_file_mode']}" acme/request.pem
                state+=('new-acme-request')

            fi

        elif [ -r "acme/${acme_library}.conf" ] && [ ! -r acme/request.pem ] ; then

            "generate_${acme_library}_request" "acme/${acme_library}.conf" acme/request.pem
            chgrp "${config['acme_file_group']}" acme/request.pem
            chmod "${config['acme_file_mode']}" acme/request.pem
            state+=('new-acme-request')

        fi

    fi

}

process_public_files () {
    # Select the preferred authority in a given PKI realm (external, acme,
    # internal) and check all of the files, make sure that everything is in order

    local authority_preference
    read -r -a authority_preference <<< "$(echo "${config['pki_authority_preference']}" | tr "/" " ")"

    for authority in "${authority_preference[@]}" ; do

        if [ -r "${authority}/cert.pem" ] && ( check_expiration_time "${authority}/cert.pem" $(( 60 * 60 * 24 * 10 )) || [ "${authority}" = "${authority_preference[-1]}" ] ) ; then

            if ! ( diff -q -N "${authority}/cert.pem" public/cert.pem > /dev/null && check_file_signature public/cert.pem ) ; then

                for source_file in "${authority}/cert.pem" "${authority}/intermediate.pem" "${authority}/root.pem" "${authority}/alt_intermediate.pem" "${authority}/alt_root.pem" ; do

                    if [ -r "${source_file}" ] ; then
                        if ! diff -q -N "${source_file}" "${source_file/${authority}/public}" > /dev/null ; then
                            ln -sf "$(get_relative_path "${source_file}" "public")" "${source_file/${authority}/public}.tmp"
                        fi
                    elif [ ! -r "${source_file}" ] && [ -r "${source_file/${authority}/public}" ] ; then
                        rm -f "${source_file/${authority}/public}"
                    fi

                done

                if [ -r public/cert.pem ] ; then
                    if ! check_file_signature public/cert.pem ; then
                        if [ -r public/intermediate.pem ] ; then
                            if [ -r public/cert_intermediate.pem.tmp ] ; then
                                rm -f public/cert_intermediate.pem.tmp
                            fi
                            cat public/cert.pem public/intermediate.pem > public/cert_intermediate.pem.tmp
                        fi
                    fi
                fi

                if [ -r public/cert.pem.tmp ] && [ -r public/intermediate.pem.tmp ] ; then
                        cat public/cert.pem.tmp public/intermediate.pem.tmp > public/cert_intermediate.pem.tmp
                elif [ -r public/cert.pem.tmp ] && [ -r public/intermediate.pem ] ; then
                        cat public/cert.pem.tmp public/intermediate.pem > public/cert_intermediate.pem.tmp
                fi

                if [ -r public/intermediate.pem.tmp ] && [ -r public/root.pem.tmp ] ; then
                    cat public/intermediate.pem.tmp public/root.pem.tmp > public/intermediate_root.pem.tmp
                elif [ -r public/intermediate.pem ] && [ -r public/root.pem.tmp ] ; then
                    cat public/intermediate.pem public/root.pem.tmp > public/intermediate_root.pem.tmp
                fi

                if [ -r public/cert.pem.tmp ] && [ -r public/intermediate_root.pem.tmp ] ; then
                    cat public/cert.pem.tmp public/intermediate_root.pem.tmp > public/full.pem.tmp
                elif [ -r public/cert.pem.tmp ] && [ -r public/intermediate_root.pem ] ; then
                    cat public/cert.pem.tmp public/intermediate_root.pem > public/full.pem.tmp
                elif [ -r public/cert.pem ] && [ -r public/intermediate_root.pem.tmp ] ; then
                    cat public/cert.pem public/intermediate_root.pem.tmp > public/full.pem.tmp
                elif [ -r public/cert.pem.tmp ] && [ -r public/root.pem.tmp ] ; then
                    cat public/cert.pem.tmp public/root.pem.tmp > public/full.pem.tmp
                elif [ -r public/cert.pem.tmp ] && [ -r public/root.pem ] ; then
                    cat public/cert.pem.tmp public/root.pem > public/full.pem.tmp
                elif [ -r public/cert.pem ] && [ -r public/root.pem.tmp ] ; then
                    cat public/cert.pem public/root.pem.tmp > public/full.pem.tmp
                fi

                if [ -r public/alt_intermediate.pem.tmp ] && [ -r public/alt_root.pem.tmp ] ; then
                    cat public/alt_intermediate.pem.tmp public/alt_root.pem.tmp > public/alt_intermediate_root.pem.tmp
                elif [ -r public/alt_intermediate.pem ] && [ -r public/alt_root.pem.tmp ] ; then
                    cat public/alt_intermediate.pem public/alt_root.pem.tmp > public/alt_intermediate_root.pem.tmp
                fi

                for public_file in public/*.pem ; do
                    if [ ! -r "${public_file/public/${authority}}" ] && [ ! -r "${public_file}.tmp" ] ; then
                        touch "${public_file}.rm"
                    fi
                done

            fi

            if [ -r private/key.pem ] && [ -r public/cert.pem ] && [ -r public/intermediate.pem ] && [ -r public/cert_intermediate.pem ] ; then

                if ! check_openssl_key_crt_match private/key.pem public/cert_intermediate.pem ; then
                    if [ -r public/cert_intermediate.pem.tmp ] ; then
                        rm -f public/cert_intermediate.pem.tmp
                    fi
                    cat public/cert.pem public/intermediate.pem > public/cert_intermediate.pem.tmp
                fi

            fi

            if [ "${config['pki_dhparam']}" = "true" ] && [ -n "${config['dhparam_file']}" ] && [ -r "${config['dhparam_file']}" ] ; then

                if [ -r public/cert_intermediate.pem.tmp ] ; then
                    cat public/cert_intermediate.pem.tmp "${config['dhparam_file']}" > public/cert_intermediate_dhparam.pem.tmp
                    state+=("changed-dhparam")
                elif [ -r public/cert.pem.tmp ] ; then
                    cat public/cert.pem.tmp "${config['dhparam_file']}" > public/cert_dhparam.pem.tmp
                    state+=("changed-dhparam")
                elif [ ! -r public/cert_intermediate_dhparam.pem ] && [ -r public/cert_intermediate.pem ] ; then
                    cat public/cert_intermediate.pem "${config['dhparam_file']}" > public/cert_intermediate_dhparam.pem.tmp
                    state+=("changed-dhparam")
                elif [ ! -r public/cert_dhparam.pem ] && [ ! -r public/cert_intermediate.pem ] && [ -r public/cert.pem ] ; then
                    cat public/cert.pem "${config['dhparam_file']}" > public/cert_dhparam.pem.tmp
                    state+=("changed-dhparam")
                elif [ -r public/cert_intermediate_dhparam.pem ] ; then
                    if ! diff -q -N "${config['dhparam_file']}" <( sed -n '/-----BEGIN DH PARAMETERS-----/,/-----END DH PARAMETERS-----/p' public/cert_intermediate_dhparam.pem ) > /dev/null ; then
                        cat public/cert_intermediate.pem "${config['dhparam_file']}" > public/cert_intermediate_dhparam.pem.tmp
                        state+=("changed-dhparam")
                    fi
                elif [ -r public/cert_dhparam.pem ] ; then
                    if ! diff -q -N "${config['dhparam_file']}" <( sed -n '/-----BEGIN DH PARAMETERS-----/,/-----END DH PARAMETERS-----/p' public/cert_dhparam.pem ) > /dev/null ; then
                        cat public/cert.pem "${config['dhparam_file']}" > public/cert_dhparam.pem.tmp
                        state+=("changed-dhparam")
                    elif ! check_file_signature public/cert.pem ; then
                        cat public/cert.pem "${config['dhparam_file']}" > public/cert_dhparam.pem.tmp
                        state+=("changed-dhparam")
                    fi
                fi

            fi

            break

        elif [ ! -r "${authority}/cert.pem" ] && [ -r "${authority}/root.pem" ] ; then

            if ! diff -q -N "${authority}/root.pem" public/root.pem > /dev/null ; then

                for source_file in "${authority}/intermediate.pem" "${authority}/root.pem" "${authority}/alt_intermediate.pem" "${authority}/alt_root.pem" ; do

                    if [ -r "${source_file}" ] ; then
                        if ! diff -q -N "${source_file}" "${source_file/${authority}/public}" > /dev/null ; then
                            ln -sf "$(get_relative_path "${source_file}" "public")" "${source_file/${authority}/public}.tmp"
                        fi
                    fi

                done

                if [ -r public/intermediate.pem.tmp ] && [ -r public/root.pem.tmp ] ; then
                    cat public/intermediate.pem.tmp public/root.pem.tmp > public/intermediate_root.pem.tmp
                elif [ -r "${authority}/intermediate.pem" ] && [ -r public/intermediate.pem ] && [ -r public/root.pem.tmp ] ; then
                    cat public/intermediate.pem public/root.pem.tmp > public/intermediate_root.pem.tmp
                fi

                if [ -r public/alt_intermediate.pem.tmp ] && [ -r public/alt_root.pem.tmp ] ; then
                    cat public/alt_intermediate.pem.tmp public/alt_root.pem.tmp > public/alt_intermediate_root.pem.tmp
                elif [ -r "${authority}/alt_intermediate.pem" ] && [ -r public/alt_intermediate.pem ] && [ -r public/alt_root.pem.tmp ] ; then
                    cat public/alt_intermediate.pem public/alt_root.pem.tmp > public/alt_intermediate_root.pem.tmp
                fi

                for public_file in public/*.pem ; do
                    if [ ! -r "${public_file/public/${authority}}" ] && [ ! -r "${public_file}.tmp" ] ; then
                        touch "${public_file}.rm"
                    fi
                done

            fi

            break

        fi

    done

}

process_private_files () {

    local current_umask
    current_umask="$(umask)"
    umask 027

    local authority_preference
    read -r -a authority_preference <<< "$(echo "${config['pki_authority_preference']}" | tr "/" " ")"

    for authority in "${authority_preference[@]}" ; do

        if [ -r "${authority}/cert.pem" ] && ( check_expiration_time "${authority}/cert.pem" $(( 60 * 60 * 24 * 10 )) || [ "${authority}" = "${authority_preference[-1]}" ] ) ; then

            if [ ! -r private/key.pem ] ; then

                if check_openssl_key_crt_match private/realm_key.pem "${authority}/cert.pem" ; then

                    if [ -r private/key.pem.tmp ] ; then
                        rm -f private/key.pem.tmp
                    fi
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key.pem.tmp
                                fi
                            done
                        fi
                    fi
                    cat private/realm_key.pem >> private/key.pem.tmp

                fi
            fi

            if [ -r public/cert.pem.tmp ] || [ -r public/cert_intermediate.pem.tmp ] ; then

                if [ -r public/cert.pem.tmp ] && [ -r private/realm_key.pem ] ; then

                    if ! check_openssl_key_crt_match private/key.pem public/cert.pem.tmp ; then

                        if [ -r private/key.pem.tmp ] ; then
                            rm -f private/key.pem.tmp
                        fi
                        install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key.pem.tmp
                        if type setfacl > /dev/null 2>&1 ; then
                            local private_file_acl_groups_r
                            read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                            if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                                for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                    if getent group "${acl_group}" > /dev/null ; then
                                        setfacl -m "g:${acl_group}:r" private/key.pem.tmp
                                    fi
                                done
                            fi
                        fi
                        cat private/realm_key.pem >> private/key.pem.tmp

                    fi

                fi

                if [ -r public/cert_intermediate.pem.tmp ] ; then

                    if [ -r private/key_chain.pem.tmp ] ; then
                        rm -f private/key_chain.pem.tmp
                    fi
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key_chain.pem.tmp
                                fi
                            done
                        fi
                    fi
                    if [ -r private/key.pem ] && check_openssl_key_crt_match private/key.pem public/cert_intermediate.pem.tmp ; then
                        cat private/key.pem public/cert_intermediate.pem.tmp >> private/key_chain.pem.tmp
                    elif [ -r private/realm_key.pem ] && check_openssl_key_crt_match private/realm_key.pem public/cert_intermediate.pem.tmp ; then
                        cat private/realm_key.pem public/cert_intermediate.pem.tmp >> private/key_chain.pem.tmp
                    fi

                elif [ -r public/cert.pem.tmp ] ; then

                    if [ -r private/key_chain.pem.tmp ] ; then
                        rm -f private/key_chain.pem.tmp
                    fi
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key_chain.pem.tmp
                                fi
                            done
                        fi
                    fi
                    if [ -r private/key.pem ] ; then
                        cat private/key.pem public/cert.pem.tmp >> private/key_chain.pem.tmp
                    elif [ -r private/realm_key.pem ] ; then
                        cat private/realm_key.pem public/cert.pem.tmp >> private/key_chain.pem.tmp
                    fi

                fi
            fi

            if [ -r private/key.pem ] && [ -r public/cert_intermediate.pem ] ; then

                if ! check_openssl_key_crt_match private/key.pem public/cert_intermediate.pem ; then
                    if [ -r private/key_chain.pem.tmp ] ; then
                        rm -f private/key_chain.pem.tmp
                    fi
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key_chain.pem.tmp
                                fi
                            done
                        fi
                    fi
                    cat private/key.pem public/cert_intermediate.pem >> private/key_chain.pem.tmp
                fi

            elif [ -r private/key.pem ] && [ -r public/cert.pem ] ; then

                if ! check_openssl_key_crt_match private/key.pem public/cert.pem ; then
                    if [ -r private/key_chain.pem.tmp ] ; then
                        rm -f private/key_chain.pem.tmp
                    fi
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key_chain.pem.tmp
                                fi
                            done
                        fi
                    fi
                    cat private/key.pem public/cert.pem >> private/key_chain.pem.tmp
                elif ! check_file_signature public/cert.pem ; then
                    if [ -r private/key_chain.pem.tmp ] ; then
                        rm -f private/key_chain.pem.tmp
                    fi
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key_chain.pem.tmp
                                fi
                            done
                        fi
                    fi
                    cat private/key.pem public/cert.pem >> private/key_chain.pem.tmp
                fi

            fi

            if [ "${config['pki_dhparam']}" = "true" ] && [ -n "${config['dhparam_file']}" ] && [ -r "${config['dhparam_file']}" ] ; then
                if [ -r private/key_chain.pem.tmp ] ; then
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain_dhparam.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key_chain_dhparam.pem.tmp
                                fi
                            done
                        fi
                    fi
                    cat private/key_chain.pem.tmp "${config['dhparam_file']}" >> private/key_chain_dhparam.pem.tmp
                    state+=("changed-dhparam")
                elif [ ! -r private/key_chain_dhparam.pem ] && [ -r private/key_chain.pem ] ; then
                    install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain_dhparam.pem.tmp
                    if type setfacl > /dev/null 2>&1 ; then
                        local private_file_acl_groups_r
                        read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                        if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                            for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                if getent group "${acl_group}" > /dev/null ; then
                                    setfacl -m "g:${acl_group}:r" private/key_chain_dhparam.pem.tmp
                                fi
                            done
                        fi
                    fi
                    cat private/key_chain.pem "${config['dhparam_file']}" >> private/key_chain_dhparam.pem.tmp
                    state+=("changed-dhparam")
                elif [ -r private/key_chain_dhparam.pem ] ; then
                    if ! diff -q -N "${config['dhparam_file']}" <( sed -n '/-----BEGIN DH PARAMETERS-----/,/-----END DH PARAMETERS-----/p' private/key_chain_dhparam.pem ) > /dev/null ; then
                        install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key_chain_dhparam.pem.tmp
                        if type setfacl > /dev/null 2>&1 ; then
                            local private_file_acl_groups_r
                            read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                            if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                                for acl_group in "${private_file_acl_groups_r[@]}" ; do
                                    if getent group "${acl_group}" > /dev/null ; then
                                        setfacl -m "g:${acl_group}:r" private/key_chain_dhparam.pem.tmp
                                    fi
                                done
                            fi
                        fi
                        cat private/key_chain.pem "${config['dhparam_file']}" >> private/key_chain_dhparam.pem.tmp
                        state+=("changed-dhparam")
                    fi
                fi
            fi

            break

        elif [ ! -r "${authority}/cert.pem" ] && [ -r "${authority}/root.pem" ] ; then

            if ! diff -q -N "${authority}/root.pem" public/root.pem > /dev/null ; then

                if [ -r private/key_chain.pem ] ; then
                    touch private/key_chain.pem.rm
                fi
                if [ -r private/key_chain_dhparam.pem ] ; then
                    touch private/key_chain_dhparam.pem.rm
                fi

            fi

            break
        fi

    done

    umask "${current_umask}"
}

activate_new_files () {

    if [ -r public/cert.pem.tmp ] || [ -r public/root.pem.tmp ] ; then

        if [ ! -r public/intermediate.pem.tmp ] && [ -r public/intermediate.pem ] ; then
            rm -f public/intermediate.pem public/cert_intermediate.pem public/cert_intermediate_dhparam.pem public/chain.pem public/intermediate_root.pem
            state+=("file-change")
        fi

        if [ ! -r public/alt_intermediate.pem.tmp ] && [ -r public/alt_intermediate.pem ] ; then
            rm -f public/alt_intermediate.pem public/alt_intermediate_root.pem public/alt_trusted.pem
            state+=("file-change")
        fi

        state+=("changed-certificate")
    fi

    while IFS= read -r -d '' rm_file
    do
        rm -f "${rm_file/.rm/}" "${rm_file}"
        state+=("file-deletion")
    done <   <(find public/ -maxdepth 1 -name '*.rm' -print0)

    if [ ! -r public/cert.pem ] && [ -e public/cert.pem.sig ] ; then
        rm -f public/cert.pem.sig
    fi

    while IFS= read -r -d '' new_file
    do
        mv --force "${new_file}" "${new_file/.tmp/}"
        state+=("changed-public-file")
        state+=("file-change")
    done <   <(find public/ -maxdepth 1 -name '*.tmp' -print0)

    while IFS= read -r -d '' rm_file
    do
        rm -f "${rm_file/.rm/}" "${rm_file}"
        state+=("file-deletion")
    done <   <(find private/ -maxdepth 1 -name '*.rm' -print0)

    while IFS= read -r -d '' new_file
    do
        mv --force "${new_file}" "${new_file/.tmp/}"
        state+=("changed-private-file")
        state+=("file-change")
    done <   <(find private/ -maxdepth 1 -name '*.tmp' -print0)

    update_file_signature public/cert.pem
    update_realm_symlinks

}

update_realm_symlinks () {

    if [ -r public/intermediate.pem ] ; then
        cd public || exit 1
        update_symlink chain.pem cert_intermediate_dhparam.pem cert_intermediate.pem cert_dhparam.pem cert.pem
        update_symlink trusted.pem intermediate_root.pem root.pem
        cd - > /dev/null
    fi

    if [ -r public/alt_intermediate_root.pem ] ; then
        cd public || exit 1
        update_symlink alt_trusted.pem alt_intermediate_root.pem alt_root.pem
        cd - > /dev/null
    fi

    update_symlink default.crt public/chain.pem public/cert_dhparam.pem public/cert.pem
    update_symlink default.key private/key.pem
    update_symlink default.pem private/key_chain_dhparam.pem private/key_chain.pem
    update_symlink CA.crt public/alt_trusted.pem public/alt_root.pem public/trusted.pem public/root.pem "${config['pki_default_ca_bundle']}"
    update_symlink trusted.crt public/trusted.pem public/root.pem

}

sub_new-realm () {

    local -A args

    local optspec=":hn-:"
    while getopts "${optspec}" optchar; do
        case "${optchar}" in
            -)
                case "${OPTARG}" in
                    name)
                        args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    name=*)
                        args["name"]=${OPTARG#*=}
                        ;;
                    library)
                        args["pki_library"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    library=*)
                        args["pki_library"]=${OPTARG#*=}
                        ;;
                    acme-library)
                        args["pki_acme_library"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-library=*)
                        args["pki_acme_library"]=${OPTARG#*=}
                        ;;
                    internal)
                        args["pki_internal"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    internal=*)
                        args["pki_internal"]=${OPTARG#*=}
                        ;;
                    subject)
                        args["subject"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    subject=*)
                        args["subject"]=${OPTARG#*=}
                        ;;
                    domains)
                        args["domains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    domains=*)
                        args["domains"]=${OPTARG#*=}
                        ;;
                    subdomains)
                        args["subdomains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    subdomains=*)
                        args["subdomains"]=${OPTARG#*=}
                        ;;
                    subject-alt-names)
                        args["subject_alt_names"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    subjest-alt-names=*)
                        args["subject_alt_names"]=${OPTARG#*=}
                        ;;
                    acme)
                        args["pki_acme"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme=*)
                        args["pki_acme"]=${OPTARG#*=}
                        ;;
                    acme-type)
                        args["acme_type"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-type=*)
                        args["acme_type"]=${OPTARG#*=}
                        ;;
                    acme-subject)
                        args["acme_subject"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-subject=*)
                        args["acme_subject"]=${OPTARG#*=}
                        ;;
                    acme-domains)
                        args["acme_domains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-domains=*)
                        args["acme_domains"]=${OPTARG#*=}
                        ;;
                    acme-subdomains)
                        args["acme_subdomains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-subdomains=*)
                        args["acme_subdomains"]=${OPTARG#*=}
                        ;;
                    acme-alt-names)
                        # shellcheck disable=SC2154
                        args["acme_alt_names"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-alt-names=*)
                        args["acme_alt_names"]=${OPTARG#*=}
                        ;;
                    *)
                        if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                            echo "Unknown option --${OPTARG}" >&2
                        fi
                        ;;
                esac
                ;;
            h)
                echo "usage: ${script_name} new-realm <-n|--name[=]realm>" >&2
                exit 2
                ;;
            n)
                args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                ;;
            *)
                if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                    echo "Non-option argument: '-${OPTARG}'" >&2
                fi
                ;;
        esac
    done

    if key_exists args "name" ; then

        local pidfile="${config['lock_dir']}/${script_name}-${args['name']}.pid"

        wait_for_pid "${pidfile}"
        create_lock "${pidfile}"

        enter_realm "${args['name']}"

        local config_changed="false"

        for key in "${!args[@]}" ; do
            if [ -n "${args[${key}]+x}" ] ; then
                if key_exists config "${key}" ; then
                    if [ "${config[${key}]}" != "${args[${key}]}" ] ; then
                        local config_changed="true"
                        config["${key}"]="${args[${key}]}"
                    fi
                else
                    local config_changed="true"
                    config["${key}"]="${args[${key}]}"
                fi
            fi
        done

        create_public_directories "${config['public_dir_group']}" config external internal public

        if [ "${config['pki_internal']}" = "false" ] ; then
            create_public_directories "${config['public_dir_group']}" selfsigned
        fi

        create_private_directories "${config['private_dir_group']}" private
        create_private_noacl_directories "${config['acme_dir_group']}" acme

        if [ "${config_changed}" = "true" ] ; then
            save_realm_config config/realm.conf
        fi

        local name="${config['name']}"
        local library="${config['pki_library']}"
        local acme_library="${config['pki_acme_library']}"

        "generate_${library}_rsa_realm_key" private/realm_key.pem
        if [ ! -r private/key.pem ] ; then
            install -g "${config['private_file_group']}" -m "${config['private_file_mode']}" /dev/null private/key.pem.tmp
            if type setfacl > /dev/null 2>&1 ; then
                local private_file_acl_groups_r
                read -r -a private_file_acl_groups_r <<< "$(echo "${config['private_file_acl_groups_r']}" | tr "/" " ")"
                if [ ${#private_file_acl_groups_r[@]} -ne 0 ] ; then
                    for acl_group in "${private_file_acl_groups_r[@]}" ; do
                        if getent group "${acl_group}" > /dev/null ; then
                            setfacl -m "g:${acl_group}:r" private/key.pem.tmp
                        fi
                    done
                fi
            fi
            cat private/realm_key.pem >> private/key.pem.tmp
            mv private/key.pem.tmp private/key.pem
            state+=("new-private-key")
        fi

        if [[ ${name} != *.* && ${name} != *@* ]] ; then

            if [ "${config['pki_internal']}" = "false" ] ; then
                "create_${library}_config" "selfsigned/${library}.conf" selfsigned "${config['subject']:-${config['pki_default_subject']}}" "${config['domains']:-${config['pki_default_domain']}}" "${config['subdomains']:-${config['pki_default_subdomains']}}" "${config['subject_alt_names']:-}"
            fi

            "create_${library}_config" "internal/${library}.conf" internal "${config['subject']:-${config['pki_default_subject']}}" "${config['domains']:-${config['pki_default_domain']}}" "${config['subdomains']:-${config['pki_default_subdomains']}}" "${config['subject_alt_names']:-}"

            if [ "${config['pki_acme']}" = "true" ] ; then
                "create_${acme_library}_config" "acme/${acme_library}.conf" acme "${config['acme_subject']:-${config['acme_default_subject']}}" "${config['acme_domains']:-${config['acme_default_domain']}}" "${config['acme_subdomains']:-${config['acme_default_subdomains']}}" "${config['acme_alt_names']:-}"
            fi

        elif [[ ${name} == *.* && ${name} != *@* ]] ; then

            if [ "${config['pki_internal']}" = "false" ] ; then
                "create_${library}_config" "selfsigned/${library}.conf" selfsigned "${config['subject']:-cn=${name}}" "${config['domains']:-${name:-${config['pki_default_domain']}}}" "${config['subdomains']:-${config['pki_default_subdomains']}}" "${config['subject_alt_names']:-}"
            fi

            "create_${library}_config" "internal/${library}.conf" internal "${config['subject']:-cn=${name}}" "${config['domains']:-${name:-${config['pki_default_domain']}}}" "${config['subdomains']:-${config['pki_default_subdomains']}}" "${config['subject_alt_names']:-}"

            if [ "${config['pki_acme']}" = "true" ] ; then
                "create_${acme_library}_config" "acme/${acme_library}.conf" acme "${config['acme_subject']:-cn=${name}}" "${config['acme_domains']:-${name:-${config['acme_default_domain']}}}" "${config['acme_subdomains']:-${config['acme_default_subdomains']}}" "${config['acme_alt_names']:-}"
            fi

        fi

        if [ "${config['pki_internal']}" = "false" ] ; then
            "generate_${library}_request" "selfsigned/${library}.conf" selfsigned/request.pem
            "selfsign_${library}_request"
            state+=("new-selfsigned-certificate")
        fi

        "generate_${library}_request" "internal/${library}.conf" internal/request.pem
        state+=("new-internal-request")
        if [ "${config['pki_acme']}" = "true" ] ; then
            "generate_${library}_rsa_private_key" acme/account_key.pem 4096 "${config['acme_file_group']}" false
            "generate_${acme_library}_request" "acme/${acme_library}.conf" acme/request.pem
            chgrp "${config['acme_file_group']}" acme/request.pem
            chmod "${config['acme_file_mode']}" acme/request.pem
            chmod "${config['acme_dir_mode']}" acme
            state+=("new-acme-request")
        fi

        update_realm_symlinks

        run_pki_hooks

    fi
}

sub_init () {

    local -A args

    local optspec=":hn-:"
    while getopts "${optspec}" optchar; do
        case "${optchar}" in
            -)
                case "${OPTARG}" in
                    name)
                        args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    name=*)
                        args["name"]=${OPTARG#*=}
                        ;;
                    public-dir-group)
                        args["public_dir_group"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    public-dir-group=*)
                        args["public_dir_group"]=${OPTARG#*=}
                        ;;
                    private-dir-group)
                        args["private_dir_group"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    private-dir-group=*)
                        args["private_dir_group"]=${OPTARG#*=}
                        ;;
                    private-file-group)
                        args["private_file_group"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    private-file-group=*)
                        args["private_file_group"]=${OPTARG#*=}
                        ;;
                    private-dir-acl-groups)
                        args["private_dir_acl_groups_x"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    private-dir-acl-groups=*)
                        args["private_dir_acl_groups_x"]=${OPTARG#*=}
                        ;;
                    private-file-acl-groups)
                        args["private_file_acl_groups_r"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    private-file-acl-groups=*)
                        args["private_file_acl_groups_r"]=${OPTARG#*=}
                        ;;
                    authority-preference)
                        args["pki_authority_preference"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    authority-preference=*)
                        args["pki_authority_preference"]=${OPTARG#*=}
                        ;;
                    library)
                        args["pki_library"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    library=*)
                        args["pki_library"]=${OPTARG#*=}
                        ;;
                    internal)
                        args["pki_internal"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    internal=*)
                        args["pki_internal"]=${OPTARG#*=}
                        ;;
                    selfsigned-sign-days)
                        args["selfsigned_sign_days"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    selfsigned-sign-days=*)
                        args["selfsigned_sign_days"]=${OPTARG#*=}
                        ;;
                    subject)
                        args["subject"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    subject=*)
                        args["subject"]=${OPTARG#*=}
                        ;;
                    domains)
                        args["domains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    domains=*)
                        args["domains"]=${OPTARG#*=}
                        ;;
                    subdomains)
                        args["subdomains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    subdomains=*)
                        args["subdomains"]=${OPTARG#*=}
                        ;;
                    subject-alt-names)
                        args["subject_alt_names"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    subjest-alt-names=*)
                        args["subject_alt_names"]=${OPTARG#*=}
                        ;;
                    default-domain)
                        args["pki_default_domain"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    default-domain=*)
                        args["pki_default_domain"]=${OPTARG#*=}
                        ;;
                    default-subdomains)
                        args["pki_default_subdomains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    default-subdomains=*)
                        args["pki_default_subdomains"]=${OPTARG#*=}
                        ;;
                    dhparam)
                        args["pki_dhparam"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    dhparam=*)
                        args["pki_dhparam"]=${OPTARG#*=}
                        ;;
                    dhparam-file)
                        args["dhparam_file"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    dhparam-file=*)
                        args["dhparam_file"]=${OPTARG#*=}
                        ;;
                    realm-key-size)
                        args["realm_key_size"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    realm-key-size=*)
                        args["realm_key_size"]=${OPTARG#*=}
                        ;;
                    acme)
                        args["pki_acme"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme=*)
                        args["pki_acme"]=${OPTARG#*=}
                        ;;
                    acme-type)
                        args["acme_type"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-type=*)
                        args["acme_type"]=${OPTARG#*=}
                        ;;
                    acme-subject)
                        args["acme_subject"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-subject=*)
                        args["acme_subject"]=${OPTARG#*=}
                        ;;
                    acme-domains)
                        args["acme_domains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-domains=*)
                        args["acme_domains"]=${OPTARG#*=}
                        ;;
                    acme-subdomains)
                        args["acme_subdomains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-subdomains=*)
                        args["acme_subdomains"]=${OPTARG#*=}
                        ;;
                    acme-alt-names)
                        args["acme_alt_names"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-alt-names=*)
                        args["acme_alt_names"]=${OPTARG#*=}
                        ;;
                    acme-ca)
                        args["acme_ca"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-ca=*)
                        args["acme_ca"]=${OPTARG#*=}
                        ;;
                    acme-ca-api)
                        args["acme_ca_api"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-ca-api=*)
                        args["acme_ca_api"]=${OPTARG#*=}
                        ;;
                    acme-contacts)
                        args["acme_contacts"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-contacts=*)
                        args["acme_contacts"]=${OPTARG#*=}
                        ;;
                    acme-default-subdomains)
                        args["acme_default_subdomains"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-default-subdomains=*)
                        args["acme_default_subdomains"]=${OPTARG#*=}
                        ;;
                    acme-challenge-dir)
                        args["acme_challenge_dir"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    acme-challenge-dir=*)
                        args["acme_challenge_dir"]=${OPTARG#*=}
                        ;;
                    *)
                        if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                            echo "Unknown option --${OPTARG}" >&2
                        fi
                        ;;
                esac
                ;;
            h)
                echo "usage: ${script_name} init <-n|--name[=]realm>" >&2
                exit 2
                ;;
            n)
                args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                ;;
            *)
                if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                    echo "Non-option argument: '-${OPTARG}'" >&2
                fi
                ;;
        esac
    done

    if key_exists args "name" ; then

        local pidfile="${config['lock_dir']}/${script_name}-${args['name']}.pid"

        wait_for_pid "${pidfile}"
        create_lock "${pidfile}"

        enter_realm "${args['name']}"

        local config_changed="false"

        for key in "${!args[@]}" ; do
            if [ -n "${args[${key}]+x}" ] ; then
                if key_exists config "${key}" ; then
                    if [ "${config[${key}]}" != "${args[${key}]}" ] ; then
                        local config_changed="true"
                        config["${key}"]="${args[${key}]}"
                    fi
                else
                    local config_changed="true"
                    config["${key}"]="${args[${key}]}"
                fi
            fi
        done

        create_public_directories "${config['public_dir_group']}" config external internal public
        create_private_directories "${config['private_dir_group']}" private
        create_private_noacl_directories "${config['acme_dir_group']}" acme

        if [ "${config['pki_internal']}" = "false" ] ; then
            create_public_directories "${config['public_dir_group']}" selfsigned
        fi

        if [ "${config_changed}" = "true" ] ; then
            save_realm_config config/realm.conf
        fi

        run_pki_hooks

    fi
}

sub_run () {

    local -A args

    local optspec=":hn-:"
    while getopts "${optspec}" optchar; do
        case "${optchar}" in
            -)
                case "${OPTARG}" in
                    name)
                        args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    name=*)
                        args["name"]=${OPTARG#*=}
                        ;;
                    *)
                        if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                            echo "Unknown option --${OPTARG}" >&2
                        fi
                        ;;
                esac
                ;;
            h)
                echo "usage: ${script_name} run <-n|--name[=]realm>" >&2
                exit 2
                ;;
            n)
                args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                ;;
            *)
                if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                    echo "Non-option argument: '-${OPTARG}'" >&2
                fi
                ;;
        esac
    done

    if key_exists args "name" ; then

        local pidfile="${config['lock_dir']}/${script_name}-${args['name']}.pid"

        wait_for_pid "${pidfile}"
        create_lock "${pidfile}"

        enter_realm "${args['name']}"

        check_files
        request_acme_certificate
        run_external_script
        process_public_files
        process_private_files
        activate_new_files
        update_realm_symlinks
        check_public_certificate
        run_pki_hooks

    fi
}

schedule_job () {
    local name="${1}"
    local scheduler_type="${2}"
    local min_delay="${3:-30}"
    local max_delay="${4:-300}"

    if [ -n "${scheduler_type}" ] && [ "${scheduler_type}" = "batch" ] ; then
        if type batch > /dev/null 2>&1 ; then
            echo "${script} run -n '${name}'" | batch > /dev/null 2>&1
        fi
    elif [ -n "${scheduler_type}" ] && [ "${scheduler_type}" = "sleep" ] ; then
        ( sleep $(( ( RANDOM % max_delay ) + min_delay )) ; ${script} run -n "${name}" ) &
    else
        if type batch > /dev/null 2>&1 ; then
            echo "${script} run -n '${name}'" | batch > /dev/null 2>&1
        else
            ( sleep $(( ( RANDOM % max_delay ) + min_delay )) ; ${script} run -n "${name}" ) &
        fi
    fi
}

sub_schedule () {

    local -A args

    local optspec=":hn-:"
    while getopts "${optspec}" optchar; do
        case "${optchar}" in
            -)
                case "${OPTARG}" in
                    name)
                        args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    name=*)
                        args["name"]=${OPTARG#*=}
                        ;;
                    realm-dir)
                        # shellcheck disable=SC2154
                        args["realm_dir"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    realm-dir=*)
                        args["realm_dir"]=${OPTARG#*=}
                        ;;
                    type)
                        # shellcheck disable=SC2154
                        args["type"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    type=*)
                        args["type"]=${OPTARG#*=}
                        ;;
                    min-delay)
                        args["min_delay"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    min-delay=*)
                        args["min_delay"]=${OPTARG#*=}
                        ;;
                    max-delay)
                        args["max_delay"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                        ;;
                    max-delay=*)
                        args["max_delay"]=${OPTARG#*=}
                        ;;
                    *)
                        if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                            echo "Unknown option --${OPTARG}" >&2
                        fi
                        ;;
                esac
                ;;
            h)
                echo "usage: ${script_name} schedule <-n|--name[=]realm>" >&2
                exit 2
                ;;
            n)
                args["name"]="${!OPTIND}"; OPTIND=$(( OPTIND + 1 ))
                ;;
            *)
                if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                    echo "Non-option argument: '-${OPTARG}'" >&2
                fi
                ;;
        esac
    done

    if key_exists args "name" ; then
        schedule_job "${args['name']}" "${args['type']:-}" "${args['min_delay']:-}" "${args['max_delay']:-}"
    elif key_exists args "realm_dir" ; then
        for realm in "${args['realm_dir']}"/* ; do
            schedule_job "$(basename "${realm}")" "${args['type']:-}" "${args['min_delay']:-}" "${args['max_delay']:-}"
        done
    else
        if [ -d "${config['pki_realms']}" ] ; then
            for realm in "${config['pki_realms']}"/* ; do
                schedule_job "$(basename "${realm}")" "${args['type']:-}" "${args['min_delay']:-}" "${args['max_delay']:-}"
            done
        fi
    fi
}

print_usage () {
    cat << EOF
Usage: ${script_name} <command> [parameters]
Available commands:

            init
            "usage: ${script_name} init <-n|--name[=]realm>"

            new-realm
            "usage: ${script_name} new-realm <-n|--name[=]realm>"

            run
            "usage: ${script_name} run <-n|--name[=]realm>"

            schedule
            "usage: ${script_name} schedule <-n|--name[=]realm>"
EOF

}

initialize_environment

if [ $# -gt 0 ] ; then

    subcommand="${1}"

    if [ -n "${subcommand}" ] ; then
        case "${subcommand}" in
            init|new-realm|run|schedule)
                shift
                "sub_${subcommand}" "${@}"
                ;;

            *)
                echo "${script_name}: Error: unknown subcommand '${subcommand}'" >&2
                print_usage
                exit 1
                ;;
        esac
    fi

else
    print_usage
    exit 1
fi
