Amazon AWS4 Signature Generator

I had to write my own AWS4 signing app and last night I had to update it to work with Lucee’s Hmac output. So I thought I would share it here for anyone who finds they may need an AWS4 signing app for some reason or another. Eventually I will update it to into cfscript format. It needs to be updated to take any AWS key. The way it is right now it uses a specific stored key (SQS) vs being told what key to use.

It is created in a way to use multiple different accounts setup in the session management.

<!--- amazonAWS cfc --->
<cffunction name="getAWS4SignatureSQS" access="remote" output="no" returntype="struct">
        <cfargument name="HTTPVerb" type="string" required="true" />
        <cfargument name="HTTPRequestURI" type="string" required="true" />
        <cfargument name="RawQueryString" type="string" required="true" />
        <cfargument name="HostHeader" type="string" required="true" />
        <cfargument name="CanonicalHeaders" type="struct" required="true" />
        <cfargument name="SigningHeaders" type="string" required="true" />
        <cfargument name="Region" type="string" required="true" />
        <cfargument name="Service" type="string" required="true" />
        <cfargument name="TermString" type="string" required="true" />
        <cfargument name="Payload" type="string" required="false" default="" />
        <cfargument name="thisCompany" type="string" required="true" />



        <cfset var st = StructNew() />
        <cfset st.error = StructNew() />
        <cfset st.error.err = 0 />
        <cfset st.error.msg = '' />
        <cfset st.data = StructNew() />
        <cfset st.data.url = '' />
        <cfset st.data.date = '' />
        <cfset st.data.amzStamp = '' />
        <cfset st.data.accessToken = '' />
        <cfset st.data.signature = '' />
        <cfset st.data.hashedContent = '' />

        <cfset arN = 0 />
        <!--- get the ArrayNumber for company listed, check to shortName --->
        <cfloop from="1" to="#ArrayLen(Session.Account)#" index="i">
            <cfif Session.Account[i].shortName EQ arguments.thisCompany>
                <cfset arN = i />
                <cfbreak />
            </cfif>
        </cfloop>

        <cfif arN EQ 0 >
            <cfset st.error.err = 1 />
            <cfset st.error.msg = "Company name not found. Please send shortname." />
            <cfreturn st />
        </cfif>

        <!--- need to reorder the query string, it needs to be in Alpha Order --->
        <cfset encodedQueryString = "" />
        <!--- URI encode the string --->
        <cfloop list="#arguments.RawQueryString#" delimiters="&" index="i">
            <cfset name = listGetAt(i, 1, "=")>
            <cfset value = "">
            <!--- if this item has a value encode it --->
            <cfif listLen(i, "=") gt 1>
                <cfset value = replacelist(UrlEncodedFormat(listGetAt(i, 2, "="),"utf-8"), "%2D,%2E,%5F,%7E","-,.,_,~" )>
                <!--- old broken method <cfset value = replace(replace(replace(listGetAt(i, 2, "="), ",", "%2C", "ALL"), ":", "%3A", "ALL"), " ", "%20", "ALL")> --->
            </cfif>
            
            <!--- build the new query string with encoded values --->
            <cfset encodedQueryString = listAppend(encodedQueryString, "#name#=#value#", "&")>
        </cfloop>

        <cfset sortedQueryString = listSort(encodedQueryString, "text", "asc", "&")>

        <cfset st.data.date = dateConvert("local2Utc",Now()) />
        <cfset st.data.amzStamp = "#DateFormat(st.data.date,'yyyymmdd')#T#TimeFormat(st.data.date,'HHmmss')#Z" />
        <cfset st.data.verb = arguments.HTTPVerb />
        <cfset st.data.hashedContent = lcase(hash(arguments.Payload, "SHA-256")) />


        <cfset canMsg = "" />
        <cfset ky = structKeyArray(arguments.CanonicalHeaders) />
        <cfset error = ArraySort(ky, 'text') />     

        <cfloop from="1" to="#ArrayLen(ky)#" index="i" >
            <cfif ky[i] EQ "x-amz-content-sha256">
                <cfif arguments.Payload EQ "UNSIGNED-PAYLOAD">
                    <cfset canMsg = "#canMsg##ky[i]#:UNSIGNED-PAYLOAD#chr(10)#" />
                <cfelse>
                    <cfset canMsg = "#canMsg##ky[i]#:#lcase(hash(arguments.Payload, "SHA-256"))##chr(10)#" />   
                </cfif>
                
            <cfelseif ky[i] EQ "x-amz-date">
                <cfset canMsg = "#canMsg##ky[i]#:#st.data.amzStamp##chr(10)#" />
            <cfelse>
                <cfset canMsg = "#canMsg##ky[i]#:#arguments.CanonicalHeaders[ky[i]]##chr(10)#" />
            </cfif>
        </cfloop>

        <cfif arguments.Payload EQ "UNSIGNED-PAYLOAD">
            <cfset canonicalRequest = 
                "#arguments.HTTPVerb#" & chr(10)
                & "#arguments.HTTPRequestURI#" & chr(10)
                & "#sortedQueryString#" & chr(10)
                & "#canMsg#"
                & "" & chr(10)
                & "#arguments.SigningHeaders#" & chr(10)
                & "UNSIGNED-PAYLOAD"
            />
        <cfelse>
            <cfset canonicalRequest = 
                "#arguments.HTTPVerb#" & chr(10)
                & "#arguments.HTTPRequestURI#" & chr(10)
                & "#sortedQueryString#" & chr(10)
                & "#canMsg#"
                & "" & chr(10)
                & "#arguments.SigningHeaders#" & chr(10)
                & lcase(hash(arguments.Payload, "SHA-256"))
            />
        </cfif>

        <cfset hashedCanonicalRequest = lcase(hash(trim(canonicalRequest), "SHA-256")) />

        <cfset stringToSign = 
            "AWS4-HMAC-SHA256" & chr(10) 
            & "#st.data.amzStamp#" & chr(10) 
            & "#DateFormat(st.data.date,'yyyymmdd')#/#arguments.Region#/#arguments.Service#/#arguments.TermString#" & chr(10) 
            & "#hashedCanonicalRequest#"
        />

        <cfset kSecret = "AWS4#Session.Account[arN].SQS_AWS_Key#" />

        <cfset signingKey = Hmac(DateFormat(st.data.date,'yyyymmdd'), kSecret, "HmacSHA256") />
        <cfset signingKey = binaryDecode( signingKey, "hex" ) />

        <cfset signingKey = Hmac(arguments.Region, signingKey, "HmacSHA256") />
        <cfset signingKey = binaryDecode( signingKey, "hex" ) />

        <cfset signingKey = Hmac(arguments.Service, signingKey, "HmacSHA256") />
        <cfset signingKey = binaryDecode( signingKey, "hex" ) />

        <cfset signingKey = Hmac(arguments.TermString, signingKey, "HmacSHA256") />
        <cfset signingKey = binaryDecode( signingKey, "hex" ) />

        <cfset signature = Hmac(stringToSign, signingKey, "HmacSHA256") />
        <cfset signature = binaryDecode( signature, "hex" ) />
        <cfset signature = binaryEncode( signature, "hex" ) />

        <cfset st.data.signature = lcase(signature) />
        <cfif arguments.RawQueryString NEQ "">
            <cfset st.data.url = "https://#arguments.HostHeader#/#arguments.HTTPRequestURI#?#encodedQueryString#" />
        <cfelse>
            <cfset st.data.url = "https://#arguments.HostHeader#/#arguments.HTTPRequestURI#" />
        </cfif>

        <cfreturn st />
    </cffunction>

