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.