#!/bin/bash

#########################################################
# Generates 3 keys and CSRs in preparation for HPKP	#
#########################################################
# By Christopher A. Wadge, Oct 26, 2017			#
#							#
# https://files.tuxhelp.org/scripts/hpkp-generator.sh	#
#							#
# Licensed under the GPL version 3. A copy of the GPL	#
# version 3 is included with this script.  If the file, #
# COPYING, is not included, you can find the GPL 	#
# version 3 at the following URL online:		#
#							#
# http://www.gnu.org/licenses/gpl-3.0.html		#
#########################################################


## VARIABLES ##
# The following are pre-determined variables that are not handled interactively.
#####
#
# SHA-2 hash function. Can be one of: 256, 384, or 512
SHA_HASH_BITS="512"
#
# RSA key size. Can be one of: 2084, 4096, or 8192
RSA_KEY_SIZE="4096"
#
# Max-age pinning directive. Time in seconds client will remember your HPKP settings.
HPKP_MAX_AGE="5184000"
#
# Directory where our private keys will be stored
PRIVKEY_DIR="hpkp_gen/key"
#
# Directory where our CSRs will be stored
CSR_DIR="hpkp_gen/csr"
#
# Directory where our SHA256 fingerprints will be stored
FINGERPRINT_DIR="hpkp_gen/sum"


## BINARIES ##
# Paths to system tools. Auto-detected by default, but may be hard-coded.
#####
#
# Path to openssl binary
OPENSSLBIN=""
#
# Path to base64 binary
BASE64BIN=""


## FUNCTIONS ##
# Use a function for each sub-task for flexibility.
#####

###### Begin color handling functions ######

export black='\033[0m'
export boldblack='\033[1;0m'
export red='\033[31m'
export boldred='\033[1;31m'
export green='\033[32m'
export boldgreen='\033[1;32m'
export yellow='\033[33m'
export boldyellow='\033[1;33m'
export blue='\033[34m'
export boldblue='\033[1;34m'
export magenta='\033[35m'
export boldmagenta='\033[1;35m'
export cyan='\033[36m'
export boldcyan='\033[1;36m'
export white='\033[37m'
export boldwhite='\033[1;37m'


cecho ()
{
local default_msg=" "
message=${1:-$default_msg}
color=${2:-black}
case $color in
	black)
		 printf "$black" ;;
	boldblack)
		 printf "$boldblack" ;;
	red)
		 printf "$red" ;;
	boldred)
		 printf "$boldred" ;;
	green)
		 printf "$green" ;;
	boldgreen)
		 printf "$boldgreen" ;;
	yellow)
		 printf "$yellow" ;;
	boldyellow)
		 printf "$boldyellow" ;;
	blue)
		 printf "$blue" ;;
	boldblue)
		 printf "$boldblue" ;;
	magenta)
		 printf "$magenta" ;;
	boldmagenta)
		 printf "$boldmagenta" ;;
	cyan)
		 printf "$cyan" ;;
	boldcyan)
		 printf "$boldcyan" ;;
	white)
		 printf "$white" ;;
	boldwhite)
		 printf "$boldwhite" ;;
esac
  printf "%s\n"  "$message"
  tput sgr0
  printf "$black"

return
}

cechon ()		
{
local default_msg=" "
message=${1:-$default_msg}
color=${2:-black}
case $color in
	black)
		printf "$black" ;;
	boldblack)
		printf "$boldblack" ;;
	red)
		printf "$red" ;;
	boldred)
		printf "$boldred" ;;
	green)
		printf "$green" ;;
	boldgreen)
		printf "$boldgreen" ;;
	yellow)
		printf "$yellow" ;;
	boldyellow)
		printf "$boldyellow" ;;
	blue)
		printf "$blue" ;;
	boldblue)
		printf "$boldblue" ;;
	magenta)
		printf "$magenta" ;;
	boldmagenta)
		printf "$boldmagenta" ;;
	cyan)
		printf "$cyan" ;;
	boldcyan)
		printf "$boldcyan" ;;
	white)
		printf "$white" ;;
	boldwhite)
		printf "$boldwhite" ;;
