#!/usr/bin/bash

#
# sample/swtpm-localca
#
# Authors: Stefan Berger <stefanb@us.ibm.com>
#
# (c) Copyright IBM Corporation 2014, 2015.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the names of the IBM Corporation nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

# some flags
SETUP_TPM2_F=1
# for TPM 2 EK
ALLOW_SIGNING_F=2
DECRYPTION_F=4

LOCALCA_OPTIONS="swtpm-localca.options"
if [ -n "$XDG_CONFIG_HOME" ] && [ -r "$XDG_CONFIG_HOME/$LOCALCA_OPTIONS" ]; then
    LOCALCA_OPTIONS="$XDG_CONFIG_HOME/$LOCALCA_OPTIONS"
elif [ -n "$HOME" ] && [ -r "$HOME/.config/$LOCALCA_OPTIONS" ]; then
    LOCALCA_OPTIONS="$HOME/.config/$LOCALCA_OPTIONS"
else
    LOCALCA_OPTIONS="/etc/$LOCALCA_OPTIONS"
fi

LOCALCA_CONFIG="swtpm-localca.conf"
if [ -n "$XDG_CONFIG_HOME" ] && [ -r "$XDG_CONFIG_HOME/$LOCALCA_CONFIG" ]; then
    LOCALCA_CONFIG="$XDG_CONFIG_HOME/$LOCALCA_CONFIG"
elif [ -n "$HOME" ] && [ -r "$HOME/.config/$LOCALCA_CONFIG" ]; then
    LOCALCA_CONFIG="$HOME/.config/$LOCALCA_CONFIG"
else
    LOCALCA_CONFIG="/etc/$LOCALCA_CONFIG"
fi

# Default logging goes to stderr
LOGFILE=""

UNAME_S=$(uname -s)

logit()
{
	if [ -z "$LOGFILE" ]; then
		echo "$@" >&1
	else
		echo "$@" >> "$LOGFILE"
	fi
}

logerr()
{
	if [ -z "$LOGFILE" ]; then
		echo "Error: $*" >&2
	else
		echo "Error: $*" >> "$LOGFILE"
	fi
}

flock_fd()
{
	local fd=$1

	case "${UNAME_S}" in
	Darwin)
		flock "$fd";;
	*)
		flock -x "$fd";;
	esac
}

# Escape a string for call with eval. Replace any occurrences of '$(' with
# '(' to avoid subshells and escape '][;|&()'`'<>!\#*' since those are shell
# special characters.  Unescape any already-escaped characters and then
# escape again. Do NOT escape '${' since we use it for variable substitution
# but escape all other $-sequences. Squeeze all sequences of multiple '\'.
escape_eval() {
	echo "$1" | \
	sed	-e 's/\$(/(/g' \
		-e 's/\\\([][;|&()`<>!#*]\)/\1/g' -e 's/\([][;|&()`<>!#*]\)/\\\1/g' \
		-e "s/\\\'/'/g" -e "s/'/\\\'/g" \
		-e 's/$\([^{]\)/\\$\\\1/g' | \
	tr -s '\\'
}

# Get a configuration value from a configuration file
# @param1: The file with the options
# @param2: The name of the option
# @param3: The default value
get_config_value() {
	local configfile="$1"
	local configname="$(echo "$2" | sed 's/-/\\-/g')"
	local defaultvalue="$3"
	local tmp

	if [ ! -r "$configfile" ]; then
		logerr "Cannot read config file $configfile"
		return 1
	fi

	tmp=$(sed -n "s/^${configname}[[:space:]]*=[[:space:]]*//p" \
		"$configfile")
	tmp=${tmp%% }
	if [ -z "$tmp" ]; then
		if [ -n "$defaultvalue" ]; then
			echo "$defaultvalue"
		else
			return 1
		fi
	else
		# only use eval if there are any ${ that are not \${
		# or the command starts with '${'
		if [ "$tmp" != "${tmp%[^\\]\${*}" ] || \
		   [ "${tmp:0:2}" = '${' ]; then
			tmp="$(escape_eval "$tmp")"
			echo "$(eval echo "$tmp")"
		else
			# unescape any previously required '\;'
			echo "$tmp" | sed -e 's/\\;/;/g'
		fi
	fi

	return 0
}

make_dir() {
	local dir="$1"

	if [ ! -d "$dir" ]; then
		logit "Creating swtpm-local dir '${dir}'."
		mkdir -p "$dir"
		if [ $? -ne 0 ]; then
			logerr "Could not create directory '${dir}."
			exit 1
		fi
	fi
}

# Get the next serial number for the certificate
#
# If an error occurs nothing is echo'ed and the return code 1 is returned,
# otherwise the next serial number is echo'ed with a return code of 0.
get_next_cert_serial() {
	local serial

	touch "${LOCK}"
	(
		# Avoid concurrent creation of next serial
		flock_fd 100
		if [ $? -ne 0 ]; then
			logerr "Could not get lock ${LOCK}"
			return 1
		fi
		if [ ! -r "${CERTSERIAL}" ]; then
			echo -n "0" > "${CERTSERIAL}"
		fi
		serial=$(cat "${CERTSERIAL}")
		if ! [[ "$serial" =~ ^[0-9]+$ ]]; then
			serial=1
		else
			serial=$((serial+1))
		fi
		echo -n $serial > "${CERTSERIAL}"
		if [ $? -ne 0 ]; then
			logerr "Could not write cert serial number file"
			return 1
		fi
		echo $serial
	) 100>"${LOCK}"

	return 0
}

create_cert() {
	local flags="$1"
	local typ="$2"
	local dir="$3"
	local ek="$4"
	local vmid="$5"
	local tpm_spec_params="$6"
	local tpm_attr_params="$7"

	local options="" rc=0 keyparms="" serial tmp subj

	serial=$(get_next_cert_serial)
	if [ -z "$serial" ]; then
		return 1
	fi

	if [ -r "${LOCALCA_OPTIONS}" ]; then
		options=$(cat "${LOCALCA_OPTIONS}")
	fi

	if [ -n "$vmid" ]; then
		subj="CN=$vmid"
	else
		subj="CN=unknown"
	fi

	if [ $((flags & SETUP_TPM2_F)) -ne 0 ]; then
		options="$options --tpm2"
	else
		# TPM 1.2 cert needs a header
		options="$options --add-header"
	fi

	if [ "$typ" == "ek" ]; then
		if [ $((flags & ALLOW_SIGNING_F)) -ne 0 ]; then
			options="$options --allow-signing"
		fi
		if [ $((flags & DECRYPTION_F)) -ne 0 ]; then
			options="$options --decryption"
		fi
	fi

	# if ek contains x=..,y=... it's an ECC key
	if [[ "$ek" =~ x=.*,y=.* ]]; then
		keyparms="--ecc-x $(echo "$ek" | \
		                sed -n 's/x=\([[:xdigit:]]*\),.*/\1/p') "
		keyparms+="--ecc-y $(echo "$ek" | \
		                sed -n 's/.*y=\([[:xdigit:]]*\).*/\1/p')"
		tmp="$(echo "$ek" | \
		                sed -n 's/.*id=\([^,]*\).*/\1/p')"
		if [ -n "$tmp" ]; then
			keyparms+=" --ecc-curveid ${tmp}"
		fi
	else
		keyparms="--modulus ${ek}"
	fi

	case "$typ" in
	ek)
		if [ -z "$(type -p swtpm_cert)" ]; then
			logerr "Missing swtpm_cert tool"
			rc=1
		else
			swtpm_cert \
			--subject "$subj" \
			$options \
			${SIGNKEY_PASSWORD:+--signkey-pwd file:<(echo -en "$SIGNKEY_PASSWORD")} \
			${PARENTKEY_PASSWORD:+--parentkey-pwd file:<(echo -en "$PARENTKEY_PASSWORD")} \
			$tpm_spec_params \
			$tpm_attr_params \
			--signkey "${SIGNKEY}" \
			--issuercert "${ISSUERCERT}" \
			--out-cert "${dir}/ek.cert" \
			$keyparms \
			--days 3650 \
			--serial "$serial"
			if [ $? -eq 0 ]; then
				logit "Successfully created EK certificate locally."
			else
				logerr "Could not create EK certificate locally."
				rc=1
			fi
		fi
		;;
	platform)
		if [ -z "$(type -p swtpm_cert)" ]; then
			logerr "Missing swtpm_cert tool"
			rc=1
		else
			swtpm_cert \
			--subject "$subj" \
			$options \
			$tpm_attr_params \
			--type platform \
			--signkey "${SIGNKEY}" \
			--issuercert "${ISSUERCERT}" \
			--out-cert "${dir}/platform.cert" \
			$keyparms \
			--days 3650 \
			--serial "$serial"
			if [ $? -eq 0 ]; then
				logit "Successfully created platform certificate locally."
			else
				logerr "Could not create platform certificate locally."
				rc=1
			fi
		fi
		;;
	esac

	return $rc
}

