#! /bin/bash
#
# Copyright (c) 2015, 2016 Karol Babioch <karol@babioch.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Version string
VERSION="1"

# Various exit codes used throughout the program
EXIT_INVALID_OPTION=1
EXIT_NO_TMPDIR=2
EXIT_OPENSSL_ERROR=3

# OpenSSL directories
OPENSSL_CSR_DIR="csr"
OPENSSL_CERTS_DIR="certs"
OPENSSL_KEYS_DIR="keys"
OPENSSL_PKCS12_DIR="pkcs12"

# OpenSSL files
OPENSSL_DATABASE="database"
OPENSSL_SERIAL="serial"

# Flag for PKCS12 mode
PKCS12="False"

# Flag for verbose output
VERBOSE="False"

# Debug (will keep temporary files and activate verbose output)
DEBUG="False"

# Flag for forcibly overwriting output directory
OVERWRITE="False"

# Output a prefixed error message and exit with given code
# $1: Exit code
# $2: Error message (optional)
error()
{

    if [[ ! -z "$2" ]]; then

        echo "ERROR: $2" >&2

    fi

    exit $1

}

# Output usage help
help()
{

    cat << EOF

    Usage: $0 [OPTIONS]... [SERVERNAME] CLIENTNAME

    OPTIONS:

      -c, --config=         Location of OpenSSL configuration file

          --section=        Section in configuration file to use (otca_ca by default)
          --ext-ca=         Name of extensions for CA certificate (otca_ext_ca by default)
          --ext-server=     Name of extensions for server certificate (otca_ext_server by default)
          --ext-client=     Name of extensions for client certificate (otca_ext_client by default)

      -b, --bits=           Number of bits of the RSA keys, 2048 by default
      -y, --years=          Number of years certificates should be valid for, 3 by default
          --startdate=      Start date of the certificates (notBefore), yesterday by default, format: YYYY-MM-DD
          --enddate=        End date of the certificates (notAfter), see -y/--years for default, format: YYYY-MM-DD

      -p, --pkcs12          Create PKCS12 files instead of PEM keys and certificates

      -o, --outdir          Name of output directory, "SERVERNAME+CLIENTNAME" by default
      -f, --force           Force overwrite if output directory already exists

      -v, --verbose         Enable verbose output
      -d, --debug           Enable debug mode (potentially DANGEROUS, since it will keep temporary keys around)

      -h, --help            Show this usage help
          --version         Show version information

    SERVERNAME: Name of the server, optional since hostname(1) is used by default
    CLIENTNAME: Name of the client that the OTCA should be set up for, mandatory

EOF

}

# Output version information
version()
{

    cat << EOF

    Version: $VERSION

    Copyright (c) 2015, 2016 Karol Babioch <karol@babioch.de>

    This program comes with ABSOLUTELY NO WARRANTY; This is free software, and
    you are welcome to redistribute it under certain conditions;

EOF

}

# Output information about OTCA configuration to user
info()
{

    cat << EOF

    One-Time CA for

        Server:     $SERVERNAME
        Client:     $CLIENTNAME

        Config:     $CONFFILE
        Section:    $SECTION
        CA ext:     ${EXT_CA}
        Server ext: ${EXT_SERVER}
        Client ext: ${EXT_CLIENT}

        Bits:       $BITS

        Years:      $YEARS
        Start date: $STARTDATE
        End date:   $ENDDATE

        PKCS12:     $PKCS12
        Output dir: $OUTDIR
        Overwrite:  $OVERWRITE

        Verbose:    $VERBOSE
        Debug:      $DEBUG
        Tempdir:    $TMPDIR

EOF

}

# Returns hex string with given length
# $1: Requested string length
random_hex()
{

    # 2 hex bytes needed for representation of a single byte -> /2
    openssl rand -hex $(($1/2))

}

# Writes random serial to given file
#
# According to RFC5280 serial might be up to 20 octets long, so this is what
# we use here.
randomize_serial()
{

    random_hex 20 > ${OPENSSL_SERIAL}

}

