Compare commits

...

10 Commits

2 changed files with 126 additions and 60 deletions

View File

@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Accept a `password-file` command line option to read password from a file
* Accept `--days` and `--end-date` command line options
* CA key length is configurable (minimum 4096)
* Add `--non-interactive` command line option
* Add `--replace-existing` command line option
* Copy files if destination exists
### Changed
@ -25,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Extract variables for files
* Use inline pass phrase arguments
* Remove "set -e" and add many return code checks
* Prevent use of uninitialized variables
### Deprecated

182
shellpki
View File

@ -3,6 +3,8 @@
# shellpki is a wrapper around OpenSSL to manage a small PKI
#
set -u
VERSION="1.0.0"
show_version() {
@ -53,7 +55,8 @@ init() {
if [ ! -f "${CA_KEY}" ]; then
"${OPENSSL_BIN}" genrsa \
-out "${CA_KEY}" \
-aes256 ${CA_KEY_LENGTH} \
-aes256 \
${CA_KEY_LENGTH} \
>/dev/null 2>&1
if [ "$?" -ne 0 ]; then
error "Error generating the CA key"
@ -225,14 +228,17 @@ ask_ca_password() {
if [ "${attempt}" -ge "${max_attempts}" ]; then
error "Maximum number of attempts reached (${max_attempts})."
fi
if [ -z "${CA_PASSWORD}" ]; then
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
if [ -z "${CA_PASSWORD:-}" ] || ! verify_ca_password; then
unset CA_PASSWORD
attempt=$(( attempt + 1 ))
ask_ca_password "${attempt}"
@ -241,20 +247,50 @@ ask_ca_password() {
ask_user_password() {
trap 'unset PASSWORD' 0
stty -echo
printf "Password for user key: "
read -r PASSWORD
stty echo
printf "\n"
if [ -z "${PASSWORD}" ]; then
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
resp="y"
else
error "${cn} already exists, use \`--replace-existing' to force"
fi
else
if [ "${replace_existing}" -eq 1 ]; then
resp="y"
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')
fi
fi
if [ "${resp}" = "y" ]; then
revoke "${cn}"
else
error "Aborted"
fi
}
create() {
from_csr=0
ask_pass=0
non_interactive=0
replace_existing=0
days=""
end_date=""
# Parse options
# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
@ -270,8 +306,7 @@ create() {
fi
shift
else
printf 'ERROR: "--csr-file" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--csr-file' requires a value"
fi
;;
--file=?*|--csr-file=?*)
@ -284,8 +319,7 @@ create() {
;;
--file=|--csr-file=)
# csr-file options, without value
printf 'ERROR: "--csr-file" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--csr-file' requires a value"
;;
-p|--password)
ask_pass=1
@ -299,8 +333,7 @@ create() {
fi
shift
else
printf 'ERROR: "--password-file" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--password-file' requires a value"
fi
;;
--password-file=?*)
@ -312,8 +345,7 @@ create() {
;;
--password-file=)
# password-file options, without value
printf 'ERROR: "--password-file" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--password-file' requires a value"
;;
--days)
# days option, with value separated by space
@ -321,8 +353,7 @@ create() {
days=${2}
shift
else
printf 'ERROR: "--days" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--days' requires a value"
fi
;;
--days=?*)
@ -331,8 +362,7 @@ create() {
;;
--days=)
# days options, without value
printf 'ERROR: "--days" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--days' requires a value"
;;
--end-date)
# end-date option, with value separated by space
@ -340,8 +370,7 @@ create() {
end_date=${2}
shift
else
printf 'ERROR: "--end-date" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--end-date' requires a value"
fi
;;
--end-date=?*)
@ -350,8 +379,13 @@ create() {
;;
--end-date=)
# end-date options, without value
printf 'ERROR: "--end-date" requires a non-empty option argument.\n' >&2
exit 1
error "Argument error: \`--end-date' requires a value"
;;
--non-interactive)
non_interactive=1
;;
--replace-existing)
replace_existing=1
;;
--)
# End of all options.
@ -360,7 +394,7 @@ create() {
;;
-?*)
# ignore unknown options
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
warning "Warning: unknown option (ignored): \`$1'"
;;
*)
# Default case: If no more options then break out of the loop.
@ -387,6 +421,11 @@ create() {
crt_expiration_arg="-enddate ${cert_end_date}"
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
@ -426,19 +465,18 @@ create() {
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
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} \
@ -449,7 +487,6 @@ create() {
else
echo "The certificate file is available at \`${crt_file}'"
fi
else
if [ -z "${cn}" ]; then
show_usage >&2
@ -463,14 +500,7 @@ create() {
# check if CN already exist
if [ -f "${crt_file}" ]; 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
replace_existing_or_abort "${cn}"
fi
# ask for CA passphrase
@ -481,34 +511,36 @@ create() {
fi
# generate private key
PASS_ARGS=""
pass_args=""
if [ -n "${password_file}" ]; then
PASS_ARGS="-aes256 -passout file:${password_file}"
pass_args="-aes256 -passout file:${password_file}"
elif [ -n "${PASSWORD}" ]; then
PASS_ARGS="-aes256 -passout pass:${PASSWORD}"
pass_args="-aes256 -passout pass:${PASSWORD}"
fi
"${OPENSSL_BIN}" genrsa \
-out "${key_file}" \
${PASS_ARGS} \
${pass_args} \
${KEY_LENGTH} \
>/dev/null 2>&1
if [ "$?" -ne 0 ]; then
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=""
pass_args=""
if [ -n "${password_file}" ]; then
PASS_ARGS="-passin file:${password_file}"
pass_args="-passin file:${password_file}"
elif [ -n "${PASSWORD}" ]; then
PASS_ARGS="-passin pass:${PASSWORD}"
pass_args="-passin pass:${PASSWORD}"
fi
"${OPENSSL_BIN}" req \
-batch \
-new \
-key "${key_file}" \
-out "${csr_file}" \
${PASS_ARGS} \
${pass_args} \
-config /dev/stdin <<EOF
$(cat "${CONF_FILE}")
commonName_default = ${cn}
@ -519,6 +551,7 @@ EOF
# ca sign and generate cert
"${OPENSSL_BIN}" ca \
${batch_arg} \
-config "${CONF_FILE}" \
-passin pass:${CA_PASSWORD} \
-in "${csr_file}" \
@ -546,18 +579,18 @@ EOF
echo "The CRT file is available in ${crt_file}"
# generate pkcs12 format
PASS_ARGS=""
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}"
pass_args="-passin file:${password_file} -passout file:${password_file_out}"
elif [ -n "${PASSWORD}" ]; then
PASS_ARGS="-passin pass:${PASSWORD} -passout pass:${PASSWORD}"
pass_args="-passin pass:${PASSWORD} -passout pass:${PASSWORD}"
else
PASS_ARGS="-passout pass:"
pass_args="-passout pass:"
fi
"${OPENSSL_BIN}" pkcs12 \
-export \
@ -565,7 +598,7 @@ EOF
-inkey "${key_file}" \
-in "${crt_file}" \
-out "${pkcs12_file}" \
${PASS_ARGS}
${pass_args}
if [ "$?" -ne 0 ]; then
error "Error generating the pkcs12 file"
fi
@ -597,6 +630,33 @@ 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
chown -R ${PKI_USER}:${PKI_USER} "${COPY_DIR}/"
chmod -R u=rwX,g=rwX,o= "${COPY_DIR}/"
fi
fi
}
@ -757,6 +817,8 @@ main() {
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."