# Create the local CA's certificate if it doesn't already exist.
# The local CA will be an intermediate CA with a root CA we create
# here as well so that we get an Authority Key Id in our EK cert.
#
create_localca_cert() {
	touch "${LOCK}"
	(
		# Avoid concurrent creation of keys and certs
		flock_fd 100
		if [ $? -ne 0 ]; then
			logerr "Could not get lock ${LOCK}"
			return 1
		fi
		if [ ! -d "${STATEDIR}" ]; then
			# RPM installation must have created this already ...
			# so user tss can use it (user tss cannot create it)
			mkdir -p "${STATEDIR}"
		fi
		if [ ! -r "${SIGNKEY}" ]; then
			local dir=$(dirname "${SIGNKEY}")
			local cakey=${dir}/swtpm-localca-rootca-privkey.pem
			local cacert=${dir}/swtpm-localca-rootca-cert.pem
			local msg

			# create a CA first
			msg=$("${CERTTOOL}" \
				--generate-privkey \
				${SWTPM_ROOTCA_PASSWORD:+--password "${SWTPM_ROOTCA_PASSWORD}"} \
				--outfile "${cakey}" \
				2>&1)
			[ $? -ne 0 ] && {
				logerr "Could not create root-CA key ${cakey}."
				logerr "${msg}"
				return 1
			}
			chmod 640 "${cakey}"

			local tmp=$(mktemp)
			echo "cn=swtpm-localca-rootca" > "${tmp}"
			echo "ca" >> "${tmp}"
			echo "cert_signing_key" >> "${tmp}"
			echo "expiration_days = 3650" >> "${tmp}"

			msg=$(GNUTLS_PIN="${SWTPM_ROOTCA_PASSWORD}" "${CERTTOOL}" \
				--generate-self-signed \
				--template "${tmp}" \
				--outfile "${cacert}" \
				--load-privkey "${cakey}" \
				2>&1)
			[ $? -ne 0 ] && {
				logerr "Could not create root CA."
				logerr "${msg}"
				rm -f "${cakey}"
				return 1
			}

			# now our signing CA
			if [ -n "${SIGNKEY_PASSWORD}" ]; then
				export GNUTLS_PIN=${SIGNKEY_PASSWORD}
			fi

			msg=$("${CERTTOOL}" \
				--generate-privkey \
				--outfile "${SIGNKEY}" \
				2>&1)
			[ $? -ne 0 ] && {
				rm -f "${cakey}" "${cacert}"
				logerr "Could not create local-CA key ${SIGNKEY}."
				logerr "${msg}"
				return 1
			}
			chmod 640 "${SIGNKEY}"

			echo "cn=swtpm-localca" > "${tmp}"
			echo "ca" >> "${tmp}"
			echo "cert_signing_key" >> "${tmp}"
			echo "expiration_days = 3650" >> "${tmp}"

			msg=$(GNUTLS_PIN="${SWTPM_ROOTCA_PASSWORD}" "${CERTTOOL}" \
				--generate-certificate \
				--template "${tmp}" \
				--outfile "${ISSUERCERT}" \
				--load-privkey "${SIGNKEY}" \
				--load-ca-privkey "${cakey}" \
				--load-ca-certificate "${cacert}" \
				2>&1)
			[ $? -ne 0 ] && {
				rm -f "${cakey}" "${cacert}" "${SIGNKEY}"
				logerr "Could not create local CA."
				logerr "${msg}"
				return 1
			}
			rm -f "${tmp}"
		fi
	) 100>"${LOCK}"

	return 0
}

