#!/bin/bash ### AUTOR: Evgeny Samorokov certhelm@certhelm.com ### # Variables site_name="example.com" # Change this to your site's name SAN_DNS=("example.com" "www.example.com") SAN_IP=("123.123.123.123" "124.124.124.124") KEY_LENGTH=2048 KEY_OWNER="root" # default is 'root', but could be 'www-data' COUNTRY_CODE="US" STATE="California" CITY="Roseville" COMPANY="Example Company" OU="Example Organization Unit" EMAIL="admin@example.com" port="443" # Change this to the desired port # Replace the "$engine" variable value with the appropriate service name to restart your web server # For example, for Apache, you can use "apache2" or "httpd" engine="httpd" ################################### ### NO CHANGES BEYOND THIS LINE ### ################################### today=$(date +%Y%m%d) ssl_dir="/opt/ssl/$site_name" cert_dropin="$ssl_dir/cert_dropin" cert_installed="$ssl_dir/cert_installed" certs_archived="$ssl_dir/certs_archived" scripts_dir="$ssl_dir/scripts" cert_new="$ssl_dir/cert_new" conf_path="$ssl_dir/site-ssl.conf" cert_path="$cert_new/$site_name.${today}.crt" key_path="$cert_new/$site_name.${today}.key" csr_path="$cert_new/$site_name.${today}.csr" output_p7b_file="$cert_new/$site_name.${today}.pfx" ca_path="$ssl_dir/cert_new/${site_name}_ca_${today}.crt" fullchain_path="$cert_new/${site_name}_fullchain.${today}.crt" previous_version="$ssl_dir/.before" script_path="$0" generate_site_ssl_conf() { echo "[req] default_bits = $KEY_LENGTH prompt = no default_md = sha256 distinguished_name = req_distinguished_name req_extensions = req_ext [req_distinguished_name] C = $COUNTRY_CODE ST = $STATE L = $CITY O = $COMPANY OU = $OU CN = $site_name emailAddress = $EMAIL [req_ext] subjectAltName = @alt_names [alt_names] " > $conf_path local n=1 for i in ${SAN_DNS[@]} do echo "DNS.${n} = ${i}" >> $conf_path n=$(($n + 1)) done local n=1 if [[ ! -z $SAN_IP ]]; then for i in ${SAN_IP[@]} do echo "IP.${n} = ${i}" >> $conf_path n=$(($n + 1)) done fi } warn_about_cert_path() { echo "#################### WARNING #####################" echo "# Make sure certificate/CA/key links are changed #" echo "# in your site ssl configuration as follows: #" echo "# Cert: ${cert_installed}/${site_name}.crt #" echo "# Key: ${cert_installed}/${site_name}.key #" echo "# CA: $cert_installed/ca.crt #" echo "# Fullchain: $cert_installed/fullchain.crt #" echo "##################################################" } initialize_ssl_automation() { if [[ ! "$site_name" = "example.com" ]]; then if [[ ! -d "$cert_dropin" || ! -d "$cert_installed" || ! -d "$certs_archived" || ! -d "$cert_new" ]]; then mkdir -p $cert_dropin mkdir -p $cert_installed mkdir -p $certs_archived mkdir -p $cert_new mkdir -p $ssl_dir/scripts cp $0 $ssl_dir/scripts/ssl-script.sh chmod +x $ssl_dir/scripts/ssl-script.sh fi generate_site_ssl_conf echo "The script has been installed in $scripts_dir" warn_about_cert_path else echo "Edit "$0" file and modify 'site_name' and below variables before executing the script! " exit 1 fi } if [[ "$1" = "install" ]]; then rm -rf $cert_installed/* fi get_current_links() { rm -f "${previous_version}_tmp" find "$cert_installed" -type l | while read symlink; do source_file=$(readlink -f "$symlink") echo "$source_file $symlink" >> "${previous_version}_tmp" done if [[ ! $(diff "$previous_version" "${previous_version}_tmp") = "" ]]; then rm -f "$previous_version" mv "${previous_version}_tmp" "$previous_version" fi } revert_to_prev_version() { while read -r line; do local src=$(echo "$line" | awk '{print $1}') local dst=$(echo "$line" | awk '{print $2}') ln -s "$src" "$dst" done < "$previous_version" systemctl restart "$engine" } # Function to generate CSR generate_csr() { if [ -f "$conf_path" ]; then openssl req -new -newkey rsa:$KEY_LENGTH -nodes -keyout "$key_path" -out "$csr_path" -config "$conf_path" csr_content=$(cat "$csr_path") echo "CSR generated at: $csr_path" echo "CSR content:" echo "$csr_content" generate_windows_ca_attributes else echo "site-ssl.conf does not exist. Please fill out the necessary input and generate the file for future CSR requests." openssl req -new -newkey rsa:$KEY_LENGTH -nodes -keyout "$key_path" -out "$csr_path" -config "$conf_path" fi chmod 400 $key_path } # Function to validate that CSR matches the private key validate_csr() { # Check if both CSR and Key files exist if [ ! -f "$csr_path" ]; then echo "Error: CSR file does not exist at $csr_path" return 1 fi if [ ! -f "$key_path" ]; then echo "Error: Key file does not exist at $key_path" return 1 fi # Extract public keys from CSR and Key csr_pub_key=$(openssl req -in "$csr_path" -noout -pubkey) key_pub_key=$(openssl rsa -in "$key_path" -pubout 2>/dev/null) # Compare the public keys if [ "$csr_pub_key" = "$key_pub_key" ]; then echo "Success: CSR matches the private key." else echo "Error: CSR does not match the private key." return 1 fi } generate_windows_ca_attributes() { san_dns=$(IFS="&"; echo "${SAN_DNS[*]/#/dns=}") san_ip=$(IFS="&"; echo "${SAN_IP[*]/#/ipaddress=}") additional_attributes="san:$san_dns&$san_ip" echo "Additional Attributes: \"$additional_attributes\"" } # Function to download intermediate certificates download_intermediate_certs() { intermediates=($(openssl x509 -in "$cert_path" -noout -text | grep -o 'http://[^ ]*')) if [ ${#intermediates[@]} -eq 0 ]; then echo "No intermediate certificates found in the received certificate." echo "Please provide the CA certificate(s) content (including the BEGIN and END lines) and press Ctrl+D when finished:" cat > "$ca_path" return fi for ((i=0; i<${#intermediates[@]}; i++)); do intermediate_url="${intermediates[$i]}" intermediate_filename="intermediate$i.crt" # Download the intermediate certificate if curl -o "$cert_new/$intermediate_filename" -s "$intermediate_url"; then echo "Intermediate certificate $intermediate_filename downloaded successfully." else echo "Failed to download intermediate certificate $intermediate_filename from $intermediate_url." echo "Please provide the CA certificate(s) content (including the BEGIN and END lines) and press Ctrl+D when finished:" cat > "$ca_path" return fi done } input_intermediate_certs() { if [[ ! -f "$ca_path" ]]; then echo "Please provide the CA certificate(s) content (including the BEGIN and END lines) and press Ctrl+D when finished:" cat > "$ca_path" fi } # Function to install certificate install_certificate() { if [ ! -f "$cert_path" ]; then echo "Certificate not found in $cert_new. Please provide the certificate content (including the BEGIN and END lines) and press Ctrl+D when finished:" cat > "$cert_path" fi echo "Certificate installed at: $cert_path" # Create a symlink to the installed certificate ln -s "$cert_path" "${cert_installed}/${site_name}.crt" } restart_service() { if [[ $restart_ok = true ]]; then # Restart the web server echo "Restarting $engine service..." # Replace the $engine variable with the appropriate service name to restart your web server # For example, for Apache, you can use "apache2" or "httpd" # For Nginx, you can use "nginx" systemctl restart $engine fi } # Function to create the certificate chain create_certificate_chain() { cat "$cert_path" "$ca_path" > "$fullchain_path" echo "Certificate chain created at: $fullchain_path" } # Function to test certificate and key modulus test_certificate_key() { cert_modulus=$(openssl x509 -noout -modulus -in "$cert_path" | openssl md5) key_modulus=$(openssl rsa -noout -modulus -in "$key_path" | openssl md5) if [ "$cert_modulus" == "$key_modulus" ]; then echo "Certificate and key modulus match." else echo "Certificate and key modulus do not match. Aborting installation." exit 1 fi } # Function to test the certificate/fullchain test_certificate_fullchain() { if [ -f "$fullchain_path" ]; then echo "Certificate chain found at: $fullchain_path" else echo "Certificate chain not found. Aborting installation." exit 1 fi } # Function to install the certificate chain install_certificate_chain() { ln -s "$fullchain_path" "$cert_installed/fullchain.crt" echo "Certificate chain was installed" } # Function to install the key install_key() { ln -s "$key_path" "$cert_installed/$site_name.key" echo "Key was installed" } # Function to install the CA install_ca() { ln -s "$ca_path" "$cert_installed/ca.crt" echo "CA was installed" } convert_to_p7b() { # Create a PKCS#12 file openssl pkcs12 -export -out "$output_p7b_file" -inkey $key_path -in $cert_path if [[ -f "$output_p7b_file" ]]; then echo "Conversion complete. P7B file: $output_p7b_file" else echo "Conversion has FAILED! (hint: check if you hassword matched)" fi } unpack_pfx() { local pfx_file="$1" if [[ -z "$pfx_file" || ! -f "$pfx_file" ]]; then echo "Error: PFX file not found: $pfx_file" echo "Usage: unpack_pfx " return 1 fi echo "Enter PFX password (leave blank if none):" read -s pfx_password echo # Prepare -passin option local passopt if [[ -n "$pfx_password" ]]; then passopt="-passin pass:$pfx_password" else passopt="-passin pass:" fi # Temporary raw files (may include Bag Attributes, subject, issuer, etc.) local tmp_key="${key_path}.raw" local tmp_cert="${cert_path}.raw" local tmp_ca="${ca_path}.raw" # 1) Extract private key (may contain Bag Attributes) if ! openssl pkcs12 -in "$pfx_file" -nocerts -nodes $passopt -out "$tmp_key" 2>/dev/null; then echo "Error: Failed to extract private key from PFX." rm -f "$tmp_key" "$tmp_cert" "$tmp_ca" return 1 fi # 2) Extract leaf certificate if ! openssl pkcs12 -in "$pfx_file" -clcerts -nokeys $passopt -out "$tmp_cert" 2>/dev/null; then echo "Error: Failed to extract certificate from PFX." rm -f "$tmp_key" "$tmp_cert" "$tmp_ca" return 1 fi # 3) Extract CA chain (may not exist) if ! openssl pkcs12 -in "$pfx_file" -cacerts -nokeys $passopt -out "$tmp_ca" 2>/dev/null; then echo "Warning: Failed to extract CA certificates from PFX (maybe none)." > "$tmp_ca" fi # Helper: strip everything except PEM blocks strip_pem() { local src="$1" local dst="$2" awk '/-----BEGIN /,/-----END / {print}' "$src" > "$dst" } # 4) Clean up to pure PEM # Private key (PKCS#8 or traditional) strip_pem "$tmp_key" "$key_path" # Leaf certificate # Use openssl x509 to normalize and ensure it's a clean PEM cert if ! openssl x509 -in "$tmp_cert" -out "$cert_path" 2>/dev/null; then # Fallback: just strip PEM by pattern strip_pem "$tmp_cert" "$cert_path" fi # CA bundle (may contain multiple certs) strip_pem "$tmp_ca" "$ca_path" # 5) Full chain = leaf + CA (if any) cat "$cert_path" "$ca_path" > "$fullchain_path" # Permissions chmod 400 "$key_path" # Cleanup raw files rm -f "$tmp_key" "$tmp_cert" "$tmp_ca" echo "PFX unpack complete:" echo " Key: $key_path" echo " Certificate: $cert_path" echo " CA Bundle: $ca_path" echo " Fullchain: $fullchain_path" } usage() { echo "Usage:" echo " $0 init" echo " $0 request" echo " $0 install [restart]" echo " $0 revert" echo " $0 convert" echo " $0 unpack-pfx " echo " $0 validate-csr" echo " $0 test-cert" echo "Author: Alfatek team @ evgeny_samorokov@questsys.com" } # Main script if [ $# -lt 1 ]; then usage exit 1 fi restart_ok=false case "$1" in "init") if [ $# -ne 1 ]; then usage exit 1 fi initialize_ssl_automation ;; "request") if [ $# -ne 1 ]; then usage exit 1 fi generate_csr warn_about_cert_path ;; "install") if [ $# -gt 2 ] || { [ $# -eq 2 ] && [[ "$2" != "restart" ]]; }; then usage exit 1 fi if [[ "$2" = "restart" ]]; then restart_ok=true; fi get_current_links install_certificate input_intermediate_certs install_ca test_certificate_key && install_key create_certificate_chain test_certificate_fullchain && install_certificate_chain warn_about_cert_path generate_windows_ca_attributes restart_service ;; "revert") if [ $# -ne 1 ]; then usage exit 1 fi revert_to_prev_version ;; "convert") if [ $# -ne 1 ]; then usage exit 1 fi convert_to_p7b ;; "unpack-pfx") if [ $# -ne 2 ]; then echo "Error: missing PFX file path." echo "Usage: $0 unpack-pfx " exit 1 fi unpack_pfx "$2" ;; "validate-csr") if [ $# -ne 1 ]; then usage exit 1 fi validate_csr ;; "test-cert") if [ $# -ne 1 ]; then usage exit 1 fi test_certificate_key ;; *) echo "Invalid option!" usage exit 1 ;; esac