You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2017/05/31 18:03:41 UTC

[trafficserver] branch 7.1.x updated: Issue #1605 AWS Signature Version 4

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  024074b   Issue #1605 AWS Signature Version 4
024074b is described below

commit 024074be22dc35a399debe59353743dd90c33eba
Author: Gancho Tenev <gt...@gmail.com>
AuthorDate: Tue Apr 25 13:02:44 2017 -0700

    Issue #1605 AWS Signature Version 4
    
    Signature Calculations for the Authorization Header:
    Transferring Payload in a Single Chunk (Unsigned payload option)
    
    http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
    (cherry picked from commit fa731369b474cb3c46b7b8ddf98490c198cd4605)
    
     Conflicts:
    	plugins/s3_auth/s3_auth.cc
---
 doc/admin-guide/plugins/s3_auth.en.rst | 114 ++++--
 plugins/s3_auth/Makefile.inc           |   2 +-
 plugins/s3_auth/aws_auth_v4.cc         | 691 +++++++++++++++++++++++++++++++++
 plugins/s3_auth/aws_auth_v4.h          | 207 ++++++++++
 plugins/s3_auth/s3_auth.cc             | 273 ++++++++++++-
 5 files changed, 1247 insertions(+), 40 deletions(-)

diff --git a/doc/admin-guide/plugins/s3_auth.en.rst b/doc/admin-guide/plugins/s3_auth.en.rst
index d6f5709..7293188 100644
--- a/doc/admin-guide/plugins/s3_auth.en.rst
+++ b/doc/admin-guide/plugins/s3_auth.en.rst
@@ -27,57 +27,119 @@ to use ``S3`` as your origin server, yet want to avoid direct user access to
 the content.
 
 Using the plugin
-----------------
+================
 
-There are three configuration options for this plugin::
-
-    --access_key    <key>
-    --secret_key    <key>
-    --virtual_host
-    --config        <config file>
 
+Using the plugin in a remap rule would be e.g.::
 
-Using the first two in a remap rule would be e.g.::
+   # remap.config
 
    ...  @plugin=s3_auth @pparam=--access_key @pparam=my-key \
                         @pparam=--secret_key @pparam=my-secret \
 			@pparam=--virtual_host
 
 
-Alternatively, you can store the access key and secret in an external
-configuration file, and point the remap rule(s) to it:
+Alternatively, you can store the access key and secret in an external configuration file, and point the remap rule(s) to it::
 
-   ...  @plugin=s3_auth @pparam=--config @pparam=s3.config
+   # remap.config
 
+   ...  @plugin=s3_auth @pparam=--config @pparam=s3_auth_v2.config
 
-Where s3.config would look like::
 
-    # AWS S3 authentication
-        access_key=my-key
-        secret_key=my-secret
-        virtual_host=yes
+Where ``s3.config`` could look like::
 
+    # s3_auth_v2.config
 
-For more details on the S3 auth, see::
+    access_key=my-key
+    secret_key=my-secret
+    version=2
+    virtual_host=yes
 
-  http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
+Both ways could be combined as well
 
 
-ToDo
-----
+AWS Authentication version 4
+============================
 
-This is a pretty barebone start for the S3 services, it is missing a number of features:
+The s3_auth plugin fully implements: `AWS Signing Version 4 <http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html>`_ / `Authorization Header <http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html>`_ / `Transferring Payload in a Single Chunk <http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html>`_ / Unsigned Payload Option
 
-- It does not do UTF8 encoding (as required)
+Configuration options::
 
-- It only implements the v2 authentication mechanism. For details on v4, see
+    # Mandatory options
+    --access_key=<access_id>
+    --secret_key=<key>
+    --version=4
 
-  http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
+    # Optional
+    --v4-include-headers=<comma-separated-list-of-headers-to-be-signed>
+    --v4-exclude-headers=<comma-separated-list-of-headers-not-to-be-signed>
+    --v4-region-map=region_map.config
 
-- It does not deal with canonicalization of AMZ headers.
 
-- It does not handle POST requests (but do we need to ?)
+If the following option is used then the options could be specified in a file::
+
+    --config=s3_auth_v4.config
+
+
+The ``s3_auth_v4.config`` config file could look like this::
+
+    # s3_auth_v4.config
+
+    access_key=<access_id>
+    secret_key=<secret_key>
+    version=4
+    v4-include-headers=<comma-separated-list-of-headers-to-be-signed>
+    v4-exclude-headers=<comma-separated-list-of-headers-not-to-be-signed>
+    v4-region-map=region_map.config
+
+Where the ``region_map.config`` defines the entry-point hostname to region mapping i.e.::
+
+    # region_map.config
+
+    # "us-east-1"
+    s3.amazonaws.com                     : us-east-1
+    s3-external-1.amazonaws.com          : us-east-1
+    s3.dualstack.us-east-1.amazonaws.com : us-east-1
+
+    # us-west-1
+    s3-us-west-1.amazonaws.com           : us-west-1
+    s3.dualstack.us-west-1.amazonaws.com : us-west-1
+
+    # Default region if no entry-point matches:
+    : s3.amazonaws.com
+
+If ``--v4-region-map`` is not specified the plugin defaults to the mapping defined in `"Regions and Endpoints - S3" <http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region>`_
 
+According to `Transferring Payload in a Single Chunk <http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html>`_ specification
+the ``CanonicalHeaders`` list *must* include the ``Host`` header,  the ``Content-Type`` header if present in the request and all the ``x-amz-*`` headers
+so ``--v4-include-headers`` and ``--v4-exclude-headers`` do not impact those headers and they are *always* signed.
+
+The ``Via`` and ``X-Forwarded-For`` headers are *always* excluded from the signature since they are meant to be changed by the proxies and signing them could lead to invalidation of the signatue.
+
+If ``--v4-include-headers`` is not specified all headers except those specified in ``--v4-exclude-headers`` will be signed.
+
+If ``--v4-include-headers`` is specified only the headers specified will be signed except those specified in ``--v4-exclude-headers``
+
+
+AWS Authentication version 2
+============================
+
+For more details on the S3 auth version 2 , see: `Signing and Authenticating REST Requests <http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html>`_
+
+
+There are 4 plugin configuration options for version 2::
+
+    --access_key    <access_id>
+    --secret_key    <secret_key>
+    --virtual_host
+    --config        <config file>
+    --version=2
+
+This is a pretty barebone start for the S3 services, it is missing a number of features:
+
+- It does not do UTF8 encoding (as required)
+- It does not deal with canonicalization of AMZ headers.
+- It does not handle POST requests (but do we need to ?)
 - It does not incorporate query parameters.
 
 
diff --git a/plugins/s3_auth/Makefile.inc b/plugins/s3_auth/Makefile.inc
index eb63887..7865d5e 100644
--- a/plugins/s3_auth/Makefile.inc
+++ b/plugins/s3_auth/Makefile.inc
@@ -15,4 +15,4 @@
 #  limitations under the License.
 
 pkglib_LTLIBRARIES += s3_auth/s3_auth.la
-s3_auth_s3_auth_la_SOURCES = s3_auth/s3_auth.cc
+s3_auth_s3_auth_la_SOURCES = s3_auth/s3_auth.cc s3_auth/aws_auth_v4.cc
diff --git a/plugins/s3_auth/aws_auth_v4.cc b/plugins/s3_auth/aws_auth_v4.cc
new file mode 100644
index 0000000..65c65b9
--- /dev/null
+++ b/plugins/s3_auth/aws_auth_v4.cc
@@ -0,0 +1,691 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file aws_auth_v4.cc
+ * @brief AWS Auth v4 signing utility.
+ * @see aws_auth_v4.h
+ */
+
+#include <cstring>        /* strlen() */
+#include <ctime>          /* strftime(), time(), gmtime_r() */
+#include <iomanip>        /* std::setw */
+#include <sstream>        /* std::stringstream */
+#include <openssl/sha.h>  /* SHA(), sha256_Update(), SHA256_Final, etc. */
+#include <openssl/hmac.h> /* HMAC() */
+
+#undef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+#include <iostream>
+#endif
+
+#include "aws_auth_v4.h"
+
+/**
+ * @brief Lower-case Base16 encode a character string (hexadecimal format)
+ *
+ * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ * Base16 RFC4648: https://tools.ietf.org/html/rfc4648#section-8
+ *
+ * @param in ptr to an input counted string to be base16 encoded.
+ * @param inLen input character string length
+ * @return base16 encoded string.
+ */
+String
+base16Encode(const char *in, size_t inLen)
+{
+  if (nullptr == in || inLen == 0) {
+    return {};
+  }
+
+  std::stringstream result;
+
+  const char *src    = in;
+  const char *srcEnd = in + inLen;
+
+  while (src < srcEnd) {
+    result << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>((*src) & 0xFF);
+    src++;
+  }
+  return result.str();
+}
+
+/**
+ * @brief URI-encode 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
+ *
+ * @param in string to be URI encoded
+ * @param isObjectName if true don't encode '/', keep it as it is.
+ * @return encoded string.
+ */
+String
+uriEncode(const String in, bool isObjectName)
+{
+  std::stringstream result;
+
+  for (std::string::size_type i = 0; i < in.length(); i++) {
+    if (isalnum(in[i]) || in[i] == '-' || in[i] == '_' || in[i] == '.' || in[i] == '~') {
+      /* URI encode every byte except the unreserved characters:
+       * 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'. */
+      result << in[i];
+    } else if (in[i] == ' ') {
+      /* The space character is a reserved character and must be encoded as "%20" (and not as "+"). */
+      result << "%20";
+    } else if (isObjectName && in[i] == '/') {
+      /* Encode the forward slash character, '/', everywhere except in the object key name. */
+      result << "/";
+    } else {
+      /* Letters in the hexadecimal value must be upper-case, for example "%1A". */
+      result << "%" << std::uppercase << std::setfill('0') << std::setw(2) << std::hex << (int)in[i];
+    }
+  }
+
+  return result.str();
+}
+
+/**
+ * @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
+ * @param inLen input character count
+ * @param newLen trimmed string character count.
+ * @return pointer to the trimmed string.
+ */
+const char *
+trimWhiteSpaces(const char *in, size_t inLen, size_t &newLen)
+{
+  if (nullptr == in || inLen == 0) {
+    return in;
+  }
+
+  const char *first = in;
+  while (size_t(first - in) < inLen && isspace(*first)) {
+    first++;
+  }
+
+  const char *last = in + inLen - 1;
+  while (last > in && isspace(*last)) {
+    last--;
+  }
+
+  newLen = last - first + 1;
+  return first;
+}
+
+/**
+ * @brief Trim white spaces from beginning and end.
+ * @returns trimmed string
+ */
+String
+trimWhiteSpaces(const String &s)
+{
+  /* @todo do this better? */
+  static const String whiteSpace = " \t\n\v\f\r";
+  size_t start                   = s.find_first_not_of(whiteSpace);
+  if (String::npos == start) {
+    return String();
+  }
+  size_t stop = s.find_last_not_of(whiteSpace);
+  return s.substr(start, stop - start + 1);
+}
+
+/*
+ * Group of static inline helper function for less error prone parameter handling and unit test logging.
+ */
+inline static void
+sha256Update(SHA256_CTX *ctx, const char *in, size_t inLen)
+{
+  SHA256_Update(ctx, in, inLen);
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+  std::cout << String(in, inLen);
+#endif
+}
+
+inline static void
+sha256Update(SHA256_CTX *ctx, const char *in)
+{
+  sha256Update(ctx, in, strlen(in));
+}
+
+inline static void
+sha256Update(SHA256_CTX *ctx, const String &in)
+{
+  sha256Update(ctx, in.c_str(), in.length());
+}
+
+inline static void
+sha256Final(unsigned char hex[SHA256_DIGEST_LENGTH], SHA256_CTX *ctx)
+{
+  SHA256_Final(hex, ctx);
+}
+
+/**
+ * @brief: Payload SHA 256 = Hex(SHA256Hash(<payload>) (no new-line char at end)
+ *
+ * @todo support for signing of PUSH, POST content / payload
+ * @param signPayload specifies whether the content / payload should be signed
+ * @return signature of the content or "UNSIGNED-PAYLOAD" to mark that the payload is not signed
+ */
+String
+getPayloadSha256(bool signPayload)
+{
+  static const String UNSIGNED_PAYLOAD("UNSIGNED-PAYLOAD");
+
+  if (!signPayload) {
+    return UNSIGNED_PAYLOAD;
+  }
+
+  unsigned char payloadHash[SHA256_DIGEST_LENGTH];
+  SHA256((const unsigned char *)"", 0, payloadHash); /* empty content */
+
+  return base16Encode((char *)payloadHash, SHA256_DIGEST_LENGTH);
+}
+
+/**
+ * @brief Get Canonical Uri SHA256 Hash
+ *
+ * Hex(SHA256Hash(<CanonicalRequest>))
+ * AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ *
+ * @param api an TS API wrapper that will provide interface to HTTP request elements (method, path, query, headers, etc).
+ * @param signPayload specifies if the content / payload should be signed.
+ * @param includeHeaders headers that must be signed
+ * @param excludeHeaders headers that must not be signed
+ * @param signedHeaders a reference to a string to which the signed headers names will be appended
+ * @return SHA256 hash of the canonical request.
+ */
+String
+getCanonicalRequestSha256Hash(TsInterface &api, bool signPayload, const StringSet &includeHeaders, const StringSet &excludeHeaders,
+                              String &signedHeaders)
+{
+  int length;
+  const char *str = nullptr;
+  unsigned char canonicalRequestSha256Hash[SHA256_DIGEST_LENGTH];
+  SHA256_CTX canonicalRequestSha256Ctx;
+
+  SHA256_Init(&canonicalRequestSha256Ctx);
+
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+  std::cout << "<CanonicalRequest>";
+#endif
+
+  /* <HTTPMethod>\n */
+  str = api.getMethod(&length);
+  sha256Update(&canonicalRequestSha256Ctx, str, length);
+  sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+  /* URI Encoded Canonical URI
+   * <CanonicalURI>\n */
+  str = api.getPath(&length);
+  String path("/");
+  path.append(str, length);
+  String canonicalUri = uriEncode(path, /* isObjectName */ true);
+  sha256Update(&canonicalRequestSha256Ctx, canonicalUri);
+  sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+  /* Sorted Canonical Query String
+   * <CanonicalQueryString>\n */
+  const char *query = api.getQuery(&length);
+
+  StringSet paramNames;
+  StringMap paramsMap;
+  std::istringstream istr(String(query, length));
+  String token;
+  StringSet container;
+
+  while (std::getline(istr, token, '&')) {
+    String::size_type pos(token.find_first_of('='));
+    String param(token.substr(0, pos == String::npos ? token.size() : pos));
+    String value(pos == String::npos ? "" : token.substr(pos + 1, token.size()));
+
+    String encodedParam = uriEncode(param, /* isObjectName */ false);
+
+    paramNames.insert(encodedParam);
+    paramsMap[encodedParam] = uriEncode(value, /* isObjectName */ false);
+  }
+
+  String queryStr;
+  for (StringSet::iterator it = paramNames.begin(); it != paramNames.end(); it++) {
+    if (!queryStr.empty()) {
+      queryStr.append("&");
+    }
+    queryStr.append(*it);
+    queryStr.append("=").append(paramsMap[*it]);
+  }
+  sha256Update(&canonicalRequestSha256Ctx, queryStr);
+  sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+  /* Sorted Canonical Headers
+   *  <CanonicalHeaders>\n */
+  StringSet signedHeadersSet;
+  StringMap headersMap;
+
+  for (HeaderIterator it = api.headerBegin(); it != api.headerEnd(); it++) {
+    int nameLen;
+    int valueLen;
+    const char *name  = it.getName(&nameLen);
+    const char *value = it.getValue(&valueLen);
+
+    if (nullptr == name || 0 == nameLen) {
+      continue;
+    }
+
+    String lowercaseName(name, nameLen);
+    std::transform(lowercaseName.begin(), lowercaseName.end(), lowercaseName.begin(), ::tolower);
+
+    /* Host, content-type and x-amx-* headers are mandatory */
+    bool xAmzHeader        = (lowercaseName.length() >= X_AMZ.length() && 0 == lowercaseName.compare(0, X_AMZ.length(), X_AMZ));
+    bool contentTypeHeader = (0 == CONTENT_TYPE.compare(lowercaseName));
+    bool hostHeader        = (0 == HOST.compare(lowercaseName));
+    if (!xAmzHeader && !contentTypeHeader && !hostHeader) {
+      /* Skip internal headers (starting with '@'*/
+      if ('@' == name[0] /* exclude internal headers */) {
+        continue;
+      }
+
+      /* @todo do better here, since iterating over the headers in ATS is known to be less efficient,
+       * come up with a better way if include headers set is non-empty */
+      bool include =
+        (!includeHeaders.empty() && includeHeaders.end() != includeHeaders.find(lowercaseName)); /* requested to be included */
+      bool exclude =
+        (!excludeHeaders.empty() && excludeHeaders.end() != excludeHeaders.find(lowercaseName)); /* requested to be excluded */
+
+      if ((includeHeaders.empty() && exclude) || (!includeHeaders.empty() && (!include || exclude))) {
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+        std::cout << "ignore header: " << String(name, nameLen) << std::endl;
+#endif
+        continue;
+      }
+    }
+
+    size_t trimValueLen   = 0;
+    const char *trimValue = trimWhiteSpaces(value, valueLen, trimValueLen);
+
+    signedHeadersSet.insert(lowercaseName);
+    headersMap[lowercaseName] = String(trimValue, trimValueLen);
+  }
+
+  for (StringSet::iterator it = signedHeadersSet.begin(); it != signedHeadersSet.end(); it++) {
+    sha256Update(&canonicalRequestSha256Ctx, *it);
+    sha256Update(&canonicalRequestSha256Ctx, ":");
+    sha256Update(&canonicalRequestSha256Ctx, headersMap[*it]);
+    sha256Update(&canonicalRequestSha256Ctx, "\n");
+  }
+  sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+  for (StringSet::iterator it = signedHeadersSet.begin(); it != signedHeadersSet.end(); ++it) {
+    if (!signedHeaders.empty()) {
+      signedHeaders.append(";");
+    }
+    signedHeaders.append(*it);
+  }
+
+  sha256Update(&canonicalRequestSha256Ctx, signedHeaders);
+  sha256Update(&canonicalRequestSha256Ctx, "\n");
+
+  /* Hex(SHA256Hash(<payload>) (no new-line char at end)
+   * @TODO support non-empty content, i.e. POST */
+  String payloadSha256Hash = getPayloadSha256(signPayload);
+  sha256Update(&canonicalRequestSha256Ctx, payloadSha256Hash);
+
+  /* Hex(SHA256Hash(<CanonicalRequest>)) */
+  sha256Final(canonicalRequestSha256Hash, &canonicalRequestSha256Ctx);
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+  std::cout << "</CanonicalRequest>" << std::endl;
+#endif
+  return base16Encode((char *)canonicalRequestSha256Hash, SHA256_DIGEST_LENGTH);
+}
+
+/**
+ * @brief Default AWS entry-point host name to region based on (S3):
+ *
+ * @see http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
+ * it is used to get the region programmatically  w/o configuration
+ * parameters and can (meant to) be overwritten if necessary.
+ */
+const StringMap
+createDefaultRegionMap()
+{
+  StringMap m;
+  /* us-east-2 */
+  m["s3.us-east-2.amazonaws.com"]           = "us-east-2";
+  m["s3-us-east-2.amazonaws.com"]           = "us-east-2";
+  m["s3.dualstack.us-east-2.amazonaws.com"] = "us-east-2";
+  /* "us-east-1" */
+  m["s3.amazonaws.com"]                     = "us-east-1";
+  m["s3-external-1.amazonaws.com"]          = "us-east-1";
+  m["s3.dualstack.us-east-1.amazonaws.com"] = "us-east-1";
+  /* us-west-1 */
+  m["s3-us-west-1.amazonaws.com"]           = "us-west-1";
+  m["s3.dualstack.us-west-1.amazonaws.com"] = "us-west-1";
+  /* us-west-2 */
+  m["s3-us-west-2.amazonaws.com"]           = "us-west-2";
+  m["s3.dualstack.us-west-2.amazonaws.com"] = "us-west-2";
+  /* ca-central-1 */
+  m["s3.ca-central-1.amazonaws.com"]           = "ca-central-1";
+  m["s3-ca-central-1.amazonaws.com"]           = "ca-central-1";
+  m["s3.dualstack.ca-central-1.amazonaws.com"] = "ca-central-1";
+  /* ap-south-1 */
+  m["s3.ap-south-1.amazonaws.com"]           = "ap-south-1";
+  m["s3-ap-south-1.amazonaws.com"]           = "ap-south-1";
+  m["s3.dualstack.ap-south-1.amazonaws.com"] = "ap-south-1";
+  /* ap-northeast-2 */
+  m["s3.ap-northeast-2.amazonaws.com"]           = "ap-northeast-2";
+  m["s3-ap-northeast-2.amazonaws.com"]           = "ap-northeast-2";
+  m["s3.dualstack.ap-northeast-2.amazonaws.com"] = "ap-northeast-2";
+  /* ap-southeast-1 */
+  m["s3-ap-southeast-1.amazonaws.com"]           = "ap-southeast-1";
+  m["s3.dualstack.ap-southeast-1.amazonaws.com"] = "ap-southeast-1";
+  /* ap-southeast-2 */
+  m["s3-ap-southeast-2.amazonaws.com"]           = "ap-southeast-2";
+  m["s3.dualstack.ap-southeast-2.amazonaws.com"] = "ap-southeast-2";
+  /* ap-northeast-1 */
+  m["s3-ap-northeast-1.amazonaws.com"]           = "ap-northeast-1";
+  m["s3.dualstack.ap-northeast-1.amazonaws.com"] = "ap-northeast-1";
+  /* eu-central-1 */
+  m["s3.eu-central-1.amazonaws.com"]           = "eu-central-1";
+  m["s3-eu-central-1.amazonaws.com"]           = "eu-central-1";
+  m["s3.dualstack.eu-central-1.amazonaws.com"] = "eu-central-1";
+  /* eu-west-1 */
+  m["s3-eu-west-1.amazonaws.com"]           = "eu-central-1";
+  m["s3.dualstack.eu-west-1.amazonaws.com"] = "eu-central-1";
+  /* eu-west-2 */
+  m["s3.eu-west-2.amazonaws.com"]           = "eu-west-2";
+  m["s3-eu-west-2.amazonaws.com"]           = "eu-west-2";
+  m["s3.dualstack.eu-west-2.amazonaws.com"] = "eu-west-2";
+  /* sa-east-1 */
+  m["s3-sa-east-1.amazonaws.com"]           = "sa-east-1";
+  m["s3.dualstack.sa-east-1.amazonaws.com"] = "sa-east-1";
+  /* default "us-east-1" * */
+  m[""] = "us-east-1";
+  return m;
+}
+const StringMap defaultDefaultRegionMap = createDefaultRegionMap();
+
+/**
+ * @description default list of headers to be excluded from the signing
+ */
+const StringSet
+createDefaultExcludeHeaders()
+{
+  StringSet m;
+  /* exclude headers that are meant to be changed */
+  m.insert("x-forwarded-for");
+  m.insert("via");
+  return m;
+}
+const StringSet defaultExcludeHeaders = createDefaultExcludeHeaders();
+
+/**
+ * @description default list of headers to be included in the signing
+ */
+const StringSet
+createDefaultIncludeHeaders()
+{
+  StringSet m;
+  return m;
+}
+const StringSet defaultIncludeHeaders = createDefaultIncludeHeaders();
+
+/**
+ * @brief Get AWS (S3) region from the entry-point
+ *
+ * @see Implementation based on the following:
+ *   http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
+ *   http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
+ *
+ * @param regionMap map containing entry-point to region mapping
+ * @param entryPoint entry-point name
+ * @param entryPointLen - entry point string length
+ */
+String
+getRegion(const StringMap &regionMap, const char *entryPoint, size_t entryPointLen)
+{
+  String region;
+  size_t dot = String::npos;
+  String hostname(entryPoint, entryPointLen);
+
+  /* Start looking for a match from the top-level domain backwards to keep the mapping generic
+   * (so we can override it if we need later) */
+  do {
+    String name;
+    dot = hostname.rfind('.', dot - 1);
+    if (String::npos != dot) {
+      name = hostname.substr(dot + 1);
+    } else {
+      name = hostname;
+    }
+    if (regionMap.end() != regionMap.find(name)) {
+      region = regionMap.at(name);
+      break;
+    }
+  } while (String::npos != dot);
+
+  if (region.empty() && regionMap.end() != regionMap.find("")) {
+    region = regionMap.at(""); /* default region if nothing matches */
+  }
+
+  return region;
+}
+
+/**
+ * @brief Constructs the string to sign
+ *
+ * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+
+ * @param entryPoint  entry-point name
+ * @param entryPointLen entry-point name length
+ * @param dateTime - ISO 8601 time
+ * @param dateTimeLen - ISO 8601 time length
+ * @param region AWS region name
+ * @param region AWS region name length
+ * @param service service name
+ * @param serviceLen service name length
+ * @param sha256Hash canonical request SHA 256 hash
+ * @param sha256HashLen canonical request SHA 256 hash length
+ * @returns the string to sign
+ */
+String
+getStringToSign(const char *entryPoint, size_t EntryPointLen, const char *dateTime, size_t dateTimeLen, const char *region,
+                size_t regionLen, const char *service, size_t serviceLen, const char *sha256Hash, size_t sha256HashLen)
+{
+  String stringToSign;
+
+  /* AWS4-HMAC-SHA256\n (hard-coded, other values? */
+  stringToSign.append("AWS4-HMAC-SHA256\n");
+
+  /* time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>\n */
+  stringToSign.append(dateTime, dateTimeLen);
+  stringToSign.append("\n");
+
+  /* Scope: date.Format(<YYYYMMDD>) + "/" + <region> + "/" + <service> + "/aws4_request" */
+  stringToSign.append(dateTime, 8); /* Get only the YYYYMMDD */
+  stringToSign.append("/");
+  stringToSign.append(region, regionLen);
+  stringToSign.append("/");
+  stringToSign.append(service, serviceLen);
+  stringToSign.append("/aws4_request\n");
+  stringToSign.append(sha256Hash, sha256HashLen);
+
+  return stringToSign;
+}
+
+/**
+ * @brief Calculates the final signature based on the following parameters and base16 encodes it.
+ *
+ * signing key = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + "<awsSecret>", <dateTime>),
+ *                   <awsRegion>), <awsService>),"aws4_request")
+ *
+ * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
+ *
+ * @param awsSecret AWS secret
+ * @param awsSecretLen AWS secret length
+ * @param awsRegion AWS region
+ * @param awsRegionLen AWS region length
+ * @param awsService AWS Service name
+ * @param awsServiceLen AWS service name length
+ * @param dateTime ISO8601 date/time
+ * @param dateTimeLen ISO8601 date/time length
+ * @param stringToSign string to sign
+ * @param stringToSignLen length of the string to sign
+ * @param base16Signature output buffer where the base16 signature will be stored
+ * @param base16SignatureLen size of the signature buffer = EVP_MAX_MD_SIZE (at least)
+ *
+ * @return number of characters written to the output buffer
+ */
+size_t
+getSignature(const char *awsSecret, size_t awsSecretLen, const char *awsRegion, size_t awsRegionLen, const char *awsService,
+             size_t awsServiceLen, const char *dateTime, size_t dateTimeLen, const char *stringToSign, size_t stringToSignLen,
+             char *signature, size_t signatureLen)
+{
+  unsigned int dateKeyLen = EVP_MAX_MD_SIZE;
+  unsigned char dateKey[EVP_MAX_MD_SIZE];
+  unsigned int dateRegionKeyLen = EVP_MAX_MD_SIZE;
+  unsigned char dateRegionKey[EVP_MAX_MD_SIZE];
+  unsigned int dateRegionServiceKeyLen = EVP_MAX_MD_SIZE;
+  unsigned char dateRegionServiceKey[EVP_MAX_MD_SIZE];
+  unsigned int signingKeyLen = EVP_MAX_MD_SIZE;
+  unsigned char signingKey[EVP_MAX_MD_SIZE];
+
+  size_t keyLen = 4 + awsSecretLen;
+  char key[keyLen];
+  strncpy(key, "AWS4", 4);
+  strncpy(key + 4, awsSecret, awsSecretLen);
+
+  unsigned int len = signatureLen;
+  if (HMAC(EVP_sha256(), key, keyLen, (unsigned char *)dateTime, dateTimeLen, dateKey, &dateKeyLen) &&
+      HMAC(EVP_sha256(), dateKey, dateKeyLen, (unsigned char *)awsRegion, awsRegionLen, dateRegionKey, &dateRegionKeyLen) &&
+      HMAC(EVP_sha256(), dateRegionKey, dateRegionKeyLen, (unsigned char *)awsService, awsServiceLen, dateRegionServiceKey,
+           &dateRegionServiceKeyLen) &&
+      HMAC(EVP_sha256(), dateRegionServiceKey, dateRegionServiceKeyLen, (unsigned char *)"aws4_request", 12, signingKey,
+           &signingKeyLen) &&
+      HMAC(EVP_sha256(), signingKey, signingKeyLen, (unsigned char *)stringToSign, stringToSignLen, (unsigned char *)signature,
+           &len)) {
+    return len;
+  }
+
+  return 0;
+}
+
+/**
+ * @brief formats the time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>
+ */
+size_t
+getIso8601Time(time_t *now, char *dateTime, size_t dateTimeLen)
+{
+  struct tm tm;
+  return strftime(dateTime, dateTimeLen, "%Y%m%dT%H%M%SZ", gmtime_r(now, &tm));
+}
+
+/**
+ * @brief formats the time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>
+ */
+const char *
+AwsAuthV4::getDateTime(size_t *dateTimeLen)
+{
+  *dateTimeLen = sizeof(_dateTime) - 1;
+  return _dateTime;
+}
+
+/**
+ * @brief: HTTP content / payload SHA 256 = Hex(SHA256Hash(<payload>)
+ * @return signature of the content or "UNSIGNED-PAYLOAD" to mark that the payload is not signed
+ */
+String
+AwsAuthV4::getPayloadHash()
+{
+  return getPayloadSha256(_signPayload);
+}
+
+/**
+ * @brief Get the value of the Authorization header (AWS authorization) v4
+ * @return the Authorization header value
+ */
+String
+AwsAuthV4::getAuthorizationHeader()
+{
+  String signedHeaders;
+  String canonicalReq = getCanonicalRequestSha256Hash(_api, _signPayload, _includedHeaders, _excludedHeaders, signedHeaders);
+
+  int hostLen      = 0;
+  const char *host = _api.getHost(&hostLen);
+
+  String awsRegion = getRegion(_regionMap, host, hostLen);
+
+  String stringToSign = getStringToSign(host, hostLen, _dateTime, sizeof(_dateTime) - 1, awsRegion.c_str(), awsRegion.length(),
+                                        _awsService, _awsServiceLen, canonicalReq.c_str(), canonicalReq.length());
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+  std::cout << "<StringToSign>" << stringToSign << "</StringToSign>" << std::endl;
+#endif
+
+  char signature[EVP_MAX_MD_SIZE];
+  size_t signatureLen =
+    getSignature(_awsSecretAccessKey, _awsSecretAccessKeyLen, awsRegion.c_str(), awsRegion.length(), _awsService, _awsServiceLen,
+                 _dateTime, 8, stringToSign.c_str(), stringToSign.length(), signature, EVP_MAX_MD_SIZE);
+
+  String base16Signature = base16Encode(signature, signatureLen);
+#ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
+  std::cout << "<SignatureProvided>" << base16Signature << "</SignatureProvided>" << std::endl;
+#endif
+
+  std::stringstream authorizationHeader;
+  authorizationHeader << "AWS4-HMAC-SHA256 ";
+  authorizationHeader << "Credential=" << String(_awsAccessKeyId, _awsAccessKeyIdLen) << "/" << String(_dateTime, 8) << "/"
+                      << awsRegion << "/" << String(_awsService, _awsServiceLen) << "/"
+                      << "aws4_request"
+                      << ",";
+  authorizationHeader << "SignedHeaders=" << signedHeaders << ",";
+  authorizationHeader << "Signature=" << base16Signature;
+
+  return authorizationHeader.str();
+}
+
+/**
+ * @brief Authorization v4 constructor
+ *
+ * @param api wrapper providing access to HTTP request elements (URI host, path, query, headers, etc.)
+ * @param now current time-stamp
+ * @param signPayload defines if the HTTP content / payload needs to be signed
+ * @param awsAccessKeyId AWS access key ID
+ * @param awsAccessKeyIdLen AWS access key ID length
+ * @param awsSecretAccessKey AWS secret
+ * @param awsSecretAccessKeyLen AWS secret length
+ * @param awsService AWS Service name
+ * @param awsServiceLen AWS service name length
+ * @param includeHeaders set of headers to be signed
+ * @param excludeHeaders set of headers not to be signed
+ * @param regionMap entry-point to AWS region mapping
+ */
+AwsAuthV4::AwsAuthV4(TsInterface &api, time_t *now, bool signPayload, const char *awsAccessKeyId, size_t awsAccessKeyIdLen,
+                     const char *awsSecretAccessKey, size_t awsSecretAccessKeyLen, const char *awsService, size_t awsServiceLen,
+                     const StringSet &includedHeaders, const StringSet &excludedHeaders, const StringMap &regionMap)
+  : _api(api),
+    _signPayload(signPayload),
+    _awsAccessKeyId(awsAccessKeyId),
+    _awsAccessKeyIdLen(awsAccessKeyIdLen),
+    _awsSecretAccessKey(awsSecretAccessKey),
+    _awsSecretAccessKeyLen(awsSecretAccessKeyLen),
+    _awsService(awsService),
+    _awsServiceLen(awsServiceLen),
+    _includedHeaders(includedHeaders.empty() ? defaultIncludeHeaders : includedHeaders),
+    _excludedHeaders(excludedHeaders.empty() ? defaultExcludeHeaders : excludedHeaders),
+    _regionMap(regionMap.empty() ? defaultDefaultRegionMap : regionMap)
+{
+  getIso8601Time(now, _dateTime, sizeof(_dateTime));
+}
diff --git a/plugins/s3_auth/aws_auth_v4.h b/plugins/s3_auth/aws_auth_v4.h
new file mode 100644
index 0000000..1959ddf
--- /dev/null
+++ b/plugins/s3_auth/aws_auth_v4.h
@@ -0,0 +1,207 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file aws_auth_v4.h
+ * @brief AWS Auth v4 signing utility.
+ * @see aws_auth_v4.cc
+ */
+
+#ifndef PLUGINS_S3_AUTH_AWS_AUTH_V4_CC_
+#define PLUGINS_S3_AUTH_AWS_AUTH_V4_CC_
+
+#include <algorithm> /* transform() */
+#include <cstddef>   /* soze_t */
+#include <string>    /* std::string */
+#include <sstream>   /* std::stringstream */
+#include <map>       /* std::map */
+#include <set>       /* std::set */
+
+#include <ts/ts.h>
+
+typedef std::string String;
+typedef std::set<std::string> StringSet;
+typedef std::map<std::string, std::string> StringMap;
+
+class HeaderIterator;
+
+class TsInterface
+{
+public:
+  virtual ~TsInterface(){};
+  virtual const char *getMethod(int *length) = 0;
+  virtual const char *getHost(int *length)   = 0;
+  virtual const char *getPath(int *length)   = 0;
+  virtual const char *getQuery(int *length)  = 0;
+  virtual HeaderIterator headerBegin()       = 0;
+  virtual HeaderIterator headerEnd()         = 0;
+};
+
+/* Define a header iterator to be used in the plugin using ATS API */
+class HeaderIterator
+{
+public:
+  HeaderIterator() : _bufp(nullptr), _hdrs(TS_NULL_MLOC), _field(TS_NULL_MLOC) {}
+  HeaderIterator(TSMBuffer bufp, TSMLoc hdrs, TSMLoc field) : _bufp(bufp), _hdrs(hdrs), _field(field) {}
+  HeaderIterator(const HeaderIterator &it)
+  {
+    _bufp  = it._bufp;
+    _hdrs  = it._hdrs;
+    _field = it._field;
+  }
+  ~HeaderIterator() {}
+  HeaderIterator &
+  operator=(HeaderIterator &it)
+  {
+    _bufp  = it._bufp;
+    _hdrs  = it._hdrs;
+    _field = it._field;
+    return *this;
+  }
+  HeaderIterator &operator++()
+  {
+    /* @todo this is said to be slow in the API call comments, do something better here */
+    TSMLoc next = TSMimeHdrFieldNext(_bufp, _hdrs, _field);
+    TSHandleMLocRelease(_bufp, _hdrs, _field);
+    _field = next;
+    return *this;
+  }
+  HeaderIterator operator++(int)
+  {
+    HeaderIterator tmp(*this);
+    operator++();
+    return tmp;
+  }
+  bool
+  operator!=(const HeaderIterator &it)
+  {
+    return _bufp != it._bufp || _hdrs != it._hdrs || _field != it._field;
+  }
+  bool
+  operator==(const HeaderIterator &it)
+  {
+    return _bufp == it._bufp && _hdrs == it._hdrs && _field == it._field;
+  }
+  const char *
+  getName(int *len)
+  {
+    return TSMimeHdrFieldNameGet(_bufp, _hdrs, _field, len);
+  }
+  const char *
+  getValue(int *len)
+  {
+    return TSMimeHdrFieldValueStringGet(_bufp, _hdrs, _field, -1, len);
+  }
+  TSMBuffer _bufp;
+  TSMLoc _hdrs;
+  TSMLoc _field;
+};
+
+/* Define a API to be used in the plugin using ATS API */
+class TsApi : public TsInterface
+{
+public:
+  TsApi(TSMBuffer bufp, TSMLoc hdrs, TSMLoc url) : _bufp(bufp), _hdrs(hdrs), _url(url) {}
+  ~TsApi() {}
+  const char *
+  getMethod(int *len)
+  {
+    return TSHttpHdrMethodGet(_bufp, _hdrs, len);
+  }
+  const char *
+  getHost(int *len)
+  {
+    return TSHttpHdrHostGet(_bufp, _hdrs, len);
+  }
+  const char *
+  getPath(int *len)
+  {
+    return TSUrlPathGet(_bufp, _url, len);
+  }
+  const char *
+  getQuery(int *len)
+  {
+    return TSUrlHttpQueryGet(_bufp, _url, len);
+  }
+  HeaderIterator
+  headerBegin()
+  {
+    return HeaderIterator(_bufp, _hdrs, TSMimeHdrFieldGet(_bufp, _hdrs, 0));
+  }
+  HeaderIterator
+  headerEnd()
+  {
+    return HeaderIterator(_bufp, _hdrs, TS_NULL_MLOC);
+  }
+  TSMBuffer _bufp;
+  TSMLoc _hdrs;
+  TSMLoc _url;
+};
+
+/* S3 auth v4 utility API */
+
+static const String X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256";
+static const String X_AMX_DATE           = "x-amz-date";
+static const String X_AMZ                = "x-amz-";
+static const String CONTENT_TYPE         = "content-type";
+static const String HOST                 = "host";
+
+String trimWhiteSpaces(const String &s);
+
+template <typename ContainerType>
+void
+commaSeparateString(ContainerType &ss, const String &input, bool trim = true, bool lowerCase = true)
+{
+  std::istringstream istr(input);
+  String token;
+
+  while (std::getline(istr, token, ',')) {
+    token = trim ? trimWhiteSpaces(token) : token;
+    if (lowerCase) {
+      std::transform(token.begin(), token.end(), token.begin(), ::tolower);
+    }
+    ss.insert(ss.end(), token);
+  }
+}
+
+class AwsAuthV4
+{
+public:
+  AwsAuthV4(TsInterface &api, time_t *now, bool signPayload, const char *awsAccessKeyId, size_t awsAccessKeyIdLen,
+            const char *awsSecretAccessKey, size_t awsSecretAccessKeyLen, const char *awsService, size_t awsServiceLen,
+            const StringSet &includedHeaders, const StringSet &excludedHeaders, const StringMap &regionMap);
+  const char *getDateTime(size_t *dateTimeLen);
+  String getPayloadHash();
+  String getAuthorizationHeader();
+
+private:
+  TsInterface &_api;
+  char _dateTime[sizeof "20170428T010203Z"];
+  bool _signPayload               = false;
+  const char *_awsAccessKeyId     = nullptr;
+  size_t _awsAccessKeyIdLen       = 0;
+  const char *_awsSecretAccessKey = nullptr;
+  size_t _awsSecretAccessKeyLen   = 0;
+  const char *_awsService         = nullptr;
+  size_t _awsServiceLen           = 0;
+
+  const StringSet &_includedHeaders;
+  const StringSet &_excludedHeaders;
+  const StringMap &_regionMap;
+};
+#endif /* PLUGINS_S3_AUTH_AWS_AUTH_V4_CC_ */
diff --git a/plugins/s3_auth/s3_auth.cc b/plugins/s3_auth/s3_auth.cc
index 81eb80a..9b0c5cd 100644
--- a/plugins/s3_auth/s3_auth.cc
+++ b/plugins/s3_auth/s3_auth.cc
@@ -29,6 +29,7 @@
 #include <ctype.h>
 #include <sys/time.h>
 