usage() {
       cat <<_EOF_
Usage: $(basename "$1") [options]

The following options are supported:

--type type           The type of certificate to create: 'ek' or 'platform'
--ek key-param        The modulus of an RSA key or x=...,y=,... for an EC key
--dir directory       The directory to write the resulting certificate into
--vmid vmid           The ID of the virtual machine
--optsfile file       A file containing options to pass to swtpm_cert
--configfile file     A file containing configuration parameters for directory,
                      signing key and password and certificate to use
--logfile file        A file to write a log into
--tpm-spec-family s   The implemented spec family, e.g., '2.0'
--tpm-spec-revision i The spec revision of the TPM as integer; e.g., 146
--tpm-spec-level i    The spec level of the TPM; must be an integer; e.g. 00
--tpm-manufacturer s  The manufacturer of the TPM; e.g., id:00001014
--tpm-model s         The model of the TPM; e.g., 'swtpm'
--tpm-version i       The (firmware) version of the TPM; e.g., id:20160511
--tpm2                Generate a certificate for a TPM 2
--allow-signing       The TPM 2's EK can be used for signing
--decryption          The TPM 2's EK can be used for decryption
--help, -h, -?        Display this help screen and exit


The following environment variables are supported:

SWTPM_ROOTCA_PASSWORD  The root CA's private key password

_EOF_
}

