#!/bin/sh
# @(#) $Id: create-cert.sh.in 70 2023-05-09 23:09:30Z leres $ (LBL)
#
#  Copyright (c) 2004, 2008, 2010, 2011, 2013, 2014, 2018, 2020, 2021, 2023
#  The Regents of the University of California. 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 name of the University 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 REGENTS 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.
#
# create-cert - Create openssl client key and certificates
#
prog="`basename $0`"
usage() {
	echo "${prog} version 2.11" 1>&2
	echo "usage: ${prog} [-nv] [-c config] -I" 1>&2
	echo "       ${prog} [-nv] [-c config] -C cert" 1>&2
	echo "       ${prog} [-nv] [-c config] -R" 1>&2
	echo "       ${prog} [-nvf] [-c config] [-b bits] [-d days] [-D digest] FQDN" 1>&2
	exit 1
}
#
#    -b - override the number of bits
#    -d - override the number of days
#    -D - override the digest
#    -f - allow use of non-FQDNs
#    -n - show commands but do not execute
#    -v - show commands
#
# This script creates a host key and certificate.
#
# Certificates are stored in certs.
# Keys are stored in private.
# The configuration is stored in '.'
#

t1=/tmp/${prog}.1.$$
t2=/tmp/${prog}.2.$$
t3=/tmp/${prog}.3.$$

trap 'rm -f ${t1} ${t2} ${t3}; exit 1' 1 2 3 15

PRIVATEUMASK="umask 077"

# Check for existance of openssl

# We need openssl
cmd="openssl"
type ${cmd} > /dev/null
if [ $? -ne 0 ]; then
	echo "${prog}: Can't find ${cmd}" 1>&2
	exit 1
fi

# We optionally need c_rehash
cmd="c_rehash"
type ${cmd} > /dev/null
if [ $? -ne 0 ]; then
	echo "${prog}: Warning: Can't find ${cmd}" 1>&2
	have_c_rehash=0
else
	have_c_rehash=1
fi

args=`getopt b:c:C:d:D:fInP:Rv $*`
if [ $? -ne 0 ]; then
	usage
fi

set -- ${args}

config="create-cert.conf"
doinit=0
dorootca=0
dryrun=0
force=0
initpem=""
override_bits=""
override_days=""
override_digest=""
verbose=0

for a do
	case "${a}" in

	-b)
		override_bits="$2"
		shift
		shift
		;;

	-c)
		config="$2"
		shift
		shift
		;;

	-C)
		doinit=1
		initpem="$2"
		shift
		shift
		;;

	-d)
		override_days="$2"
		shift
		shift
		;;

	-D)
		override_digest="$2"
		shift
		shift
		;;

	-f)
		force=1
		shift
		;;

	-I)
		doinit=1
		shift
		;;

	-n)
		dryrun=1
		verbose=1
		shift
		;;

	-P)
		# Undocumented
		testpem="$2"
		shift
		shift
		;;

	-R)
		dorootca=1
		shift
		;;

	-v)
		verbose=1
		shift
		;;

	--)
		shift
		break
		;;
	esac
done

# Reject flag combinations that don't make sense
if [ ${doinit} -ne 0 -a ${dorootca} -ne 0 ]; then
	usage
fi

if [ ${doinit} -ne 0 -o ${dorootca} -ne 0 ]; then
	if [ ${force} -ne 0 ]; then
		usage
	fi
	if [ -n "${override_bits}" -o -n "${override_days}" -o \
	    -n "${override_digest}" ]; then
		usage
	fi
fi

# Run a command handling dryrun logic
run() {
	cmd="$1"
	if [ ${verbose} -ne 0 ]; then
		echo "+ ${cmd}"
	fi
	if [ ${dryrun} -eq 0 ]; then
		eval "${cmd}"
		if [ $? -ne 0 ]; then
			echo "${prog}: Failed"
			rm -f ${t1} ${t2} ${t3}
			exit 1
		fi
	fi
}

# Output the default config file
catdefaultconf() {
	cat << DEFAULTCONF
`date +"# Default config generated on %B %e, %U at %H:%M:%S"`

# Country code (2 characters)
country="${country}"

# State or province
state="${state}"

# City or locality
city="${city}"

# Organization
organization="${organization}"

# Authority
authority="${authority}"

# Root CA name
rootname="${rootname}"

# email contact address
email="${email}"

# bits
bits="${bits}"

# Message digest
digest="${digest}"

# Host certificate length in days
days="${days}"
DEFAULTCONF
}

