#!/usr/bin/env bash
#
# Copyright: Patrick Koppen
# License:   GPLv3
# Version:   1.2
# Date:      29.12.2012

# Locking code to try to make sure that two instances do not run together and
# corrupt the CA files.
LOCKDIR="/tmp/burp_ca.lockdir"
PIDFILE="$LOCKDIR/pid"

cleanup()
{
	# Perform program exit housekeeping
	# Optionally accepts an exit status
	rm -rf $LOCKDIR
	trap - INT TERM EXIT
	exit $1
}

error_exit()
{
	# Display error message and exit
	[ -n "$2" ] && echo "$(basename $0): error $1 on executing $2" 1>&2
	cleanup $1
}

lock()
{
	# Originally from http://wiki.bash-hackers.org/howto/mutex, and
	# adjusted somewhat.
	if mkdir "$LOCKDIR" 2>/dev/null ; then
		trap cleanup INT TERM EXIT
		echo $$ > "$PIDFILE" || return 1
		return 0
	else
		oldpid="$(cat "$PIDFILE")" || return 1

		if ! ps -p "$oldpid" &>/dev/null; then
			echo "Removing stale lock with pid $oldpid" 1>&2
			rm -rf "$LOCKDIR" || return 1
			echo "Restarting myself" 1>&2
			exec "$0" "$@"
		fi
		return 1
	fi
}

set +e

maxwait=60
sleeptime=1
slept=0

while ! lock; do
	sleep $sleeptime
	let slept+=$sleeptime
	let sleeptime*=2
	if [ $slept -ge $maxwait ]; then
		echo "Failed to acquire lock $LOCKDIR in $slept seconds" 1>&2
		exit 1
	fi
done

# Solaris derivates and AIX don't support 'hostname -f' and, in fact,
# set the hostname to '-f' with that call.
case "$(uname -s)" in
	SunOS) name=$( (check-hostname | cut -d' ' -f7) 2>/dev/null ) ;;
	AIX) name=$(host $(hostname) | awk '{ print $1}' 2>/dev/null ) ;;
	*) name=$(hostname -f 2>/dev/null) ;;
esac
# Try NetBSD style.
[ -n "$name" ] || name=$(hostname 2>/dev/null)
if [ -z "$name" ] ; then
	echo "Could not determine hostname" 1>&2
	error_exit 1 hostname
fi

etc=/etc/burp
dir=${etc}/CA
conf=${etc}/CA.cnf
ca_days=7300
size=2048

def_umask=022
sec_umask=077

function help() {
  cat <<EOF
$0: Help:
    -h|--help               show help
    -i|--init               inititalize CA
    -k|--key                generate new key
    -K|--keypath <path>     path to new key
    -r|--request            generate certificate sign request
    -R|--requestpath <path> path to certificate sign request
    -s|--sign               sign csr (use --ca <ca> and --name <name>)
       --batch              do not prompt for anything
       --revoke <number>    revoke certificate with serial number
       --crl                generate certificate revoke list
    -d|--dir <dir>          ca output dir (default: $dir)
    -c|--config             config file (default: $conf)
    -n|--name               name (default: $name)
    -D|--days               valid days for certificate (default in config file)
       --ca_days            valid days for CA certificate (default: $ca_days)
    -S|--size               key size (default: $size)
    -a|--ca                 ca name if different from name
    -f|--dhfile <path>      generate Diffie-Hellman file
    -A|--altname            subjectAltName
EOF
}

check_second_arg()
{
	if [ "$1" -eq 0 ] ; then
		help
		exit 1
	fi
}

while [ $# -gt 0 ]
do
	case $1 in
		-h|--help) help; exit 0 ;;
		-i|--init) init=yes ;;
		-k|--key) key=yes ;;
		-K|--keypath) check_second_arg $#; keypath=$2; shift ;;
		-r|--request) request=yes ;;
		-R|--requestpath) check_second_arg $#; requestpath=$2; shift ;;
		-s|--sign) sign=yes ;;
		   --batch) batch="-batch" ;;
		   --revoke) check_second_arg $#; revoke=$2; shift ;;
		   --crl) crl=yes ;;
		-d|--dir) check_second_arg $#; dir=$2; shift ;;
		-c|--config) check_second_arg $#; conf=$2; shift ;;
		-n|--name) check_second_arg $#; name=$2; shift ;;
		-D|--days) check_second_arg $#; days="-days $2"; shift ;;
		   --ca_days) check_second_arg $#; ca_days=$2; shift ;;
		-S|--size) check_second_arg $#; size=$2; shift ;;
		-a|--ca) check_second_arg $#; ca=$2; shift ;;
		-f|--dhfile) check_second_arg $#; dhfile=$2; shift ;;
		-A|--altname) check_second_arg $#; altname=$2; shift ;;
		--) shift; break;;
		-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
		*) break;;
	esac
	shift