+#include <fstream> /* std::ifstream */
 #include <string>
 #include <unordered_map>
 
@@ -41,6 +42,7 @@
 
 // Special snowflake here, only availbale when building inside the ATS source tree.
 #include "ts/ink_atomic.h"
+#include "aws_auth_v4.h"
 
 ///////////////////////////////////////////////////////////////////////////////
 // Some constants.
@@ -48,6 +50,91 @@
 static const char PLUGIN_NAME[] = "s3_auth";
 static const char DATE_FMT[]    = "%a, %d %b %Y %H:%M:%S %z";
 
+/**
+ * @brief Rebase a relative path onto the configuration directory.
+ */
+static String
+makeConfigPath(const String &path)
+{
+  if (path.empty() || path[0] == '/') {
+    return path;
+  }
+
+  return String(TSConfigDirGet()) + "/" + path;
+}
+
+/**
+ * @brief a helper function which loads the entry-point to region from files.
+ * @param args classname + filename in '<classname>:<filename>' format.
+ * @return true if successful, false otherwise.
+ */
+static bool
+loadRegionMap(StringMap &m, const String &filename)
+{
+  static const char *EXPECTED_FORMAT = "<s3-entry-point>:<s3-region>";
+
+  String path(makeConfigPath(filename));
+
+  std::ifstream ifstr;
+  String line;
+  unsigned lineno = 0;
+
+  ifstr.open(path.c_str());
+  if (!ifstr) {
+    TSError("[%s] failed to load s3-region map from '%s'", PLUGIN_NAME, path.c_str());
+    return false;
+  }
+
+  TSDebug(PLUGIN_NAME, "loading region mapping from '%s'", path.c_str());
+
+  m[""] = ""; /* set a default just in case if the user does not specify it */
+
+  while (std::getline(ifstr, line)) {
+    String::size_type pos;
+
+    ++lineno;
+
+    // Allow #-prefixed comments.
+    pos = line.find_first_of('#');
+    if (pos != String::npos) {
+      line.resize(pos);
+    }
+
+    if (line.empty()) {
+      continue;
+    }
+
+    std::size_t d = line.find(':');
+    if (String::npos == d) {
+      TSError("[%s] failed to parse region map string '%s', expected format: '%s'", PLUGIN_NAME, line.c_str(), EXPECTED_FORMAT);
+      return false;
+    }
+
+    String entrypoint(trimWhiteSpaces(String(line, 0, d)));
+    String region(trimWhiteSpaces(String(line, d + 1, String::npos)));
+
+    if (region.empty()) {
+      TSDebug(PLUGIN_NAME, "<s3-region> in '%s' cannot be empty (skipped), expected format: '%s'", line.c_str(), EXPECTED_FORMAT);
+      continue;
+    }
+
+    if (entrypoint.empty()) {
+      TSDebug(PLUGIN_NAME, "added default region %s", region.c_str());
+    } else {
+      TSDebug(PLUGIN_NAME, "added entry-point:%s, region:%s", entrypoint.c_str(), region.c_str());
+    }
+
+    m[entrypoint] = region;
+  }
+
+  if (m.at("").empty()) {
+    TSDebug(PLUGIN_NAME, "default region was not defined");
+  }
+
+  ifstr.close();
+  return true;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Cache for the secrets file, to avoid reading / loding them repeatedly on
 // a reload of remap.config. This gets cached for 60s (not configurable).
@@ -96,7 +183,29 @@ public:
   bool
   valid() const
   {
-    return _secret && (_secret_len > 0) && _keyid && (_keyid_len > 0) && (2 == _version);
+    /* Check mandatory parameters first */
+    if (!_secret || !(_secret_len > 0) || !_keyid || !(_keyid_len > 0) || (2 != _version && 4 != _version)) {
+      return false;
+    }
+
+    /* Optional parameters, issue warning if v2 parameters are used with v4 and vice-versa (wrong parameters are ignored anyways) */
+    if (2 == _version) {
+      if (_v4includeHeaders_modified && !_v4includeHeaders.empty()) {
+        TSError("[%s] headers are not being signed with AWS auth v2, included headers parameter ignored", PLUGIN_NAME);
+      }
+      if (_v4excludeHeaders_modified && !_v4excludeHeaders.empty()) {
+        TSError("[%s] headers are not being signed with AWS auth v2, excluded headers parameter ignored", PLUGIN_NAME);
+      }
+      if (_region_map_modified && !_region_map.empty()) {
+        TSError("[%s] region map is not used with AWS auth v2, parameter ignored", PLUGIN_NAME);
+      }
+    } else {
+      /* 4 == _version */
+      if (_virt_host_modified) {
+        TSError("[%s] virtual host not used with AWS auth v4, parameter ignored", PLUGIN_NAME);
+      }
+    }
+    return true;
   }
 
   void
@@ -131,10 +240,28 @@ public:
     }
 
     if (src->_version_modified) {
-      _version = src->_version;
+      _version          = src->_version;
+      _version_modified = true;
     }