# Output the rootca cert creation config
catrootcaconf() {
	cat <<ROOTCACONF >${t1}
[ req ]
prompt				= no
default_bits			= ${bits}
default_md			= ${digest}
default_keyfile			= ${rootcakey}
distinguished_name		= req_distinguished_name
x509_extensions			= v3_ca
string_mask			= nombstr
req_extensions			= v3_req

[ req_distinguished_name ]
countryName			= ${country}
stateOrProvinceName		= ${state}
localityName			= ${city}
0.organizationName		= ${organization}
organizationalUnitName		= ${authority}
commonName			= ${rootname}
emailAddress			= ${email}

[ v3_ca ]
basicConstraints		= critical,CA:true
subjectKeyIdentifier		= hash

[ v3_req ]
nsCertType			= objsign,server
ROOTCACONF
}

# Output the fqdn config
catfqdnconf() {
	cat <<FQDNCONF >${t1}
[ req ]
prompt				= no
default_bits			= ${bits}
default_md			= ${digest}
distinguished_name		= req_distinguished_name
string_mask			= nombstr
req_extensions			= v3_req

[ req_distinguished_name ]
countryName			= ${country}
stateOrProvinceName		= ${state}
localityName			= ${city}
0.organizationName		= ${organization}
organizationalUnitName		= ${authority}
commonName			= ${fqdn}
emailAddress			= ${email}

[ v3_req ]
basicConstraints		= critical,CA:false
nsCertType			= client, server, email
keyUsage			= nonRepudiation, digitalSignature, \
				  keyEncipherment
extendedKeyUsage		= serverAuth, clientAuth, codeSigning, \
				  emailProtection
FQDNCONF
}

# Output the fqdn signing config
catfqdnsigningconf() {
	cat <<FQDNSIGNINGCONF >${t3}
[ ca ]
default_ca			= default_CA

[ default_CA ]
dir				= ${certdir}
certs				= ${certdir}
new_certs_dir			= ${certdir}
database			= ${database}
serial				= ${serial}
certificate			= ${rootcapem}
private_key			= ${rootcakey}
default_days			= ${days}
default_md			= ${digest}
preserve			= yes
x509_extensions			= user_cert
policy				= policy_anything
copy_extensions			= copy

[ policy_anything ]
commonName			= supplied
emailAddress			= supplied

[ user_cert ]
authorityKeyIdentifier		= keyid:always
subjectAltName			= @alt_names
keyUsage			= digitalSignature, keyEncipherment

[ alt_names ]
FQDNSIGNINGCONF
	# Output the altnames
	dns=1
	ip=1
	for name in ${altnames}; do
		case "${name}" in
		[0-9]*.[0-9]*.[0-9]*.[0-9]*)
			# IPv4 address (fall through)
			;;

		*:*)
			# IPv6 address (fall through)
			;;

		*)
			# Not an IP address so must be a name
			printf "DNS.${dns}\t\t\t\t= ${name}\n" >>${t3}
			dns="`expr ${dns} + 1`"
			continue
			;;
		esac

		# IP address
		printf "IP.${ip}\t\t\t\t= ${name}\n" >>${t3}
		ip="`expr ${ip} + 1`"
	done
}

# Create a new rootca
createrootca() {
	# Private directory (protected)
	if [ ! -d ${privatedir} ]; then
		run "(${PRIVATEUMASK} ; mkdir ${privatedir})"
	fi

	# Create directories of missing; abort if files already exist
	ret=0
	for fn in ${rootcakey} ${rootcapem} ${database} ${serial}; do
		dn="`dirname ${fn}`"
		if [ ! -d ${dn} ]; then
			run "mkdir ${dn}"
		fi
		if [ -f ${fn} ]; then
			echo "${prog}: ${fn} already exists" 1>&2
			ret=1
		fi
	done
	if [ ${ret} -ne 0 ]; then
		exit ${ret}
	fi

	# Create a key for the root certificate authority
	echo "${prog}: Creating the key for the new rootca"
	run "(${PRIVATEUMASK} ; openssl genrsa -out ${rootcakey} ${bits} ; chmod -w ${rootcakey})"

	# Create temporary config
	catrootcaconf >${t1}
	echo "${prog}: Creating temporary rootca config"
	run "catrootcaconf > ${t1}"
	if [ ${verbose} -ne 0 ]; then
		cat ${t1}
	fi

	# Self sign the the root certificate authority
	echo "${prog}: Creating the cert for the new rootca"
	run "openssl req -batch -new -x509 -nodes -days ${days} -config ${t1} -key ${rootcakey} -out ${rootcapem} ; chmod -w ${rootcapem}"
	rm ${t1}

	# Create the default database file
	if [ ! -s ${database} ]; then
		echo "${prog}: Creating the database file for the new rootca"
		run "touch ${database}"

	fi

	# Create the default serial file
	if [ ! -s ${serial} ]; then
		echo "${prog}: Creating the serial file for the new rootca"
		run "echo 01 >${serial}"

	fi
}

