Skip to content

Instantly share code, notes, and snippets.

@Leigh-
Last active July 17, 2018 08:20
Show Gist options
  • Save Leigh-/fcd1be0744a10fd4ea12a477d7e982d0 to your computer and use it in GitHub Desktop.
Save Leigh-/fcd1be0744a10fd4ea12a477d7e982d0 to your computer and use it in GitHub Desktop.
ColdFusion: AWS Task 1: Create a Canonical Request for Signature Version 4
<!---
CFML translation of Amazon Web Services Example - Task 1:
http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
--->
<h1>Task 1: Create a Canonical Request for Signature Version 4 (ColdFusion)</h1>
<div>
<strong>Canonical request pseudocode</strong>
<pre>CanonicalRequest = HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
HexEncode(Hash(RequestPayload))
</pre>
</div>
<div>
<strong>Example request</strong>
<pre>
GET https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08 HTTP/1.1
Host: iam.amazonaws.com
Content-Type: application/x-www-form-urlencoded; charset=utf-8
X-Amz-Date: 20150830T123600Z
</pre>
</div>
<div>
<strong>Example hashed canonical request</strong>
<pre>
f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59
</pre>
</div>
<cfscript>
canonicalRequest = "";
/*
STEP 1: Start with the HTTP request method (GET, PUT, POST, etc.), followed by a newline character.
*/
requestMethod = "GET";
writeOutput("<br>requestMethod: <code>"& requestMethod &"</code>");
/*
STEP 2: Add the (encoded) canonical URI parameter, followed by a newline character.
Note: The RFC 3986 logic was placed in a custom UDF below, as it will be used multiple times
*/
originalURI = "";
// If the absolute path is empty, use a forward slash (/)
originalURI = len(trim(originalURI)) ? originalURI : "/"& originalURI;
// Encode URI and preserve forward slashes
canonicalURI = replace( encodeRFC3986( originalURI ), "%2F", "/", "all");
writeOutput("<br>canonicalURI: <code>"& canonicalURI &"</code>");
/*
STEP 3: Add the canonical query string, followed by a newline character
*/
queryParams = { "Action"="ListUsers", "Version"="2010-05-08" };
// a) Encode parameter names and values
encodedParams = {};
structEach( queryParams, function(key, value) {
encodedParams[ encodeRFC3986(arguments.key) ] = encodeRFC3986( arguments.value);
});
// b) Sort the encoded parameter in ascending order (ASCII order)
encodedKeyNames = structKeyArray( encodedParams );
arraySort( encodedKeyNames, "text" );
// c) Build the canonical query string. Starting with first parameter, append encoded
// parameter name, followed by character '=' (ASCII code 61), followed by the encoded value
encodedPairs = [];
for (key in encodedKeyNames) {
arrayAppend( encodedPairs, key &"="& encodedParams[ key ] );
}
// e) Append the character '&' (ASCII code 38) after each parameter value, except for the last value in the list.
canonicalQueryString = arrayToList( encodedPairs, "&");
writeOutput("<br>canonicalQueryString: <code>"& canonicalQueryString &"</code>");
/*
STEP 4: Add the canonical headers, followed by a newline character.
*/
requestHeaders = { "Content-type"= "application/x-www-form-urlencoded; charset=utf-8"
, "Host" = "iam.amazonaws.com"
, "X-Amz-Date" = "20150830T123600Z"
};
// a) Convert all header names to lowercase and remove leading spaces and trailing spaces.
// Convert sequential spaces in the header value to a single space.
cleanedHeaders = {};
structEach( requestHeaders, function(key, value) {
headerName = reReplace( trim(arguments.key), "\s+", " ", "all");
headerValue = reReplace( trim(arguments.value), "\s+", " ", "all");
cleanedHeaders[ lcase(headerName) ] = headerValue;
});
// b) [sort] the (lowercase) headers by character code
sortedHeaderNames = structKeyArray( cleanedHeaders );
arraySort( sortedHeaderNames, "text" );
// c) Append the lowercase header name followed by a colon.
// Do not sort the values in headers that have multiple values.
cleanedPairs = [];
for (key in sortedHeaderNames) {
arrayAppend( cleanedPairs, key &":"& cleanedHeaders[ key ] );
}
// e) Append new line after each header pair. Should END WITH a new line
canonicalHeaderString = arrayToList( cleanedPairs, chr(10) ) & chr(10) ;
writeOutput("<br> canonicalHeaderString: <code>"& canonicalHeaderString &"</code>");
/*
STEP 5: Add the signed headers, followed by a newline character
*/
// To create the signed headers list, convert all header names to lowercase,
// sort them by character code, and use a semicolon to separate the header names.
// Note, we already have the sorted names from the step 4, canonical header logic
signedHeaderString = arrayToList( sortedHeaderNames, ";" );
writeOutput("<br>signedHeaderString: <code>"& signedHeaderString &"</code>");
/*
STEP 6: Use a hash (digest) function like SHA256 to create a hashed value
from the payload in the body of the HTTP or HTTPS request
*/
requestPayload = "";
payloadChecksum = lcase( hash( requestPayload , "SHA256" ) );
// Expected: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
writeOutput("<br>payloadChecksum: <code>"& payloadChecksum &"</code>");
/*
STEP 7: To construct the finished canonical request, combine all the
components from each step as a single string
*/
canonicalRequest = requestMethod & chr(10)
& canonicalURI & chr(10)
& canonicalQueryString & chr(10)
& canonicalHeaderString & chr(10)
& signedHeaderString & chr(10)
& payloadChecksum ;
writeOutput("<br>canonicalRequest: <pre>"& canonicalRequest &"</pre>");
/*
STEP 8: Create a digest (hash) of the canonical request with the same algorithm
that you used to hash the payload.
*/
requestDigest = lcase( hash( canonicalRequest , "SHA256" ) );
writeOutput("<br>requestDigest: <code>"& requestDigest &"</code>");
/**
* URI encoding per RFC 3986:
* <ul>
* <li>Unreserved characters that should not be escaped: ALPHA / DIGIT / "-" / "." / "_" / "~" </li>
* <li>Spaces should be encoded as %20 instead of +</li>
* <li>Reserved characters that should be escaped include: ? ## [ ] @ ! $ & ' ( ) * + , ; =</li>
* </ul>
*
* @text String to encode
* @returns URI encoded text
*/
public function encodeRFC3986(required string text) {
// Requires CF10+
Local.encoded = encodeForURL(arguments.text);
// Undo encoding of tilde "~"
Local.encoded = replace( Local.encoded, "%7E", "~", "all" );
// Change space encoding from "+" to "%20"
Local.encoded = replace( Local.encoded, "+", "%20", "all" );
// URL encode asterisk "*"
Local.encoded = replace( Local.encoded, "*", "%2A", "all" );
return Local.encoded;
}
</cfscript>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment