Last active
November 18, 2024 15:47
-
-
Save andsens/365e81437d47f29fcce861ed11a9114d to your computer and use it in GitHub Desktop.
Workaround for https://github.com/smallstep/cli/issues/1314
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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