#!/usr/bin/env bash
#
# Copyright: Patrick Koppen
# License:   GPLv3

set -eu -o pipefail

# 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
}

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
}

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 and AIX don't support 'hostname -f' and, in fact,
# set the hostname to '-f' with that call.
name=
case "$(uname -s)" in
	SunOS) name="$(check-hostname | cut -d' ' -f7)" ;;
	AIX) name="$(host "$(hostname)" | awk '{ print $1}')" ;;
	*) name="$(hostname -f)" ;;
esac
# Try NetBSD style.
[ -n "$name" ] || name="$(hostname)"

etc="/etc/burp"
dir="$etc"/CA
conf="$etc"/CA.cnf
days=
ca_days=7300
size=2048
init=
key=
keypath=
request=
requestpath=
sign=
batch=
revoke=
crl=
ca=
dhfile=
altname=
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
	chmod 600 "$dhfile"
fi

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

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

export CA_DIR="$dir"

# init CA
if [ "$init" = "yes" ]; then
	echo "Init... $ca"
	if [ ! -f "$conf" ]; then
		echo "$0: error - config $conf missing" 1>&2
		exit 1
	fi
	if [ -d "$dir" ]; then
		echo "$0: error - $dir exists, ca initialized" 1>&2
		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
	openssl req \
		-config "$TEMP" \
		-new -x509 \
		-days "$ca_days" \
		-key "$dir/CA_$ca.key" \
		-out "$dir/CA_$ca.crt" \
		-extensions v3_ca
	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"
	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
	rm -f "$TEMP"
fi


# sign 
if [ "$sign" = "yes" ]; then
	serial="$(cat "$dir"/serial.txt)"
	if [ -n "$days" ] ; then
		openssl ca \
			-config "$conf" \
			-name ca \
			-in "$dir/$name.csr" \
			-out "$dir/$name.crt" \
			-days "$days" \
			-keyfile "$dir/CA_$ca.key" \
			-cert "$dir/CA_$ca.crt" \
			$batch
	else
		openssl ca \
			-config "$conf" \
			-name ca \
			-in "$dir/$name.csr" \
			-out "$dir/$name.crt" \
			-keyfile "$dir/CA_$ca.key" \
			-cert "$dir/CA_$ca.crt" \
			$batch
	fi
	[ -f "$dir/newcerts/$serial.pem" ] || exit 0
	mv "$dir/newcerts/$serial.pem" "$dir/certs/$serial.pem"

	# rehash the certificates
	for file in "$dir"/certs/*.pem; do
		hash=$(openssl x509 -hash -noout -in "$file")
		ln -s -f "$file" "$dir/certs/$hash.0"
	done
fi

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

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

exit 0