done

if [ -n "$dhfile" ] ; then
	openssl dhparam -dsaparam -out "$dhfile" 2048 || error_exit $? openssl
	chmod 600 "$dhfile" || error_exit $? chmod
fi

if [ -z "$ca" ]; then
	ca=${name}
fi

if [ -n "$altname" ]; then
	altname="subjectAltName=$altname"
fi

# init CA
if [ "$init" = "yes" ]; then
	echo "Init... ${ca}"
	if [ ! -f ${conf} ]; then
		echo "$0: error - config ${conf} missing" 1>&2
		error_exit 1
	fi
	if [ -d ${dir} ]; then
		echo "$0: error - ${dir} exists, ca initialized" 1>&2
		error_exit 1
	fi
  
	mkdir ${dir}
	mkdir ${dir}/certs
	mkdir ${dir}/newcerts

	umask ${sec_umask}
	openssl genrsa -out ${dir}/CA_${ca}.key ${size}
	umask ${def_umask}
	TEMP=$(mktemp /tmp/burp_ca.tmp.XXXXXXXX || echo /tmp/burp_ca.tmp.$$)
	cat <<-EOF > ${TEMP}
	RANDFILE                = /dev/urandom

	[ req ]
	distinguished_name      = req_distinguished_name
	prompt                  = no

	[ v3_ca ]
	basicConstraints=CA:true
	subjectKeyIdentifier=hash
	authorityKeyIdentifier=keyid,issuer:always

	[ req_distinguished_name ]
	commonName                      = ${ca}
EOF
	CA_DIR=${dir} openssl req -config ${TEMP} -new -x509 -days $ca_days \
		-key ${dir}/CA_${ca}.key -out ${dir}/CA_${ca}.crt \
		-extensions v3_ca || error_exit $? openssl
	rm -f $TEMP

	: > ${dir}/index.txt
	echo "00" > ${dir}/serial.txt
	echo "00" > ${dir}/crlnumber.txt
fi

[ -z "$keypath" ] && keypath=${dir}/${name}.key

# generate key
if [ "$key" = "yes" ]; then
	echo "generating key ${name}: ${keypath}"
	umask ${sec_umask}
	openssl genrsa -out "${keypath}" ${size} || error_exit $? openssl
	umask ${def_umask}
fi

# generate signing request 
[ -z "$requestpath" ] && requestpath=${dir}/${name}.csr
if [ "$request" = "yes" ]; then
	echo "generating request ${name}"
        [ -d $(dirname $requestpath) ] || mkdir $(dirname $requestpath)
	TEMP=$(mktemp /tmp/burp_ca.tmp.XXXXXXXX || echo /tmp/burp_ca.tmp.$$)
	cat <<-EOF > ${TEMP}
	RANDFILE                = /dev/urandom
        req_extensions          = v3_req

	[ req ]
	distinguished_name      = req_distinguished_name
	prompt                  = no

	[ v3_req ]
	basicConstraints=CA:false
	$altname

	[ req_distinguished_name ]
	commonName                      = ${name}

EOF
	openssl req -config ${TEMP} -new -key "${keypath}" \
		-out "${requestpath}" -extensions v3_req || error_exit $?  openssl
	rm -f "$TEMP"
fi


# sign 
if [ "$sign" = "yes" ]; then
	serial=$(cat ${dir}/serial.txt)
	CA_DIR=${dir} openssl ca -config ${conf} -name ca \
		-in ${dir}/${name}.csr -out $dir/${name}.crt ${days} \
		-keyfile ${dir}/CA_${ca}.key -cert ${dir}/CA_${ca}.crt \
		${batch} || error_exit $? openssl
	[ -f ${dir}/newcerts/${serial}.pem ] || exit 0
	mv ${dir}/newcerts/${serial}.pem ${dir}/certs/${serial}.pem || error_exit $? mv
	# rehash the certificates
	for file in ${dir}/certs/*.pem; do
		ln -s -f $file \
			${dir}/certs/`openssl x509 -hash -noout -in $file`.0 || error_exit $? ln
	done
fi

# revoke
[ -n "$revoke" ] && \
	CA_DIR=${dir} openssl ca -config ${conf} -name ca \
		-revoke ${dir}/certs/${revoke}.pem \
		-keyfile ${dir}/CA_${ca}.key -cert ${dir}/CA_${ca}.crt \
		${batch}

# crl
[ -n "$crl" ] && \
	CA_DIR=${dir} openssl ca -config ${conf} -name ca \
		-gencrl -out ${dir}/CA_${ca}.crl \
		-keyfile ${dir}/CA_${ca}.key -cert ${dir}/CA_${ca}.crt

exit 0
