Commit 0b3d8fd2 authored by Dom Sekotill's avatar Dom Sekotill
Browse files

Add a mk-cert script for generating dev certificates

parent ca4fa5b7
Loading
Loading
Loading
Loading

.bin/mk-cert

0 → 100755
+198 −0
Original line number Diff line number Diff line
#!/usr/bin/env bash
usage() { tee <<END_HELP
$(basename $0) [-h] [-o OUTPUT] NAME [NAME ...]

Generate local development certificates, signed by a local CA

Options:
 -h --help           Show this message and exit
 -o --output OUTPUT  Output file name (default: stdout)

Arguments:
 NAME   A domain name that the generated certificate will validate

END_HELP
}

badarg() {
	usage
	tput setaf 1
	echo "Bad arguments: $*"
	tput sgr0
	exit 2
} >&2

set -eu -o pipefail
shopt -s extglob failglob globstar nullglob
shopt -s inherit_errexit lastpipe
source ~/.shell/lib/builtins.bash

declare -r CONFIG_SERIAL=1
declare -r KEYNAME=secp384r1
declare -r CACHE=${XDG_CACHE_HOME:=$HOME/.cache}/dev-certs
declare -r CONFIG=$CACHE/config-$CONFIG_SERIAL.ini
declare -rx CA_DIR=$CACHE/ca
declare -rx CERTS_DIR=$CACHE/certs
declare -r OPENSSL=$(type -P openssl)

if [[ -z $OPENSSL || $($OPENSSL version) =~ LibreSSL ]]; then
	echo >&2 "Please install OpenSSL (not LibreSSL)"
	exit 3
fi

declare -a ARGS_NAMES=()
declare ARG_OUTPUT

until [[ $# -eq 0 ]]; do
	case $1 in
		-h|--help) usage; exit ;;
		-o|--output) ARG_OUTPUT=$2; shift ;;
		--) ARGS_NAMES+=( "$@" ); break ;;
		-*) badarg "Unknown option: $1" ;;
		*) ARGS_NAMES+=( $1 ) ;;
	esac
	shift
done

[[ ${#ARGS_NAMES[*]} -gt 0 ]] ||
	badarg "Need at least one domain name for the certificate"

openssl() { $OPENSSL "$@"; }

join() {
	local IFS=$1
	echo "${*:2}"
}

get_pass() {
	while [[ ! -v CA_PASS ]]; do
		read -sp 'CA Key password: ' CA_PASS; echo >&2
		[[ ${1-nocheck} != check || -z $CA_PASS ]] && return
		read -sp 'Confirm password: ' check; echo >&2
		if [[ $check != $CA_PASS ]]; then
			unset CA_PASS
			echo >&2 "Passwords did not match, try again"
		fi
	done
}

check_for_current() {
	local now=$(date +%y%m%d%H%M%S)
	local exp serial subject _1 _2
	while read _1 exp serial _2 subject; do
		[[ $subject = $1 ]] || continue
		if [[ $now -lt ${exp%Z} ]]; then
			echo >&2 "Certificate already exists!"
			echo $serial
			return
		fi
	done <$CA_DIR/index
}

# generate a local CA cert if not already present
generate_ca() {
	mkdir -p "$CA_DIR" "$CERTS_DIR"
	[[ -e $CA_DIR/index ]] || touch "$CA_DIR/index"
	[[ -e $CA_DIR/serial ]] || echo "01" >$CA_DIR/serial
	[[ -e $CA_DIR/ca.crt ]] && return

	get_pass check

	openssl ecparam -genkey -name $KEYNAME |
	openssl ec -aes256 -passout fd:3 3<<<"$CA_PASS" |
	tee "$CA_DIR/ca.key" |
	openssl req -x509 -new -batch \
		-config "$CONFIG" \
		-key /dev/stdin \
		-passin fd:3 3<<<"$CA_PASS" \
		-out "$CA_DIR/ca.crt" \
		-days $((365*10)) \
		-subj '/O=Local Development/CN=Local Development CA'

}

# generate a local certificate for lists of subjects
generate_cert() (
	[[ $# -gt 0 ]] || return

	(IFS=:; sort -u <<<"$*") | md5sum | read ID _
	export ID

	[[ -e $CACHE/$ID.pem ]] && return

	local alt_subjects=$(join , "${@/#/DNS:}")
	local subject="/O=Local Development/CN=Local Dev Certificate ($ID)"

	local serial=$(check_for_current "$subject")
	if [[ -z $serial ]]; then
		serial=$(cat "$CA_DIR/serial")

		mkdir -p "$CERTS_DIR/$ID"
		get_pass

		openssl ecparam -genkey \
			-name $KEYNAME \
			-outform pem |
		tee "$CERTS_DIR/$ID/$serial.key" |
		openssl req -new -batch \
			-config "$CONFIG" \
			-addext "subjectAltName = $alt_subjects" \
			-key /dev/stdin \
			-subj "$subject" |
		openssl ca -batch \
			-config "$CONFIG" \
			-days 365 \
			-passin fd:3 3<<<"$CA_PASS" \
			-in /dev/stdin
	fi >/dev/null

	cat >${ARG_OUTPUT-/dev/stdout} \
		"$CERTS_DIR/$ID/$serial.pem" \
		"$CERTS_DIR/$ID/$serial.key" \
		"$CA_DIR/ca.crt"
)

generate_config() {
	[[ -e $CONFIG ]] && return
	mkdir -p $(dirname "$CONFIG")
	tee >$CONFIG <<-'END_INI'
		# default CA directory
		CA_DIR = ${ENV::HOME}/.cache/dev-certs/ca
		ID=null

		[ req ]
		prompt = no
		distinguished_name = distinguished_name

		[ ca ]
		default_ca = ca_default
		x509_extensions = ca_extensions

		[ ca_extensions ]
		basicConstraints = critical, CA:FALSE

		[ ca_default ]
		dir = ${ENV::CA_DIR}
		database = $dir/index
		new_certs_dir = $dir/../certs/${ENV::ID}
		certificate = $dir/ca.crt
		serial = $dir/serial
		private_key = $dir/ca.key

		policy = ca_policy
		copy_extensions = copy
		unique_subject = no
		default_md = sha512

		[ ca_policy ]
		organizationName = match
		commonName = supplied

		[ distinguished_name ]
		# O = Local Development
	END_INI
}

generate_config
generate_ca
generate_cert "${ARGS_NAMES[@]}"
+23 −0
Original line number Diff line number Diff line
test -n "$BASH" || return

load() {
	local src=${BASH%/bin/bash}/lib/bash/$1
	test -r "$src" || return 0
	enable -f "$src" ${2-$1}
}

load basename
load dirname
load head
load id
load ln
load mkdir
load realpath
load rmdir
load seq
load sleep
load tee
load uname
load unlink

unset -f load