#!/usr/bin/env bash

set -e
set -u
set -o pipefail

NAME="cert-gen"
VERSION="v0.10"
DATE="2022-12-18"

# Generate default options
DEF_KEYSIZE=2048
DEF_DAYS=825
DEF_SIGN_SIGNATURE="sha256"
# Subject default options
DEF_COUNTRY=
DEF_STATE=
DEF_CITY=
DEF_ORG=
DEF_UNIT=
DEF_CN=
DEF_EMAIL=

# v3 subject alt names
DEF_ALT_NAME=
DEF_ALT_IP_NAME=

# Verbosity
DEF_VERBOSE=

log() {
	local type="${1}"      # err, warn, info
	local message="${2}"   # message to log

	if [ "${type}" = "err" ]; then
		printf "%s: [ERR]  %s\n" "${NAME}" "${message}" 1>&2	# stdout -> stderr
	fi
	if [ "${type}" = "warn" ]; then
		printf "%s: [WARN] %s\n" "${NAME}" "${message}" 1>&2	# stdout -> stderr
	fi
	if [ "${DEF_VERBOSE:-}" = "1" ]; then
		if [ "${type}" = "info" ]; then
			printf "%s: [INFO] %s\n" "${NAME}" "${message}"
		fi
	fi
}

print_version() {
	echo "${NAME}: Version ${VERSION} (${DATE}) by cytopia"
	echo "https://github.com/devilbox/cert-gen/"
}

print_help() {
	echo "USAGE: ${NAME} -n CN [-kdcsloueav] <ca-key> <ca-crt> <key> <csr> <crt>"
	echo "       ${NAME} --help"
	echo "       ${NAME} --version"
	echo
	echo "Required arguments"
	echo "  -n CN       Common Name"
	echo
	echo "Optional arguments"
	echo "  -k int      Key size in bits"
	echo "  -d int      Validity in days"
	echo "  -c C        Subject two letter country name (C)"
	echo "  -s ST       Subject state name (ST)"
	echo "  -l L        Subject location (L)"
	echo "  -o O        Subject organization (O)"
	echo "  -u OU       Subject organizational unit (OU)"
	echo "  -e Email    Subject email (emailAddress)"
	echo "  -a names    Comma separated list of alt names (subjectAltName)"
	echo "  -i ips      Comma separated list of alt ip addresses (subjectAltName)"
	echo "  -v          Verbose output"
	echo
	echo "Required parameter"
	echo "  <ca-key>    Path to existing CA key file"
	echo "  <ca-crt>    Path to existing CA crt file"
	echo "  <key>       Path to output certificate key file"
	echo "  <csr>       Path to output certificate csr file"
	echo "  <crt>       Path to output certificate crt file"
}


################################################################################
# Entrypoint: Parse cmd args
################################################################################

# Get options
while [ ${#} -gt 0 ]; do
	case "${1}" in
		# ---- Help / version
		--version)
			print_version
			exit
			;;
		--help)
			print_help
			exit
			;;
		-v)
			DEF_VERBOSE=1
			shift
			;;
		# ---- Options
		-k)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -k requires an argument."
				exit 1
			fi
			DEF_KEYSIZE="${1}"
			shift
			;;
		-d)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -d requires an argument."
				exit 1
			fi
			DEF_DAYS="${1}"
			shift
			;;
		-c)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -c requires an argument."
				exit 1
			fi
			DEF_COUNTRY="${1}"
			shift
			;;
		-s)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -s requires an argument."
				exit 1
			fi
			DEF_STATE="${1}"
			shift
			;;
		-l)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -l requires an argument."
				exit 1
			fi
			DEF_CITY="${1}"
			shift
			;;
		-o)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -o requires an argument."
				exit 1
			fi
			DEF_ORG="${1}"
			shift
			;;
		-u)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -u requires an argument."
				exit 1
			fi
			DEF_UNIT="${1}"
			shift
			;;
		-n)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -n requires an argument."
				exit 1
			fi
			DEF_CN="${1}"
			shift
			;;
		-e)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -e requires an argument."
				exit 1
			fi
			DEF_EMAIL="${1}"
			shift
			;;
		-a)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -a requires an argument."
				exit 1
			fi
			DEF_ALT_NAME="${1}"
			shift
			;;
		-i)
			shift
			if [ -z "${1:-}" ]; then
				log "err" "Usage: -i requires an argument."
				exit 1
			fi
			DEF_ALT_IP_NAME="${1}"
			shift
			;;
		# ---- Stop here
		--) # End of all options
			shift
			break
			;;
		-*) # Unknown option
			log "err" "Usage: Unknown option: ${1}"
			exit 1
			;;
		*)  # No more options
			break
			;;
	esac
done


################################################################################
# Entrypoint: Validate cmd args
################################################################################

if [ -z "${DEF_CN}" ]; then
	log "err" "Usage: -n is required. See --help for help."
	exit 1
fi