# Parse an existing pem file and set corresponding shell variables
parsepem() {
	if [ $# -gt 0 ]; then
		echo "${prog}: parsepem: bad arguments" 1>&2
		exit 1
	fi
	awk 'BEGIN {
		# key map
		map["countryName"] = "country"
		map["stateOrProvinceName"] = "state"
		map["localityName"] = "city"
		map["organizationName"] = "organization"
		map["organizationalUnitName"] = "authority"
		map["commonName"] = "rootname"
		map["emailAddress"] = "email"
		bits = "bits"
		map[bits] = "bits"
		signature = "Signature"
		map[signature] = "digest"
	}
	$1 == signature {
		if ($3 == "shaWithRSAEncryption") {
			vals[$1] = "sha"
		} else if ($3 == "sha1WithRSAEncryption") {
			vals[$1] = "sha1"
		} else if ($3 == "sha224WithRSAEncryption") {
			vals[$1] = "sha224"
		} else if ($3 == "sha256WithRSAEncryption") {
			vals[$1] = "sha256"
		} else if ($3 == "sha384WithRSAEncryption") {
			vals[$1] = "sha384"
		} else if ($3 == "sha512WithRSAEncryption") {
			vals[$1] = "sha512"
		} else if (index($3, "md5") > 0) {
			vals[$1] = "md5"
		} else if (index($3, "md2") > 0) {
			vals[$1] = "md2"
		} else if (index($3, "mdc2") > 0) {
			vals[$1] = "mdc2"
		} else if (index($3, "rmd160") > 0) {
			vals[$1] = "rmd160"
		}
		next
	}
	$NF == "bit)" {
		val = $(NF - 1)
		if (substr(val, 1, 1) == "(") {
			vals[bits] = substr(val, 2)
		}
		next
	}
	$1 in map {
		if (vals[$1] == "") {
			i = index($0, "= ")
			if (i > 0) {
				vals[$1] = substr($0, i + 2)
			}
		}
		next
	}
	END {
		# Print out results
		fail = 0
		result = ""
		for (key in map) {
			val = vals[key]
			if (val == "") {
				printf "Input cert missing \"%s\"\n", key
				++fail
				continue
			}
			line = sprintf("%s=\042%s\042", map[key], vals[key])
			if (result == "")
				result = line
			else
				result = result "\n" line
		}
		if (fail)
			exit(1)
		print result
	}'
}

