feat(role): Add SSH key pair and derivatives generation function
function ssh_keysuite_gen generates an OpenSSH-formatted SSH
public-private key pair. This is the OpenSSH default key format, its
plain text file header string reads:
-----
BEGIN OPENSSH PRIVATE KEY
-----
Depending on key type it then generates all possible additional
formats for both public and private key. For all key types
currently recommended by ssh-keygen (ed25519, rsa2, ecdsa) the
function generates an RFC4716-formatted public key. This plain text
file has header string:
-----
BEGIN SSH2 PUBLIC KEY
-----
Also for all key types currently recommended by ssh-keygen the
function then converts the private key into PuTTYgen format.
Afterwards rsa2 and ecdsa keys are further processed. These steps do
not apply to ed25519 keys as no such processing is possible. For an
rsa2 key the function converts its private key into a PEM-encoded
PKCS#1 key with plain text file header string:
-----
BEGIN RSA PRIVATE KEY
-----
For an elliptic curve key (i.e. an ecdsa key) the function converts
it into a PEM-encoded Elliptic Curve key with the following plain
text file header string:
-----
BEGIN EC PRIVATE KEY
-----
For both rsa2 and ecdsa keys the function then converts the private
key to PEM-encoded PKCS#8 format marked by either one of the
following two plain text file header strings:
-----
BEGIN PRIVATE KEY
-----
or
-----
BEGIN ENCRYPTED PRIVATE KEY
-----
Also for both rsa2 and ecdsa key the function converts the public key
into SubjectPublicKeyInfo (SPKI) format which looks like so:
-----
BEGIN PUBLIC KEY
-----
This function serves as a shortcut to cover all bases for systems and
software that depend on SSH keys in specific formats.
This commit is contained in:
@@ -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 <john.doe@example.com>'"'"'' \
|
||||
'' \
|
||||
'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 <john.doe@example.com>'"'"'' \
|
||||
'' \
|
||||
'Option '"'"'rsa'"'"' without key size '"'"':<number>'"'"' 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 <john.doe@example.com>'"'"'' \
|
||||
'' \
|
||||
'Option '"'"'ecdsa'"'"' without EC length '"'"':<number>'"'"' 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 <john.doe@example.com>'"'"''}"
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user