2
\$\begingroup\$

Follow up on previous review: Sign an EC2/S3 URL

I have fixed the previous comments. I have also added the code needed to sign HTTP header fields as part of the request.

#!/usr/bin/env bash

url=$1
key=$2
secret=$3

if [[ $# < 3  || $# > 6 ]]; then
    echo "Usage:"
    echo "    sign <url> <key> <secret> [<TimeStamp>] [--headers <fileName>]"
    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

#
# Pull flags out of the argument list
declare -a ARGS
declare -a PARAM
declare -a PMAP
index=""
for var in "$@"; do
    if [[ "${index}" != "" ]]; then
        PMAP[$index]=${#PARAM[@]}
        PARAM[${#PARAM[@]}]=$var
        index=""
        continue
    fi  
    if [[ "$var" = '--headers' ]]; then
        index=headers
        continue
    fi  
    ARGS[${#ARGS[@]}]="$var"
done

dateTime=${ARGS[3]-$(date -u +"%Y%m%dT%H%M%SZ")}

#
# Read the list of headers.
# Create the appropriate variables for the HERE Document below
if [[ ${PARAM[headers]} ]]; then
    file=${PARAM[headers]}
    IFS=$'\n' read -d '' -r -a headers < ${file}

    crheaders=()
    headerExtra=""
    prev="host"
    headerList=""
    for loop in "${headers[@]}"; do
        name=$(tr '[A-Z]' '[a-z]' <<< "${loop%:*}")
        crheaders+=("${name}:${loop#*:}")
        headerExtra+="%3B${name}"
        headerList+=";${name}"

        if [[ "${name}" < "${prev}" ]]; then
            echo "Headers Not in Lexographical Order"
            echo "$name < ${prev}"
            exit 1
        fi
        prev=${name}
    done
fi

#
# 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##*/}
expires=3600

#
# Build the canonical request
# Note: If crheaders is empty we get an extra blank line.
#       So run through uniq to remove the blank.
IFS=$'\n'
cr=$((cat - | uniq) <<CanonicalRequest
GET
${path}
X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${key}%2F${date}%2F${region}%2F${service}%2Faws4_request&X-Amz-Date=${dateTime}&X-Amz-Expires=${expires}&X-Amz-SignedHeaders=host${headerExtra}
host:${host}
${crheaders[*]}

host${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=$(echo -n ${date}        | openssl dgst -sha256 -hmac "AWS4${secret}")
kRegnH=$(echo -n ${region}      | openssl dgst -sha256 -hmac "${kDate}")
kServH=$(echo -n ${service}     | openssl dgst -sha256 -hmac "${kRegn}")
kSignH=$(echo -n "aws4_request" | openssl dgst -sha256 -hmac "${kServ}")

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}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${key}%2F${date}%2F${region}%2F${service}%2Faws4_request&X-Amz-Date=${dateTime}&X-Amz-Expires=${expires}&X-Amz-SignedHeaders=host;x-amx-extrastuff;x-amz-stuff&X-Amz-Signature=${signature}


CURL COMMAND:
=============
IntermediateValues

echo -n "curl -o ${file} "
for loop in "${headers[@]}"; do
    echo -n "--header \"${loop}\" "
done

echo "\"${url}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${key}%2F${date}%2F${region}%2F${service}%2Faws4_request&X-Amz-Date=${dateTime}&X-Amz-Expires=${expires}&X-Amz-SignedHeaders=host;x-amx-extrastuff;x-amz-stuff&X-Amz-Signature=${signature}\""
\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$

This can be written a bit simpler:

index=""
for var in "$@"; do
    if [[ "${index}" != "" ]]; then
        PMAP[$index]=${#PARAM[@]}
        PARAM[${#PARAM[@]}]=$var
        index=""
        continue
    fi  
    if [[ "$var" = '--headers' ]]; then
        index=headers
        continue
    fi  
    ARGS[${#ARGS[@]}]="$var"
done

Like this:

index=
for var; do
    if [ "${index}" ]; then
        PMAP[$index]=${#PARAM[@]}
        PARAM+=("$var")
        index=
        continue
    fi  
    if [ "$var" = --headers ]; then
        index=headers
        continue
    fi  
    ARGS+=("$var")
done

This too:

cr=$((cat - | uniq) <<EOF
...
EOF)

Like this:

cr=$(uniq <<EOF
...
EOF
)

Though in this case I get a warning if I don't break the line after the closing EOF, and in your version I don't. I was surprised your version worked at all, because I thought the here-document must always end with a line containing only the start label and nothing else. To be honest, as a general practice, I would not risk it, and just never put anything on the same line.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.