processpem() {
	local fn

	if [ $# -ne 1 ]; then
		echo "${prog}: processpem: missing pem argument" 1>&2
		exit 1
	fi
	fn=$1

	openssl x509 -in ${fn} -noout -text -nameopt multiline > ${t1}
	if [ $? -ne 0 ]; then
		echo "${prog}: openssl -text failed" 1>&2
		rm ${t1}
		exit 1
	fi

	# Parse the pem file
	parsepem < ${t1} > ${t2}
	if [ $? != 0 ]; then
		echo "${prog}: ${initpem}: Failed to parse:" 1>&2
		cat ${t2} 1>&2
		exit 1
	fi
	if [ ${verbose} -ne 0 ]; then
		cat ${t2}
	fi

	# Set shell variables
	. ${t2}

	# Clean up
	rm ${t1} ${t2}

	echo "${prog}: ${initpem}: Parsed successfully"
}

##############################################################################

if [ -n "${testpem}" ]; then
	parsepem < ${testpem}
	if [ $? != 0 ]; then
		echo "${prog}: ${testpem}: Failed to parse:" 1>&2
		cat ${t2} 1>&2
		exit 1
	fi
	echo "${prog}: ${testpem}: Parsed successfully"
	exit 0
fi

# Defaults
country="ZZ"
state="Utopia"
city="Atlantis"
organization="Unsuitable for security at any keysize"
authority="Bogon Anonymous Certificate Authority"
rootname="test CA 1"
email="root@localhost"
days="3650"

# legacy defaults (create-cert 2.0 and older)
bits="1024"
digest="md5"

# Create a default config (-I/-C)?
if [ ${doinit} -ne 0 ]; then
	if [ $# -ne 0 ]; then
		usage
	fi

	if [ -r ${config} ]; then
		echo "${prog}: Error: ${config} already exists" 1>&2
		exit 1
	fi

	# Defaults (create-cert 2.4 and newer)
	bits="2048"
	digest="sha256"

	# Optionally parse defaults from pem file
	if [ -n "${initpem}" ]; then
		processpem ${initpem}
	fi

	# Directory
	confdir="`dirname ${config}`"
	if [ ! -d ${confdir} ]; then
		run "mkdir -p ${confdir}"
	fi

	# Config
	echo "${prog}: Creating a default config in in ${config}"
	run "catdefaultconf > ${config}"
	if [ ${verbose} -ne 0 ]; then
		cat ${config}
	fi
	exit 0
fi

# Must have a config to pass this point
if [ ! -r ${config} ]; then
	echo "${prog}: Please use -I or -C to create a config (${config})" 1>&2
	if [ ${dryrun} -eq 0 ]; then
		exit 1
	fi
fi

# Load the config
if [ ${verbose} -ne 0 ]; then
	echo ". ${config}"
fi
. ${config}

# Apply overrides
if [ -n "${override_bits}" ]; then
	bits=${override_bits}
fi
if [ -n "${override_days}" ]; then
	days=${override_days}
fi

# Check for missing config variables
ret=0
for name in country state city organization authority rootname email; do
	eval "v=\$${name}"
	if [ -z "${v}" ]; then
		echo "${prog}: ${config}: missing ${name}" 1>&2
		ret=1
		continue
	fi
	if [ ${name} == "country" -a ${#v} -ne 2 ]; then
		echo "${prog}: ${config}: country must be 2 characters long" 1>&2
		ret=1
		continue
	fi
done
if [ ${ret} -ne 0 ]; then
	exit ${ret}
fi

# Create the self-signed rootca?
# Pick our files and directories
privatedir="private"
certdir="certs"

serial="${privatedir}/serial"
database="${certdir}/rootca.index"

rootcakey="${privatedir}/rootca.key"
rootcapem="${certdir}/rootca.pem"

if [ ${dorootca} -ne 0 ]; then
	if [ $# -ne 0 ]; then
		usage
	fi
	ret=0
	for fn in ${rootcakey} ${rootcapem}; do
		if [ -r ${fn} ]; then
			echo "${prog}: Error: ${fn} exists" 1>&2
			ret=1
		fi
	done
	if [ ${ret} -ne 0 ]; then
		exit ${ret}
	fi
	createrootca
	exit 0
fi

# Need a rootca
if [ ! -r ${rootcakey} -o ! -r ${rootcapem} ]; then
	echo "${prog}: Please use the -R flag to create a rootca" 1>&2
	if [ ${dryrun} -eq 0 ]; then
		exit 1
	fi
fi

# Require at least one name
if [ $# -lt 1 ]; then
	usage
fi
fqdn=$1

# All arguments will be submitted as alt_names
altnames=$*

# Validate the names
for name in ${altnames}; do
	case "${name}" in

	*.key|*.pem)
		if [ ${force} -ne 0 ]; then
			continue
		fi
		echo "${prog}: ${name}: FQDN may not end with '.key' or '.pem' (use -f to override)" 1>&2
		exit 1
		;;

	*.*)
		# FQDN or IPv4 address
		continue
		;;

	*:*)
		# Any number of colons is probably an IPv6 address
		continue
		;;

	*)
		if [ ${force} -ne 0 ]; then
			continue
		fi
		echo "${prog}: ${name}: Please provide a FQDN or IP address (use -f to override)" 1>&2
		exit 1
		;;
	esac
done

# Create the host key and cert (finally!)
key="${privatedir}/${fqdn}.key"
pem="${certdir}/${fqdn}.pem"

ret=0
for fn in ${key} ${pem}; do
	if [ -r ${fn} ]; then
		echo "${prog}: Error: ${fn} exists" 1>&2
		ret=1
	fi
done
if [ ${ret} -ne 0 ]; then
	exit ${ret}
fi

echo "${prog}: Creating the key for ${fqdn}"
run "(${PRIVATEUMASK} ; openssl genrsa -out ${key} ${bits} ; chmod -w ${key})"

echo "${prog}: Create a cert config for ${fqdn}"
run "catfqdnconf > ${t1}"
if [ ${verbose} -ne 0 ]; then
	cat ${t1}
fi

echo "${prog}: Create a CSR config for ${fqdn}"
run "openssl req -batch -new -config ${t1} -key ${key} -out ${t2}"
if [ ${verbose} -ne 0 ]; then
	cat ${t2}
fi

oldserialnum="`cat ${serial}`"

echo "${prog}: Create a CSR for ${fqdn}"
run "catfqdnsigningconf > ${t3}"
if [ ${verbose} -ne 0 ]; then
	cat ${t3}
fi

echo "${prog}: Sign the certificate request for ${fqdn}"
run "openssl ca -batch -config ${t3} -out ${pem} -infiles ${t2} ; chmod -w ${pem}"

echo "${prog}: Verify the the csr for ${fqdn}"
run "openssl verify -CAfile ${rootcapem} ${pem}"

echo "${prog}: Remove junk we don't need"
run "rm -f ${certdir}/${oldserialnum}.pem ${serial}.old ${database}.old"

if [ ${have_c_rehash} -ne 0 ]; then
	echo "${prog}: Rehashing the cert directory"
	run "c_rehash ${certdir}"
else
	echo "${prog}: Not rehashing the cert directory (c_rehash missing)"
fi

echo "${prog}: Cert and key for ${fqdn} successfully created"

rm -f ${t1} ${t2} ${t3}
