#!/bin/bash # generate-p12 - generate a PKCS#12 client certificate for SFCC mTLS code uploads # Tested in GNU bash, version 3.2.57(1)-release (2007) (x86_64-apple-darwin23) # # Usage: # generate-p12 [cert_bundle_directory] # generate-p12 -h # # Run from the directory the cert bundle was unzipped to or provide that path # as an argument. Requires a CA cert bundle downloaded from Salesforce Business # Manager (Global Preferences -> Keys & Certificates -> MFA Certificates) _generate_p12() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="generate-p12" ;; esac _show_help() { local s; [ -t 1 ] && s=$'\033[4m' local r; [ -t 1 ] && r=$'\033[24m' echo "NAME" echo " $SCRIPT_NAME - generate a PKCS#12 client certificate for SFCC mTLS code uploads" echo "SYNOPSIS" echo " $SCRIPT_NAME [${s}cert_bundle_directory${r}]" echo " $SCRIPT_NAME -h" echo "DESCRIPTION" echo " Generates a PKCS#12 (.p12) client certificate for Salesforce" echo " Commerce Cloud (SFCC) B2C Commerce. The generated .p12 is used" echo " for mutual-TLS (mTLS) client authentication when uploading code" echo " to staging instances." echo "" echo " Note: 'MFA' in SFCC parlance refers to mTLS client-certificate" echo " auth, not two-factor authentication." echo "" echo " Interactive. Run from the directory the cert bundle was unzipped" echo " to, or provide that path as an argument. Prompts for: hostname," echo " cert expiration, CSR fields, and an export password to encrypt" echo " the .p12 file." echo "PRECONDITIONS" echo " Requires a CA cert bundle downloaded from Salesforce Business" echo " Manager (Global Preferences -> Keys & Certificates -> MFA" echo " Certificates). The bundle should contain files of the form:" echo " _NN.crt CA certificate" echo " _NN.key CA private key" echo " _NN.txt CA password" echo " .srl CA serial number" echo " where NN is a two-digit suffix (_01, _02, ...). The highest" echo " complete suffix is selected automatically." echo "EXIT STATUS" echo " 0 Success" echo " 1 Runtime failure (bundle files missing, openssl step failed)" echo " 2 Usage error (directory arg is not accessible)" echo " 3 Dependency error (openssl missing)" echo "DEPENDENCIES" echo " openssl" } # Install cleanup trap before any return path (including --help) so all # exits run __unset. Helpers below haven't been defined yet, but unset -f # is silent on missing names, so listing them all up-front is fine case "$1" in -h|--help) _show_help return 0 ;; esac if ! command -v openssl >/dev/null 2>&1; then _error "openssl is required" return 3 fi # Example hostname: "cert.staging.customer.realm.demandware.net" # However, any BM hostname or as little as "customer.realm" or "customer-realm" may be provided # Text formatting control characters for later use - by convention wrapped in {} when used below, for legibility local teal=$'\033[36m' local default_color="$teal" local bold=$'\033[1m' # start bolding text local smul=$'\033[4m' # start underlining text local rmul=$'\033[24m' # stop underlining text local reset=$'\033[0m' # reset all formatting to terminal default # Applies provided formatting (typically color) to output _color() { local color="$1" shift if [ -t 0 ]; then # input is not piped or redirected (_func hi) printf "${color}%s\\n${reset}" "$@" else # input is piped or redirected (cat file | _func, _func &2 done else printf '%s[ERR][%s] %s%s\n' "$red_on" "$SCRIPT_NAME" "$(cat)" "$reset" >&2 fi } # Checks if file(s) exist and prints an error message if not _file_exists() { local any_missing=0 local file for file in "$@"; do if [ ! -f "$file" ]; then _error "File not found: $file" any_missing=1 fi done return $any_missing } local wd="${1:-"$PWD"}" # Resolve to absolute path (the cd runs in a subshell via command substitution) if ! wd="$(cd "$wd" && pwd)"; then _error "Failed to access specified directory" return 2 fi local hostname _prompt "BM Hostname: " read -r hostname echo #newline # Make sure we end up with a good hostname no matter what form it's provided in # Any PIG instance's BM hostname will end up in the "cert.staging" form, or even providing as little as "customer-realm" hostname="$(printf %s "$hostname" | \ sed '# Convert underscores and hyphens to periods s/_/./g; s/-/./g; # Remove any existing instance type identifier s/^production\.//; s/^development\.//; s/^staging\.//; s/^cert\.staging\.//; # Prepend with "cert.staging." s/^/cert.staging./; # Remove trailing ".demandware.net" if it exists s/\.demandware\.net$//; # Append ".demandware.net" s/$/.demandware.net/' )" # Find the highest cert bundle suffix (_01, _02, etc.) with all 3 required files local ca_suffix="" local candidate local suffix for candidate in "$wd/${hostname}_"[0-9][0-9].crt; do [ -f "$candidate" ] || continue # no matches -- glob returned literally suffix="${candidate%.crt}" suffix="${suffix##*_}" if [ -f "$wd/${hostname}_${suffix}.key" ] && \ [ -f "$wd/${hostname}_${suffix}.txt" ]; then ca_suffix="$suffix" # glob is ascending, so last match is highest fi done if [ -z "$ca_suffix" ]; then _error "No complete cert bundle found for ${hostname}" _error "Expected files: ${hostname}_XX.crt, .key, .txt" return 1 fi # Announce the auto-selected suffix so the user sees which bundle was chosen # Use printf directly rather than _echo/_color -- _color reads stdin when # stdin isn't a TTY, and we can't have it consuming the pending years prompt printf "${default_color}%s${reset}\n" "Using cert bundle suffix _${ca_suffix} (from ${hostname}_${ca_suffix}.crt, ${hostname}_${ca_suffix}.key, ${hostname}_${ca_suffix}.txt)" # Assemble cert bundle filenames local ca_cert_filename="$wd/${hostname}_${ca_suffix}.crt" local ca_key_filename="$wd/${hostname}_${ca_suffix}.key" local ca_pass_filename="$wd/${hostname}_${ca_suffix}.txt" local ca_serial_filename="$wd/${hostname}.srl" # .srl never has a numeric suffix # Verify necessary files exist, printing any which are missing if ! _file_exists "$ca_cert_filename" "$ca_key_filename" "$ca_pass_filename" "$ca_serial_filename"; then _error "${bold}Necessary file(s) missing, aborting p12 generation" return 1 fi # Define output file names local user="$USER" local req_filename="$wd/$user-$hostname.req" local key_filename="$wd/$user-$hostname.key" local pem_filename="$wd/$user-$hostname.pem" local p12_filename="$wd/$user-$hostname.p12" # Print the filenames which will be used _echo </dev/null; then break fi echo "Please enter a valid integer." # terminal default color done local days=$((years*365)) echo #newline ### Step 1 of 3: Generate a CSR and private key # -nodes (no DES) means do not encrypt the generated private key if ! openssl req -new -sha256 -newkey rsa:2048 -nodes -out "$req_filename" -keyout "$key_filename"; then _error "${bold}\`openssl req\` (step 1/3) failed - .req and .key files not generated" return 1 fi echo #newline _bold "Created CSR:" _echo "$req_filename" _bold "Created private key:" _echo "$key_filename" echo #newline ### Step 2 of 3: Create a certificate from the CSR using the CA files and password # Create X.509 certificate to be bundled with the user's private key in the PKCS#12 keystore if ! openssl x509 -req -in "$req_filename" -out "$pem_filename" -days "$days" -CA "$ca_cert_filename" -CAkey "$ca_key_filename" -passin "file:$ca_pass_filename" -CAserial "$ca_serial_filename"; then _error "${bold}\`openssl x509\` (step 2/3) failed - .pem file not generated" return 1 fi echo #newline _bold "Created X.509 intermediate certificate:" _echo "$pem_filename" echo #newline ### Step 3 of 3: Bundle the private key (Step 1), the CA-signed certificate (Step 2), and CA certificate into a .p12 client certificate # Below lines are printed right before the "Enter Export Password" prompt from the pkcs12 export command _echo "${bold}NOTE:${reset} ${default_color}This encrypts the client certificate itself, so that it cannot be used without this password." _echo "${smul}secure prompt: no text will appear as you type${rmul}" if ! openssl pkcs12 -export -in "$pem_filename" -inkey "$key_filename" -certfile "$ca_cert_filename" -name "$user-$hostname" -out "$p12_filename"; then _error "${bold}\`openssl pkcs12\` (step 3/3) failed - .p12 file not generated" return 1 fi echo #newline _bold "Created PKCS12 client certificate:" _echo "$p12_filename" echo #newline ### DONE _bold "Finished successfully. Full path to p12:" _echo "$p12_filename" ) _generate_p12 "$@" __generate_p12_rc=$? unset -f _generate_p12 if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __generate_p12_rc; return $__generate_p12_rc" fi eval "unset __generate_p12_rc; exit $__generate_p12_rc"