# Sets up the OTCA
#
# This initializes the OTCA. It creates a CSR, self-signs it and creates the
# environment that is needed by OpenSSL's ca(1) utility.
init_otca()
{

    # Create necessary files and directories (required by OpenSSL's ca(1))
    touch "${OPENSSL_DATABASE}"
    mkdir -p "${OPENSSL_CSR_DIR}" "${OPENSSL_CERTS_DIR}" "${OPENSSL_KEYS_DIR}" "${OPENSSL_PKCS12_DIR}"

    # Create CSR for OTCA itself
    openssl req \
        -newkey rsa:"$BITS" \
        -keyout "${OPENSSL_KEYS_DIR}/otca.key" \
        -nodes \
        -config "$CONFFILE" \
        -subj "/CN=OTCA $SERVERNAME+$CLIENTNAME" \
        -out "${OPENSSL_CSR_DIR}/otca.csr" \
        -batch \
        >&3 2>&3

    # Randomize serial
    randomize_serial

    # Self-sign OTCA
    openssl ca \
        -config "$CONFFILE" \
        -selfsign \
        -keyfile "${OPENSSL_KEYS_DIR}/otca.key" \
        -name "$SECTION" \
        -extensions "${EXT_CA}" \
        -in "${OPENSSL_CSR_DIR}/otca.csr" \
        -startdate $(date_asn1_utc "$STARTDATE") \
        -enddate $(date_asn1_utc "$ENDDATE") \
        -out "${OPENSSL_CERTS_DIR}/otca.pem" \
        -batch \
        >&3 2>&3

}

# Create a CSR for entity with given common name
#
#$1: commonName (CN)
create_csr()
{

    # Create CSR
    openssl req \
        -newkey rsa:"$BITS" \
        -keyout "${OPENSSL_KEYS_DIR}/$1.key" \
        -nodes \
        -config "$CONFFILE" \
        -subj "/CN=$1" \
        -out "${OPENSSL_CSR_DIR}/$1.csr" \
        -batch \
        >&3 2>&3

}

# Sign a CSR with CAs private key
#
#$1: name
#$2: extensions
sign_csr()
{

    # Randomize serial
    randomize_serial

    # Sign CSR
    openssl ca \
        -config "$CONFFILE" \
        -keyfile "${OPENSSL_KEYS_DIR}/otca.key" \
        -cert "${OPENSSL_CERTS_DIR}/otca.pem" \
        -name "$SECTION" \
        -extensions "$2" \
        -in "${OPENSSL_CSR_DIR}/$1.csr" \
        -startdate $(date_asn1_utc "$STARTDATE") \
        -enddate $(date_asn1_utc "$ENDDATE") \
        -out "${OPENSSL_CERTS_DIR}/$1.pem" \
        -batch \
        >&3 2>&3

}

# Create PKCS12 from private key and certificate
#
#$1: name
create_pkcs12()
{

    # Create PKCS12 file
    openssl pkcs12 \
        -export \
        -out "${OPENSSL_PKCS12_DIR}/$1.p12" \
        -passout pass: \
        -inkey "${OPENSSL_KEYS_DIR}/$1.key" \
        -in "${OPENSSL_CERTS_DIR}/$1.pem" \
        -certfile "${OPENSSL_CERTS_DIR}/otca.pem" \
        >&3 2>&3

}

# Various clean up tasks performend whenever the script is exited
cleanup()
{

    # Change back to original working directory
    popd > /dev/null 2>&1

    # Remove temporary scratch space
    rm -r "$TMPDIR" 2> /dev/null

}

# Validates a given date
#
# $1: Date to validate
#
# Returns 0 if valid, 1 otherwise
date_validate()
{

    date -d "$1" > /dev/null 2>&1
    return $?

}

# Converts given date to ASN1 UTCTime structure as required by OpenSSL
#
# $1: Date (format: YYYY-MM-DD)
#
# Returns converted date in YYMMDDHHMMSSZ format
date_asn1_utc()
{

    date "+%y%m%d000000Z" -d "$1"

}