And here is an example call (for an SQS Receive Message):

<!--- for canical headers needed by the call --->
<cfset canSt = StructNew() />
        <cfset canSt['x-amz-target'] = "AmazonSQS.ReceiveMessage" />
        <cfset canSt['host'] = "sqs.#st.region#.amazonaws.com" />
        <cfset canSt['x-amz-date'] = "" />
<!--- the sqs receiveMessage JSON--->
        <cfset Payload = StructNew() />
        <cfset Payload.QueueUrl = st.sqsUrl />
        <cfset Payload.MaxNumberOfMessages = st.messageCount />
        <cfset Payload.AttributeNames = ["All"] />
        <cfif st.usePolling NEQ 0>
            <cfset Payload.WaitTimeSeconds = st.usePolling />
        </cfif>


        <cfset HTTPVerb = "POST" />
        <cfset HTTPRequestURI = "/" />
        <cfset RawQueryString = "" />
        <cfset HostHeader = "sqs.#st.region#.amazonaws.com" />
        <cfset CanonicalHeaders = "#canSt#" />
        <cfset SigningHeaders = "host;x-amz-date;x-amz-target" />
        <cfset SigningHeadersValue = "" />
        <cfset region = "#st.region#" /><!-- ie; us-east-1 --->
        <cfset service = "sqs" />
        <cfset termString = "aws4_request" />

        <cfset r = StructNew() />
        <cfinvoke component="amazonAWS" method="getAWS4SignatureSQS" returnvariable="r">
            <cfinvokeargument name="HTTPVerb" value="#HTTPVerb#" />
            <cfinvokeargument name="HTTPRequestURI" value="#HTTPRequestURI#" />
            <cfinvokeargument name="RawQueryString" value="#RawQueryString#" />
            <cfinvokeargument name="HostHeader" value="#HostHeader#" />
            <cfinvokeargument name="CanonicalHeaders" value="#CanonicalHeaders#" />
            <cfinvokeargument name="SigningHeaders" value="#SigningHeaders#" />
            <cfinvokeargument name="Region" value="#region#" />
            <cfinvokeargument name="Service" value="#service#" />
            <cfinvokeargument name="TermString" value="#termString#" />
            <cfinvokeargument name="Payload" value="#serializeJSON(Payload)#" />
            <cfinvokeargument name="thisCompany" value="#Session.Account[arN].shortName#" />
        </cfinvoke>

You will get all the bits needed to make an API call to the SQS Service.

I should note, this does not show it but st.message and st.usePolling need to have parseNumber() applied to there input, the cfargument will cast it to a string regardless.

Had to write your own? I required Wasabi S3 support for a non-Lucee CFML app and wrote my own UDF too, but then realized that I could have just used the aws-cfml library all along.

I see that they also have a specific service module for SQS support.