+
     if (src->_virt_host_modified) {
-      _virt_host = src->_virt_host;
+      _virt_host          = src->_virt_host;
+      _virt_host_modified = true;
+    }
+
+    if (src->_v4includeHeaders_modified) {
+      _v4includeHeaders          = src->_v4includeHeaders;
+      _v4includeHeaders_modified = true;
+    }
+
+    if (src->_v4excludeHeaders_modified) {
+      _v4excludeHeaders          = src->_v4excludeHeaders;
+      _v4excludeHeaders_modified = true;
+    }
+
+    if (src->_region_map_modified) {
+      _region_map          = src->_region_map;
+      _region_map_modified = true;
     }
   }
 
@@ -169,6 +296,30 @@ public:
     return _keyid_len;
   }
 
+  int
+  version() const
+  {
+    return _version;
+  }
+
+  const StringSet &
+  v4includeHeaders()
+  {
+    return _v4includeHeaders;
+  }
+
+  const StringSet &
+  v4excludeHeaders()
+  {
+    return _v4excludeHeaders;
+  }
+
+  const StringMap &
+  v4RegionMap()
+  {
+    return _region_map;
+  }
+
   // Setters
   void
   set_secret(const char *s)
@@ -197,6 +348,31 @@ public:
     _version_modified = true;
   }
 
