diff --git a/files/root/.config/bash/bashrc-includes.d/system_linux_func_ssh_keysuite_gen b/files/root/.config/bash/bashrc-includes.d/system_linux_func_ssh_keysuite_gen new file mode 100644 index 0000000..93d4013 --- /dev/null +++ b/files/root/.config/bash/bashrc-includes.d/system_linux_func_ssh_keysuite_gen @@ -0,0 +1,224 @@ +# SPDX-License-Identifier: MIT +function ssh_keysuite_gen () { + if [[ "${#}" -eq '0' ]]; then + printf -- '%s\n' \ + 'Usage:' \ + '' \ + 'ssh_keysuite_gen \' \ + ' '"'"'ed25519'"'"' \' \ + ' '"'"'john.doe@example.com_ed25519_'"'"'"$(date +'%F')" \' \ + ' "$(date +'%F')" \' \ + ' "$(pwd)" \' \ + ' '"'"'John Doe '"'"'' \ + '' \ + 'or for an RSA key (here with 4096 bits key size):' \ + '' \ + 'ssh_keysuite_gen \' \ + ' '"'"'rsa:4096'"'"' \' \ + ' '"'"'john.doe@example.com_rsa2_4096_'"'"'"$(date +'%F')" \' \ + ' "$(date +'%F')" \' \ + ' "$(pwd)" \' \ + ' '"'"'John Doe '"'"'' \ + '' \ + 'Option '"'"'rsa'"'"' without key size '"'"':'"'"' will generate' \ + 'a key with 3072 bits key size.' \ + '' \ + 'For an ECDSA key (here with 521 bits elliptic curve size):' \ + '' \ + 'ssh_keysuite_gen \' \ + ' '"'"'ecdsa:521'"'"' \' \ + ' '"'"'john.doe@example.com_ecdsa_521_'"'"'"$(date +'%F')" \' \ + ' "$(date +'%F')" \' \ + ' "$(pwd)" \' \ + ' '"'"'John Doe '"'"'' \ + '' \ + 'Option '"'"'ecdsa'"'"' without EC length '"'"':'"'"' will generate' \ + 'a key with 256 bits elliptic curve length.' + return + fi + + # We're assuming coreutils and bash internals are available (chmod, + # command, cp, fmt, mktemp, printf etc.), we check for moderately + # unconventional binaries + local SSH_REQUIRED_CMDS=('grep' 'openssl' 'puttygen' 'sed' 'ssh-keygen') + local SSH_MISSING_CMDS=() + + for ssh_required_cmd in "${SSH_REQUIRED_CMDS[@]}"; do + command -v "${ssh_required_cmd}" &> '/dev/null' + if [[ "${?}" -gt '0' ]]; then + SSH_MISSING_CMDS+=("${ssh_required_cmd}") + fi + done + if [[ "${#SSH_MISSING_CMDS[@]}" -gt '0' ]]; then + for ssh_missing_cmd in "${SSH_MISSING_CMDS[@]}"; do + printf -- 'Missing binary: '"'"'%s'"'"'\n' "${ssh_missing_cmd}" + done + printf -- '\n%s\n' 'Please make sure all required commands are available.' + return + fi + + IFS= read -sp 'Enter password for new SSH key: ' 'SSH_KEY_PW' + echo + + local TMPFILE="$(mktemp)" + local TMPFILE2="$(mktemp)" + trap '[[ -f '"'${TMPFILE}'"' ]] && rm '"'${TMPFILE}'"'; [[ -f '"'${TMPFILE2}'"' ]] && rm '"'${TMPFILE2}'"';' RETURN + printf -- '%s' "${SSH_KEY_PW}" > "${TMPFILE}" + printf -- '%s' "${SSH_KEY_PW}" > "${TMPFILE2}" + + sleep 1 + + local SSH_KEY_ALGO="${1:-ed25519}" + local SSH_KEY_BASENAME="${2:?'Arg 2 must be SSH key file base name such as '"'"'john.doe@example.com_ed25519_2025-09-15'"'"''}" + local SSH_KEY_CREATION_TIMESTAMP="${3:-"$(date +'%F')"}" + local SSH_KEY_OUTPUT_DIR="${4:-"$(pwd)"}" + local SSH_KEY_PURPOSE="${5:?'Arg 5 must be SSH key purpose for use in key comment such as '"'"'John Doe '"'"''}" + + local SSH_KEY_PATH_RAW="${SSH_KEY_OUTPUT_DIR}"'/'"${SSH_KEY_BASENAME}"'.SSH_KEY_FORMAT.SSH_KEY_FILE_EXTENSION' + local SSH_KEY_COMMENT="${SSH_KEY_PURPOSE}"' ('"${SSH_KEY_CREATION_TIMESTAMP}"')' + + if [[ "${SSH_KEY_ALGO}" =~ ^rsa ]]; then + local SSH_DO_RSA='true' + local SSH_RSA_KEY_LENGTH="${SSH_KEY_ALGO#*:}" + if [[ "${#SSH_RSA_KEY_LENGTH}" -eq '0' || "${SSH_RSA_KEY_LENGTH}" == "${SSH_KEY_ALGO}" ]]; then + SSH_RSA_KEY_LENGTH='' + fi + local SSH_KEY_ALGO='rsa' + elif [[ "${SSH_KEY_ALGO}" =~ ^ecdsa ]]; then + local SSH_DO_ECDSA='true' + local SSH_EC_LENGTH="${SSH_KEY_ALGO#*:}" + if [[ "${#SSH_EC_LENGTH}" -eq '0' || "${SSH_EC_LENGTH}" == "${SSH_KEY_ALGO}" ]]; then + SSH_EC_LENGTH='' + fi + local SSH_KEY_ALGO='ecdsa' + fi + + if [[ "${SSH_DO_RSA}" || "${SSH_DO_ECDSA}" ]]; then + if [[ "${#SSH_KEY_PW}" -lt '5' ]]; then + printf -- '%s\n' \ + '' \ + "$(fmt <<<'Key password must me at least 5 characters long as that is what '"'"'ssh-keygen'"'"' enforces for PEM-encoded private keys for example when converting from an OpenSSH-encoded private key.')" + return + fi + fi + + local SSH_KEY_FILE_LIST=() + + # Gen OpenSSH-encoded private key + # Can be OpenSSH-encoded private RSA key + # Can be OpenSSH-encoded private EC key + # (a wrapper around all keys supported by OpenSSH) + # BEGIN OPENSSH PRIVATE KEY + local SSH_KEY_FORMAT='openssh-encoded' + local SSH_KEY_FILE_EXTENSION='key' + local SSH_KEY_PATH_OPENSSH="$(sed -r \ + -e 's'$'\x1''SSH_KEY_FORMAT'$'\x1'"${SSH_KEY_FORMAT}"$'\x1''g' \ + -e 's'$'\x1''SSH_KEY_FILE_EXTENSION'$'\x1'"${SSH_KEY_FILE_EXTENSION}"$'\x1''g' \ + <<<"${SSH_KEY_PATH_RAW}")" + ssh-keygen -t "${SSH_KEY_ALGO}"$(if [[ "${SSH_DO_RSA}" && "${SSH_RSA_KEY_LENGTH}" ]]; then printf -- '%s' ' -b '"${SSH_RSA_KEY_LENGTH}"; elif [[ "${SSH_DO_ECDSA}" && "${SSH_EC_LENGTH}" ]]; then printf -- '%s' ' -b '"${SSH_EC_LENGTH}"; fi) -N "${SSH_KEY_PW}" -C "${SSH_KEY_COMMENT}" -f "${SSH_KEY_PATH_OPENSSH}" + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_OPENSSH}") + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_OPENSSH}"'.pub') + + # Convert to PEM-encoded PKCS#1 key + # (only RSA keys are PKCS#1) + # BEGIN RSA PRIVATE KEY + if [[ "${SSH_DO_RSA}" ]]; then + local SSH_KEY_FORMAT='pem-encoded-pkcs1-rsa' + local SSH_KEY_PATH_PEM_PKCS1_RSA_PRIVATE="$(sed -r \ + -e 's'$'\x1''SSH_KEY_FORMAT'$'\x1'"${SSH_KEY_FORMAT}"$'\x1''g' \ + -e 's'$'\x1''SSH_KEY_FILE_EXTENSION'$'\x1'"${SSH_KEY_FILE_EXTENSION}"$'\x1''g' \ + <<<"${SSH_KEY_PATH_RAW}")" + cp "${SSH_KEY_PATH_OPENSSH}" "${SSH_KEY_PATH_PEM_PKCS1_RSA_PRIVATE}" + ssh-keygen -p -P "${SSH_KEY_PW}" -N "${SSH_KEY_PW}" -m 'pem' -f "${SSH_KEY_PATH_PEM_PKCS1_RSA_PRIVATE}" + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_PEM_PKCS1_RSA_PRIVATE}") + fi + + # Convert to PEM-encoded PKCS#8 key + # (a wrapper around both RSA and EC keys) + # BEGIN PRIVATE KEY + # or + # BEGIN ENCRYPTED PRIVATE KEY + if [[ "${SSH_DO_RSA}" || "${SSH_DO_ECDSA}" ]]; then + local SSH_KEY_FORMAT='pem-encoded-pkcs8' + local SSH_KEY_PATH_PEM_PKCS8="$(sed -r \ + -e 's'$'\x1''SSH_KEY_FORMAT'$'\x1'"${SSH_KEY_FORMAT}"$'\x1''g' \ + -e 's'$'\x1''SSH_KEY_FILE_EXTENSION'$'\x1'"${SSH_KEY_FILE_EXTENSION}"$'\x1''g' \ + <<<"${SSH_KEY_PATH_RAW}")" + local SSH_KEY_FORMAT='pem-encoded-SubjectPublicKeyInfo' + local SSH_KEY_PATH_PEM_SPKI_PUBLIC="$(sed -r \ + -e 's'$'\x1''SSH_KEY_FORMAT'$'\x1'"${SSH_KEY_FORMAT}"$'\x1''g' \ + -e 's'$'\x1''SSH_KEY_FILE_EXTENSION'$'\x1'"${SSH_KEY_FILE_EXTENSION}"$'\x1''g' \ + <<<"${SSH_KEY_PATH_RAW}")" + cp "${SSH_KEY_PATH_OPENSSH}" "${SSH_KEY_PATH_PEM_PKCS8}" + ssh-keygen -p -P "${SSH_KEY_PW}" -N "${SSH_KEY_PW}" -m 'pkcs8' -f "${SSH_KEY_PATH_PEM_PKCS8}" + ssh-keygen -e -P "${SSH_KEY_PW}" -f "${SSH_KEY_PATH_PEM_PKCS8}" -m 'pkcs8' > "${SSH_KEY_PATH_PEM_SPKI_PUBLIC}"'.pub' + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_PEM_PKCS8}") + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_PEM_SPKI_PUBLIC}"'.pub') + fi + + # Convert to PEM-encoded Elliptic Curve key + # (looks like PKCS#1 but is not) + # BEGIN EC PRIVATE KEY + if [[ "${SSH_DO_ECDSA}" ]]; then + local SSH_KEY_FORMAT='pem-encoded-ec' + local SSH_KEY_PATH_PEM_EC_PRIVATE="$(sed -r \ + -e 's'$'\x1''SSH_KEY_FORMAT'$'\x1'"${SSH_KEY_FORMAT}"$'\x1''g' \ + -e 's'$'\x1''SSH_KEY_FILE_EXTENSION'$'\x1'"${SSH_KEY_FILE_EXTENSION}"$'\x1''g' \ + <<<"${SSH_KEY_PATH_RAW}")" + openssl ec -in "${SSH_KEY_PATH_PEM_PKCS8}" -out "${SSH_KEY_PATH_PEM_EC_PRIVATE}" -passin 'file:'"${TMPFILE}" -passout 'file:'"${TMPFILE2}" + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_PEM_EC_PRIVATE}") + fi + + # Convert pub key to RFC4716 + local SSH_KEY_FORMAT='rfc4716-encoded' + local SSH_KEY_FILE_EXTENSION='key' + local SSH_KEY_PATH_RFC4716="$(sed -r \ + -e 's'$'\x1''SSH_KEY_FORMAT'$'\x1'"${SSH_KEY_FORMAT}"$'\x1''g' \ + -e 's'$'\x1''SSH_KEY_FILE_EXTENSION'$'\x1''key'$'\x1''g' \ + <<<"${SSH_KEY_PATH_RAW}")" + # Write a proper comment line. ssh-keygen by default writes key type + # and local user account as comment when converting to RFC4716 + # format instead of using the comment already present in source + # file. We write our own proper comment here. Break at 71 + # characters. End each line with a trailing backslash at position + # 72. The first sed comment replaces all end-of-lines ($) with a + # backslash (\). We have to escape the backslash so that the first + # sed command inserts a literal backslash at the end of each line (\\). A + # few lines further down we 'ssh-keygen | sed' again where we + # replace the RFC4716 comment with ours (which now includes literal + # trailing backslashes). In order to preserve literal trailing + # backslashes in "${var}" when doing: + # + # sed -r 's/g/'"${var}"'/g' + # + # we have to make sure that "${var}" contains three (3) literal + # backslashes. That's why we do '\\\\\\' here. When we then do: + # + # sed -r 's/g/'"${var}"'/g' + # + # three literal backslashes in "${var}" are interpreted to one. + local SSH_RFC4716_KEYTYPE="$(ssh-keygen -m 'RFC4716' -ef "${SSH_KEY_PATH_OPENSSH}"'.pub' | grep -Pio -- '^(Comment:[^,]+)')" + local SSH_RFC4716_COMMENT="$(fmt --width 71 <<<"${SSH_RFC4716_KEYTYPE}"', '"${SSH_KEY_COMMENT}"'"' \ + | sed -r \ + -e 's'$'\x1''$'$'\x1''\\\\\\'$'\x1''g' \ + -e '$ s'$'\x1''\\+'$'\x1'''$'\x1''g' \ + )" + ssh-keygen -m 'RFC4716' -ef "${SSH_KEY_PATH_OPENSSH}"'.pub' \ + | sed -r 's'$'\x1''^Comment: [^\r\n\f]*'$'\x1'"${SSH_RFC4716_COMMENT}"$'\x1''g' \ + > "${SSH_KEY_PATH_RFC4716}"'.pub' + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_RFC4716}"'.pub') + + # Convert to PuTTY PPK + local SSH_KEY_FORMAT='putty-encoded' + local SSH_KEY_FILE_EXTENSION='ppk' + local SSH_KEY_PATH_PUTTY="$(sed -r \ + -e 's'$'\x1''SSH_KEY_FORMAT'$'\x1'"${SSH_KEY_FORMAT}"$'\x1''g' \ + -e 's'$'\x1''SSH_KEY_FILE_EXTENSION'$'\x1'"${SSH_KEY_FILE_EXTENSION}"$'\x1''g' \ + <<<"${SSH_KEY_PATH_RAW}")" + puttygen "${SSH_KEY_PATH_OPENSSH}" --old-passphrase "${TMPFILE}" -o "${SSH_KEY_PATH_PUTTY}" + SSH_KEY_FILE_LIST+=("${SSH_KEY_PATH_PUTTY}") + + for file in "${SSH_KEY_FILE_LIST[@]}"; do + chmod --verbose '0600' "${file}" + done +}