Last active
July 17, 2018 08:20
-
-
Save Leigh-/fcd1be0744a10fd4ea12a477d7e982d0 to your computer and use it in GitHub Desktop.
ColdFusion: AWS Task 1: Create a Canonical Request for Signature Version 4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!--- | |
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