+  void
+  set_include_headers(const char *s)
+  {
+    ::commaSeparateString<StringSet>(_v4includeHeaders, s);
+    _v4includeHeaders_modified = true;
+  }
+
+  void
+  set_exclude_headers(const char *s)
+  {
+    ::commaSeparateString<StringSet>(_v4excludeHeaders, s);
+    _v4excludeHeaders_modified = true;
+
+    /* Exclude headers that are meant to be changed */
+    _v4excludeHeaders.insert("x-forwarded-for");
+    _v4excludeHeaders.insert("via");
+  }
+
+  void
+  set_region_map(const char *s)
+  {
+    loadRegionMap(_region_map, s);
+    _region_map_modified = true;
+  }
+
   // Parse configs from an external file
   bool parse_config(const std::string &filename);
 
@@ -220,6 +396,12 @@ private:
   bool _virt_host_modified = false;
   TSCont _cont             = nullptr;
   volatile int _ref_count  = 1;
+  StringSet _v4includeHeaders;
+  bool _v4includeHeaders_modified = false;
+  StringSet _v4excludeHeaders;
+  bool _v4excludeHeaders_modified = false;
+  StringMap _region_map;
+  bool _region_map_modified = false;
 };
 
 bool
@@ -268,6 +450,12 @@ S3Config::parse_config(const std::string &config_fname)
         set_version(pos2 + 8);
       } else if (0 == strncasecmp(pos2, "virtual_host", 12)) {
         set_virt_host();
+      } else if (0 == strncasecmp(pos2, "v4-include-headers=", 19)) {
+        set_include_headers(pos2 + 19);
+      } else if (0 == strncasecmp(pos2, "v4-exclude-headers=", 19)) {
+        set_exclude_headers(pos2 + 19);
+      } else if (0 == strncasecmp(pos2, "v4-region-map=", 14)) {
+        set_region_map(pos2 + 14);
       } else {
         // ToDo: warnings?
       }