main() {
	local typ ek dir vmid tmp
	local tpm_spec_params="" tpm_attr_params=""
	local flags=0

	while [ $# -ne 0 ]; do
		case "$1" in
		--type)
			shift
			typ="$1"
			;;
		--ek)
			shift
			ek="$1"
			;;
		--dir)
			shift
			dir="$1"
			;;
		--vmid)
			shift
			vmid="$1"
			;;
		--optsfile)
			shift
			LOCALCA_OPTIONS="$1"
			;;
		--configfile)
			shift
			LOCALCA_CONFIG="$1"
			;;
		--logfile)
			shift
			LOGFILE="$1"
			;;
		--tpm-spec-family)
			shift
			tpm_spec_params+="--tpm-spec-family $1 "
			;;
		--tpm-spec-revision)
			shift
			tpm_spec_params+="--tpm-spec-revision $1 "
			;;
		--tpm-spec-level)
			shift
			tpm_spec_params+="--tpm-spec-level $1 "
			;;
		--tpm-manufacturer)
			shift
			tpm_attr_params+="--tpm-manufacturer $1 "
			;;
		--tpm-model)
			shift
			tpm_attr_params+="--tpm-model $1 "
			;;
		--tpm-version)
			# this is the firmware version!
			shift
			tpm_attr_params+="--tpm-version $1 "
			;;
		--tpm2)
			flags=$((flags | SETUP_TPM2_F))
			;;
		--allow-signing)
			flags=$((flags | ALLOW_SIGNING_F))
			;;
		--decryption)
			flags=$((flags | DECRYPTION_F))
			;;
                --help|-h|-?)
                        usage "$0"
                        exit 0
                        ;;
		*)
			logerr "Unsupported option $1"
			exit 1
			;;
		esac
		shift
	done

	if [ -n "$LOGFILE" ]; then
		touch "$LOGFILE" &>/dev/null
		if [ ! -w "$LOGFILE" ]; then
			logerr "Cannot write to logfile ${LOGFILE}."
			exit 1
		fi
	fi

	if [ ! -r "$LOCALCA_OPTIONS" ]; then
		logerr "Cannot access options file ${LOCALCA_OPTIONS}."
		exit 1
	fi

	if [ ! -r "$LOCALCA_CONFIG" ]; then
		logerr "Cannot access config file ${LOCALCA_CONFIG}."
		exit 1
	fi

	tmp=$(get_config_value "$LOCALCA_CONFIG" "statedir")
	if [ -z "$tmp" ]; then
		logerr "Missing 'statedir' config value in config file ${LOCALCA_CONFIG}"
		exit 1
	fi
	STATEDIR="$tmp"
	make_dir "$STATEDIR"
	LOCK="${STATEDIR}/.lock.swtpm-localca"
	if [ ! -w "${LOCK}" ]; then
		touch "$LOCK"
		if [ ! -w "${LOCK}" ]; then
			logerr "Could not create lock file ${LOCK}."
			exit 1
		fi
	fi

	SIGNKEY=$(get_config_value "$LOCALCA_CONFIG" "signingkey")
	if [ -z "$SIGNKEY" ]; then
		logerr "Missing signingkey variable in config file $LOCALCA_CONFIG."
		exit 1
	fi
	# SIGNKEY may be a GNUTLS url like tpmkey:file= or tpmkey:uuid=
	if ! [[ "${SIGNKEY}" =~ ^tpmkey:(file|uuid)= ]]; then
		make_dir "$(dirname "$SIGNKEY")"
	fi
	SIGNKEY_PASSWORD=$(get_config_value "$LOCALCA_CONFIG" "signingkey_password")
	PARENTKEY_PASSWORD=$(get_config_value "$LOCALCA_CONFIG" "parentkey_password")

	ISSUERCERT=$(get_config_value "$LOCALCA_CONFIG" "issuercert")
	if [ -z "$ISSUERCERT" ]; then
		logerr "Missing issuercert variable in config file $LOCALCA_CONFIG."
		exit 1
	fi
	make_dir "$(dirname "$ISSUERCERT")"

	# set global CERTTOOL to gnutls's certtool
	case "${UNAME_S}" in
	Darwin)
		CERTTOOL="gnutls-certtool";;
	*)
		CERTTOOL="certtool";;
	esac

	# TPM keys are GNUTLS URLs...
	if [[ "$SIGNKEY" =~ ^tpmkey:(uuid|file)= ]]; then
		export TSS_TCSD_HOSTNAME=$(get_config_value "$LOCALCA_CONFIG" \
			"TSS_TCSD_HOSTNAME" "localhost")
		export TSS_TCSD_PORT=$(get_config_value "$LOCALCA_CONFIG" \
			"TSS_TCSD_PORT" "30003")
		logit "CA uses a GnuTLS TPM key; using TSS_TCSD_HOSTNAME=${TSS_TCSD_HOSTNAME}" \
			"TSS_TCSD_PORT=${TSS_TCSD_PORT}"
	elif [[ "$SIGNKEY" =~ ^pkcs11: ]]; then
		export SWTPM_PKCS11_PIN=$(get_config_value "$LOCALCA_CONFIG" \
			"SWTPM_PKCS11_PIN" "swtpm-tpmca")
		logit "CA uses a PKCS#11 key; using SWTPM_PKCS11_PIN"
	else
		if [ ! -r "$SIGNKEY" ]; then
			if [ -f "$SIGNKEY" ]; then
				logerr "Signing key $SIGNKEY exists but cannot access" \
				       "it as $(id -un):$(id -gn)."
				exit 1
			fi
			# Create the signing key and issuer cert since it will be missing
			logit "Creating root CA and a local CA's signing key and issuer cert."
			create_localca_cert
			if [ $? -ne 0 ]; then
				logerr "Error creating local CA's signing key and cert"
				exit 1
			fi
		fi

		if [ ! -r "$SIGNKEY" ]; then
			logerr "Cannot access signing key ${SIGNKEY}."
			exit 1
		fi
	fi

	if [ ! -r "$ISSUERCERT" ]; then
		logerr "Cannot access issuer certificate ${ISSUERCERT}."
		exit 1
	fi

	CERTSERIAL=$(get_config_value "$LOCALCA_CONFIG" "certserial" \
	    "${STATEDIR}/certserial")
	make_dir "$(dirname "$CERTSERIAL")"

	create_cert "$flags" "$typ" "$dir" "$ek" "$vmid" "$tpm_spec_params" \
		"$tpm_attr_params"

	exit $?
}

main "$@" # 2>&1 | tee -a /tmp/localca.log
