Skip to content

Instantly share code, notes, and snippets.

@andsens
Last active November 18, 2024 15:47
Show Gist options
  • Save andsens/365e81437d47f29fcce861ed11a9114d to your computer and use it in GitHub Desktop.
Save andsens/365e81437d47f29fcce861ed11a9114d to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -Eeo pipefail; shopt -s inherit_errexit
main() {
DOC="step-kms-renew - Renew a KMS backed certificate
Usage:
step-kms-renew [options] (<crt-file>|<kms-crt-uri>) <kms-key>
Options:
--context=CTX The context name to apply for the given command.
--ca-url=URI URI of the targeted Step Certificate Authority.
Defaults to the current context.
--root=PATH The path to the PEM file used as the root certificate
authority. Defaults to the current context.
--jwt-alg=ALG The hashing algorithm to use. Falls back to SHA512 for
RSA & OKP keytypes, and ES256 for EC keytypes.
--force Force the overwrite of files without asking.
--out=FILE The new certificate file path. Defaults to overwriting the
crt-file positional argument
--kms=KMS The KMS URI to use. Defaults to the one configured in the
profile of the selected context.
"
# docopt parser below, refresh this parser with `docopt.sh step-kms-renew`
# shellcheck disable=2016,2086,2317,1075
docopt() { parse() { if ${DOCOPT_DOC_CHECK:-true}; then local doc_hash;if \
doc_hash=$(printf "%s" "$DOC" | (sha256sum 2>/dev/null || shasum -a 256)); then
if [[ ${doc_hash:0:5} != "$digest" ]]; then stderr "The current usage doc \
(${doc_hash:0:5}) does not match what the parser was generated with (${digest})
Run \`docopt.sh\` to refresh the parser.";_return 70;fi;fi;fi;local root_idx=$1
shift;params=();testdepth=0;local argv=("$@") arg i o;while [[ ${#argv[@]} -gt \
0 ]]; do if [[ ${argv[0]} = "--" ]]; then for arg in "${argv[@]}"; do
params+=("a:$arg");done;break;elif [[ ${argv[0]} = --* ]]; then local \
long=${argv[0]%%=*};if ${DOCOPT_ADD_HELP:-true} && [[ $long = "--help" ]]; then
stdout "$trimmed_doc";_return 0;elif [[ ${DOCOPT_PROGRAM_VERSION:-false} != 'f'\
'alse' && $long = "--version" ]]; then stdout "$DOCOPT_PROGRAM_VERSION"
_return 0;fi;local similar=() match=false;i=0;for o in "${options[@]}"; do if \
[[ $o = *" $long "? ]]; then similar+=("$long");match=$i;break;fi;: $((i++))
done;if [[ $match = false ]]; then i=0;for o in "${options[@]}"; do if [[ $o = \
*" $long"*? ]]; then local long_match=${o#* };similar+=("${long_match% *}")
match=$i;fi;: $((i++));done;fi;if [[ ${#similar[@]} -gt 1 ]]; then error \
"${long} is not a unique prefix: ${similar[*]}?";elif [[ ${#similar[@]} -lt 1 \
]]; then error;else if [[ ${options[$match]} = *0 ]]; then if [[ ${argv[0]} = \
*=* ]]; then local long_match=${o#* };error "${long_match% *} must not have an \
argument";else params+=("$match:true");argv=("${argv[@]:1}");fi;else if [[ \
${argv[0]} = *=* ]]; then params+=("$match:${argv[0]#*=}");argv=("${argv[@]:1}")
else if [[ ${#argv[@]} -le 1 || ${argv[1]} = '--' ]]; then error "${long} \
requires argument";fi;params+=("$match:${argv[1]}");argv=("${argv[@]:2}");fi;fi
fi;elif [[ ${argv[0]} = -* && ${argv[0]} != "-" ]]; then local \
remaining=${argv[0]#-};while [[ -n $remaining ]]; do local \
short="-${remaining:0:1}";if ${DOCOPT_ADD_HELP:-true} && [[ $short = "-h" ]]; \
then stdout "$trimmed_doc";_return 0;fi;local matched=false
remaining="${remaining:1}";i=0;for o in "${options[@]}"; do if [[ $o = "$short \
"* ]]; then if [[ $o = *1 ]]; then if [[ $remaining = '' ]]; then if [[ \
${#argv[@]} -le 1 || ${argv[1]} = '--' ]]; then error "${short} requires \
argument";fi;params+=("$i:${argv[1]}");argv=("${argv[@]:1}");break 2;else
params+=("$i:$remaining");break 2;fi;else params+=("$i:true");matched=true;break
fi;fi;: $((i++));done;$matched || error;done;argv=("${argv[@]:1}");elif \
${DOCOPT_OPTIONS_FIRST:-false}; then for arg in "${argv[@]}"; do
params+=("a:$arg");done;break;else params+=("a:${argv[0]}")
argv=("${argv[@]:1}");fi;done;if ! "node_$root_idx" || [ ${#params[@]} -gt 0 \
]; then error;fi;return 0;};sequence() { local initial_params=("${params[@]}") \
node_idx;: $((testdepth++));for node_idx in "$@"; do if ! "node_$node_idx"; then
params=("${initial_params[@]}");: $((testdepth--));return 1;fi;done;if [[ \
$((--testdepth)) -eq 0 ]]; then params=("${initial_params[@]}");for node_idx \
in "$@"; do "node_$node_idx";done;fi;return 0;};choice() { local \
initial_params=("${params[@]}") best_match_idx unmatched_count node_idx;: \
$((testdepth++));for node_idx in "$@"; do if "node_$node_idx"; then if [[ -z \
$unmatched_count || ${#params[@]} -lt $unmatched_count ]]; then
best_match_idx=$node_idx;unmatched_count=${#params[@]};fi;fi
params=("${initial_params[@]}");done;: $((testdepth--));if [[ -n \
$best_match_idx ]]; then "node_$best_match_idx";return 0;fi;return 1;}
optional() { local node_idx;for node_idx in "$@"; do "node_$node_idx";done
return 0;};switch() { local i param;for i in "${!params[@]}"; do local \
param=${params[$i]};if [[ $param = "$2" || $param = "$2":* ]]; then
params=("${params[@]:0:$i}" "${params[@]:((i+1))}");[[ $testdepth -gt 0 ]] && \
return 0;if [[ $3 = true ]]; then eval "((var_$1++))" || true;else eval \
"var_$1=true";fi;return 0;elif [[ $param = a:* && $2 = a:* ]]; then return 1;fi
done;return 1;};value() { local i param;for i in "${!params[@]}"; do local \
param=${params[$i]};if [[ $param = "$2":* ]]; then params=("${params[@]:0:$i}" \
"${params[@]:((i+1))}");[[ $testdepth -gt 0 ]] && return 0;local value
value=$(printf -- "%q" "${param#*:}");if [[ $3 = true ]]; then eval \
"var_$1+=($value)";else eval "var_$1=$value";fi;return 0;fi;done;return 1;}
stdout() { printf -- "cat <<'EOM'\n%s\nEOM\n" "$1";};stderr() { printf -- "cat \
<<'EOM' >&2\n%s\nEOM\n" "$1";};error() { [[ -n $1 ]] && stderr "$1";stderr \
"$usage";_return 1;};_return() { printf -- "exit %d\n" "$1";exit "$1";};set -e
trimmed_doc=${DOC:0:886};usage=${DOC:48:70};digest=26364;options=(' --context '\
'1' ' --ca-url 1' ' --root 1' ' --jwt-alg 1' ' --force 0' ' --out 1' ' --kms 1')
node_0(){ value __context 0;};node_1(){ value __ca_url 1;};node_2(){ value \
__root 2;};node_3(){ value __jwt_alg 3;};node_4(){ switch __force 4;};node_5(){
value __out 5;};node_6(){ value __kms 6;};node_7(){ value _crt_file_ a;}
node_8(){ value _kms_crt_uri_ a;};node_9(){ value _kms_key_ a;};node_10(){
optional 0 1 2 3 4 5 6;};node_11(){ choice 7 8;};node_12(){ sequence 10 11 9;}
cat <<<' docopt_exit() { [[ -n $1 ]] && printf "%s\n" "$1" >&2;printf "%s\n" \
"${DOC:48:70}" >&2;exit 1;}';local varnames=(__context __ca_url __root \
__jwt_alg __force __out __kms _crt_file_ _kms_crt_uri_ _kms_key_) varname;for \
varname in "${varnames[@]}"; do unset "var_$varname";done;parse 12 "$@";local \
p=${DOCOPT_PREFIX:-''};for varname in "${varnames[@]}"; do unset "$p$varname"
done;eval $p'__context=${var___context:-};'$p'__ca_url=${var___ca_url:-};'$p'_'\
'_root=${var___root:-};'$p'__jwt_alg=${var___jwt_alg:-};'$p'__force=${var___fo'\
'rce:-false};'$p'__out=${var___out:-};'$p'__kms=${var___kms:-};'$p'_crt_file_='\
'${var__crt_file_:-};'$p'_kms_crt_uri_=${var__kms_crt_uri_:-};'$p'_kms_key_=${'\
'var__kms_key_:-};';local docopt_i=1;[[ $BASH_VERSION =~ ^4.3 ]] && docopt_i=2
for ((;docopt_i>0;docopt_i--)); do for varname in "${varnames[@]}"; do declare \
-p "$p$varname";done;done;}
# docopt parser above, complete command for generating this parser is `docopt.sh step-kms-renew`
eval "$(docopt "$@")"
local step_base_path authority_path profile_path
step_base_path=$(step path --base)
if [[ -n $__context ]]; then
authority_path=$step_base_path/authorities/$(jq -re --arg ctx "$__context" '.[$ctx].authority' "$(step path --base)/contexts.json")
profile_path=$step_base_path/profiles/$(jq -re --arg ctx "$__context" '.[$ctx].profile' "$(step path --base)/contexts.json")
else
authority_path=${STEPPATH:-$(step path)}
profile_path=$(step path --profile)
fi
[[ -n $__ca_url ]] || __ca_url=$(jq -re '.["ca-url"]' "$authority_path/config/defaults.json")
[[ -n $__root ]] || __root=$(jq -re '.["root"]' "$authority_path/config/defaults.json")
[[ -n $__kms ]] || __kms=$(jq -re '.["kms"]' "$profile_path/config/defaults.json")
local chain_raw chain=() cert_from_kms=false
# shellcheck disable=SC2154
if [[ -e $_crt_file_ ]]; then
chain_raw=$(cat "$_crt_file_")
else
cert_from_kms=true
chain_raw=$(step kms certificate --kms "$__kms" "$_crt_file_")
fi
chain+=("${chain_raw%%'-----END CERTIFICATE-----'*}-----END CERTIFICATE-----")
chain_raw=${chain_raw#*$'-----END CERTIFICATE-----'}
chain_raw=${chain_raw#$'\n'}
while [[ -n $chain_raw ]]; do
chain+=("${chain_raw%%'-----END CERTIFICATE-----'*}-----END CERTIFICATE-----")
chain_raw=${chain_raw#*'-----END CERTIFICATE-----'}
chain_raw=${chain_raw#$'\n'}
done
local cert_data sub
cert_data=$(step certificate inspect --format json - <<<"${chain[0]}")
sub=$(jq -r '.subject.common_name | first' <<<"$cert_data")
if [[ -z $__jwt_alg ]]; then
local kty
# shellcheck disable=SC2154
kty=$(step kms key --kms "$__kms" "$_kms_key_" | step crypto key format --jwk | jq -re .kty)
case "$kty" in
EC) __jwt_alg=ES256 ;;
RSA|OKP) __jwt_alg=SHA512 ;;
esac
fi
local header_json header payload
# shellcheck disable=SC2154
header_json=$(jq -n --arg alg "$__jwt_alg" '{
"alg": $alg,
"typ": "JWT",
"x5cInsecure": []
}')
local cert
for cert in "${chain[@]}"; do
header_json=$(jq --arg pem "$(oneline_pem <<<"$cert")" '.x5cInsecure += [$pem]' <<<"$header_json")
done
header=$(jq -cS . <<<"$header_json" | base64url)
# shellcheck disable=SC2154
payload=$(jq -n --arg ca_url "$__ca_url" --arg now "$(date +%s)" --arg sub "$sub" '$now | tonumber as $epoch | {
"aud": "\($ca_url)/1.0/renew",
"exp": ($epoch + 300),
"iat": $epoch,
"iss": "step-ca-client/1.0",
"nbf": $epoch,
"sub": $sub
}' | jq -cS . | base64url)
signature=$(step kms sign --format jws --in <(printf "%s.%s" "$header" "$payload") --kms "$__kms" "$_kms_key_")
local response
response=$(curl --silent -XPOST --cacert "$__root" -H "Authorization: Bearer $header.$payload.$signature" "$__ca_url/renew")
if jq -e '. | has("message")' <<<"$response" &>/dev/null; then
jq -re .message <<<"$response" >&2
return 1
fi
local new_chain
new_chain=$(jq -re '.certChain | join("")' <<<"$response")
if $cert_from_kms && [[ -z $out ]]; then
step kms certificate --kms "$__kms" --import <(printf "%s\n" "$new_chain") "$_crt_file_"
else
__out=${__out:-$_crt_file_}
# shellcheck disable=SC2154
if [[ -e $__out ]] && ! $__force; then
printf 'Would you like to overwrite %s [y/n]: ' "$__out" >&2
read -r yesno
[[ $yesno = y ]] || { printf 'file exists\n' >&2; return 1; }
fi
printf "%s\n" "$new_chain" >"$__out"
fi
}
oneline_pem() {
grep -v -- '-----\(BEGIN\|END\) CERTIFICATE-----' | tr -d '\n'
}
# shellcheck disable=SC2120
base64url() {
if [[ $1 = -d ]]; then
tr '_-' '/+' | base64 -w0 -d
elif [[ $# -eq 0 ]]; then
base64 -w0 | tr -d '=' | tr '/+' '_-'
else
printf "Usage: base64url [-d]" >&2
return 1
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment