6

(Disclaimer: This is intended as a "How-to", since I could not find any CF examples when I was implementing AWS Signature Version 4)

How do you implement Task 1: Create a Canonical Request for Signature Version 4 in CF?

Summary:

  1. Start with the HTTP request method (GET, PUT, POST, etc.), followed by a newline character.
  2. Add the canonical URI parameter, followed by a newline character.
  3. Add the canonical query string, followed by a newline character
  4. Add the canonical headers, followed by a newline character.
  5. Add the signed headers, followed by a newline character.
  6. Use a hash (digest) function like SHA256 to create a hashed value from the payload in the body of the request
  7. Construct the finished canonical request, by combining the components from each step as a single string.
  8. Create a digest (hash) of the canonical request with the same algorithm used to hash the payload.
Leigh
  • 28,765
  • 10
  • 55
  • 103

1 Answers1

5

Below is a cfscript implementation of Task 1: Create a Canonical Request for Signature Version 4

Result:

f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59

Code:

  1. Start with the HTTP request method (GET, PUT, POST, etc.)

    requestMethod = "GET";
    writeOutput("<br>requestMethod: <code>"& requestMethod &"</code>");
    
  2. Add the (encoded) canonical URI parameter, followed by a newline character.

    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>");
    
  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 ] ); 
    }
    // d) 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>");
    
  4. Add 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 ] ); 
    }
    
    // d) 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>");
    
  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 canonical header logic (step 4)
    signedHeaderString = arrayToList( sortedHeaderNames, ";" );
    writeOutput("<br>signedHeaderString: <code>"& signedHeaderString &"</code>");
    
  6. Create a hash of the payload in the body of the http/https request

    requestPayload = "";
    payloadChecksum = lcase( hash( requestPayload , "SHA256" ) );
    writeOutput("<br>payloadChecksum: <code>"& payloadChecksum &"</code>");
    
  7. Construct canonical request, by combining 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>");
    
  8. Create a digest (hash) of canonical request with same algorithm used to hash the payload

    requestDigest = lcase( hash( canonicalRequest , "SHA256" ) );
    writeOutput("<br>requestDigest: <code>"& requestDigest &"</code>");
    

UDF encodeRFC3986:

    /**
    * 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;
    }
Leigh
  • 28,765
  • 10
  • 55
  • 103