@@ -289,17 +477,12 @@ S3Config::parse_config(const std::string &config_fname)
 S3Config *
 ConfigCache::get(const char *fname)
 {
-  std::string config_fname;
   struct timeval tv;
 
   gettimeofday(&tv, nullptr);
 
   // Make sure the filename is an absolute path, prepending the config dir if needed
-  if (*fname != '/') {
-    config_fname = TSConfigDirGet();
-    config_fname += "/";
-  }
-  config_fname += fname;
+  std::string config_fname = makeConfigPath(fname);
 
   auto it = _cache.find(config_fname);
 
@@ -328,7 +511,7 @@ ConfigCache::get(const char *fname)
 
     if (s3->parse_config(config_fname)) {
       _cache[config_fname] = std::make_pair(s3, tv.tv_sec);
-      TSDebug(PLUGIN_NAME, "Parsing and caching configuration from %s", config_fname.c_str());
+      TSDebug(PLUGIN_NAME, "Parsing and caching configuration from %s, version:%d", config_fname.c_str(), s3->version());
     } else {
       s3->release();
       return nullptr;
@@ -367,6 +550,8 @@ public:
     return true;
   }
 
+  TSHttpStatus authorizeV2(S3Config *s3);
+  TSHttpStatus authorizeV4(S3Config *s3);
   TSHttpStatus authorize(S3Config *s3);
   bool set_header(const char *header, int header_len, const char *val, int val_len);
 
@@ -439,6 +624,55 @@ str_concat(char *dst, size_t dst_len, const char *src, size_t src_len)
   return to_copy;
 }
 
