#!/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' cat < Keys & Certificates -> MFA Certificates). The bundle should contain files of the form: _NN.crt CA certificate _NN.key CA private key _NN.txt CA password .srl CA serial number where NN is a two-digit suffix (_01, _02, ...). The highest complete suffix is selected automatically. EXIT STATUS 0 Success 1 Runtime failure (bundle files missing, openssl step failed) 2 Usage error (directory arg is not accessible) 3 Dependency error (openssl missing) DEPENDENCIES openssl EOF } # Text formatting control characters for later use - by convention wrapped in {} when used below, for legibility. # Empty unless stdout is a TTY (or CLICOLOR_FORCE is set) and NO_COLOR is unset, # so pipes/redirects and the test harness stay plain by default; CLICOLOR_FORCE=1 # keeps color in a capture. NO_COLOR wins when both are set. These helpers all # write to stdout, so the guard is [ -t 1 ] (unlike _error's stderr [ -t 2 ]). # https://no-color.org/ local teal="" local bold="" # start bolding text local smul="" # start underlining text local rmul="" # stop underlining text local reset="" # reset all formatting to terminal default if { [ -t 1 ] || [ -n "${CLICOLOR_FORCE:-}" ]; } && [ -z "${NO_COLOR:-}" ]; then teal=$'\033[36m' bold=$'\033[1m' smul=$'\033[4m' rmul=$'\033[24m' reset=$'\033[0m' fi local default_color="$teal" # 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 } # 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 } # Helpers (incl. _error) are defined above so the --help and dependency # checks below can use them. This is an executable subshell-body script, # so no cleanup trap is needed -- the subshell discards everything on return 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 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"