#!/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 ${CA_KEY_LENGTH} \ >/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 } verify_ca_password() { CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL_BIN}" rsa \ -in "${CA_KEY}" \ -passin env:CA_PASSWORD \ >/dev/null 2>&1 } ask_ca_password() { attempt=${1:-0} max_attempt=3 trap 'unset CA_PASSWORD' 0 if [ ! -f "${CA_KEY}" ]; then error "You must initialize your PKI with \`shellpki init' !" fi if [ "${attempt}" -gt 0 ]; then warning "Invalid password, retry." fi if [ "${attempt}" -ge "${max_attempt}" ]; then error "Maximum number of attempts reached (${max_attempt})." fi if [ -z "${CA_PASSWORD}" ]; then stty -echo printf "Password for CA key : " read -r CA_PASSWORD stty echo printf "\n" fi if [ -z "${CA_PASSWORD}" ] || ! verify_ca_password; then unset CA_PASSWORD attempt=$(( attempt + 1 )) ask_ca_password "${attempt}" fi } 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 if [ "$?" -ne 0 ]; then error "${csr_file} is not a valid CSR !" fi # check if csr_file contain a CN "${OPENSSL_BIN}" req \ -noout \ -subject \ -in "${csr_file}" \ | grep -Eo "CN\s*=[^,/]*" \ >/dev/null 2>&1 if [ "$?" -ne 0 ]; then error "${csr_file} doesn't contain a CommonName !" fi # 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 printf "%s already exists, do you revoke and recreate it ? [y/N] " "${cn}" read -r REPLY resp=$(echo "${REPLY}" | tr 'Y' 'y') if [ "${resp}" = "y" ]; then revoke "${cn}" else error "Abort" fi 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 printf "%s already exists, do you revoke and recreate it ? [y/N] " "${cn}" read -r REPLY resp=$(echo "${REPLY}" | tr 'Y' 'y') if [ "${resp}" = "y" ]; then revoke "${cn}" else error "Abort" fi 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 if [ "$?" -ne 0 ]; then rm -f "${CRT_DIR}/${cn}.crt" fi 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 if [ "$?" -ne 0 ]; then error "${CRT_DIR}/${cn}.crt is not a valid CRT, you must delete it !" fi # 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" CA_KEY_LENGTH=4096 if [ "${CA_KEY_LENGTH}" -lt 4096 ]; then error "CA key must be at least 4096 bits long." fi KEY_LENGTH=2048 if [ "${KEY_LENGTH}" -lt 2048 ]; then error "User key must be at least 2048 bits long." fi 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 "$@"