# Parse options provided as command line arguments
for i in "$@"
do

    case "$i" in

        # OpenSSL configuration file (short)
        -c)
            CONFFILE="$2"
            shift 2
            ;;

        # OpenSSL configuration file (long)
        --config=*)
            CONFFILE="${i#*=}"
            shift
            ;;

        # OpenSSL section name (long)
        --section=*)
            SECTION="${i#*=}"
            shift
            ;;

        # OpenSSL extensions for CA (long)
        --ext-ca=*)
            EXT_CA="${i#*=}"
            shift
            ;;

        # OpenSSL extensions for server (long)
        --ext-server=*)
            EXT_SERVER="${i#*=}"
            shift
            ;;

        # OpenSSL extensions for client (long)
        --ext-client=*)
            EXT_CLIENT="${i#*=}"
            shift
            ;;

        # Set number of bits (short)
        -b)
            BITS="$2"
            shift 2
            ;;

        # Set number of bits (long)
        --bits=*)
            BITS="${i#*=}"
            shift
            ;;

        # Set number of years (short)
        -y)
            YEARS="$2"
            shift 2
            ;;

        # Set number of years (long)
        --years=*)
            YEARS="${i#*=}"
            shift
            ;;

        # Set startdate (long)
        --startdate=*)
            STARTDATE="${i#*=}"
            shift
            ;;

        # Set end date (long)
        --enddate=*)
            ENDDATE="${i#*=}"
            shift
            ;;

        # Enable PKCS12 mode (short & long)
        -p|--pkcs12)
            PKCS12="True"
            shift
            ;;

        # Set output directory (short)
        -o)
            OUTDIR="$2"
            shift 2
            ;;

        # Set output directory (long)
        --outdir=*)
            OUTDIR="${i#*=}"
            shift
            ;;

        # Set force flag (short & long)
        -f|--force)
            OVERWRITE="True"
            shift
            ;;

        # Enable verbose mode (short & long)
        -v|--verbose)
            VERBOSE="True"
            shift
            ;;

        # Enable debug mode (short & long)
        -d|--debug)
            DEBUG="True"
            # Also enable verbose mode implicetely
            VERBOSE="True"
            shift
            ;;

        # Usage help
        -h|--help)
            help
            exit
            ;;

        # Version information
        --version)
            version
            exit
            ;;

    esac

done

# Check number of arguments
if [[ "$#" == 1 ]]; then

    # Only clientname was given
    CLIENTNAME="$1"

elif [[ "$#" == 2 ]]; then

    # Server and clientname were given
    SERVERNAME="$1"
    CLIENTNAME="$2"

else

    # Invalid number of arguments
    error $EXIT_INVALID_OPTION "Wrong number of arguments!"

fi

# Check for empty servername
if [[ -z "$SERVERNAME" ]]; then

    # Use hostname if not provided as argument
    SERVERNAME=$(hostname)

fi

# Check for empty clientname
if [[ -z "$CLIENTNAME" ]]; then

    error $EXIT_INVALID_OPTION "Clientname must not be empty!"

fi

# Check if servername is different from clientname
if [[ "$SERVERNAME" == "$CLIENTNAME" ]]; then

    error $EXIT_INVALID_OPTION "Servername and clientname must be different!"

fi

# Check for empty bits
if [[ -z "$BITS" ]]; then

    # 2048 by default
    BITS="2048"

else

    # Check for integer
    if [[ ! $BITS =~ ^[0-9]+$ ]]; then

        error $EXIT_INVALID_OPTION "Invalid number of bits!"

    fi

fi

# Check for empty years
if [[ -z "$YEARS" ]]; then

    # 5 years by default
    YEARS="5"

else

    # Check for integer
    if [[ ! $YEARS =~ ^[0-9]+$ ]]; then

        error $EXIT_INVALID_OPTION "Invalid number of years!"

    fi

fi

if [[ -z "$STARTDATE" ]]; then

    # yesterday by default
    STARTDATE=$(date "+%Y-%m-%d" -d "- 1 day")

else

    # Check if provided startdate is actually valid
    if ! date_validate "$STARTDATE"; then

        error $EXIT_INVALID_OPTION "Invalid startdate provided!"

    fi