if [ "${#}" -lt "5" ]; then
	log "err" "Usage: <ca-key> <ca-crt> <key> <csr> and <crt> are required. See --help for help."
	exit 1
fi

CA_KEY_FILE="${1}"
CA_CRT_FILE="${2}"
KEY_FILE="${3}"
CSR_FILE="${4}"
CRT_FILE="${5}"

if [ ! -f "${CA_KEY_FILE}" ]; then
	log "err" "Usage: <ca-key> file does not exist in: ${CA_KEY_FILE}"
	exit 1
fi
if [ ! -f "${CA_CRT_FILE}" ]; then
	log "err" "Usage: <ca-crt> file does not exist in: ${CA_CRT_FILE}"
	exit 1
fi




################################################################################
# Entrypoint: Execute
################################################################################

###
### Build subject
###
SUBJECT=
if [ -n "${DEF_COUNTRY}" ]; then
	SUBJECT="${SUBJECT}/C=${DEF_COUNTRY}"
fi
if [ -n "${DEF_STATE}" ]; then
	SUBJECT="${SUBJECT}/ST=${DEF_STATE}"
fi
if [ -n "${DEF_CITY}" ]; then
	SUBJECT="${SUBJECT}/L=${DEF_CITY}"
fi
if [ -n "${DEF_ORG}" ]; then
	SUBJECT="${SUBJECT}/O=${DEF_ORG}"
fi
if [ -n "${DEF_UNIT}" ]; then
	SUBJECT="${SUBJECT}/OU=${DEF_UNIT}"
fi
if [ -n "${DEF_CN}" ]; then
	SUBJECT="${SUBJECT}/CN=${DEF_CN}"
fi
if [ -n "${DEF_EMAIL}" ]; then
	SUBJECT="${SUBJECT}/emailAddress=${DEF_EMAIL}"
fi

# Create subject altnames
ALT_NAMES="DNS.1:${DEF_CN}"
i=2
if [ -n "${DEF_ALT_NAME}" ]; then
	for cn in ${DEF_ALT_NAME//,/ }; do
		ALT_NAMES="${ALT_NAMES},DNS.${i}:${cn}"
	done
fi

i=1
if [ -n "${DEF_ALT_IP_NAME}" ]; then
	for cn in ${DEF_ALT_IP_NAME//,/ }; do
		ALT_NAMES="${ALT_NAMES},IP.${i}:${cn}"
	done
fi

###
### Build commands
###

###
### 1. Key and Signing Request
###

OPENSSL_CONFIG="$( cat <<'HEREDOC'
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req

[req_distinguished_name]

[ v3_req ]
basicConstraints = critical, CA:FALSE
subjectKeyIdentifier = hash
keyUsage = critical, digitalSignature, keyEncipherment
authorityKeyIdentifier = keyid:always,issuer:always
extendedKeyUsage = serverAuth, clientAuth
subjectAltName=${ALT_NAMES}
HEREDOC
)"

# Command
cmd="openssl req \
  -newkey rsa:${DEF_KEYSIZE} \
  -${DEF_SIGN_SIGNATURE} \
  -nodes \
  -extensions v3_req \
  -config <(echo \"${OPENSSL_CONFIG}\") \
  -keyout ${KEY_FILE} \
  -subj '${SUBJECT}' \
  -out ${CSR_FILE}"

# Trim newlines/whitespaces
cmd="$( echo "${cmd}" | tr -s " " )"


# Execute
log "info" "Create CSR file: ${CSR_FILE}"
if ! out="$( eval "${cmd}" 2>&1 )"; then
	log "err" "Command: ${cmd}"
	log "err" "Output: ${out}"
	exit 1
fi


###
### 2. Create Certificate
###

# Command
# shellcheck disable=SC1117
cmd="openssl x509 \
  -req \
  -${DEF_SIGN_SIGNATURE} \
  -extensions v3_req \
  -extfile <(echo \"${OPENSSL_CONFIG}\") \
  -days ${DEF_DAYS} \
  -in ${CSR_FILE} \
  -CA ${CA_CRT_FILE} \
  -CAkey ${CA_KEY_FILE} \
  -CAcreateserial \
  -out ${CRT_FILE}"

# Trim newlines/whitespaces
cmd="$( echo "${cmd}" | tr -s " " )"


# Execute
log "info" "Create CRT file: ${CRT_FILE}"
if ! out="$( eval "${cmd}" 2>&1 )"; then
	log "err" "Command: ${cmd}"
	log "err" "Output: ${out}"
	exit 1
fi


###
### 4. Validate
###
log "info" "Verify CRT file: ${CRT_FILE}"
if ! out="$( openssl x509 -in "${CRT_FILE}" -text -noout )"; then
	log "err" "CRT verification failed: ${out}"
	exit 1
fi

log "info" "Verify CRT against CA file: ${CA_CRT_FILE}"
if ! out="$( openssl verify -verbose -CAfile "${CA_CRT_FILE}" "${CRT_FILE}" )"; then
	log "err" "CA verification failed: ${out}"
	exit 1
fi
