This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 7.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/7.1.x by this push:
     new 6f91503  AWS auth v4: fixed query param value URI-encoding
6f91503 is described below

commit 6f91503febcc6e8b75decb7a9dc13e7f5a0cae14
Author: Gancho Tenev <gan...@apache.com>
AuthorDate: Tue Aug 29 12:28:15 2017 -0700

    AWS auth v4: fixed query param value URI-encoding
    
    It seems AWS servers do not validate the signature according the spec they 
published here:
      
http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html 
("Task 1: Create a Canonical Request”)
    
    When AWS servers validate the signature (CanonicalQueryString inside the 
CanonicalRequest)
    they seem to check if the query parameter value is already encoded and 
don’t encode if it is,
    which is nowhere mentioned neither in the spec nor in its examples.
    
    Added a check to be consistent with AWS behavior while still waiting for 
response/confirmation from AWS.
    
    (cherry picked from commit 3e20b077bb96ef7eead26e48ea9e255902453a6e)
---
 plugins/s3_auth/aws_auth_v4.cc                 | 46 ++++++++++++-
 plugins/s3_auth/unit-tests/test_aws_auth_v4.cc | 91 ++++++++++++++++++++++++++
 plugins/s3_auth/unit-tests/test_aws_auth_v4.h  |  1 +
 3 files changed, 137 insertions(+), 1 deletion(-)

diff --git a/plugins/s3_auth/aws_auth_v4.cc b/plugins/s3_auth/aws_auth_v4.cc
index 5b8708a..1475111 100644
--- a/plugins/s3_auth/aws_auth_v4.cc
+++ b/plugins/s3_auth/aws_auth_v4.cc
@@ -23,6 +23,7 @@
  */
 
 #include <cstring>        /* strlen() */
+#include <string>         /* stoi() */
 #include <ctime>          /* strftime(), time(), gmtime_r() */
 #include <iomanip>        /* std::setw */
 #include <sstream>        /* std::stringstream */
@@ -69,6 +70,9 @@ base16Encode(const char *in, size_t inLen)
  *
  * @see AWS spec: 
http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
  *
+ * @todo Consider reusing / converting to TSStringPercentEncode() using a 
custom map to account for the AWS specific rules.
+ *       Currently we don't build a library/archive so we could link with the 
unit-test binary. Also using
+ *       different sets of encode/decode functions during runtime and 
unit-testing did not seem as a good idea.
  * @param in string to be URI encoded
  * @param isObjectName if true don't encode '/', keep it as it is.
  * @return encoded string.
@@ -99,6 +103,35 @@ uriEncode(const String &in, bool isObjectName)
 }
 
 /**
+ * @brief URI-decode a character string (AWS specific version, see spec)
+ *
+ * @see AWS spec: 
http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ *
+ * @todo Consider reusing / converting to TSStringPercentDecode()
+ *       Currently we don't build a library/archive so we could link with the 
unit-test binary. Also using
+ *       different sets of encode/decode functions during runtime and 
unit-testing did not seem as a good idea.
+ * @param in string to be URI decoded
+ * @return encoded string.
+ */
+String
+uriDecode(const String in)
+{
+  std::string result;
+  result.reserve(in.length());
+  size_t i = 0;
+  while (i < in.length()) {
+    if (in[i] == '%') {
+      result += static_cast<char>(std::stoi(in.substr(i + 1, 2), nullptr, 16));
+      i += 3;
+    } else {
+      result += in[i];
+      i++;
+    }
+  }
+  return result;
+}
+
+/**
  * @brief trim the white-space character from the beginning and the end of the 
string ("in-place", just moving pointers around)
  *
  * @param in ptr to an input string
@@ -256,7 +289,18 @@ getCanonicalRequestSha256Hash(TsInterface &api, bool 
signPayload, const StringSe
     String encodedParam = uriEncode(param, /* isObjectName */ false);
 
     paramNames.insert(encodedParam);
-    paramsMap[encodedParam] = uriEncode(value, /* isObjectName */ false);
+
+    /* Look for '%' first trying to avoid as many uri-decode calls as possible.
+     * it is hard to estimate which is more likely use-case - (1) URIs with 
uri-encoded query parameter
+     * values or (2) with unencoded which defines the success of this 
optimization */
+    if (nullptr == memchr(value.c_str(), '%', value.length()) || 0 == 
uriDecode(value).compare(value)) {
+      /* Not URI-encoded */
+      paramsMap[encodedParam] = uriEncode(value, /* isObjectName */ false);
+    } else {
+      /* URI-encoded, then don't encode since AWS does not encode which is not 
mentioned in the spec,
+       * asked AWS, still waiting for confirmation */
+      paramsMap[encodedParam] = value;
+    }
   }
 
   String queryStr;