esac
  printf "%s"  "$message"
  tput sgr0
  printf "$black"
return
}

###### End color handling functions ######

Error_Handler ()
{
	cecho "[FATAL] Script aborted while $CURRENT_STEP." boldred
	echo
	exit 1
}

Detect_Bin_Paths ()
{
	if [ -z $OPENSSLBIN ]; then
		OPENSSLBIN="`which openssl`"
	fi
    if [ -z $BASE64BIN ]; then
		BASE64BIN="`which base64`"
	fi
}

Sanity_Check ()
{
	CURRENT_STEP="performing sanity check"
	WE_ARE_SANE=true
	if [ ! -f $OPENSSLBIN ]; then
		cecho "[ERROR] The openssl binary ($OPENSSLBIN) doesn't seem to exist." red
		WE_ARE_SANE=false
	fi
    if [ ! -f $BASE64BIN ]; then
		cecho "[ERROR] The base64 binary ($BASE64BIN) doesn't seem to exist." red
		WE_ARE_SANE=false
	fi
	if [ "$WE_ARE_SANE" = "false" ]; then
		Error_Handler
	fi
}

Set_BASH_Options ()
{
    CURRENT_STEP="setting BASH options"
    if [ -z "$BASH_VERSION" ]; then
        echo
        cecho "[WARNING] This script wasn't run in BASH (are you launching it with 'sh'?)" boldyellow
        cecho "          It may function, but certain error handling won't be as robust." boldyellow
        echo
        read -p "==> Continue anyway? [N/y] " REPLY
        case "$REPLY" in
        ( [yY]|[yY][eE][sS] ) echo "" ;;
        *) echo ; Error_Handler ;;
        esac
    else
        set -o pipefail
        trap 'Error_Handler' ERR
    fi
}

Make_Directories ()
{
    CURRENT_STEP="creating $PRIVKEY_DIR"
    mkdir -p $PRIVKEY_DIR || Error_Handler
    CURRENT_STEP="setting permissions for $PRIVKEY_DIR"
    chmod 0700 $PRIVKEY_DIR || Error_Handler
    CURRENT_STEP="creating $CSR_DIR"
    mkdir -p $CSR_DIR || Error_Handler
    CURRENT_STEP="setting permissions for $CSR_DIR"
    chmod 0700 $CSR_DIR || Error_Handler
    CURRENT_STEP="creating $FINGERPRINT_DIR"
    mkdir -p $FINGERPRINT_DIR || Error_Handler
    CURRENT_STEP="setting permissions for $FINGERPRINT_DIR"
    chmod 0700 $FINGERPRINT_DIR || Error_Handler
}

CSR_Gen_SetVars ()
{
CURRENT_STEP="setting CSR information"
clear
cecho "===============================================================" boldmagenta
cecho "||                   CSR generation setup                    ||" boldmagenta
cecho "===============================================================" boldmagenta
echo
echo
echo -n "Enter Common Name (AKA domain name), e.g. example.org : "
echo
read -p "==> " Domain_CN
	if [ -z ${Domain_CN} ] ; then
		echo
		cecho '[ERROR] This field is always mandatory!' red
		echo
		read -p 'Press [Return] key to continue...' PAUSE
		CSR_Gen_SetVars
	else
        echo
        echo -n "Enter two-character Country Code, e.g. US (optional): "
        echo
        read -p "==> " Domain_C
        echo
        echo -n "Enter State, e.g. California (optional): "
        echo
        read -p "==> " Domain_ST
        echo
        echo -n "Enter Location / city, e.g. San Francisco (optional): "
        echo
        read -p "==> " Domain_L
        echo
        echo -n "Enter Organization, e.g. My Company, Inc. (optional): "
        echo
        read -p "==> " Domain_O
        echo
        echo -n "Enter OU / department, e.g. Operations (optional): "
        echo
        read -p "==> " Domain_OU
        echo
	fi
}

