#!/bin/sh # # shellpki is a wrapper around openssl to manage a small PKI # set -e VERSION="1.0.0" show_version() { cat <, Thomas Martin , Gregory Colpart , Romain Dessort , Benoit Série , Victor Laborie , Daniel Jakots , Patrick Marchand , Jérémy Lecour and others. shellpki comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the MIT Licence for details. END } init() { umask 0177 [ -d "${CA_DIR}" ] || mkdir -m 0750 "${CA_DIR}" [ -d "${CRT_DIR}" ] || mkdir -m 0750 "${CRT_DIR}" [ -f "${INDEX_FILE}" ] || touch "${INDEX_FILE}" [ -f "${CRL}" ] || touch "${CRL}" [ -f "${SERIAL}" ] || echo "01" > "${SERIAL}" cn="${1:-}" if [ -z "${cn}" ]; then show_usage >&2 exit 1 fi if [ -f "${CA_KEY}" ]; then printf "%s already exists, do you really want to erase it ? [y/N] " "${CA_KEY}" read -r REPLY resp=$(echo "${REPLY}" | tr 'Y' 'y') if [ "${resp}" = "y" ]; then rm -f "${CA_KEY}" "${CA_CERT}" fi fi if [ ! -f "${CA_KEY}" ]; then "${OPENSSL_BIN}" genrsa \ -out "${CA_KEY}" \ -aes256 4096 \ >/dev/null 2>&1 fi if [ -f "${CA_CERT}" ]; then printf "%s already exists, do you really want to erase it ? [y/N] " "${CA_CERT}" read -r REPLY resp=$(echo "${REPLY}" | tr 'Y' 'y') if [ "${resp}" = "y" ]; then rm "${CA_CERT}" fi fi if [ ! -f "${CA_CERT}" ]; then ask_ca_password 0 fi if [ ! -f "${CA_CERT}" ]; then CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL_BIN}" req \ -new \ -batch \ -sha512 \ -x509 \ -days 3650 \ -extensions v3_ca \ -key "${CA_KEY}" \ -out "${CA_CERT}" \ -passin env:CA_PASSWORD \ -config /dev/stdin <&2 exit 1 fi url=$(echo "${ocsp_uri}" | cut -d':' -f1) port=$(echo "${ocsp_uri}" | cut -d':' -f2) if [ ! -f "${OCSP_KEY}" ]; then "${OPENSSL_BIN}" genrsa \ -out "${OCSP_KEY}" \ ${KEY_LENGTH} \ >/dev/null 2>&1 fi "${OPENSSL_BIN}" req \ -batch \ -new \ -key "${OCSP_KEY}" \ -out "${CSR_DIR}/ocsp.csr" \ -config /dev/stdin < [options] [CommonName] Initialize PKI (create CA key and self-signed cert) : ${0} init Run OCSP_D server : ${0} ocsp Create a client cert with key and CSR directly generated on server (use -p or --password-file to set a password on the client key) : ${0} create [-p|--password-file=] Create a client cert from a CSR (doesn't need key) : ${0} create -f Revoke a client cert with is commonName (CN) : ${0} revoke List all actually valid commonName (CN) : ${0} list [-a|v|r] Check expiration date of valid certificates : ${0} check EOF } error() { echo "${1}" >&2 exit 1 } warning() { echo "${1}" >&2 } ask_ca_password() { [ ! -f "${CA_KEY}" ] && error "You must initialize your's PKI with shellpki init !" attempt=$((${1} + 1)) if [ "${attempt}" -gt 1 ]; then warning "Invalid password, retry." fi trap 'unset CA_PASSWORD' 0 stty -echo printf "Password for CA key : " read -r CA_PASSWORD stty echo printf "\n" if [ -z "${CA_PASSWORD}" ]; then ask_ca_password "${attempt}" fi CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL_BIN}" rsa \ -in "${CA_KEY}" \ -passin env:CA_PASSWORD \ >/dev/null 2>&1 \ || ask_ca_password "${attempt}" } create() { from_csr=0 ask_pass=0 # Parse options # based on https://gist.github.com/deshion/10d3cb5f88a21671e17a while :; do case $1 in -f|--file|--csr-file) # csr-file option, with value separated by space if [ -n "$2" ]; then from_csr=1 csr_file=$(readlink -f -- "${2}") shift else printf 'ERROR: "--csr-file" requires a non-empty option argument.\n' >&2 exit 1 fi ;; --file=?*|--csr-file=?*) from_csr=1 # csr-file option, with value separated by = csr_file=$(readlink -f -- "${1#*=}") ;; --file=|--csr-file=) # csr-file options, without value printf 'ERROR: "--csr-file" requires a non-empty option argument.\n' >&2 exit 1 ;; -p|--password) ask_pass=1 ;; --password-file) # password-file option, with value separated by space if [ -n "$2" ]; then password_file=$(readlink -f -- "${2}") shift else printf 'ERROR: "--password-file" requires a non-empty option argument.\n' >&2 exit 1 fi ;; --password-file=?*) # password-file option, with value separated by = password_file=$(readlink -f -- "${1#*=}") ;; --password-file=) # password-file options, without value printf 'ERROR: "--password-file" requires a non-empty option argument.\n' >&2 exit 1 ;; --) # End of all options. shift break ;; -?*) # ignore unknown options printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 ;; *) # Default case: If no more options then break out of the loop. break ;; esac shift done cn="${1:-}" if [ "${from_csr}" -eq 1 ]; then if [ "${ask_pass}" -eq 1 ]; then warning "Warning: -p|--password is ignored with -f|--file|--crt-file" fi if [ -n "${password_file}" ]; then warning "Warning: --password-file is ignored with -f|--file|--crt-file" fi # ask for CA passphrase ask_ca_password 0 # check if csr_file is a CSR "${OPENSSL_BIN}" req \ -noout -subject \ -in "${csr_file}" \ >/dev/null 2>&1 \ || error "${csr_file} is not a valid CSR !" # check if csr_file contain a CN "${OPENSSL_BIN}" req \ -noout -subject \ -in "${csr_file}" \ | grep -Eo "CN\s*=[^,/]*" \ >/dev/null 2>&1 \ || error "${csr_file} don't contain a CommonName !" # get CN from CSR cn=$("${OPENSSL_BIN}" req -noout -subject -in "${csr_file}" | grep -Eo "CN\s*=[^,/]*" | cut -d'=' -f2 | xargs) # check if CN already exist if [ -f "${CRT_DIR}/${cn}.crt" ]; then error "${cn} already used !" fi # ca sign and generate cert CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL_BIN}" ca \ -config "${CONF_FILE}" \ -in "${csr_file}" \ -passin env:CA_PASSWORD \ -out "${CRT_DIR}/${cn}.crt" echo "The CRT file is available in ${CRT_DIR}/${cn}.crt" else if [ -z "${cn}" ]; then show_usage >&2 exit 1 fi # check if CN already exist if [ -f "${CRT_DIR}/${cn}.crt" ]; then error "${cn} already used !" fi # ask for CA passphrase ask_ca_password 0 if [ -n "${password_file}" ] && [ -r "${password_file}" ]; then PASSWORD=$(head -n 1 "${password_file}" | tr -d '\n') if [ -z "${PASSWORD}" ]; then warning "Warning: empty password from file \`${password_file}'" fi elif [ "${ask_pass}" -eq 1 ]; then trap 'unset PASSWORD' 0 stty -echo printf "Password for user key : " read -r PASSWORD stty echo printf "\n" if [ -z "${PASSWORD}" ]; then warning "Warning: empty password from input" fi fi # generate private key if [ -n "${PASSWORD}" ]; then PASSWORD="${PASSWORD}" "${OPENSSL_BIN}" genrsa \ -aes256 \ -passout env:PASSWORD \ -out "${KEY_DIR}/${cn}-${SUFFIX}.key" \ ${KEY_LENGTH} \ >/dev/null 2>&1 else "${OPENSSL_BIN}" genrsa \ -out "${KEY_DIR}/${cn}-${SUFFIX}.key" \ ${KEY_LENGTH} \ >/dev/null 2>&1 fi if [ -n "${PASSWORD}" ]; then # generate csr req PASSWORD="${PASSWORD}" "${OPENSSL_BIN}" req \ -batch \ -new \ -key "${KEY_DIR}/${cn}-${SUFFIX}.key" \ -passin env:PASSWORD \ -out "${CSR_DIR}/${cn}-${SUFFIX}.csr" \ -config /dev/stdin </dev/null 2>&1 \ || rm -f "${CRT_DIR}/${cn}.crt" if [ ! -f "${CRT_DIR}/${cn}.crt" ]; then error "Error in CSR creation" fi chmod 640 "${CRT_DIR}/${cn}.crt" echo "The CRT file is available in ${CRT_DIR}/${cn}.crt" # generate pkcs12 format if [ -n "${PASSWORD}" ]; then PASSWORD="${PASSWORD}" "${OPENSSL_BIN}" pkcs12 \ -export \ -nodes \ -passin env:PASSWORD \ -passout env:PASSWORD \ -inkey "${KEY_DIR}/${cn}-${SUFFIX}.key" \ -in "${CRT_DIR}/${cn}.crt" \ -out "${PKCS12_DIR}/${cn}-${SUFFIX}.p12" else "${OPENSSL_BIN}" pkcs12 \ -export \ -nodes \ -passout pass: \ -inkey "${KEY_DIR}/${cn}-${SUFFIX}.key" \ -in "${CRT_DIR}/${cn}.crt" \ -out "${PKCS12_DIR}/${cn}-${SUFFIX}.p12" fi chmod 640 "${PKCS12_DIR}/${cn}-${SUFFIX}.p12" echo "The PKCS12 config file is available in ${PKCS12_DIR}/${cn}-${SUFFIX}.p12" # generate openvpn format if [ -e "${CA_DIR}/ovpn.conf" ]; then cat "${CA_DIR}/ovpn.conf" - > "${OVPN_DIR}/${cn}-${SUFFIX}.ovpn" < $(cat "${CA_CERT}") $(cat "${CRT_DIR}/${cn}.crt") $(cat "${KEY_DIR}/${cn}-${SUFFIX}.key") EOF chmod 640 "${OVPN_DIR}/${cn}-${SUFFIX}.ovpn" echo "The OpenVPN config file is available in ${OVPN_DIR}/${cn}-${SUFFIX}.ovpn" fi fi } revoke() { if [ "${1}" = "" ]; then show_usage >&2 exit 1 fi # get CN from param cn="${1}" # check if CRT exists if [ ! -f "${CRT_DIR}/${cn}.crt" ]; then error "Unknow CN : ${cn}" fi # check if CRT is a valid "${OPENSSL_BIN}" x509 \ -noout \ -subject \ -in "${CRT_DIR}/${cn}.crt" \ >/dev/null 2>&1 \ || error "${CRT_DIR}/${cn}.crt is not a valid CRT, you must delete it !" # ask for CA passphrase ask_ca_password 0 echo "Revoke certificate ${CRT_DIR}/${cn}.crt :" CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL_BIN}" ca \ -config "${CONF_FILE}" \ -passin env:CA_PASSWORD \ -revoke "${CRT_DIR}/${cn}.crt" \ && rm "${CRT_DIR}/${cn}.crt" CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL_BIN}" ca \ -config "${CONF_FILE}" \ -passin env:CA_PASSWORD \ -gencrl -out "${CRL}" } list() { if [ ! -f "${INDEX_FILE}" ]; then exit 0 fi list_valid=0 list_revoked=1 while :; do case "${1}" in -a|--all) list_valid=0 list_revoked=0 shift;; -v|--valid) list_valid=0 list_revoked=1 shift;; -r|--revoked) list_valid=1 list_revoked=0 shift;; --) shift break;; -?*) warning "unknow option ${1} (ignored)" shift;; *) break;; esac done if [ "${list_valid}" -eq 0 ]; then certs=$(grep "^V" "${INDEX_FILE}") fi if [ "${list_revoked}" -eq 0 ]; then certs=$(grep "^R" "${INDEX_FILE}") fi if [ "${list_valid}" -eq 0 ] && [ "${list_revoked}" -eq 0 ]; then certs=$(cat "${INDEX_FILE}") fi echo "${certs}" | grep -Eo "CN\s*=[^,/]*" | cut -d'=' -f2 | xargs -n1 } check() { # default expiration alert # TODO : permit override with parameters min_day=90 cur_epoch=$(date -u +'%s') for cert in ${CRT_DIR}/*; do end_date=$(openssl x509 -noout -enddate -in "${cert}" | cut -d'=' -f2) end_epoch=$(date -ud "${end_date}" +'%s') diff_epoch=$((end_epoch - cur_epoch)) diff_day=$((diff_epoch/60/60/24)) if [ "${diff_day}" -lt "${min_day}" ]; then if [ "${diff_day}" -le 0 ]; then echo "${cert} has expired" else echo "${cert} expire in ${diff_day} days" fi fi done } main() { # default config # TODO : override with /etc/default/shellpki CONF_FILE="/etc/shellpki/openssl.cnf" if [ "$(uname)" = "OpenBSD" ]; then PKI_USER="_shellpki" else PKI_USER="shellpki" fi if [ "${USER}" != "root" ] && [ "${USER}" != "${PKI_USER}" ]; then error "Please become root before running ${0} !" fi # retrieve CA path from config file CA_DIR=$(grep -E "^dir" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1) CA_KEY=$(grep -E "^private_key" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~") CA_CERT=$(grep -E "^certificate" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~") OCSP_KEY="${CA_DIR}/ocsp.key" OCSP_CERT="${CA_DIR}/ocsp.pem" CRT_DIR=$(grep -E "^certs" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~") TMP_DIR=$(grep -E "^new_certs_dir" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~") INDEX_FILE=$(grep -E "^database" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~") SERIAL=$(grep -E "^serial" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~") CRL=$(grep -E "^crl" "${CONF_FILE}" | cut -d'=' -f2 | xargs -n1 | sed "s~\$dir~${CA_DIR}~") # directories for clients key, csr, crt KEY_DIR="${CA_DIR}/private" CSR_DIR="${CA_DIR}/requests" PKCS12_DIR="${CA_DIR}/pkcs12" OVPN_DIR="${CA_DIR}/openvpn" KEY_LENGTH=2048 OPENSSL_BIN=$(command -v openssl) SUFFIX=$(/bin/date +"%s") if ! getent passwd "${PKI_USER}" >/dev/null || ! getent group "${PKI_USER}" >/dev/null; then error "You must create ${PKI_USER} user and group !" fi if [ ! -e "${CONF_FILE}" ]; then error "${CONF_FILE} is missing" fi mkdir -p "${CA_DIR}" "${CRT_DIR}" "${KEY_DIR}" "${CSR_DIR}" "${PKCS12_DIR}" "${OVPN_DIR}" "${TMP_DIR}" command=${1:-help} case "${command}" in init) shift init "$@" ;; ocsp) shift ocsp "$@" ;; create) shift create "$@" ;; revoke) shift revoke "$@" ;; list) shift list "$@" ;; check) shift check "$@" ;; version) show_version exit 0 ;; help) show_usage exit 0 ;; *) show_usage >&2 exit 1 ;; esac # fix right chown -R "${PKI_USER}":"${PKI_USER}" "${CA_DIR}" chmod 750 "${CA_DIR}" "${CRT_DIR}" "${KEY_DIR}" "${CSR_DIR}" "${PKCS12_DIR}" "${OVPN_DIR}" "${TMP_DIR}" chmod 600 "${INDEX_FILE}"* "${SERIAL}"* "${CA_KEY}" "${CRL}" chmod 640 "${CA_CERT}" } main "$@"