+TSHttpStatus
+S3Request::authorize(S3Config *s3)
+{
+  TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+  switch (s3->version()) {
+  case 2:
+    status = authorizeV2(s3);
+    break;
+  case 4:
+    status = authorizeV4(s3);
+    break;
+  default:
+    break;
+  }
+  return status;
+}
+
+TSHttpStatus
+S3Request::authorizeV4(S3Config *s3)
+{
+  TsApi api(_bufp, _hdr_loc, _url_loc);
+  time_t now = time(0);
+
+  AwsAuthV4 util(api, &now, /* signPayload */ false, s3->keyid(), s3->keyid_len(), s3->secret(), s3->secret_len(), "s3", 2,
+                 s3->v4includeHeaders(), s3->v4excludeHeaders(), s3->v4RegionMap());
+  String payloadHash = util.getPayloadHash();
+  if (!set_header(X_AMZ_CONTENT_SHA256.c_str(), X_AMZ_CONTENT_SHA256.length(), payloadHash.c_str(), payloadHash.length())) {
+    return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+  }
+
+  /* set x-amz-date header */
+  size_t dateTimeLen   = 0;
+  const char *dateTime = util.getDateTime(&dateTimeLen);
+  if (!set_header(X_AMX_DATE.c_str(), X_AMX_DATE.length(), dateTime, dateTimeLen)) {
+    return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+  }
+
+  String auth = util.getAuthorizationHeader();
+  if (auth.empty()) {
+    return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+  }
+
+  if (!set_header(TS_MIME_FIELD_AUTHORIZATION, TS_MIME_LEN_AUTHORIZATION, auth.c_str(), auth.length())) {
+    return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+  }
+
+  return TS_HTTP_STATUS_OK;
+}
+
 // Method to authorize the S3 request:
 //
 // StringToSign = HTTP-VERB + "\n" +