CSR_Gen_CheckVars ()
{
CURRENT_STEP="checking user inputted variables"
clear
cecho "=== You've chosen the following options for your new CSRs:  ===" boldcyan
cecho "Domain Name      : ${Domain_CN}" boldblack
cecho "Country Code     : ${Domain_C}" boldblack
cecho "State            : ${Domain_ST}" boldblack
cecho "Location         : ${Domain_L}" boldblack
cecho "Organization     : ${Domain_O}" boldblack
cecho "Department       : ${Domain_OU}" boldblack
cecho "===============================================================" boldcyan
echo
read -p "==> Is this OK? [N/y] " REPLY
case "$REPLY" in
( [yY]|[yY][eE][sS] ) echo "" ;;
*) clear ; CSR_Gen_SetVars ; CSR_Gen_CheckVars ;;
esac
}

CSR_Gen_Format ()
{
CURRENT_STEP="formatting user input for openssl"
if [ -z "${Domain_C}" ] ; then
    CSR_C=""
    else
    CSR_C="/C="
fi
if [ -z "${Domain_ST}" ] ; then
    CSR_ST=""
    else
    CSR_ST="/ST="
fi
if [ -z "${Domain_L}" ] ; then
    CSR_L=""
    else
    CSR_L="/L="
fi
if [ -z "${Domain_O}" ] ; then
    CSR_O=""
    else
    CSR_O="/O="
fi
if [ -z "${Domain_OU}" ] ; then
    CSR_OU=""
    else
    CSR_OU="/OU="
fi
}

CSR_Generate ()
{
    CURRENT_STEP="generating ${PRIVKEY_DIR}/${Domain_CN}.key & ${CSR_DIR}/${Domain_CN}.csr"
        cecho "[INFO] Now $CURRENT_STEP ..." cyan
        $OPENSSLBIN req -out ${CSR_DIR}/${Domain_CN}.csr -new -sha${SHA_HASH_BITS} -newkey rsa:${RSA_KEY_SIZE} -nodes -keyout ${PRIVKEY_DIR}/${Domain_CN}.key -subj "${CSR_C}${Domain_C}${CSR_ST}${Domain_ST}${CSR_L}${Domain_L}${CSR_O}${Domain_O}${CSR_OU}${Domain_OU}/CN=${Domain_CN}" || Error_Handler
    CURRENT_STEP="generating SHA256 fingerprint ${FINGERPRINT_DIR}/${Domain_CN}.sum"
        cecho "[INFO] Now $CURRENT_STEP ..." cyan
        $OPENSSLBIN req -pubkey < ${CSR_DIR}/${Domain_CN}.csr | $OPENSSLBIN pkey -pubin -outform der | $OPENSSLBIN dgst -sha256 -binary | $BASE64BIN > ${FINGERPRINT_DIR}/${Domain_CN}.sum || Error_Handler
    CURRENT_STEP="generating ${PRIVKEY_DIR}/${Domain_CN}.standby1.key & ${CSR_DIR}/${Domain_CN}.standby1.csr"
        cecho "[INFO] Now $CURRENT_STEP ..." cyan
        $OPENSSLBIN req -out ${CSR_DIR}/${Domain_CN}.standby1.csr -new -sha${SHA_HASH_BITS} -newkey rsa:${RSA_KEY_SIZE} -nodes -keyout ${PRIVKEY_DIR}/${Domain_CN}.standby1.key -subj "${CSR_C}${Domain_C}${CSR_ST}${Domain_ST}${CSR_L}${Domain_L}${CSR_O}${Domain_O}${CSR_OU}${Domain_OU}/CN=${Domain_CN}" || Error_Handler
    CURRENT_STEP="generating SHA256 fingerprint ${FINGERPRINT_DIR}/${Domain_CN}.standby1.sum"
        cecho "[INFO] Now $CURRENT_STEP ..." cyan
        $OPENSSLBIN req -pubkey < ${CSR_DIR}/${Domain_CN}.standby1.csr | $OPENSSLBIN pkey -pubin -outform der | $OPENSSLBIN dgst -sha256 -binary | $BASE64BIN > ${FINGERPRINT_DIR}/${Domain_CN}.standby1.sum || Error_Handler
    CURRENT_STEP="generating ${PRIVKEY_DIR}/${Domain_CN}.standby2.key & ${CSR_DIR}/${Domain_CN}.standby2.csr"
        cecho "[INFO] Now $CURRENT_STEP ..." cyan
        $OPENSSLBIN req -out ${CSR_DIR}/${Domain_CN}.standby2.csr -new -sha${SHA_HASH_BITS} -newkey rsa:${RSA_KEY_SIZE} -nodes -keyout ${PRIVKEY_DIR}/${Domain_CN}.standby2.key -subj "${CSR_C}${Domain_C}${CSR_ST}${Domain_ST}${CSR_L}${Domain_L}${CSR_O}${Domain_O}${CSR_OU}${Domain_OU}/CN=${Domain_CN}" || Error_Handler
    CURRENT_STEP="generating SHA256 fingerprint ${FINGERPRINT_DIR}/${Domain_CN}.standby2.sum"
        cecho "[INFO] Now $CURRENT_STEP ..." cyan
        $OPENSSLBIN req -pubkey < ${CSR_DIR}/${Domain_CN}.standby2.csr | $OPENSSLBIN pkey -pubin -outform der | $OPENSSLBIN dgst -sha256 -binary | $BASE64BIN > ${FINGERPRINT_DIR}/${Domain_CN}.standby2.sum || Error_Handler
}