diff --git a/plugins/s3_auth/unit-tests/test_aws_auth_v4.cc 
b/plugins/s3_auth/unit-tests/test_aws_auth_v4.cc
index 4589aca..f325179 100644
--- a/plugins/s3_auth/unit-tests/test_aws_auth_v4.cc
+++ b/plugins/s3_auth/unit-tests/test_aws_auth_v4.cc
@@ -68,6 +68,39 @@ TEST_CASE("uriEncode(): encode reserved chars in an object 
name", "[AWS][auth][u
   
CHECK_FALSE(encoded.compare("%20/%21%22%23%24%25%26%27%28%29%2A%2B%2C%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E%60%7B%7C%7D"));
 }
 
+TEST_CASE("uriDecode(): decode empty input", "[AWS][auth][utility]")
+{
+  String encoded("");
+  String decoded = uriDecode(encoded);
+  CHECK(0 == decoded.length()); /* 0 encoded because of the invalid input */
+}
+
+TEST_CASE("uriDecode(): decode unreserved chars", "[AWS][auth][utility]")
+{
+  const String encoded = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                         "abcdefghijklmnopqrstuvwxyz"
+                         "0123456789"
+                         "-._~";
+  String decoded = uriDecode(encoded);
+
+  CHECK(encoded.length() == encoded.length());
+  CHECK_FALSE(decoded.compare("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                              "abcdefghijklmnopqrstuvwxyz"
+                              "0123456789"
+                              "-._~"));
+}
+
+TEST_CASE("uriDecode(): decode reserved chars", "[AWS][auth][utility]")
+{
+  const String encoded =
+    
"%20%2F%21%22%23%24%25%26%27%28%29%2A%2B%2C%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E%60%7B%7C%7D";
 /* some printable but
+                                                                               
                   reserved chars */
+  String decoded = uriDecode(encoded);
+
+  CHECK(3 * decoded.length() == encoded.length()); /* size of "%NN" = 3 */
+  CHECK_FALSE(decoded.compare(" /!\"#$%&'()*+,:;<=>?@[\\]^`{|}"));
+}
+
 /* base16Encode() 
**************************************************************************************************************
 */
 
 TEST_CASE("base16Encode(): base16 encode empty string", "[utility]")
@@ -265,15 +298,18 @@ ValidateBench(TsInterface &api, bool signPayload, time_t 
*now, const char *bench
   AwsAuthV4 util(api, now, signPayload, awsAccessKeyId, 
strlen(awsAccessKeyId), awsSecretAccessKey, strlen(awsSecretAccessKey),
                  awsService, strlen(awsService), includedHeaders, 
excludedHeaders, defaultDefaultRegionMap);
   String authorizationHeader = util.getAuthorizationHeader();
+  CAPTURE(authorizationHeader);
   CHECK_FALSE(authorizationHeader.compare(bench[0]));
 
   /* Test payload hash */
   String payloadHash = util.getPayloadHash();
+  CAPTURE(payloadHash);
   CHECK_FALSE(payloadHash.compare(bench[5]));
 
   /* Test the date time header content */
   size_t dateLen   = 0;
   const char *date = util.getDateTime(&dateLen);
+  CAPTURE(String(date, dateLen));
   CHECK_FALSE(String(date, dateLen).compare(bench[2]));
 
   /* Now test particular test points to pinpoint problems easier in case of 
regression */
@@ -281,12 +317,15 @@ ValidateBench(TsInterface &api, bool signPayload, time_t 
*now, const char *bench
   /* test the canonization of the request */
   String signedHeaders;
   String canonicalReq = getCanonicalRequestSha256Hash(api, signPayload, 
includedHeaders, excludedHeaders, signedHeaders);
+  CAPTURE(canonicalReq);
   CHECK_FALSE(canonicalReq.compare(bench[1]));
+  CAPTURE(signedHeaders);
   CHECK_FALSE(signedHeaders.compare(bench[6]));
 
   /* Test the formating of the date and time */
   char dateTime[sizeof("20170428T010203Z")];
   size_t dateTimeLen = getIso8601Time(now, dateTime, sizeof(dateTime));
+  CAPTURE(String(dateTime, dateTimeLen));
   CHECK_FALSE(String(dateTime, dateTimeLen).compare(bench[2]));
 
   /* Test the region name */
@@ -297,6 +336,7 @@ ValidateBench(TsInterface &api, bool signPayload, time_t 
*now, const char *bench
   /* Test string to sign calculation */
   String stringToSign = getStringToSign(host, hostLen, dateTime, dateTimeLen, 
awsRegion.c_str(), awsRegion.length(), awsService,
                                         strlen(awsService), 
canonicalReq.c_str(), canonicalReq.length());
+  CAPTURE(stringToSign);
   CHECK_FALSE(stringToSign.compare(bench[3]));
 
   /* Test the signature calculation */
@@ -305,6 +345,7 @@ ValidateBench(TsInterface &api, bool signPayload, time_t 
*now, const char *bench
     getSignature(awsSecretAccessKey, strlen(awsSecretAccessKey), 
awsRegion.c_str(), awsRegion.length(), awsService,
                  strlen(awsService), dateTime, 8, stringToSign.c_str(), 
stringToSign.length(), signature, EVP_MAX_MD_SIZE);
   String base16Signature = base16Encode(signature, signatureLen);
+  CAPTURE(base16Signature);
   CHECK_FALSE(base16Signature.compare(bench[4]));
 }
 
@@ -535,6 +576,56 @@ TEST_CASE("AWSAuthSpecByExample: GET Bucket List Objects, 
unsigned pay-load, exc
   ValidateBench(api, /*signePayload */ false, &now, bench, 
defaultIncludeHeaders, defaultExcludeHeaders);
 }
 
+/**
+ * Test based on 
docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ * but this time query param value is already encoded, it should not URI 
encode it twice
+ * according to AWS real behavior undocumented in the specification.
+ */
+TEST_CASE("AWSAuthSpecByExample: GET Bucket List Objects, query param value 
already URI encoded", "[AWS][auth]")
+{
+  time_t now = 1369353600; /* 5/24/2013 00:00:00 GMT */
+
+  /* Define the HTTP request elements */
+  MockTsInterface api;
+  api._method.assign("GET");
+  api._host.assign("examplebucket.s3.amazonaws.com");
+  api._path.assign("");
+  api._query.assign("key=TEST==");
+  api._headers["Host"]                 = "examplebucket.s3.amazonaws.com";
+  api._headers["x-amz-content-sha256"] = "UNSIGNED-PAYLOAD";
+  api._headers["x-amz-date"]           = "20130524T000000Z";
+
+  const char *bench[] = {
+    /* Authorization Header */
+    "AWS4-HMAC-SHA256 "
+    "Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,"
+    "SignedHeaders=host;x-amz-content-sha256;x-amz-date,"
+    
"Signature=60b410f6a0ffe09b91c2aef1f179945916b45ea215278e6b8f6cfb8d461e3706",
+    /* Canonical Request sha256 */
+    "1035b1d75dad9e94fa99fa6edc2cf7d489f38796109a132721621977737a41cc",
+    /* Date and time*/
+    "20130524T000000Z",
+    /* String to sign */
+    "AWS4-HMAC-SHA256\n"
+    "20130524T000000Z\n"
+    "20130524/us-east-1/s3/aws4_request\n"
+    "1035b1d75dad9e94fa99fa6edc2cf7d489f38796109a132721621977737a41cc",
+    /* Signature */
+    "60b410f6a0ffe09b91c2aef1f179945916b45ea215278e6b8f6cfb8d461e3706",
+    /* Payload hash */
+    "UNSIGNED-PAYLOAD",
+    /* Signed Headers */
+    "host;x-amz-content-sha256;x-amz-date",
+  };
+
+  ValidateBench(api, /*signePayload */ false, &now, bench, 
defaultIncludeHeaders, defaultExcludeHeaders);
+
+  /* Now make query param value encoded beforehand and expect the same result,
+   * it should not URI encode it twice according to AWS real behavior 
undocumented in the specification.*/
+  api._query.assign("key=TEST%3D%3D");
+  ValidateBench(api, /*signePayload */ false, &now, bench, 
defaultIncludeHeaders, defaultExcludeHeaders);
+}
+
 /* Utility parameters related tests 
********************************************************************************
 */
 
 void
diff --git a/plugins/s3_auth/unit-tests/test_aws_auth_v4.h 
b/plugins/s3_auth/unit-tests/test_aws_auth_v4.h
index e7889c5..3306b8e 100644
--- a/plugins/s3_auth/unit-tests/test_aws_auth_v4.h
+++ b/plugins/s3_auth/unit-tests/test_aws_auth_v4.h
@@ -120,6 +120,7 @@ public:
 /* Expose the following methods only to the unit tests */
 String base16Encode(const char *in, size_t inLen);
 String uriEncode(const String in, bool isObjectName = false);
+String uriDecode(const String in);
 String lowercase(const char *in, size_t inLen);
 const char *trimWhiteSpaces(const char *in, size_t inLen, size_t &newLen);
 

-- 
To stop receiving notification emails like this one, please contact
['"commits@trafficserver.apache.org" <commits@trafficserver.apache.org>'].

Reply via email to