Commit d6d96100 authored by Janek Bevendorff's avatar Janek Bevendorff
Browse files

Port cert script to new command framework and make email and SAN optional

parent 6753cd44
......@@ -200,11 +200,11 @@ To configure which options a command takes, add their definition to the doc stri
### : -f : --flag ::: This is a boolean flag
cmd_subcmd() {
echo "Value of -o / --opt: ${ARG_OPT}"
if ! [ -z "$ARG_OPT2" ]; then
if [ -n "$ARG_OPT2" ]; then
echo "Value of --opt2: ${ARG_OPT2}"
fi
echo "Value of --long-opt: ${ARG_LONG_OPT}"
if ! [ -z "$ARG_FLAG" ]; then
if [ -n "$ARG_FLAG" ]; then
echo "--flag was set"
fi
}
......@@ -289,7 +289,7 @@ After adding a new command and its documentation, update the `cheatsheet.md` fil
### Project guidelines and coding conventions
- Write concised and reusable commands.
- Write concise and reusable commands.
- Start with single-file commands and only upgrade to command directories if a command becomes too complex.
- Don't write Python scripts that are primarily calling external commands.
- Don't write Bash scripts that are primarily doing data processing or manipulation.
......
#!/usr/bin/env bash
### Manage VPN client, server, and ICA certificates.
. "$WEBIS_LIB_PATH"/bashhelper.sh
. "$WEBIS_LIB_PATH"/shflags
check_tools cryfs keyctl openssl
usage() {
echo "
usage:
$(basename "$0") issue|revoke [OPTIONS]
description:
Manage VPN client, server, and ICA certificates.
"
exit 1
}
CMD="$1"
shift
# Define command line arguments and parse them.
if [ "$CMD" == "issue" ]; then
DEFINE_string "cryfs_dir" "" "CryFS base dir containing the encrypted certificates." "d"
DEFINE_integer "keyring_id" -1 "Retrieve given CryFS decryption key from kernel keyring" "k"
DEFINE_string "root_cn" "" "CN of the root certificate for signing (optional)" "r"
DEFINE_string "type" "client" "Certificate type (client | server | ca)" "t"
DEFINE_string "common_name" "" "Common Name (CN) for the certificate (e.g., username)" "c"
DEFINE_string "full_name" "" "User's full name" "f"
DEFINE_string "default_subj" "/C=DE/ST=Thuringia/L=Weimar/O=Webis/" "Default Subj fields (optional)" "s"
DEFINE_string "organizational_unit" "" "Organizational Unit (OU) for the certificate (optional)" "u"
DEFINE_string "email" "" "E-Mail address for the certificate" "e"
DEFINE_boolean "no_password" false "Do not encrypt private key" "n"
DEFINE_boolean "overwrite" false "Overwrite existing certificates instead of incrementing filename (use with caution!)" "x"
DEFINE_integer "validity" 0 "Certificate validity period in days" "v"
DEFINE_string "csr" "" "CSR file (optional)" "i"
DEFINE_string "out" "/dev/stdout" "OpenVPN .ovpn config or PEM file to generate (optional)" "o"
elif [ "$CMD" == "revoke" ]; then
DEFINE_string "cryfs_dir" "" "CryFS base dir containing the encrypted certificates." "d"
DEFINE_string "root_cn" "" "CN of the root certificate for signing (optional)" "r"
DEFINE_string "type" "" "Certificate type (client | server | ca)" "t"
DEFINE_string "cert_cn" "" "CN of the certificate to revoke" "c"
fi
FLAGS_HELP=$(usage)
export FLAGS_HELP
FLAGS "$@" || exit 1 # Parse command line arguments.
eval set -- "${FLAGS_ARGV}"
. "${WEBIS_LIB_PATH}/bashhelper.sh"
export MOUNT_DIR
trap "cleanup ${MOUNT_DIR}; exit $?" INT TERM EXIT
trap 'cleanup ${MOUNT_DIR}; exit $?' INT TERM EXIT
set -e
CONF="$(dirname $0)/openssl.conf"
# ===========================================================================
# helper commands
# ===========================================================================
# Usage: cleanup MOUNT_DIR
cleanup() {
if [ ! -d "$1" ]; then
......@@ -93,7 +44,7 @@ commit_changes() {
fi
}
# Usage: mount_cryfs BASE_DIR MOUNT_DIR
# Usage: mount_cryfs BASE_DIR MOUNT_DIR [ KEYRING_ID ]
mount_cryfs() {
if [ ! -d "$1" ]; then
logError "CryFS directory '$1' does not exist!"
......@@ -102,9 +53,9 @@ mount_cryfs() {
logInfo "Mounting CryFS..."
mkdir -p "$2"
echo keyctl pipe $FLAGS_keyring_id >&2
if [ $FLAGS_keyring_id -ne -1 ]; then
< <(keyctl pipe $FLAGS_keyring_id 2>&1) cryfs "$1" "$2" >&2
if [ -n "$3" ] && [ "$3" -ne -1 ]; then
echo "keyctl pipe $3" >&2
< <(keyctl pipe "$3" 2>&1) cryfs "$1" "$2" >&2
else
cryfs "$1" "$2" >&2
fi
......@@ -120,16 +71,16 @@ choose_root_cert() {
echo -e "\n\nPlease choose a root certificate:" >&2
root_certs=()
i=0
for c in $(find "${1}/ca/" -name "*.pem"); do
if grep -q "PRIVATE KEY" "$c"; then
root_certs[$i]="$c"
printf " [%2d] $(basename $c | sed 's/\.pem$//')\n" $i >&2
i=$(($i +1))
while IFS= read -r -d '' pk; do
if grep -q "PRIVATE KEY" "$pk"; then
root_certs[$i]="$pk"
printf " [%2d] $(basename "$pk" | sed 's/\.pem$//')\n" $i >&2
i=$(($i + 1))
fi
done
done < <(find "${1}/ca/" -name "*.pem" -print0)
echo -en "\nYour choice: " >&2
read root_cert_choice
read -r root_cert_choice
local choice_counter=0
while [ $choice_counter -lt 10 ] && [ "${root_certs[$root_cert_choice]}" == "" ]; do
logError "Invalid choice, please enter a valid number."
......@@ -151,12 +102,28 @@ generate_key() {
openssl genrsa -out "$1" "$2"
}
# Usage: generate_csr CONF CERT_TYPE KEY_FILE OUT_FILE DEFAULT_SUBJ ORG_UNIT CLIENT_CN CLIENT_FULL_NAME CLIENT_EMAIL
# Usage: generate_csr CONF CERT_TYPE KEY_FILE OUT_FILE DEFAULT_SUBJ ORG_UNIT CN EMAIL SAN
generate_csr() {
logInfo "Generating CSR..."
openssl req -new -key "$3" -out "$4" -reqexts "v3_req_${2}" \
-subj "${5}OU=${6}/CN=${7}/emailAddress=${9}" -reqexts "SAN" \
-config <(cat "$1" <(printf "\n[SAN]\nsubjectAltName = 'DNS:${8}'"))
local subj="${5}"
if [ -n "$6" ]; then
subj="${subj}/OU=${6}"
fi
if [ -n "$7" ]; then
subj="${subj}/CN=${7}"
fi
if [ -n "$8" ]; then
subj="${subj}/emailAddress=${8}"
fi
local config
config="$(cat "$1")"
if [ -n "$9" ]; then
local san_reqext="-reqexts SAN"
config="$(echo "$config"; echo -e "\n[SAN]"; echo "subjectAltName = '${9}'")"
fi
openssl req -new -key "$3" -out "$4" -reqexts "v3_req_${2}" $san_reqext \
-subj "$subj" -config <(echo "$config")
}
# Usage: revoke_cert CONF CERT CA_KEY CA_CERT
......@@ -172,12 +139,12 @@ generate_crl() {
logInfo "CRL '$2' updated."
}
# Usage: sign_csr CONF CSR_IN CERT_OUT CA_KEY CA_CERT CERT_TYPE
# Usage: sign_csr CONF CSR_IN CERT_OUT CA_KEY CA_CERT CERT_TYPE VALIDITY
sign_csr() {
local validity=730
# certificate validity in days
if [ "$FLAGS_validity" -gt 0 ]; then
validity="$FLAGS_validity"
if [ "$7" -gt 0 ]; then
validity="$7"
elif [ "$6" == "server" ]; then
validity=1825
elif [ "$6" == "ca" ]; then
......@@ -222,9 +189,9 @@ resolve_ca_chain() {
# Usage: TYPE OUT_FILE CERT CA_CERT [KEY] [NO_PASSWORD]
generate_output_file() {
if [ "$5" != "" ] && [ "$6" -eq 0 ]; then
if [ -n "$5" ] && [ "$6" -eq 0 ]; then
local pem="$(cat "$5")"
elif [ "$5" != "" ]; then
elif [ -n "$5" ]; then
local extra_param=""
if ! tty -s; then
# non-interactive shell, get password from env
......@@ -291,39 +258,43 @@ EOL
return 0
}
# ===========================================================================
# issue command
# ===========================================================================
issue() {
if [ "$FLAGS_cryfs_dir" == "" ]; then
logError "--cryfs_dir is required"
### Issue a new certificate.
###
### : cryfs-dir : dpath! :
### : -r : --root-cn : str :: CN of the root certificate for signing
### : -t : --type : str : client : Certificate type (client | server | ca)
### : -c : --common-name : str :: Common Name (CN) for the certificate (e.g., username)
### : -f : --full-name : str :: User's full name
### : -s : --default-subj : str : /C=DE/ST=Thuringia/L=Weimar/O=Webis : Default Subj fields
### : -u : --organizational-unit : str :: Organizational Unit (OU) for the certificate
### : -e : --email : str :: E-Mail address for the certificate
### : -n : --no-password ::: Do not encrypt private key
### : -x : --overwrite ::: Overwrite existing certificates instead of incrementing filename (use with caution!)
### : -v : --validity : int : 0 : Certificate validity period in days
### : -i : --csr : fpath! :: CSR file
### : -o : --out : fpath : /dev/stdout : OpenVPN .ovpn config or PEM file to generate
### : -k : --keyring-id : int : -1 : Retrieve given CryFS decryption key from kernel keyring
cmd_issue() {
if [ "$ARG_CRYFS_DIR" == "" ]; then
logError "cryfs-dir is required"
exit 1;
fi
if [ "$FLAGS_type" != "client" ] && [ "$FLAGS_type" != "server" ] && [ "$FLAGS_type" != "ca" ]; then
logError "Invalid certificate type: ${FLAGS_type}"
if [ "$ARG_TYPE" != "client" ] && [ "$ARG_TYPE" != "server" ] && [ "$ARG_TYPE" != "ca" ]; then
logError "Invalid certificate type: ${ARG_TYPE}"
exit 1
fi
if [ "$FLAGS_common_name" == "" ]; then
logError "--common_name is required"
exit 1;
fi
if [ "$FLAGS_full_name" == "" ]; then
logError "--full_name is required"
exit 1;
fi
if [ "$FLAGS_email" == "" ]; then
logError "--email is required"
if [ "$ARG_COMMON_NAME" == "" ]; then
logError "--common-name is required"
exit 1;
fi
pull_repository "${FLAGS_cryfs_dir}"
pull_repository "$ARG_CRYFS_DIR"
MOUNT_DIR="${FLAGS_cryfs_dir}_mount.${RANDOM}"
mount_cryfs "$FLAGS_cryfs_dir" "$MOUNT_DIR"
MOUNT_DIR="${ARG_CRYFS_DIR}_mount.${RANDOM}"
mount_cryfs "$ARG_CRYFS_DIR" "$MOUNT_DIR" "$ARG_KEYRING_ID"
ROOT_KEY="${MOUNT_DIR}/ca/${FLAGS_root_cn}.pem"
if [ "$FLAGS_root_cn" == "" ] || [ ! -f "$ROOT_KEY" ]; then
ROOT_KEY="${MOUNT_DIR}/ca/${ARG_ROOT_CN}.pem"
if [ "$ARG_ROOT_CN" == "" ] || [ ! -f "$ROOT_KEY" ]; then
ROOT_KEY="$(choose_root_cert "$MOUNT_DIR")"
logInfo "Using '$(basename "${ROOT_KEY}" | sed 's/\.pem$//')' as the root certificate..."
fi
......@@ -334,68 +305,71 @@ issue() {
BITS=2048
# use provided CSR or generate new one
CSR="$FLAGS_csr"
CSR="$ARG_CSR"
DELETE_KEY=false
DELETE_CSR=false
local cert_filename="$FLAGS_common_name"
CERT="${MOUNT_DIR}/${FLAGS_type}/${cert_filename}.crt"
local cert_filename="$ARG_COMMON_NAME"
CERT="${MOUNT_DIR}/${ARG_TYPE}/${cert_filename}.crt"
local counter_suffix=0
while [ $FLAGS_overwrite -eq 1 ] && [ -f "$CERT" ]; do
while [ -n "$ARG_OVERWRITE" ] && [ -f "$CERT" ]; do
counter_suffix=$(($counter_suffix + 1))
CERT="${MOUNT_DIR}/${FLAGS_type}/${cert_filename}_${counter_suffix}.crt"
CERT="${MOUNT_DIR}/${ARG_TYPE}/${cert_filename}_${counter_suffix}.crt"
done
if [ $counter_suffix -gt 0 ]; then
logWarn "Certificate file '${FLAGS_type}/${cert_filename}.crt' already exists, saving as '${FLAGS_type}/${cert_filename}_${counter_suffix}.crt'."
logWarn "Certificate file '${ARG_TYPE}/${cert_filename}.crt' already exists, saving as '${ARG_TYPE}/${cert_filename}_${counter_suffix}.crt'."
cert_filename="${cert_filename}_${counter_suffix}"
fi
if [ "$CSR" == "" ] || [ ! -f "$CSR" ]; then
KEY="${MOUNT_DIR}/${FLAGS_type}/${cert_filename}.pem"
if [ $FLAGS_overwrite -eq 1 ] && [ -f "$KEY" ]; then
KEY="${MOUNT_DIR}/${ARG_TYPE}/${cert_filename}.pem"
if [ -n "$ARG_OVERWRITE" ] && [ -f "$KEY" ]; then
logError "Key file ${KEY} already exists!"
cleanup "$MOUNT_DIR"
exit 1
fi
generate_key "$KEY" "$BITS"
CSR="${MOUNT_DIR}/${FLAGS_type}/${cert_filename}.csr"
if [ $FLAGS_overwrite -eq 1 ] && [ -f "$CSR" ]; then
CSR="${MOUNT_DIR}/${ARG_TYPE}/${cert_filename}.csr"
if [ -n "$ARG_OVERWRITE" ] && [ -f "$CSR" ]; then
logError "CSR file ${CSR} already exists!"
rm -f "$KEY"
cleanup "$MOUNT_DIR"
exit 1
fi
if ! generate_csr "$CONF" "$FLAGS_type" "$KEY" "$CSR" "$FLAGS_default_subj" "$FLAGS_organizational_unit" "$FLAGS_common_name" "$FLAGS_full_name" "$FLAGS_email"; then
if [ -n "$ARG_FULL_NAME" ]; then
ARG_FULL_NAME="DNS:${ARG_FULL_NAME}"
fi
if ! generate_csr "$CONF" "$ARG_TYPE" "$KEY" "$CSR" "$ARG_DEFAULT_SUBJECT" "$ARG_ORGANIZATIONAL_UNIT" \
"$ARG_COMMON_NAME" "$ARG_EMAIL" "$ARG_FULL_NAME"; then
logError "CSR generation failed!"
rm -f "$KEY" "$CSR"
cleanup "$MOUNT_DIR"
exit 1
fi
if [ "$FLAGS_type" == "client" ]; then
if [ "$ARG_TYPE" == "client" ]; then
DELETE_KEY=true
fi
DELETE_CSR=true
fi
logInfo "Signing client certificate..."
if ! sign_csr "$CONF" "$CSR" "$CERT" "$ROOT_KEY" "$ROOT_CERT" "$FLAGS_type"; then
if ! sign_csr "$CONF" "$CSR" "$CERT" "$ROOT_KEY" "$ROOT_CERT" "$ARG_TYPE" "$ARG_VALIDITY"; then
logError "Signing failed!"
rm -f "$KEY" "$CSR" "$CERT"
cleanup "$MOUNT_DIR"
exit 1
fi
if [ "$FLAGS_type" == "client" ]; then
if [ "$ARG_TYPE" == "client" ]; then
logInfo "Writing .ovpn file..."
local type="ovpn"
else
logInfo "Writing PEM file..."
local type="pem"
fi
ROOT_CA_CERT="$(dirname $ROOT_CERT)/Webis_Root_CA.crt"
if ! generate_output_file "$type" "$FLAGS_out" "$CERT" "$ROOT_CERT" "$KEY" $FLAGS_no_password; then
if ! generate_output_file "$type" "$ARG_OUT" "$CERT" "$ROOT_CERT" "$KEY" "$ARG_NO_PASSWORD"; then
rm -f "$KEY" "$CSR" "$CERT"
cleanup "$MOUNT_DIR"
exit 1
......@@ -415,35 +389,38 @@ issue() {
generate_crl "$CONF" "$CRL" "$ROOT_KEY" "$ROOT_CERT"
cleanup "$MOUNT_DIR"
commit_changes "${FLAGS_cryfs_dir}"
commit_changes "$ARG_CRYFS_DIR"
}
# ===========================================================================
# revoke command
# ===========================================================================
revoke() {
if [ "$FLAGS_cryfs_dir" == "" ]; then
logError "--cryfs_dir is required"
### Revoke a certificate.
###
### : cryfs-dir : dpath! :
### : -r : --root-cn : str :: CN of the root certificate for signing
### : -t : --type : str : client : Certificate type (client | server | ca)
### : -c : --cert-cn : str :: CN of the certificate to revoke
### : -k : --keyring-id : int : -1 : Retrieve given CryFS decryption key from kernel keyring
cmd_revoke() {
if [ "$ARG_CRYFS_DIR" == "" ]; then
logError "cryfs-dir is required"
exit 1;
fi
if [ "$FLAGS_cert_cn" == "" ]; then
logError "--cert_cn is required"
if [ "$ARG_CERT_CN" == "" ]; then
logError "--cert-cn is required"
exit 1;
fi
if [ "$FLAGS_type" == "" ]; then
if [ "$ARG_TYPE" == "" ]; then
logError "--type is required"
exit 1;
fi
pull_repository "${FLAGS_cryfs_dir}"
pull_repository "$ARG_CRYFS_DIR"
MOUNT_DIR="${FLAGS_cryfs_dir}_mount.${RANDOM}"
mount_cryfs "$FLAGS_cryfs_dir" "$MOUNT_DIR"
MOUNT_DIR="${ARG_CRYFS_DIR}_mount.${RANDOM}"
mount_cryfs "$ARG_CRYFS_DIR" "$MOUNT_DIR" "$ARG_KEYRING_ID"
ROOT_KEY="${MOUNT_DIR}/ca/${FLAGS_root_cn}.pem"
if [ "$FLAGS_root_cn" == "" ] || [ ! -f "$ROOT_KEY" ]; then
ROOT_KEY="${MOUNT_DIR}/ca/${ARG_ROOT_CN}.pem"
if [ "$ARG_ROOT_CN" == "" ] || [ ! -f "$ROOT_KEY" ]; then
ROOT_KEY="$(choose_root_cert "$MOUNT_DIR")"
logInfo "Using '$(basename "${ROOT_KEY}" | sed 's/\.pem$//')' as the root certificate..."
fi
......@@ -451,15 +428,15 @@ revoke() {
export CA_BASE_DIR="$(echo "$ROOT_CERT" | sed 's/\.crt$//')_Data"
ensure_ca_base_dir "$CA_BASE_DIR"
CERT="${MOUNT_DIR}/${FLAGS_type}/${FLAGS_cert_cn}.crt"
CERT="${MOUNT_DIR}/${ARG_TYPE}/${ARG_CERT_CN}.crt"
if [ ! -f "$CERT" ]; then
logError "Certificate '${CERT}' does not exist."
cleanup "$MOUNT_DIR"
exit 1
fi
echo -n "Are you sure you want to revoke the ${FLAGS_type} certificate '${FLAGS_cert_cn}'? [y/N] "
read answer
echo -n "Are you sure you want to revoke the ${ARG_TYPE} certificate '${ARG_CERT_CN}'? [y/N] "
read -r answer
if [ "$answer" != "y" ] && [ "$answer" != "Y" ]; then
logError "Revocation aborted."
cleanup "$MOUNT_DIR"
......@@ -475,17 +452,7 @@ revoke() {
generate_crl "$CONF" "$CRL" "$ROOT_KEY" "$ROOT_CERT"
cleanup "$MOUNT_DIR"
commit_changes "${FLAGS_cryfs_dir}"
commit_changes "${ARG_CRYFS_DIR}"
}
# ===========================================================================
# bootstrap
# ===========================================================================
if [ "$CMD" == "issue" ] || [ "$CMD" == "revoke" ]; then
$CMD "$@"
else
logError "Invalid command '$CMD'."
usage
exit 1
fi
exec_sub_cmd "$@"
......@@ -79,7 +79,7 @@ copy_extensions = copy
[ policy_match ]
commonName = supplied
emailAddress = supplied
emailAddress = optional
countryName = optional
stateOrProvinceName = optional
localityName = optional
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment