diff --git a/backend/scripts/generate-p12.sh b/backend/scripts/generate-p12.sh new file mode 100644 index 000000000..f899c67d3 --- /dev/null +++ b/backend/scripts/generate-p12.sh @@ -0,0 +1,549 @@ +#!/bin/bash +# +# + +SCRIPT_NAME="$(basename "$0")" + +print_help() { +echo -e "\nUsage: $SCRIPT_NAME --npm [--password ] + +Options: + --npm Required. Name or identifier used for the certificate. Must be in the form 'npm-#' + --password Optional. Password to secure the PKCS#12 (.p12) file. If not provided the scipt will use the script default. + -h, --help Show this help message and exit. + +Example: + ./$SCRIPT_NAME --npm npm-123 + ./$SCRIPT_NAME --npm npm-123 --password secret123 +" +} + +# Initialize Required Input variables +NPM_NAME="" +PKCS12_PASSWORD="" + +# Exit early and show help if no arguments were provided +if [[ $# -eq 0 ]]; then + echo -e "\n\nERROR - No arguments provided." + print_help + exit 1 +fi + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --npm) + NPM_NAME="$2" + NPM_NAME="${NPM_NAME,,}" + REGEX_FOR_NPM_MATCH='^(N|n)(P|p)(M|m)\-[0-9]+$' + VALIDATE_NPM_NAME=$(echo "$NPM_NAME" | grep -P "$REGEX_FOR_NPM_MATCH") + + #Validate the NPM_NAME + if [[ -z "$2" || "$2" == --* ]] + then + echo -e "\nERROR: --npm requires a value." + exit 1 + fi + if [[ -z "$NPM_NAME" ]] + then + echo -e "\nERROR: --npm requires a value." + exit 1 + elif [[ -z "$VALIDATE_NPM_NAME" ]] + then + echo -e "\nERROR: --npm input must be in the form npm-#\n" + exit 1 + fi + + shift 2 + ;; + --password) + #Verify the argument data was not passed in as password instead if the user did not supply a password. + if [[ -z "$2" || "$2" == --* ]] + then + echo -e "\nERROR: --password requires a value." + exit 1 + fi + PKCS12_PASSWORD="$2" + shift 2 + ;; + -h|--help) + print_help + exit 0 + ;; + *) + echo -e "\nError: Unsupported argument: $1\n" + print_help + exit 1 + ;; + esac +done + + +#Use the default password if the user did not supply one. +#If the Password Variable is passed in this script will use it for encrypting the p12 private key when generating the p12. +#If the password was not provided then the default password (below) will be used. +if [[ -z "$PKCS12_PASSWORD" ]] +then + #The Password Provided by the user (Default: password) + PKCS12_PASSWORD="password" +fi + + +# Selected Cipher would be passed in from the GUI but this is an example of what the GUI could provide for a selected CIPHER +SELECTED_CIPHER="aes-256-cbc" +SELECTED_CIPHER="${SELECTED_CIPHER^^}" + +# Selected Digest would be passed in from the GUI but this is an example of what the GUI could provide for a selected DIGEST +SELECTED_DIGEST="sha256" +SELECTED_DIGEST="${SELECTED_DIGEST^^}" + +#Docker Path to live p12 files. Live is a SimLink to Archived +PATH_TO_LIVE="/etc/letsencrypt/live" + +#Docker Path to archive p12 files. +PATH_TO_ARCHIVE="/etc/letsencrypt/archive" + + +#The Specific Folder Name for the NPM cert, Should be the text "npm" followe by a hyphen and numbers (I.E. npm-3) +NPM_FOLDER="$NPM_NAME" + +#The Directory containing the certificate and privcate key for a npm configuration +NPM_PATH="$PATH_TO_LIVE/$NPM_FOLDER" + +#Private Key File Name (with extension) +PRIVATE_KEY="privkey.pem" +PRIVATE_KEY_FULL_PATH=$(realpath "$NPM_PATH/$PRIVATE_KEY") +echo "PRIVATE KEY FULL PATH: $PRIVATE_KEY_FULL_PATH" + +PRIVATE_KEY_FILENAME=$(basename "$PRIVATE_KEY_FULL_PATH") +INCREMENT_NUMBER="" +#If the private key has an incremented number after 'privkey' then save the number in a variable to use to increment the pkcs12 file that will be generated. +if [[ ! -z "$PRIVATE_KEY_FILENAME" ]] +then + INCREMENTED_FILE_NAME_CHECK='^.*[0-9]*\.pem' + INCREMENTED_NUMBER_CHECK='[0-9]*' + echo "PRIVATE KEY FILENAME: $PRIVATE_KEY_FILENAME" + #If the private key ends in numbers followed by the extension (I.E. privkey1.pem) then obtain the number portion, it will be used to increment the pkcs12 file as well. + #If the file does not end with an incremented number followed by the extension then the INCREMENT_NUMBER will be empty. + INCREMENT_NUMBER=$(echo "$PRIVATE_KEY_FILENAME" | grep -oP "$INCREMENTED_FILE_NAME_CHECK" | grep -oP "$INCREMENTED_NUMBER_CHECK") + echo "INCREMENT NUMBER: $INCREMENT_NUMBER" +fi + +#Certificate File Name (with extension) +CERTIFICATE_FILE="cert.pem" +#Find the real path +CERTIFICATE_FILE_FULL_PATH=$(realpath "$NPM_PATH/$CERTIFICATE_FILE") +echo "CERTIFICATE FILE FULL PATH: $CERTIFICATE_FILE_FULL_PATH" + +#Chain of Certificates (with extension). The certificate file containing your issued cert and any intermediate CAs +CERTIFICATE_CHAIN_FILE="fullchain.pem" +#Find the real path +CERTIFICATE_CHAIN_FILE_FULL_PATH=$(realpath "$NPM_PATH/$CERTIFICATE_CHAIN_FILE") +echo "CERTIFICATE CHAIN FILE FULL PATH: $CERTIFICATE_CHAIN_FILE_FULL_PATH" + +# if INCREMENT_NUMBER is not empty then use the filename for pkcs12 without incrementing to match the number for the private key. +if [[ ! -z "$INCREMENT_NUMBER" ]] +then + #The Name to give the PKCS12 file generated by the script (with extension). It will be placd in the same file as the private key file. + PKCS12_FILE_NAME="fullpkcs12-$INCREMENT_NUMBER.p12" + echo "PKCS12 FILE NAME: $PKCS12_FILE_NAME" + ROOT_CERTIFICATE_FILENAME="root$INCREMENT_NUMBER.pem" + PKCS12_SIMLINK_NAME="fullpkcs12.p12" + ROOT_CERTIFICATE_SIMLINK_NAME="root.pem" +else + #The Name to give the PKCS12 file generated by the script (with extension). It will be placd in the same file as the private key file. + PKCS12_FILE_NAME="fullpkcs12.p12" + echo "PKCS12 FILE NAME: $PKCS12_FILE_NAME" + ROOT_CERTIFICATE_FILENAME="root$INCREMENT_NUMBER.pem" + PKCS12_SIMLINK_NAME="fullpkcs12.p12" + ROOT_CERTIFICATE_SIMLINK_NAME="root.pem" +fi + + +#The directory that the pkcs12 file will be saved in is the same as the private key file +GET_PATH=$(realpath "$NPM_PATH/$PRIVATE_KEY") +PKCS12_FULL_PATH=$(dirname "$GET_PATH")"/$PKCS12_FILE_NAME" +echo "PKCS12 FULL PATH: $PKCS12_FULL_PATH" +PKCS12_SIMLINK_PATH="$NPM_PATH/$PKCS12_SIMLINK_NAME" +echo "PKCS12 SIMLINK PATH: $PKCS12_SIMLINK_PATH" + + +#Root Certificate File Name (with extension) the root file does not exist so will need to create it later from the fullchain +#ROOT_CERTIFICATE_FILENAME="root$INCREMENT_NUMBER.pem" +ROOT_FULL_PATH=$(dirname "$GET_PATH")"/$ROOT_CERTIFICATE_FILENAME" +echo "ROOT FULL PATH: $ROOT_FULL_PATH" +ROOT_CERTIFICATE_SIMLINK_PATH="$NPM_PATH/$ROOT_CERTIFICATE_SIMLINK_NAME" +echo "ROOT CERTIFICATE SIMLINK PATH: $ROOT_CERTIFICATE_SIMLINK_PATH" + +#Create an Array of CIPHER ALGORITHMS +#Remove the lines "Legacy:" and "Provided:" from the created List of Cipher Algorithms +LIST_OF_CIPHER_ALGORITHMS=$(openssl list -cipher-algorithms) +ARRAY_OF_CIPHER_ALGORITHMS=($(echo "${LIST_OF_CIPHER_ALGORITHMS}" | sed -E '/^Legacy:|^Provided:/d')) + +#Create an Array of DIGEST ALGORITHMS +LIST_OF_DIGEST_ALGORITHMS=$(openssl list -digest-algorithms) +ARRAY_OF_DIGEST_ALGORITHMS=($(echo "${LIST_OF_DIGEST_ALGORITHMS}" | sed -E '/^Legacy:|^Provided:/d')) + +#ARRAY of Temp files that are generated by the script. At the end they will be removed +declare -a TEMP_FILES_ARRAY=() + +#owner, used when running chown +OWNER="npm" +#Group, used when running chown +GROUP="npm" + +#Keep the root Certificate generated by this script, or delete it after using it temporarily +KEEP_ROOT_CERTIFICATE=true + +#Start This script only if The Certificate Chain File exist, and the private key exist, and both are also readable. +if [[ -z "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] && [[ -f "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] && [[ -r "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] +then + echo -e "\nERROR - Certificate Chain is empty or does not exist." + exit 1 +fi +if [[ -z "$PRIVATE_KEY_FULL_PATH" ]] && [[ -f "$PRIVATE_KEY_FULL_PATH" ]] && [[ -r "$PRIVATE_KEY_FULL_PATH" ]] +then + echo -e "\nERROR - Private Key is empty or does not exist." + exit 1 +fi + +#Search through the certificate chain and find the root certificate, if it does not exist in the chain file thewn download it from the intermediate certificate +if [[ -f "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] && [[ -r "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] +then + # Split the chain into separate certificates and process each one to try and find the root certificate + awk 'BEGIN{c=0} + /-----BEGIN CERTIFICATE-----/ {in_cert=1; c++; fname=sprintf("cert_temp_%02d.pem", c)} + in_cert { print > fname } + /-----END CERTIFICATE-----/ {in_cert=0}' "$CERTIFICATE_CHAIN_FILE_FULL_PATH" + + TEMP_FILES_ARRAY+=("cert_temp_01.pem") + TEMP_FILES_ARRAY+=("cert_temp_02.pem") + + FOUND_ROOT_CERTIFICATE_FILENAME=false + # Loop through each extracted certificate + for cert_temp_file in cert_temp_*.pem + do + echo -e "\nProcessing $cert_temp_file ..." + + # Get subject and issuer + issuer_subject=$(openssl x509 -noout -subject -issuer -in "$cert_temp_file" 2>/dev/null) + subject=$(echo "$issuer_subject" | grep 'subject=' | cut -d= -f2-) + issuer=$(echo "$issuer_subject" | grep 'issuer=' | cut -d= -f2-) + + echo " Subject: $subject" + echo " Issuer : $issuer" + + # Check if the certificate is self-signed + if [[ "$subject" == "$issuer" ]] + then + echo -e "\n Found root certificate: $cert_temp_file" + FOUND_ROOT_CERTIFICATE_FILENAME=true + #Save the certificate file in the same directory as the private key file + cp "$cert_temp_file" "$CERTIFICATE_FILE_FULL_PATH" + break + fi + done + + #If the root certificate was not found in the certificate chain + if [[ "$FOUND_ROOT_CERTIFICATE_FILENAME" == false ]] + then + #If the Root Certificate was not found within the certificate chain then obtain it from the intermediate chain + # Extract each certificate into its own file + awk 'BEGIN{c=0} + /-----BEGIN CERTIFICATE-----/ {in_cert=1; c++; fname=sprintf("cert_temp_%02d.pem", c)} + in_cert { print > fname } + /-----END CERTIFICATE-----/ {in_cert=0}' "$CERTIFICATE_CHAIN_FILE_FULL_PATH" + + # Identify the last certificate in the chain (presumably the intermediate) + last_cert=$(ls cert_temp_*.pem | sort | tail -n 1) + + echo " Attempting to download the root certificate from the Intermediate Certificate..." + #Assuming the last certificate in the chain is the Intermediate Certificate + echo -e " Using last certificate in chain: $last_cert" + + # Try to extract the AIA (issuer URL) from the intermediate cert + issuer_url=$(openssl x509 -in "$last_cert" -noout -text 2>/dev/null | \ + grep -A1 "Authority Information Access" | \ + grep "CA Issuers - URI:" | \ + sed 's/.*URI://') + + if [[ -z "$issuer_url" ]] + then + echo " No issuer URL found in the AIA field. Cannot fetch root certificate." + else + echo " Found issuer URL: $issuer_url" + + # Download as binary + curl -s -o issuer_cert_temp.der "$issuer_url" + + # Try converting to PEM + if openssl x509 -inform DER -in issuer_cert_temp.der -out "$ROOT_FULL_PATH" 2>/dev/null + then + echo " Successfully saved root certificate as PEM to: $ROOT_FULL_PATH" + TEMP_FILES_ARRAY+=("issuer_cert_temp.der") + else + echo " Conversion from DER to PEM failed. Trying to read as PEM directly..." + + if openssl x509 -in issuer_cert_temp.der -out "$ROOT_FULL_PATH" 2>/dev/null + then + #Save the certificate file in the same directory as the private key file + echo " Root certificate was already in PEM. Saved to: $ROOT_FULL_PATH" + else + echo " Failed to convert or parse the downloaded certificate." + #rm -f "$ROOT_FULL_PATH" + fi + + TEMP_FILES_ARRAY+=("issuer_cert_temp.der") + fi + + echo -e "\nProcessing $ROOT_CERTIFICATE_FILENAME ..." + + # Get subject and issuer + issuer_subject=$(openssl x509 -noout -subject -issuer -in "$ROOT_FULL_PATH" 2>/dev/null) + subject=$(echo "$issuer_subject" | grep 'subject=' | cut -d= -f2-) + issuer=$(echo "$issuer_subject" | grep 'issuer=' | cut -d= -f2-) + + echo " Subject: $subject" + echo -e " Issuer : $issuer" + + # Check if the certificate is self-signed + if [[ "$subject" == "$issuer" ]] + then + echo " Successfully found root certificate" + echo " Successfully dowenloaded root certificate to: $ROOT_FULL_PATH" + fi + fi + fi + +fi + + +echo -e "\nVerifying if the selected cipher is supported by the system..." + +for CIPHER in "${ARRAY_OF_CIPHER_ALGORITHMS[@]}" +do + FOUND_SELECTED_CIPHER=$(echo "$CIPHER" | grep -x "$SELECTED_CIPHER") + + if [[ ! -z "$FOUND_SELECTED_CIPHER" ]] + then + break; + fi +done + + +#If the Selected Cipher does not exist +if [[ -z "$FOUND_SELECTED_CIPHER" ]] +then + echo -e "\nThe selected Cipher '$SELECTED_CIPHER' does not exist" +else + #Generate the P12 + echo "Selected Cipher: $SELECTED_CIPHER" + echo "Found Selected Cipher: $FOUND_SELECTED_CIPHER" + echo "[*] The selected Cipher will be used for -certpbe and -keypbe inputs [*]" +fi + + +echo -e "\nVerifying if the selected digest is supported by the system..." +for DIGEST in "${ARRAY_OF_DIGEST_ALGORITHMS[@]}" +do + FOUND_SELECTED_DIGEST=$(echo "$DIGEST" | grep -x "$SELECTED_DIGEST") + + if [[ ! -z "$FOUND_SELECTED_DIGEST" ]] + then + break + fi +done + + +#If the Selected Digest does not exist +if [[ -z "$FOUND_SELECTED_DIGEST" ]] +then + echo -e "\nThe selected Digest '$SELECTED_DIGEST' does not exist" +else + #Generate the P12 + echo "Selected Digest: $SELECTED_DIGEST" + echo "Found Selected Digest: $FOUND_SELECTED_DIGEST" +fi + + +#If Selected Cipher and Digest exist exist, as well as password, certificate, root certificate, private key, password, etc. +if [[ ! -z "$FOUND_SELECTED_CIPHER" ]] && [[ ! -z "$FOUND_SELECTED_DIGEST" ]] && [[ ! -z "$PRIVATE_KEY_FULL_PATH" ]] && [[ ! -z "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] && [[ ! -z "$ROOT_FULL_PATH" ]] && [[ ! -z "$NPM_FOLDER" ]] && [[ ! -z "$PKCS12_PASSWORD" ]] +then + #The Private Key does not exist or is not readable + if [[ ! -f "$PRIVATE_KEY_FULL_PATH" ]] || [[ ! -r "$PRIVATE_KEY_FULL_PATH" ]] + then + echo -e "\nERROR - Private Key File '$NPM_PATH/$PRIVATE_KEY' does not exist or is not readable" + exit 1 + fi + #The Certificate Chain does not exist or is not readable + if [[ ! -f "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] || [[ ! -r "$CERTIFICATE_CHAIN_FILE_FULL_PATH" ]] + then + echo -e "\nERROR - Certificate Chain '$CERTIFICATE_CHAIN_FILE_FULL_PATH' does not exist or is not readable" + exit 1 + fi + if [[ ! -f "$ROOT_FULL_PATH" ]] || [[ ! -r "$ROOT_FULL_PATH" ]] + then + echo -e "\nERROR - Root Certificate '$ROOT_FULL_PATH' does not exist or is not readable" + exit 1 + fi + + + echo -e "\n---------------------------------------------------------------------------------" + echo -e "\nGenerating PKCS12 ..." + + #Run the openssl command to generate the p12 file + openssl pkcs12 -export -out "$PKCS12_FULL_PATH" -certpbe "$FOUND_SELECTED_CIPHER" -keypbe "$FOUND_SELECTED_CIPHER" -macalg "$FOUND_SELECTED_DIGEST" -inkey "$PRIVATE_KEY_FULL_PATH" -in "$CERTIFICATE_CHAIN_FILE_FULL_PATH" -certfile "$ROOT_FULL_PATH" -name "$NPM_FOLDER" -password "pass:$PKCS12_PASSWORD" + + + #If the last command completed successfully + if [[ $? -eq 0 ]] + then + echo -e "\nSuccessfully generated PKCS12" + echo -e "\nPKCS12 Full Path: $PKCS12_FULL_PATH" + + #Change the ownership to "$OWNER:$GROUP" + chown "$OWNER:$GROUP" "$PKCS12_FULL_PATH" + + if [[ $? -eq 0 ]] + then + echo -e "\nSuccessfully changed $PKCS12_FULL_PATH User and Group ownership to $OWNER:$GROUP" + else + echo "\nERROR - Unable to change $PKCS12_FULL_PATH User and Group ownership to $OWNER:$GROUP" + exit 1 + fi + + echo -e "\nCreating simlink for PKCS12 File ..." + echo " from: $PKCS12_SIMLINK_PATH" + echo " to: $PKCS12_FULL_PATH" + + # Get just the filename (basename) from the destination path + SYMLINK_NAME="$(basename "$PKCS12_SIMLINK_PATH")" + + # Get the directory where the symlink will be created + SYMLINK_DIR="$(dirname "$PKCS12_SIMLINK_PATH")" + + # Get the relative path from the symlink destination to the source file + RELATIVE_SOURCE="$(realpath --relative-to="$SYMLINK_DIR" "$PKCS12_FULL_PATH")" + + if [[ -z "$RELATIVE_SOURCE" ]] + then + echo -e "\nERROR - Relative Path variable is empty." + fi + + #ln -s "to-here" <- "from-here". The from-here should not exist yet, it is to be created, while the to-here should already exist. + #Create the links relative to the path + ln -sf "$RELATIVE_SOURCE" "$SYMLINK_DIR/$SYMLINK_NAME" + + if [[ $? -eq 0 ]] + then + echo " Successfully created SimLink" + else + echo "\nERROR - Unable to create SimLink" + exit 1 + fi + + #Change the ownership to "$OWNER:$GROUP" for SimLink + chown -h "$OWNER:$GROUP" "$PKCS12_SIMLINK_PATH" + + if [[ $? -eq 0 ]] + then + echo " Successfully changed $PKCS12_SIMLINK_PATH User and Group ownership to $OWNER:$GROUP" + else + echo "\nERROR - Unable to change $PKCS12_SIMLINK_PATH User and Group ownership to $OWNER:$GROUP" + exit 1 + fi + + if [[ "$KEEP_ROOT_CERTIFICATE" == false ]] + then + #The Root Certificate that was created by this script is no longer needed after generating the .p12 file so add it to the array of temporary files to delete + TEMP_FILES_ARRAY+=("$ROOT_FULL_PATH") # When $KEEP_ROOT_CERTIFICATE is set to true the script will keep the file and create a simlink for it as well + fi + + echo -e "\n\nCleaning up temporary files..." + for temp_file in "${TEMP_FILES_ARRAY[@]}" + do + echo -e "\n deleting $temp_file ..." + rm -f "$temp_file" + + if [[ $? -ne 0 ]] + then + echo -e "\n ERROR - Unable to delete '$temp_file'" + else + echo -e " Successfully deleted '$temp_file'" + fi + done + + #If the Root Path still exist then the user must have comented out the deletion of the root.pem file. So generate a simlink for it as well. + if [[ -f "$ROOT_FULL_PATH" ]] + then + echo -e "\nCreating SimLink for root certificate..." + #Change the ownership to "$OWNER:$GROUP" + chown "$OWNER:$GROUP" "$ROOT_FULL_PATH" + + # Get the directory where the symlink will be created + SYMLINK_DIR="$(dirname "$ROOT_CERTIFICATE_SIMLINK_PATH")" + + # Get the relative path from the symlink destination to the source file + RELATIVE_SOURCE="$(realpath --relative-to="$SYMLINK_DIR" "$ROOT_FULL_PATH")" + + if [[ -z "$RELATIVE_SOURCE" ]] + then + echo -e "\nERROR - Relative Path variable is empty." + fi + + #ln -s "to-here" <- "from-here". The from-here should not exist yet, it is to be created, while the to-here should already exist. + #Create the links relative to the path + ln -sf "$RELATIVE_SOURCE" "$SYMLINK_DIR/$ROOT_CERTIFICATE_SIMLINK_NAME" + + if [[ $? -eq 0 ]] + then + echo " Successfully created SimLink" + else + echo "\nERROR - Unable to create SimLink" + exit 1 + fi + + #Change the ownership to "$OWNER:$GROUP" for SimLink + chown -h "$OWNER:$GROUP" "$ROOT_CERTIFICATE_SIMLINK_PATH" + + if [[ $? -eq 0 ]] + then + echo " Successfully changed $ROOT_CERTIFICATE_SIMLINK_PATH User and Group ownership to $OWNER:$GROUP" + else + echo "\nERROR - Unable to change $ROOT_CERTIFICATE_SIMLINK_PATH User and Group ownership to $OWNER:$GROUP" + exit 1 + fi + fi + echo "" + else + echo -e "\nERROR - Unable to Generate PKCS12 '$PKCS12_FULL_PATH'\n" + fi + echo -e "---------------------------------------------------------------------------------\n" +else + #If one of the variables are empty + if [[ -z "$FOUND_SELECTED_CIPHER" ]] + then + echo -e "\nERROR - Selected Cipher is empty." + exit 1 + fi + if [[ -z "$FOUND_SELECTED_DIGEST" ]] + then + echo -e "\nERROR - Selected Digest is empty." + exit 1 + fi + if [[ -z "$ROOT_FULL_PATH" ]] || [[ -f "$ROOT_FULL_PATH" ]] + then + echo -e "\nERROR - Root Certificate is empty or does not exist." + exit 1 + fi + #The $NPM_FOLDER is used to create a friendly name in the openssl command. If its blank the friendly name that is displayed will be blank + if [[ -z "$NPM_FOLDER" ]] + then + echo -e "\nWARNING: Friendly Name value is empty." + fi + if [[ -z "$PKCS12_PASSWORD" ]] + then + echo -e "\nERROR - Password is empty." + exit 1 + fi +fi