From the previous 2 versions:
Sign an EC2/S3 URL Version 2
I fixed the comments from the previous version.
Most of the code remains the same (so I have split out the new code for review). The main body of the code is provided as reference.
The main addition is allowing six flags to be used. Four of these flags allow the addition of more headers and/or query parameters from the command line. One allows you customize the expirey time. The last one allows you to pass the authentication info in the headers rather than query parameters.
I think that is all the features I need.
Enjoy.
#!/usr/bin/env bash
declare -a ARGS
declare -a headers
declare -a querys
expires=3600
signHeaders=0
for var in "$@"; do
if [[ "${var%=*}" = '--headers' ]]; then
file=${var#*=}
while IFS= read -r line; do
headers+=("$line")
done < ${file}
continue
fi
if [[ "${var%=*}" = '--header' ]]; then
keyValue=${var#*=}
headers+=(${keyValue})
continue
fi
if [[ "${var%=*}" = '--querys' ]]; then
file=${var#*=}
while IFS= read -r keyValue; do
key=${keyValue%:*}
val=${keyValue#*:}
querys+=("${key}=${value}")
done < ${file}
continue
fi
if [[ "${var%=*}" = '--query' ]]; then
keyValue=${var#*=}
key=${keyValue%:*}
val=${keyValue#*:}
querys+=("${key}=${value}")
continue
fi
if [[ "${var}" = '--signHeaders' ]]; then
signHeaders=1
continue
fi
if [[ "${var}" = '--expires' ]]; then
expires=${var#*=}
continue
fi
ARGS+=("$var")
done
if [[ ${#ARGS[@]} < 3 || ${#ARGS[@]} > 4 ]]; then
echo "Usage:"
echo " sign <url> <key> <secret> [<TimeStamp>] [--headers=<fileName>] [--header=<Key>:<Value>] [--querys=<fileName>] [--query=<Key>:<Value>] [--signHeaders] [--expires=<TimeInSecs>]"
echo
echo "Note 1: TimeStamp: must be YYYYMMDD'T'hhmmsa'Z's"
echo " eg 20170901T230559Z"
echo " YYYY => Year MM => Month DD => day hh => hour mm => minute ss => second"
echo
echo "Note 2: Currently does not support URL with Query or Fragment sections."
exit 1
fi
url=${ARGS[0]}
key=${ARGS[1]}
secret=${ARGS[2]}
dateTime=${ARGS[3]-$(date -u +"%Y%m%dT%H%M%SZ")}
This puts all the query parameters and headers into two arrays. We use these arrays to build all the parts we need:
headers+=("Host:${host}")
if [[ "${signHeaders}" == "1" ]]; then
headers+=("X-Amz-Date:${dateTime}")
headers+=("X-Amz-Content-SHA256:UNSIGNED-PAYLOAD")
fi
crheaders=()
headerExtra=""
headerList=""
for loop in "${headers[@]}"; do
name=$(tr '[A-Z]' '[a-z]' <<< "${loop%:*}")
crheaders+=("${name}:${loop#*:}")
done
IFS=$'\n' sortedHeaders=($(sort <<<"${crheaders[*]}"))
for loop in "${sortedHeaders[@]}"; do
name=${loop%:*}
headerExtra+="%3B${name}"
headerList+=";${name}"
done
if [[ "${signHeaders}" == "0" ]]; then
querys+=("X-Amz-Algorithm=AWS4-HMAC-SHA256")
querys+=("X-Amz-Credential=${key}%2F${date}%2F${region}%2F${service}%2Faws4_request")
querys+=("X-Amz-Date=${dateTime}")
querys+=("X-Amz-Expires=${expires}")
querys+=("X-Amz-SignedHeaders=${headerExtra#%3B}")
fi
IFS=$'\n' sortedQuerys=($(sort <<<"${querys[*]}"))
queryParam=""
querySep='?'
for loop in "${sortedQuerys[@]}"; do
queryParam+="${querySep}${loop}"
querySep='&'
done
The full code is available here for reference. But this has mostly been reviewed. If you want to or have comments then please feel free to read it and comment on it.
All The Code
#!/usr/bin/env bash
declare -a ARGS
declare -a headers
declare -a querys
expires=3600
signHeaders=0
for var in "$@"; do
if [[ "${var%=*}" = '--headers' ]]; then
file=${var#*=}
while IFS= read -r line; do
headers+=("$line")
done < ${file}
continue
fi
if [[ "${var%=*}" = '--header' ]]; then
keyValue=${var#*=}
headers+=(${keyValue})
continue
fi
if [[ "${var%=*}" = '--querys' ]]; then
file=${var#*=}
while IFS= read -r keyValue; do
key=${keyValue%:*}
val=${keyValue#*:}
querys+=("${key}=${val}")
done < ${file}
continue
fi
if [[ "${var%=*}" = '--query' ]]; then
keyValue=${var#*=}
key=${keyValue%:*}
val=${keyValue#*:}
querys+=("${key}=${val}")
continue
fi
if [[ "${var}" = '--signHeaders' ]]; then
signHeaders=1
continue
fi
if [[ "${var}" = '--expires' ]]; then
expires=${var#*=}
continue
fi
ARGS+=("$var")
done
if [[ ${#ARGS[@]} < 3 || ${#ARGS[@]} > 4 ]]; then
echo "Usage:"
echo " sign <url> <key> <secret> [<TimeStamp>] [--headers=<fileName>] [--header=<Key>:<Value>] [--querys=<fileName>] [--query=<Key>:<Value>] [--signHeaders] [--expires=<TimeInSecs>] "
echo
echo "Note 1: TimeStamp: must be YYYYMMDD'T'hhmmsa'Z's"
echo " eg 20170901T230559Z"
echo " YYYY => Year MM => Month DD => day hh => hour mm => minute ss => second"
echo
echo "Note 2: Currently does not support URL with Query or Fragment sections."
exit 1
fi
url=${ARGS[0]}
key=${ARGS[1]}
secret=${ARGS[2]}
dateTime=${ARGS[3]-$(date -u +"%Y%m%dT%H%M%SZ")}
#
# The First part of <dateTime> before the T
date=${dateTime%%T*}
#
# Amazon Hosted URLS are built up in sections.
# http://<service>-<region>.<Amazon End Point><Path>
#
# Strip out these parts from the url
urlNoSchema=${url#https://}
host=${urlNoSchema%%/*}
serviceRegion=${host%%\.*}
service=${serviceRegion%%-*}
region=${serviceRegion#*-}
path=/${url#https://*/}
file=${path##*/}
headers+=("Host:${host}")
if [[ "${signHeaders}" == "1" ]]; then
headers+=("X-Amz-Date:${dateTime}")
headers+=("X-Amz-Content-SHA256:UNSIGNED-PAYLOAD")
fi
crheaders=()
headerExtra=""
headerList=""
for loop in "${headers[@]}"; do
name=$(tr '[A-Z]' '[a-z]' <<< "${loop%:*}")
crheaders+=("${name}:${loop#*:}")
done
IFS=$'\n' sortedHeaders=($(sort <<<"${crheaders[*]}"))
for loop in "${sortedHeaders[@]}"; do
name=${loop%:*}
headerExtra+="%3B${name}"
headerList+=";${name}"
done
if [[ "${signHeaders}" == "0" ]]; then
querys+=("X-Amz-Algorithm=AWS4-HMAC-SHA256")
querys+=("X-Amz-Credential=${key}%2F${date}%2F${region}%2F${service}%2Faws4_request")
querys+=("X-Amz-Date=${dateTime}")
querys+=("X-Amz-Expires=${expires}")
querys+=("X-Amz-SignedHeaders=${headerExtra#%3B}")
fi
IFS=$'\n' sortedQuerys=($(sort <<<"${querys[*]}"))
queryParam=""
querySep='?'
for loop in "${sortedQuerys[@]}"; do
queryParam+="${querySep}${loop}"
querySep='&'
done
#
# Build the canonical request
IFS=$'\n'
cr=$(uniq <<CanonicalRequest
GET
${path}
${queryParam#?}
${sortedHeaders[*]}
${headerList#;}
UNSIGNED-PAYLOAD
CanonicalRequest
)
#
# Hash the canonical request
HashedCanonicalRequest=$(echo -n "${cr}" | openssl dgst -sha256)
#
# Build the String to sign.
ss=$(cat - <<StringToSign
AWS4-HMAC-SHA256
${dateTime}
${date}/${region}/${service}/aws4_request
${HashedCanonicalRequest}
StringToSign
)
#
# Calculate the signature
kDate=$(echo -n ${date} | openssl dgst -sha256 -binary -hmac "AWS4${secret}")
kRegn=$(echo -n ${region} | openssl dgst -sha256 -binary -hmac "${kDate}")
kServ=$(echo -n ${service} | openssl dgst -sha256 -binary -hmac "${kRegn}")
kSign=$(echo -n "aws4_request" | openssl dgst -sha256 -binary -hmac "${kServ}")
signature=$(echo -n "${ss}" | openssl dgst -sha256 -hmac "${kSign}")
#
# Dump intermediate values to compare against language specific implementation.
kDateH=$(openssl dgst -sha256 -hmac "AWS4${secret}" <<< "${date}")
kRegnH=$(openssl dgst -sha256 -hmac "${kDate}" <<< "${region}")
kServH=$(openssl dgst -sha256 -hmac "${kRegn}" <<< "${service}")
kSignH=$(openssl dgst -sha256 -hmac "${kServ}" <<< "aws4_request")
cat - <<IntermediateValues
Intermediate Values
url: ${url}
key: ${key}
secret: ${secret}
dateTime: ${dateTime}
date: ${date}
host: ${host}
path: ${path}
service: ${service}
region: ${region}
expires: ${expires}
HashedCanonicalRequest: ${HashedCanonicalRequest}
kDate: ${kDateH}
kRegn: ${kRegnH}
kServ: ${kServH}
kSign: ${kSignH}
signature: ${signature}
Cononical Request:
==================
${cr}
Signing String:
===============
${ss}
Signed URL:
===========
${url}${queryParam}${querySep}X-Amz-Signature=${signature}
Headers:
========
IntermediateValues
printf "%s\n" ${headers[@]}
if [[ "${signHeaders}" == "1" ]]; then
echo "Authorization: AWS4-HMAC-SHA256 Credential=${key}/${date}/${region}/${service}/aws4_request, SignedHeaders=${headerList#;}, Signature=${signature}"
fi
echo
echo
echo "CURL COMMAND:"
echo "============="
echo -n "curl -o ${file} "
for loop in "${headers[@]}"; do
if [[ ${loop%:*} != "Host" ]]; then
echo -n "--header \"${loop}\" "
fi
done
if [[ "${signHeaders}" == "1" ]]; then
echo "--header \"Authorization: AWS4-HMAC-SHA256 Credential=${key}/${date}/${region}/${service}/aws4_request, SignedHeaders=${headerList#;}, Signature=${signature}\" \"${url}${queryParam}\""
else
echo "\"${url}${queryParam}${querySep}X-Amz-Signature=${signature}\""
fi