shellpki/shellpki
Mathieu Trossevin 38aac7b137
Use genpkey and pkey instead of genrsa and rsa
genrsa and rsa are being deprecated by OpenSSL and both genpkey and pkey
provides the same functionalities as genrsa and rsa will being more
configurable.
2022-04-06 11:40:17 +02:00

1101 lines
31 KiB
Bash
Executable file

#!/bin/sh
#
# shellpki is a wrapper around OpenSSL to manage a small PKI
#
set -u
VERSION="22.04"
show_version() {
cat <<END
shellpki version ${VERSION}
Copyright 2010-2022 Evolix <info@evolix.fr>,
Thomas Martin <tmartin@evolix.fr>,
Gregory Colpart <reg@evolix.fr>,
Romain Dessort <rdessort@evolix.fr>,
Benoit Série <bserie@evolix.fr>,
Victor Laborie <vlaborie@evolix.fr>,
Daniel Jakots <djakots@evolix.fr>,
Patrick Marchand <pmarchand@evolix.fr>,
Jérémy Lecour <jlecour@evolix.fr>,
Jérémy Dubois <jdubois@evolix.fr>
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
}
show_usage() {
cat <<EOF
Usage: ${0} <subcommand> [options] [CommonName]
Warning: [options] always must be before [CommonName] and after <subcommand>
EOF
show_usage_init
show_usage_create
show_usage_revoke
show_usage_list
show_usage_check
show_usage_ocsp
cat <<EOF
Show version :
${0} --version
Show help :
${0} --help
EOF
}
show_usage_init() {
cat <<EOF
Initialize PKI (create CA key and self-signed certificate) :
${0} init [options] <commonName_for_CA>
Options
--non-interactive do not prompt the user, and exit if an error occurs
EOF
}
show_usage_create() {
cat <<EOF
Create a client certificate with key and CSR directly generated on server :
${0} create [options] <commonName>
Options
-f, --file, --csr-file create a client certificate from a CSR (doesn't need key)
-p, --password prompt the user for a password to set on the client key
--password-file if provided with a path to a readable file, the first line is read and set as password on the client key
--days specify how many days the certificate should be valid
--end-date specify until which date the certificate should be valid, in "YYYY/MM/DD hh:mm:ss" format
--non-interactive do not prompt the user, and exit if an error occurs
--replace-existing if the certificate already exists, revoke it before creating a new one
EOF
}
show_usage_revoke() {
cat <<EOF
Revoke a client certificate :
${0} revoke [options] <commonName>
Options
--non-interactive do not prompt the user, and exit if an error occurs
EOF
}
show_usage_list() {
cat <<EOF
List certificates :
${0} list <options>
Options
-a, --all list all certificates: valid and revoked ones
-v, --valid list all valid certificates
-r, --revoked list all revoked certificates
EOF
}
show_usage_check() {
cat <<EOF
Check expiration date of valid certificates :
${0} check
EOF
}
show_usage_ocsp() {
cat <<EOF
Run OCSP_D server :
${0} ocsp <ocsp_uri:ocsp_port>
EOF
}
error() {
echo "${1}" >&2
exit 1
}
warning() {
echo "${1}" >&2
}
verify_ca_password() {
"${OPENSSL_BIN}" pkey \
-in "${CA_KEY}" \
-passin pass:"${CA_PASSWORD}" \
>/dev/null 2>&1
}
get_real_path() {
# --canonicalize is supported on Linux
# -f is supported on Linux and OpenBSD
readlink -f -- "${1}"
}
ask_ca_password() {
attempt=${1:-0}
max_attempts=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_attempts}" ]; then
error "Maximum number of attempts reached (${max_attempts})."
fi
if [ -z "${CA_PASSWORD:-}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "In non-interactive mode, you must pass CA_PASSWORD as environment variable"
fi
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
}
ask_user_password() {
trap 'unset PASSWORD' 0
if [ -z "${PASSWORD:-}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "In non-interactive mode, you must pass PASSWORD as environment variable or use --password-file"
fi
stty -echo
printf "Password for user key: "
read -r PASSWORD
stty echo
printf "\n"
fi
if [ -z "${PASSWORD:-}" ]; then
warning "Warning: empty password from input"
fi
}
replace_existing_or_abort() {
cn=${1:?}
if [ "${non_interactive}" -eq 1 ]; then
if [ "${replace_existing}" -eq 1 ]; then
revoke --non-interactive "${cn}"
else
error "${cn} already exists, use \`--replace-existing' to force"
fi
else
if [ "${replace_existing}" -eq 1 ]; then
revoke "${cn}"
else
printf "%s already exists, do you want to revoke and recreate it ? [y/N] " "${cn}"
read -r REPLY
resp=$(echo "${REPLY}" | tr 'Y' 'y')
if [ "${resp}" = "y" ]; then
revoke "${cn}"
else
error "Aborted"
fi
fi
fi
}
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}"
non_interactive=0
# Parse options
# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-} in
--non-interactive)
non_interactive=1
;;
--)
# End of all options.
shift
break
;;
-?*)
# ignore unknown options
warning "Warning: unknown option (ignored): \`$1'"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
cn="${1:-}"
if [ -z "${cn}" ]; then
show_usage_init >&2
exit 1
fi
if [ -f "${CA_KEY}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "${CA_KEY} already exists, erase it manually if you want to start over."
else
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
fi
passout_arg=""
if [ -n "${CA_PASSWORD:-}" ]; then
passout_arg="-pass pass:${CA_PASSWORD}"
elif [ "${non_interactive}" -eq 1 ]; then
error "In non-interactive mode, you must pass CA_PASSWORD as environment variable."
fi
if [ ! -f "${CA_KEY}" ]; then
"${OPENSSL_BIN}" genpkey \
-algorithm RSA \
-out "${CA_KEY}" \
${passout_arg} \
-aes256 \
-pkeyopt "rsa_keygen_bits:${CA_KEY_LENGTH}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the CA key"
fi
fi
if [ -f "${CA_CERT}" ]; then
if [ "${non_interactive}" -eq 1 ]; then
error "${CA_CERT} already exists, erase it manually if you want to start over."
else
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
fi
if [ ! -f "${CA_CERT}" ]; then
ask_ca_password 0
fi
if [ ! -f "${CA_CERT}" ]; then
"${OPENSSL_BIN}" req \
-new \
-batch \
-sha512 \
-x509 \
-days 3650 \
-extensions v3_ca \
-passin pass:"${CA_PASSWORD}" \
-key "${CA_KEY}" \
-out "${CA_CERT}" \
-config /dev/stdin <<EOF
$(cat "${CONF_FILE}")
commonName_default = ${cn}
EOF
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the CA certificate"
fi
fi
"${OPENSSL_BIN}" ca \
-config "${CONF_FILE}" \
-passin pass:${CA_PASSWORD} \
-gencrl \
-out "${CRL}"
}
ocsp() {
umask 0177
ocsp_uri="${1:-}"
if [ -z "${ocsp_uri}" ]; then
show_usage_ocsp >&2
exit 1
fi
ocsp_csr_file="${CSR_DIR}/ocsp.csr"
url=$(echo "${ocsp_uri}" | cut -d':' -f1)
port=$(echo "${ocsp_uri}" | cut -d':' -f2)
if [ ! -f "${OCSP_KEY}" ]; then
"${OPENSSL_BIN}" genpkey \
-algorithm RSA \
-out "${OCSP_KEY}" \
-pkeyopt "rsa_keygen_bits:${KEY_LENGTH}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the OCSP key"
fi
fi
"${OPENSSL_BIN}" req \
-batch \
-new \
-key "${OCSP_KEY}" \
-out "${ocsp_csr_file}" \
-config /dev/stdin <<EOF
$(cat "${CONF_FILE}")
commonName_default = ${url}
[ usr_cert ]
authorityInfoAccess = OCSP;URI:http://${ocsp_uri}
EOF
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the OCSP request"
fi
if [ ! -f "${OCSP_CERT}" ]; then
ask_ca_password 0
fi
if [ ! -f "${OCSP_CERT}" ]; then
"${OPENSSL_BIN}" ca \
-extensions v3_ocsp \
-in "${ocsp_csr_file}" \
-out "${OCSP_CERT}" \
-passin pass:"${CA_PASSWORD}" \
-config "${CONF_FILE}"
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the OCSP certificate"
fi
fi
exec "${OPENSSL_BIN}" ocsp \
-ignore_err \
-index "${INDEX_FILE}" \
-port "${port}" \
-rsigner "${OCSP_CERT}" \
-rkey "${OCSP_KEY}" \
-CA "${CA_CERT}" \
-text
}
create() {
from_csr=0
ask_pass=0
non_interactive=0
replace_existing=0
days=""
end_date=""
days_set=0
end_date_set=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=$(get_real_path "${2}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${2}'"
fi
shift
else
error "Argument error: \`--csr-file' requires a value"
fi
;;
--file=?*|--csr-file=?*)
from_csr=1
# csr-file option, with value separated by =
csr_file=$(get_real_path "${1#*=}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${1#*=}'"
fi
;;
--file=|--csr-file=)
# csr-file options, without value
error "Argument error: \`--csr-file' requires a value"
;;
-p|--password)
ask_pass=1
;;
--password-file)
# password-file option, with value separated by space
if [ -n "$2" ]; then
password_file=$(get_real_path "${2}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${2}'"
fi
shift
else
error "Argument error: \`--password-file' requires a value"
fi
;;
--password-file=?*)
# password-file option, with value separated by =
password_file=$(get_real_path "${1#*=}")
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error accessing file \`${1#*=}'"
fi
;;
--password-file=)
# password-file options, without value
error "Argument error: \`--password-file' requires a value"
;;
--days)
# days option, with value separated by space
if [ -n "$2" ]; then
days=${2}
days_set=1
shift
else
error "Argument error: \`--days' requires a value"
fi
;;
--days=?*)
# days option, with value separated by =
days=${1#*=}
days_set=1
;;
--days=)
# days options, without value
error "Argument error: \`--days' requires a value"
;;
--end-date)
# end-date option, with value separated by space
if [ -n "$2" ]; then
end_date=${2}
end_date_set=1
shift
else
error "Argument error: \`--end-date' requires a value"
fi
;;
--end-date=?*)
# end-date option, with value separated by =
end_date=${1#*=}
end_date_set=1
;;
--end-date=)
# end-date options, without value
error "Argument error: \`--end-date' requires a value"
;;
--non-interactive)
non_interactive=1
;;
--replace-existing)
replace_existing=1
;;
--)
# End of all options.
shift
break
;;
-?*)
# ignore unknown options
warning "Warning: unknown option (ignored): \`$1'"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
if [ "${days_set}" -eq 1 ] && [ "${end_date_set}" -eq 1 ]; then
error "Argument error: \`--end-date' and \`--days' cannot be used together."
fi
# The name of the certificate
cn="${1:-}"
# Set expiration argument
crt_expiration_arg=""
if [ -n "${days}" ]; then
if [ "${days}" -gt 0 ]; then
crt_expiration_arg="-days ${days}"
else
error "Argument error: \"${days}\" is not a valid value for \`--days'."
fi
fi
if [ -n "${end_date}" ]; then
if [ "${SYSTEM}" = "linux" ]; then
cert_end_date=$(TZ=:Zulu date --date "${end_date}" +"%Y%m%d%H%M%SZ" 2> /dev/null)
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Invalid end date format: \`${end_date}' can't be parsed by date(1). Expected format: YYYY/MM/DD [hh[:mm[:ss]]]."
else
crt_expiration_arg="-enddate ${cert_end_date}"
fi
elif [ "${SYSTEM}" = "openbsd" ]; then
cert_end_date=$(TZ=:Zulu date -f "%C%y/%m/%d %H:%M:%S" -j "${end_date}" +"%Y%m%d%H%M%SZ" 2> /dev/null)
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Invalid end date format: \`${end_date}' can't be parsed by date(1). Expected format: YYYY/MM/DD hh:mm:ss."
else
crt_expiration_arg="-enddate ${cert_end_date}"
fi
else
error "System ${SYSTEM} not supported."
fi
fi
if [ "${non_interactive}" -eq 1 ]; then
batch_arg="-batch"
else
batch_arg=""
fi
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
crt_file="${CRT_DIR}/${cn}.crt"
# 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
# shellcheck disable=SC2181
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
# shellcheck disable=SC2181
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_file}" ]; then
replace_existing_or_abort "${cn}"
fi
# ca sign and generate cert
if [ "${non_interactive}" -eq 1 ]; then
batch_arg="-batch"
else
batch_arg=""
fi
"${OPENSSL_BIN}" ca \
${batch_arg} \
-config "${CONF_FILE}" \
-in "${csr_file}" \
-passin pass:"${CA_PASSWORD}" \
-out "${crt_file}" \
${crt_expiration_arg}
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the certificate"
else
echo "The certificate file is available at \`${crt_file}'"
fi
else
if [ -z "${cn}" ]; then
show_usage_create >&2
exit 1
fi
csr_file="${CSR_DIR}/${cn}-${SUFFIX}.csr"
crt_file="${CRT_DIR}/${cn}.crt"
key_file="${KEY_DIR}/${cn}-${SUFFIX}.key"
ovpn_file="${OVPN_DIR}/${cn}-${SUFFIX}.ovpn"
pkcs12_file="${PKCS12_DIR}/${cn}-${SUFFIX}.p12"
# check if CN already exist
if [ -f "${crt_file}" ]; then
replace_existing_or_abort "${cn}"
fi
# ask for CA passphrase
ask_ca_password 0
if [ "${ask_pass}" -eq 1 ]; then
ask_user_password
fi
# generate private key
pass_args=""
if [ -n "${password_file:-}" ]; then
pass_args="-aes256 -pass file:${password_file}"
elif [ -n "${PASSWORD:-}" ]; then
pass_args="-aes256 -pass pass:${PASSWORD}"
fi
"${OPENSSL_BIN}" genpkey \
-algorithm RSA \
-out "${key_file}" \
${pass_args} \
-pkeyopt "rsa_keygen_bits:${KEY_LENGTH}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -eq 0 ]; then
echo "The KEY file is available at \`${key_file}'"
else
error "Error generating the private key"
fi
# generate csr req
pass_args=""
if [ -n "${password_file:-}" ]; then
pass_args="-passin file:${password_file}"
elif [ -n "${PASSWORD:-}" ]; then
pass_args="-passin pass:${PASSWORD}"
fi
"${OPENSSL_BIN}" req \
-batch \
-new \
-key "${key_file}" \
-out "${csr_file}" \
${pass_args} \
-config /dev/stdin <<EOF
$(cat "${CONF_FILE}")
commonName_default = ${cn}
EOF
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the CSR"
fi
# ca sign and generate cert
"${OPENSSL_BIN}" ca \
${batch_arg} \
-config "${CONF_FILE}" \
-passin pass:${CA_PASSWORD} \
-in "${csr_file}" \
-out "${crt_file}" \
${crt_expiration_arg}
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the certificate"
fi
# check if CRT is a valid
"${OPENSSL_BIN}" x509 \
-noout \
-subject \
-in "${crt_file}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
rm -f "${crt_file}"
fi
if [ ! -f "${crt_file}" ]; then
error "Error in CSR creation"
fi
chmod 640 "${crt_file}"
echo "The CRT file is available in ${crt_file}"
# generate pkcs12 format
pass_args=""
if [ -n "${password_file:-}" ]; then
# Hack for pkcs12 :
# If passin and passout files are the same path, it expects 2 lines
# so we make a temporary copy of the password file
password_file_out=$(mktemp)
cp "${password_file}" "${password_file_out}"
pass_args="-passin file:${password_file} -passout file:${password_file_out}"
elif [ -n "${PASSWORD:-}" ]; then
pass_args="-passin pass:${PASSWORD} -passout pass:${PASSWORD}"
else
pass_args="-passout pass:"
fi
"${OPENSSL_BIN}" pkcs12 \
-export \
-nodes \
-inkey "${key_file}" \
-in "${crt_file}" \
-out "${pkcs12_file}" \
${pass_args}
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "Error generating the pkcs12 file"
fi
if [ -n "${password_file_out:-}" ]; then
# Hack for pkcs12 :
# Destroy the temporary file
rm -f "${password_file_out}"
fi
chmod 640 "${pkcs12_file}"
echo "The PKCS12 config file is available at \`${pkcs12_file}'"
# generate openvpn format
if [ -e "${CA_DIR}/ovpn.conf" ]; then
cat "${CA_DIR}/ovpn.conf" - > "${ovpn_file}" <<EOF
<ca>
$(cat "${CA_CERT}")
</ca>
<cert>
$(cat "${crt_file}")
</cert>
<key>
$(cat "${key_file}")
</key>
EOF
chmod 640 "${ovpn_file}"
echo "The OpenVPN config file is available at \`${ovpn_file}'"
fi
# Copy files if destination exists
if [ -d "${COPY_DIR}" ]; then
for file in "${crt_file}" "${key_file}" "${pkcs12_file}" "${ovpn_file}"; do
if [ -f "${file}" ]; then
new_file="${COPY_DIR}/$(basename "${file}")"
if [ "${replace_existing}" -eq 1 ]; then
cp -f "${file}" "${COPY_DIR}/"
else
if [ "${non_interactive}" -eq 1 ]; then
if [ -f "${new_file}" ]; then
echo "File \`${file}' has not been copied to \`${new_file}', it already exists" >&2
continue
else
cp "${file}" "${COPY_DIR}/"
fi
else
cp -i "${file}" "${COPY_DIR}/"
fi
fi
echo "File \`${file}' has been copied to \`${new_file}'"
fi
done
# shellcheck disable=SC2086
chown -R ${PKI_USER}:${PKI_USER} "${COPY_DIR}/"
chmod -R u=rwX,g=rwX,o= "${COPY_DIR}/"
fi
fi
}
revoke() {
non_interactive=0
# Parse options
# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case ${1:-} in
--non-interactive)
non_interactive=1
;;
--)
# End of all options.
shift
break
;;
-?*)
# ignore unknown options
warning "Warning: unknown option (ignored): \`$1'"
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
# The name of the certificate
cn="${1:-}"
if [ -z "${cn}" ]; then
show_usage_revoke >&2
exit 1
fi
crt_file="${CRT_DIR}/${cn}.crt"
# check if CRT exists
if [ ! -f "${crt_file}" ]; then
error "Unknow CN: ${cn} (\`${crt_file}' not found)"
fi
# check if CRT is a valid
"${OPENSSL_BIN}" x509 \
-noout \
-subject \
-in "${crt_file}" \
>/dev/null 2>&1
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
error "${crt_file} is not a valid CRT, you must delete it !"
fi
# ask for CA passphrase
ask_ca_password 0
echo "Revoke certificate ${crt_file} :"
"${OPENSSL_BIN}" ca \
-config "${CONF_FILE}" \
-passin pass:"${CA_PASSWORD}" \
-revoke "${crt_file}"
# shellcheck disable=SC2181
if [ "$?" -eq 0 ]; then
rm "${crt_file}"
fi
"${OPENSSL_BIN}" ca \
-config "${CONF_FILE}" \
-passin pass:"${CA_PASSWORD}" \
-gencrl \
-out "${CRL}"
}
list() {
if [ ! -f "${INDEX_FILE}" ]; then
exit 0
fi
if [ -z "${1:-}" ]; then
show_usage_list >&2
exit 1
fi
while :; do
case "${1:-}" in
-a|--all)
list_valid=0
list_revoked=0
;;
-v|--valid)
list_valid=0
list_revoked=1
;;
-r|--revoked)
list_valid=1
list_revoked=0
;;
-?*)
warning "unknow option ${1} (ignored)"
;;
*)
break
;;
esac
shift
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
}
cert_end_date() {
"${OPENSSL_BIN}" x509 -noout -enddate -in "${1}" | cut -d'=' -f2
}
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=$(cert_end_date "${cert}")
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
}
is_user() {
getent passwd "${1}" >/dev/null
}
is_group() {
getent group "${1}" >/dev/null
}
main() {
# Know what system we are on, because OpenBSD and Linux do not implement date(1) in the same way
SYSTEM=$(uname | tr '[:upper:]' '[:lower:]')
# 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"
COPY_DIR="$(dirname "${CONF_FILE}")/copy_output"
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=$(TZ=:Zulu /bin/date +"%Y%m%d%H%M%SZ")
if ! is_user "${PKI_USER}" || ! is_group "${PKI_USER}"; 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|--version)
show_version
exit 0
;;
help|--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 "$@"