Generate_Server_Stanzas ()
{
	SHAPin1="`cat ${FINGERPRINT_DIR}/${Domain_CN}.sum`"
	SHAPin2="`cat ${FINGERPRINT_DIR}/${Domain_CN}.standby1.sum`"
	SHAPin3="`cat ${FINGERPRINT_DIR}/${Domain_CN}.standby2.sum`"
    echo
    echo
	cecho "[SUCCESS] You can add whichever of the following snippets is relevent" boldgreen
	cecho "          for your particular web server into its configuration file:" boldgreen
	echo

cecho " • Hiawatha:" boldyellow
cat <<- _EOF_
   CustomHeader = Public-Key-Pins: pin-sha256="${SHAPin1}"; pin-sha256="${SHAPin2}"; pin-sha256="${SHAPin3}"; max-age=$HPKP_MAX_AGE; includeSubDomains

_EOF_
cecho " • NGINX:" boldgreen
cat <<- _EOF_
   add_header Public-Key-Pins 'pin-sha256="${SHAPin1}"; pin-sha256="${SHAPin2}"; pin-sha256="${SHAPin3}"; max-age=$HPKP_MAX_AGE; includeSubDomains';

_EOF_
cecho " • Apache:" boldmagenta
cat <<- _EOF_
   # Load the 'headers' module if it's not already being referenced:
   LoadModule headers_module modules/mod_headers.so

   Header set Public-Key-Pins "pin-sha256=\"${SHAPin1}\"; pin-sha256=\"${SHAPin2}\"; pin-sha256=\"${SHAPin3}\"; max-age=$HPKP_MAX_AGE; includeSubDomains"

_EOF_
cecho " • Lighttpd:" boldblue
cat <<- _EOF_
   server.modules += ( "mod_setenv" )
   $HTTP["scheme"] == "https" {
     setenv.add-response-header  = ( "Public-Key-Pins" => "pin-sha256=\"${SHAPin1}\"; pin-sha256=\"${SHAPin2}\"; "pin-sha256=\"${SHAPin3}\"; max-age=$HPKP_MAX_AGE; includeSubDomains")
   }

_EOF_

cecho " • Generic:" boldwhite
cat <<- _EOF_
   Your web server should be setup to send headers like this:
   
   Public-Key-Pins: pin-sha256="${SHAPin1}"; pin-sha256="${SHAPin2}"; pin-sha256="${SHAPin3}"; max-age=$HPKP_MAX_AGE; includeSubDomains

_EOF_
}


## EXECUTION ##
# Here we call our functions
#####

Detect_Bin_Paths
Sanity_Check
Set_BASH_Options
CSR_Gen_SetVars
CSR_Gen_CheckVars
CSR_Gen_Format
Make_Directories
CSR_Generate
Generate_Server_Stanzas
echo
cecho "[INFO] Script complete, please thoroughly verify your results before deployment." cyan
echo