@@ -457,7 +691,7 @@ str_concat(char *dst, size_t dst_len, const char *src, size_t src_len)
 //  Note: This assumes that the URI path has been appropriately canonicalized by remapping
 //
 TSHttpStatus
-S3Request::authorize(S3Config *s3)
+S3Request::authorizeV2(S3Config *s3)
 {
   TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
   TSMLoc host_loc = TS_NULL_MLOC, md5_loc = TS_NULL_MLOC, contype_loc = TS_NULL_MLOC;
@@ -620,6 +854,7 @@ event_handler(TSCont cont, TSEvent event, void *edata)
 {
   TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
   S3Config *s3   = static_cast<S3Config *>(TSContDataGet(cont));
+
   S3Request request(txnp);
   TSHttpStatus status  = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
   TSEvent enable_event = TS_EVENT_HTTP_CONTINUE;
@@ -684,6 +919,9 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
     {const_cast<char *>("secret_key"), required_argument, nullptr, 's'},
     {const_cast<char *>("version"), required_argument, nullptr, 'v'},
     {const_cast<char *>("virtual_host"), no_argument, nullptr, 'h'},
+    {const_cast<char *>("v4-include-headers"), required_argument, nullptr, 'i'},
+    {const_cast<char *>("v4-exclude-headers"), required_argument, nullptr, 'e'},
+    {const_cast<char *>("v4-region-map"), required_argument, nullptr, 'm'},
     {nullptr, no_argument, nullptr, '\0'},
   };
 
@@ -720,6 +958,15 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
     case 'v':
       s3->set_version(optarg);
       break;
+    case 'i':
+      s3->set_include_headers(optarg);
+      break;
+    case 'e':
+      s3->set_exclude_headers(optarg);
+      break;
+    case 'm':
+      s3->set_region_map(optarg);
+      break;
     }
 
     if (opt == -1) {
@@ -742,8 +989,8 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
 
   // Note that we don't acquire() the s3 config, it's implicit that we hold at least one ref
   *ih = static_cast<void *>(s3);
-  TSDebug(PLUGIN_NAME, "New rule: secret_key=%s, access_key=%s, virtual_host=%s", s3->secret(), s3->keyid(),
-          s3->virt_host() ? "yes" : "no");
+  TSDebug(PLUGIN_NAME, "New rule: secret_key=%s, access_key=%s, virtual_host=%s, version=%d", s3->secret(), s3->keyid(),
+          s3->virt_host() ? "yes" : "no", s3->version());
 
   return TS_SUCCESS;
 }

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