fi

if [[ -z "$ENDDATE" ]]; then

    # today + $YEARS by default"
    ENDDATE=$(date "+%Y-%m-%d" -d "$STARTDATE + $YEARS years")

else

    # Check if provided enddate is actually valid
    if ! date_validate "$ENDDATE"; then

        error $EXIT_INVALID_OPTION "Invalid startdate provided!"

    fi

fi

# Check if OpenSSL configuration file was provided
if [[ -z "$CONFFILE" ]]; then

    # Use default one
    CONFFILE="/etc/otca/otca.cnf"

fi

# Check if OpenSSL configuration file actually exists
if [[ ! -f "$CONFFILE" ]]; then

    error $EXIT_INVALID_OPTION "OpenSSL configuration file does not exist!"

fi

# Check if section name was provided
if [[ -z "$SECTION" ]]; then

    # Default
    SECTION="otca_ca"

fi

# Check if CA extensions were provided
if [[ -z "${EXT_CA}" ]]; then

    # Default
    EXT_CA="otca_ext_ca"

fi

# Check if server extensions were provided
if [[ -z "${EXT_SERVER}" ]]; then

    # Default
    EXT_SERVER="otca_ext_server"

fi

# Check if client extensions were provided
if [[ -z "${EXT_CLIENT}" ]]; then

    # Default
    EXT_CLIENT="otca_ext_client"

fi

# Check if output directory was set
if [[ -z "$OUTDIR" ]]; then

    # Set default value if it was not set explicetely (or is empty)
    OUTDIR="$SERVERNAME+$CLIENTNAME"

fi

# Check if output directory already exists
if [[ -d "$OUTDIR" && "$OVERWRITE" == "False" ]]; then

    error $EXIT_INVALID_OPTION "Output directory already exists!"

fi

# Check verbosity flag and attach new stream to stdout or /dev/null
if [[ "$VERBOSE" == "True" ]]; then

    exec 3>&1

else

    exec 3> /dev/null

fi

# Only cleanup if not in debug mode
if [[ "$DEBUG" == "False" ]]; then

    trap cleanup SIGINT SIGQUIT SIGTERM EXIT

fi

# Create temporary scratch space
TMPDIR=$(mktemp -d) || exit $EXIT_NO_TMPDIR

# Output information about configuration
info >&3

# Save current working directory on directory stack and change to TMPDIR
pushd "$TMPDIR" > /dev/null 2>&1

# Initialize CA
init_otca

# Create CSR for server and client
create_csr "$SERVERNAME"
create_csr "$CLIENTNAME"

# Sign CSRs with appropriate extensions
sign_csr "$SERVERNAME" "${EXT_SERVER}"
sign_csr "$CLIENTNAME" "${EXT_CLIENT}"

# Create PKCS12
if [[ $PKCS12 == "True" ]]; then

    create_pkcs12 "$SERVERNAME"
    create_pkcs12 "$CLIENTNAME"

fi

# Get back to old working directory
popd > /dev/null 2>&1

# Create output directory
mkdir -p "$OUTDIR"

# Move relevant files
if [[ $PKCS12 == "True" ]]; then

    mv "$TMPDIR/${OPENSSL_PKCS12_DIR}/$SERVERNAME.p12" "$OUTDIR/"
    mv "$TMPDIR/${OPENSSL_PKCS12_DIR}/$CLIENTNAME.p12" "$OUTDIR/"

else

    mv "$TMPDIR/${OPENSSL_CERTS_DIR}/$SERVERNAME.pem" "$OUTDIR/"
    mv "$TMPDIR/${OPENSSL_KEYS_DIR}/$SERVERNAME.key" "$OUTDIR/"

    mv "$TMPDIR/${OPENSSL_CERTS_DIR}/$CLIENTNAME.pem" "$OUTDIR/"
    mv "$TMPDIR/${OPENSSL_KEYS_DIR}/$CLIENTNAME.key" "$OUTDIR/"

    mv "$TMPDIR/${OPENSSL_CERTS_DIR}/otca.pem" "$OUTDIR/"

fi

