Compare commits
10 commits
0c4d36cb57
...
a6c153b546
Author | SHA1 | Date | |
---|---|---|---|
Jérémy Lecour | a6c153b546 | ||
Jérémy Lecour | 99e5b8a386 | ||
Jérémy Lecour | fdb9f46e35 | ||
Jérémy Lecour | ab4e3e5de1 | ||
Jérémy Lecour | 123d5f5c05 | ||
Jérémy Lecour | 6bb05a6366 | ||
Jérémy Lecour | 1c4b68f571 | ||
Jérémy Lecour | 3e2bbe8de5 | ||
Jérémy Lecour | e04f686651 | ||
Jérémy Lecour | f94f7d8cd3 |
|
@ -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
182
shellpki
|
@ -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."
|
||||
|
|
Loading…
Reference in a new issue