Browse Source

http: improve AWS HTTP v4 Signature auth

- Add support services without region and service prefixes in
the URL endpoint (ex. Min.IO, GCP, Yandex Cloud, Mail.Ru Cloud Solutions, etc)
by providing region and service parameters via aws-sigv4 option.
- Add [:region[:service]] suffix to aws-sigv4 option;
- Fix memory allocation errors.
- Refactor memory management.
- Use Curl_http_method instead() STRING_CUSTOMREQUEST.
- Refactor canonical headers generating.
- Remove repeated sha256_to_hex() usage.
- Add some docs fixes.
- Add some codestyle fixes.
- Add overloaded strndup() for debug - curl_dbg_strndup().
- Update tests.

Closes #6524
Dmitry Wagin 3 years ago
parent
commit
796ce293de

+ 8 - 1
docs/cmdline-opts/aws-sigv4.d

@@ -1,5 +1,5 @@
 Long: aws-sigv4
-Arg: <provider1[:provider2]>
+Arg: <provider1[:provider2[:region[:service]]]>
 Help: Use AWS V4 signature authentication
 Category: auth http
 Added: 7.75.0
@@ -8,3 +8,10 @@ Use AWS V4 signature authentication in the transfer.
 
 The provider argument is a string that is used by the algorithm when creating
 outgoing authentication headers.
+
+The region argument is a string that points to a geographic area of
+a resources collection (region-code) when the region name is omitted from
+the endpoint.
+
+The service argument is a string that points to a function provided by a cloud
+(service-code) when the service name is omitted from the endpoint.

+ 42 - 27
docs/libcurl/opts/CURLOPT_AWS_SIGV4.3

@@ -20,37 +20,47 @@
 .\" *
 .\" **************************************************************************
 .\"
-.TH CURLOPT_AWS_SIGV4 3 "03 Jun 2020" "libcurl 7.72.0" "curl_easy_setopt options"
+.TH CURLOPT_AWS_SIGV4 3 "03 Jun 2020" "libcurl 7.75.0" "curl_easy_setopt options"
 .SH NAME
 CURLOPT_AWS_SIGV4 \- V4 signature
 .SH SYNOPSIS
 .nf
 #include <curl/curl.h>
 
-CURLcode curl_easy_setopt(CURL *handle, CURLOPT_AWS_SIGV4,
-                          char *providers_infos);
+CURLcode curl_easy_setopt(CURL *handle, CURLOPT_AWS_SIGV4, char *param);
+.fi
 .SH DESCRIPTION
-provides AWS V4 signature authentication on HTTPS header
-
-The provider argument is a string that is merged to some authentication
-parameters use by the algorithm.
-It's used by "Algorithm", "date", "request type", "signed headers" arguments,
-
-NOTE: This call set CURLOPT_HTTPAUTH to CURLAUTH_AWS_SIGV4.
-Calling CURLOPT_HTTPAUTH with CURLAUTH_AWS_SIGV4 is the same as calling
-this with "aws:amz" in paramater.
-
-Example with "Test:Try", when curl will do the algorithm, it will Generate:
-"TEST-HMAC-SHA256" for "Algorithm"
-"x-try-date" and "X-Try-Date" for "date"
-"test4_request" for "request type"
+Provides AWS V4 signature authentication on HTTP(S) header.
+.PP
+Pass a char * that is the collection of specific arguments are used for
+creating outgoing authentication headers.
+The format of the param option is:
+.IP provider1[:provider2[:region[:service]]]
+.IP provider1,\ provider2
+The providers arguments are used for generating some authentication parameters
+such as "Algorithm", "date", "request type" and "signed headers".
+.IP region
+The argument is a geographic area of a resources collection.
+It is extracted from the host name specified in the URL if omitted.
+.IP service
+The argument is a function provided by a cloud.
+It is extracted from the host name specified in the URL if omitted.
+.PP
+NOTE: This call set \fICURLOPT_HTTPAUTH(3)\fP to CURLAUTH_AWS_SIGV4.
+Calling \fICURLOPT_HTTPAUTH(3)\fP with CURLAUTH_AWS_SIGV4 is the same
+as calling this with "aws:amz" in parameter.
+.PP
+Example with "Test:Try", when curl will do the algorithm, it will generate
+"TEST-HMAC-SHA256" for "Algorithm", "x-try-date" and "X-Try-Date" for "date",
+"test4_request" for "request type",
 "SignedHeaders=content-type;host;x-try-date" for "signed headers"
-
+.PP
 If you use just "test", instead of "test:try",
 test will be use for every strings generated
-
 .SH DEFAULT
-NULL
+By default, the value of this parameter is NULL.
+Calling \fICURLOPT_HTTPAUTH(3)\fP with CURLAUTH_AWS_SIGV4 is the same
+as calling this with "aws:amz" in parameter.
 .SH PROTOCOLS
 HTTP
 .SH EXAMPLE
@@ -61,22 +71,27 @@ struct curl_slist *list = NULL;
 
 if(curl) {
   curl_easy_setopt(curl, CURLOPT_URL,
-  "https://api_type.region.example.com/uri");
+                  "https://service.region.example.com/uri");
+  curl_easy_setopt(c, CURLOPT_AWS_SIGV4, "provider1:provider2");
+
+  /* service and region also could be set in CURLOPT_AWS_SIGV4 */
+  /*
+  curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/uri");
+  curl_easy_setopt(c, CURLOPT_AWS_SIGV4,
+                   "provider1:provider2:region:service");
+  */
 
-  curl_easy_setopt(c, CURLOPT_AWS_SIGV4, "xxx:yyy");
   curl_easy_setopt(c, CURLOPT_USERPWD, "MY_ACCESS_KEY:MY_SECRET_KEY");
   curl_easy_perform(curl);
 }
 .fi
-
 .SH AVAILABILITY
 Added in 7.75.0
-
 .SH RETURN VALUE
 Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not.
-
 .SH NOTES
-this option overrides the other auth types you might have set in CURL_HTTPAUTH which should be highlighted as this makes this auth method special. It could probably also be mentioned that this method can't be combined with other auth types.
-
+This option overrides the other auth types you might have set in CURL_HTTPAUTH
+which should be highlighted as this makes this auth method special.
+This method can't be combined with other auth types.
 .SH "SEE ALSO"
 .BR CURLOPT_HEADEROPT "(3), " CURLOPT_HTTPHEADER "(3), "

+ 2 - 2
include/curl/curl.h

@@ -787,7 +787,7 @@ typedef enum {
 #define CURLAUTH_DIGEST_IE    (((unsigned long)1)<<4)
 #define CURLAUTH_NTLM_WB      (((unsigned long)1)<<5)
 #define CURLAUTH_BEARER       (((unsigned long)1)<<6)
-#define CURLAUTH_AWS_SIGV4 (((unsigned long)1)<<7)
+#define CURLAUTH_AWS_SIGV4    (((unsigned long)1)<<7)
 #define CURLAUTH_ONLY         (((unsigned long)1)<<31)
 #define CURLAUTH_ANY          (~CURLAUTH_DIGEST_IE)
 #define CURLAUTH_ANYSAFE      (~(CURLAUTH_BASIC|CURLAUTH_DIGEST_IE))
@@ -2075,7 +2075,7 @@ typedef enum {
   CURLOPT(CURLOPT_HSTSWRITEFUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 303),
   CURLOPT(CURLOPT_HSTSWRITEDATA, CURLOPTTYPE_CBPOINT, 304),
 
-  /* Provider for V4 signature */
+  /* Parameters for V4 signature */
   CURLOPT(CURLOPT_AWS_SIGV4, CURLOPTTYPE_STRINGPOINT, 305),
 
   CURLOPT_LASTENTRY /* the last unused */

+ 270 - 210
lib/http_aws_sigv4.c

@@ -26,6 +26,7 @@
 
 #include "urldata.h"
 #include "strcase.h"
+#include "strdup.h"
 #include "vauth/vauth.h"
 #include "vauth/digest.h"
 #include "http_aws_sigv4.h"
@@ -43,22 +44,18 @@
 #include "curl_memory.h"
 #include "memdebug.h"
 
-#define HMAC_SHA256(k, kl, d, dl, o)                                    \
-  do {                                                                  \
-    if(Curl_hmacit(Curl_HMAC_SHA256, (unsigned char *)k,                \
-                   (unsigned int)kl,                                    \
-                   (unsigned char *)d,                                  \
-                   (unsigned int)dl, o) != CURLE_OK) {                  \
-      ret = CURLE_OUT_OF_MEMORY;                                        \
-      goto free_all;                                                    \
-    }                                                                   \
+#define HMAC_SHA256(k, kl, d, dl, o)        \
+  do {                                      \
+    ret = Curl_hmacit(Curl_HMAC_SHA256,     \
+                      (unsigned char *)k,   \
+                      (unsigned int)kl,     \
+                      (unsigned char *)d,   \
+                      (unsigned int)dl, o); \
+    if(ret != CURLE_OK) {                   \
+      goto fail;                            \
+    }                                       \
   } while(0)
 
-#define PROVIDER_MAX_L 16
-#define REQUEST_TYPE_L (PROVIDER_MAX_L + sizeof("4_request"))
-/* secret key is 40 bytes long + PROVIDER_MAX_L + \0 */
-#define FULL_SK_L (PROVIDER_MAX_L + 40 + 1)
-
 static void sha256_to_hex(char *dst, unsigned char *sha, size_t dst_l)
 {
   int i;
@@ -71,46 +68,43 @@ static void sha256_to_hex(char *dst, unsigned char *sha, size_t dst_l)
 
 CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
 {
-  CURLcode ret = CURLE_OK;
-  char sk[FULL_SK_L] = {0};
-  const char *customrequest = data->set.str[STRING_CUSTOMREQUEST];
-  const char *hostname = data->state.up.hostname;
-  struct tm info;
-  time_t rawtime;
-  /* aws is the default because some provider that are not amazone still use
-   * aws:amz as prefix
-   */
-  const char *provider = data->set.str[STRING_AWS_SIGV4] ?
-    data->set.str[STRING_AWS_SIGV4] : "aws:amz";
-  size_t provider_l = strlen(provider);
-  char low_provider0[PROVIDER_MAX_L + 1] = {0};
-  char low_provider[PROVIDER_MAX_L + 1] = {0};
-  char up_provider[PROVIDER_MAX_L + 1] = {0};
-  char mid_provider[PROVIDER_MAX_L + 1] = {0};
+  CURLcode ret = CURLE_OUT_OF_MEMORY;
+  struct connectdata *conn = data->conn;
+  size_t len;
+  const char *tmp0;
+  const char *tmp1;
+  char *provider0_low = NULL;
+  char *provider0_up = NULL;
+  char *provider1_low = NULL;
+  char *provider1_mid = NULL;
   char *region = NULL;
-  char *uri = NULL;
-  char *api_type = NULL;
-  char date_iso[17];
+  char *service = NULL;
+  const char *hostname = conn->host.name;
+#ifdef DEBUGBUILD
+  char *force_timestamp;
+#endif
+  time_t clock;
+  struct tm tm;
+  char timestamp[17];
   char date[9];
-  char date_str[64];
-  const char *post_data = data->set.postfields ?
-    data->set.postfields : "";
   const char *content_type = Curl_checkheaders(data, "Content-Type");
-  unsigned char sha_d[32];
-  char sha_hex[65];
-  char *cred_scope = NULL;
+  char *canonical_headers = NULL;
   char *signed_headers = NULL;
-  char request_type[REQUEST_TYPE_L];
-  char *canonical_hdr = NULL;
+  Curl_HttpReq httpreq;
+  const char *method;
+  const char *post_data = data->set.postfields ? data->set.postfields : "";
+  unsigned char sha_hash[32];
+  char sha_hex[65];
   char *canonical_request = NULL;
+  char *request_type = NULL;
+  char *credential_scope = NULL;
   char *str_to_sign = NULL;
+  const char *user = conn->user ? conn->user : "";
+  const char *passwd = conn->passwd ? conn->passwd : "";
+  char *secret = NULL;
   unsigned char tmp_sign0[32] = {0};
   unsigned char tmp_sign1[32] = {0};
-  char *auth = NULL;
-  char *tmp;
-  #ifdef DEBUGBUILD
-  char *force_timestamp;
-  #endif
+  char *auth_headers = NULL;
 
   DEBUGASSERT(!proxy);
   (void)proxy;
@@ -120,214 +114,280 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy)
     return CURLE_OK;
   }
 
-  if(content_type) {
-    content_type = strchr(content_type, ':');
-    if(!content_type)
-      return CURLE_FAILED_INIT;
-    content_type++;
-    /* Skip whitespace now */
-    while(*content_type == ' ' || *content_type == '\t')
-      ++content_type;
+  /*
+   * Parameters parsing
+   * Google and Outscale use the same OSC or GOOG,
+   * but Amazon uses AWS and AMZ for header arguments.
+   * AWS is the default because most of non-amazon providers
+   * are still using aws:amz as a prefix.
+   */
+  tmp0 = data->set.str[STRING_AWS_SIGV4] ?
+    data->set.str[STRING_AWS_SIGV4] : "aws:amz";
+  tmp1 = strchr(tmp0, ':');
+  len = tmp1 ? (size_t)(tmp1 - tmp0) : strlen(tmp0);
+  if(len < 1) {
+    infof(data, "first provider can't be empty\n");
+    ret = CURLE_BAD_FUNCTION_ARGUMENT;
+    goto fail;
   }
-
-  /* Get Parameter
-     Google and Outscale use the same OSC or GOOG,
-     but Amazon use AWS and AMZ for header arguments */
-  tmp = strchr(provider, ':');
-  if(tmp) {
-    provider_l = tmp - provider;
-    if(provider_l >= PROVIDER_MAX_L) {
-      infof(data, "v4 signature argument string too long\n");
-      return CURLE_BAD_FUNCTION_ARGUMENT;
+  provider0_low = malloc(len + 1);
+  provider0_up = malloc(len + 1);
+  if(!provider0_low || !provider0_up) {
+    goto fail;
+  }
+  Curl_strntolower(provider0_low, tmp0, len);
+  provider0_low[len] = '\0';
+  Curl_strntoupper(provider0_up, tmp0, len);
+  provider0_up[len] = '\0';
+
+  if(tmp1) {
+    tmp0 = tmp1 + 1;
+    tmp1 = strchr(tmp0, ':');
+    len = tmp1 ? (size_t)(tmp1 - tmp0) : strlen(tmp0);
+    if(len < 1) {
+      infof(data, "second provider can't be empty\n");
+      ret = CURLE_BAD_FUNCTION_ARGUMENT;
+      goto fail;
     }
-    Curl_strntolower(low_provider0, provider, provider_l);
-    Curl_strntoupper(up_provider, provider, provider_l);
-    provider = tmp + 1;
-    /* if "xxx:" was pass as parameter, tmp + 1 should point to \0 */
-    provider_l = strlen(provider);
-    if(provider_l >= PROVIDER_MAX_L) {
-      infof(data, "v4 signature argument string too long\n");
-      return CURLE_BAD_FUNCTION_ARGUMENT;
+    provider1_low = malloc(len + 1);
+    provider1_mid = malloc(len + 1);
+    if(!provider1_low || !provider1_mid) {
+      goto fail;
+    }
+    Curl_strntolower(provider1_low, tmp0, len);
+    provider1_low[len] = '\0';
+    Curl_strntolower(provider1_mid, tmp0, len);
+    provider1_mid[0] = Curl_raw_toupper(provider1_mid[0]);
+    provider1_mid[len] = '\0';
+
+    if(tmp1) {
+      tmp0 = tmp1 + 1;
+      tmp1 = strchr(tmp0, ':');
+      len = tmp1 ? (size_t)(tmp1 - tmp0) : strlen(tmp0);
+      if(len < 1) {
+        infof(data, "region can't be empty\n");
+        ret = CURLE_BAD_FUNCTION_ARGUMENT;
+        goto fail;
+      }
+      region = Curl_memdup(tmp0, len + 1);
+      if(!region) {
+        goto fail;
+      }
+      region[len] = '\0';
+
+      if(tmp1) {
+        tmp0 = tmp1 + 1;
+        service = strdup(tmp0);
+        if(!service) {
+          goto fail;
+        }
+        if(strlen(service) < 1) {
+          infof(data, "service can't be empty\n");
+          ret = CURLE_BAD_FUNCTION_ARGUMENT;
+          goto fail;
+        }
+      }
     }
-    Curl_strntolower(low_provider, provider, provider_l);
-    Curl_strntolower(mid_provider, provider, provider_l);
-  }
-  else if(provider_l <= PROVIDER_MAX_L) {
-    Curl_strntolower(low_provider0, provider, provider_l);
-    Curl_strntolower(low_provider, provider, provider_l);
-    Curl_strntolower(mid_provider, provider, provider_l);
-    Curl_strntoupper(up_provider, provider, provider_l);
-    mid_provider[0] = Curl_raw_toupper(mid_provider[0]);
   }
   else {
-    infof(data, "v4 signature argument string too long\n");
-    return CURLE_BAD_FUNCTION_ARGUMENT;
+    provider1_low = Curl_memdup(provider0_low, len + 1);
+    provider1_mid = Curl_memdup(provider0_low, len + 1);
+    if(!provider1_low || !provider1_mid) {
+      goto fail;
+    }
+    provider1_mid[0] = Curl_raw_toupper(provider1_mid[0]);
+  }
+
+  if(!service) {
+    tmp0 = hostname;
+    tmp1 = strchr(tmp0, '.');
+    len = tmp1 - tmp0;
+    if(!tmp1 || len < 1) {
+      infof(data, "service missing in parameters or hostname\n");
+      ret = CURLE_URL_MALFORMAT;
+      goto fail;
+    }
+    service = Curl_memdup(tmp0, len + 1);
+    if(!service) {
+      goto fail;
+    }
+    service[len] = '\0';
+
+    if(!region) {
+      tmp0 = tmp1 + 1;
+      tmp1 = strchr(tmp0, '.');
+      len = tmp1 - tmp0;
+      if(!tmp1 || len < 1) {
+        infof(data, "region missing in parameters or hostname\n");
+        ret = CURLE_URL_MALFORMAT;
+        goto fail;
+      }
+      region = Curl_memdup(tmp0, len + 1);
+      if(!region) {
+        goto fail;
+      }
+      region[len] = '\0';
+    }
   }
 
 #ifdef DEBUGBUILD
   force_timestamp = getenv("CURL_FORCETIME");
   if(force_timestamp)
-    rawtime = 0;
+    clock = 0;
   else
+    time(&clock);
+#else
+  time(&clock);
 #endif
-    time(&rawtime);
-
-  ret = Curl_gmtime(rawtime, &info);
+  ret = Curl_gmtime(clock, &tm);
   if(ret != CURLE_OK) {
-    return ret;
+    goto fail;
   }
-
-  if(!strftime(date_iso, sizeof(date_iso), "%Y%m%dT%H%M%SZ", &info)) {
-    return CURLE_OUT_OF_MEMORY;
+  if(!strftime(timestamp, sizeof(timestamp), "%Y%m%dT%H%M%SZ", &tm)) {
+    goto fail;
   }
-
-  memcpy(date, date_iso, sizeof(date));
+  memcpy(date, timestamp, sizeof(date));
   date[sizeof(date) - 1] = 0;
-  api_type = strdup(hostname);
-  if(!api_type) {
-    ret = CURLE_OUT_OF_MEMORY;
-    goto free_all;
-  }
-
-  tmp = strchr(api_type, '.');
-  if(!tmp) {
-    ret = CURLE_URL_MALFORMAT;
-    goto free_all;
-  }
-  *tmp = 0;
 
-  /* at worst, *(tmp + 1) is a '\0' */
-  region = tmp + 1;
+  if(content_type) {
+    content_type = strchr(content_type, ':');
+    if(!content_type) {
+      ret = CURLE_FAILED_INIT;
+      goto fail;
+    }
+    content_type++;
+    /* Skip whitespace now */
+    while(*content_type == ' ' || *content_type == '\t')
+      ++content_type;
 
-  tmp = strchr(region, '.');
-  if(!tmp) {
-    ret = CURLE_URL_MALFORMAT;
-    goto free_all;
+    canonical_headers = curl_maprintf("content-type:%s\n"
+                                      "host:%s\n"
+                                      "x-%s-date:%s\n",
+                                      content_type,
+                                      hostname,
+                                      provider1_low, timestamp);
+    signed_headers = curl_maprintf("content-type;host;x-%s-date",
+                                   provider1_low);
   }
-  *tmp = 0;
-
-  uri = data->state.up.path;
-
-  if(!curl_msnprintf(request_type, REQUEST_TYPE_L, "%s4_request",
-                     low_provider0)) {
-    ret = CURLE_OUT_OF_MEMORY;
-    goto free_all;
+  else {
+    canonical_headers = curl_maprintf("host:%s\n"
+                                      "x-%s-date:%s\n",
+                                      hostname,
+                                      provider1_low, timestamp);
+    signed_headers = curl_maprintf("host;x-%s-date", provider1_low);
   }
 
-  cred_scope = curl_maprintf("%s/%s/%s/%s", date, region, api_type,
-                             request_type);
-  if(!cred_scope) {
-    ret = CURLE_OUT_OF_MEMORY;
-    goto free_all;
+  if(!canonical_headers || !signed_headers) {
+    goto fail;
   }
 
-  if(content_type) {
-    canonical_hdr = curl_maprintf(
-      "content-type:%s\n"
-      "host:%s\n"
-      "x-%s-date:%s\n", content_type, hostname, low_provider, date_iso);
-    signed_headers = curl_maprintf("content-type;host;x-%s-date",
-                                   low_provider);
-  }
-  else if(data->state.up.query) {
-    canonical_hdr = curl_maprintf(
-      "host:%s\n"
-      "x-%s-date:%s\n", hostname, low_provider, date_iso);
-    signed_headers = curl_maprintf("host;x-%s-date", low_provider);
-  }
-  else {
-    ret = CURLE_FAILED_INIT;
-    goto free_all;
+  Curl_sha256it(sha_hash,
+                (const unsigned char *) post_data, strlen(post_data));
+  sha256_to_hex(sha_hex, sha_hash, sizeof(sha_hex));
+
+  Curl_http_method(data, conn, &method, &httpreq);
+
+  canonical_request =
+    curl_maprintf("%s\n" /* HTTPRequestMethod */
+                  "%s\n" /* CanonicalURI */
+                  "%s\n" /* CanonicalQueryString */
+                  "%s\n" /* CanonicalHeaders */
+                  "%s\n" /* SignedHeaders */
+                  "%s",  /* HashedRequestPayload in hex */
+                  method,
+                  data->state.up.path,
+                  data->state.up.query ? data->state.up.query : "",
+                  canonical_headers,
+                  signed_headers,
+                  sha_hex);
+  if(!canonical_request) {
+    goto fail;
   }
 
-  if(!canonical_hdr || !signed_headers) {
-    ret = CURLE_OUT_OF_MEMORY;
-    goto free_all;
+  request_type = curl_maprintf("%s4_request", provider0_low);
+  if(!request_type) {
+    goto fail;
   }
 
-  Curl_sha256it(sha_d, (const unsigned char *)post_data, strlen(post_data));
-  sha256_to_hex(sha_hex, sha_d, sizeof(sha_hex));
-
-  canonical_request = curl_maprintf(
-    "%s\n" /* Method */
-    "%s\n" /* uri */
-    "%s\n" /* querystring */
-    "%s\n" /* canonical_headers */
-    "%s\n" /* signed header */
-    "%s" /* SHA ! */,
-    customrequest, uri,
-    data->state.up.query ? data->state.up.query : "",
-    canonical_hdr, signed_headers, sha_hex);
-  if(!canonical_request) {
-    ret = CURLE_OUT_OF_MEMORY;
-    goto free_all;
+  credential_scope = curl_maprintf("%s/%s/%s/%s",
+                                   date, region, service, request_type);
+  if(!credential_scope) {
+    goto fail;
   }
 
-  Curl_sha256it(sha_d, (unsigned char *)canonical_request,
+  Curl_sha256it(sha_hash, (unsigned char *) canonical_request,
                 strlen(canonical_request));
-  sha256_to_hex(sha_hex, sha_d, sizeof(sha_hex));
+  sha256_to_hex(sha_hex, sha_hash, sizeof(sha_hex));
 
-  /* Google allow to use rsa key instead of HMAC, so this code might change
+  /*
+   * Google allow to use rsa key instead of HMAC, so this code might change
    * In the furure, but for now we support only HMAC version
    */
-  str_to_sign = curl_maprintf("%s4-HMAC-SHA256\n"
-                              "%s\n%s\n%s",
-                              up_provider, date_iso, cred_scope, sha_hex);
+  str_to_sign = curl_maprintf("%s4-HMAC-SHA256\n" /* Algorithm */
+                              "%s\n" /* RequestDateTime */
+                              "%s\n" /* CredentialScope */
+                              "%s",  /* HashedCanonicalRequest in hex */
+                              provider0_up,
+                              timestamp,
+                              credential_scope,
+                              sha_hex);
   if(!str_to_sign) {
-    ret = CURLE_OUT_OF_MEMORY;
-    goto free_all;
+    goto fail;
   }
 
-  curl_msnprintf(sk, sizeof(sk) - 1, "%s4%s", up_provider,
-                 data->set.str[STRING_PASSWORD]);
-
-  HMAC_SHA256(sk, strlen(sk), date,
-              strlen(date), tmp_sign0);
-  sha256_to_hex(sha_hex, tmp_sign0, sizeof(sha_hex));
+  secret = curl_maprintf("%s4%s", provider0_up, passwd);
+  if(!secret) {
+    goto fail;
+  }
 
-  HMAC_SHA256(tmp_sign0, sizeof(tmp_sign0), region,
-              strlen(region), tmp_sign1);
-  HMAC_SHA256(tmp_sign1, sizeof(tmp_sign1), api_type,
-              strlen(api_type), tmp_sign0);
-  HMAC_SHA256(tmp_sign0, sizeof(tmp_sign0), request_type,
-              strlen(request_type),
-              tmp_sign1);
-  HMAC_SHA256(tmp_sign1, sizeof(tmp_sign1), str_to_sign,
-              strlen(str_to_sign), tmp_sign0);
+  HMAC_SHA256(secret, strlen(secret),
+              date, strlen(date), tmp_sign0);
+  HMAC_SHA256(tmp_sign0, sizeof(tmp_sign0),
+              region, strlen(region), tmp_sign1);
+  HMAC_SHA256(tmp_sign1, sizeof(tmp_sign1),
+              service, strlen(service), tmp_sign0);
+  HMAC_SHA256(tmp_sign0, sizeof(tmp_sign0),
+              request_type, strlen(request_type), tmp_sign1);
+  HMAC_SHA256(tmp_sign1, sizeof(tmp_sign1),
+              str_to_sign, strlen(str_to_sign), tmp_sign0);
 
   sha256_to_hex(sha_hex, tmp_sign0, sizeof(sha_hex));
 
-  auth = curl_maprintf("Authorization: %s4-HMAC-SHA256 Credential=%s/%s, "
-                       "SignedHeaders=%s, Signature=%s",
-                       up_provider, data->set.str[STRING_USERNAME], cred_scope,
-                       signed_headers, sha_hex);
-  if(!auth) {
-    ret = CURLE_OUT_OF_MEMORY;
-    goto free_all;
+  auth_headers = curl_maprintf("Authorization: %s4-HMAC-SHA256 "
+                               "Credential=%s/%s, "
+                               "SignedHeaders=%s, "
+                               "Signature=%s\r\n"
+                               "X-%s-Date: %s\r\n",
+                               provider0_up,
+                               user,
+                               credential_scope,
+                               signed_headers,
+                               sha_hex,
+                               provider1_mid,
+                               timestamp);
+  if(!auth_headers) {
+    goto fail;
   }
 
-  curl_msnprintf(date_str, sizeof(date_str), "X-%s-Date: %s",
-                 mid_provider, date_iso);
-  data->set.headers = curl_slist_append(data->set.headers, date_str);
-  if(!data->set.headers) {
-    ret = CURLE_FAILED_INIT;
-    goto free_all;
-  }
-  data->set.headers = curl_slist_append(data->set.headers, auth);
-  if(!data->set.headers) {
-    ret = CURLE_FAILED_INIT;
-    goto free_all;
-  }
-  data->state.authhost.done = 1;
-
-free_all:
-  free(canonical_request);
+  Curl_safefree(data->state.aptr.userpwd);
+  data->state.aptr.userpwd = auth_headers;
+  data->state.authhost.done = TRUE;
+  ret = CURLE_OK;
+
+fail:
+  free(provider0_low);
+  free(provider0_up);
+  free(provider1_low);
+  free(provider1_mid);
+  free(region);
+  free(service);
+  free(canonical_headers);
   free(signed_headers);
+  free(canonical_request);
+  free(request_type);
+  free(credential_scope);
   free(str_to_sign);
-  free(canonical_hdr);
-  free(auth);
-  free(cred_scope);
-  free(api_type);
+  free(secret);
   return ret;
 }
 

+ 2 - 1
lib/setopt.c

@@ -641,7 +641,8 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
 
   case CURLOPT_AWS_SIGV4:
     /*
-     * String that holds file type of the SSL certificate to use
+     * String that is merged to some authentication
+     * parameters are used by the algorithm.
      */
     result = Curl_setstropt(&data->set.str[STRING_AWS_SIGV4],
                             va_arg(param, char *));

+ 1 - 1
lib/urldata.h

@@ -1590,7 +1590,7 @@ enum dupstring {
 
   STRING_COPYPOSTFIELDS,  /* if POST, set the fields' values here */
 
-  STRING_AWS_SIGV4, /* Provider for V4 signature */
+  STRING_AWS_SIGV4, /* Parameters for V4 signature */
 
   STRING_LAST /* not used, just an end-of-list marker */
 };

+ 1 - 1
src/tool_cfgable.c

@@ -169,7 +169,7 @@ static void free_config_fields(struct OperationConfig *config)
   Curl_safefree(config->ftp_account);
   Curl_safefree(config->ftp_alternative_to_user);
 
-  Curl_safefree(config->aws_sigv4_provider);
+  Curl_safefree(config->aws_sigv4);
 }
 
 void config_free(struct OperationConfig *config)

+ 1 - 1
src/tool_cfgable.h

@@ -284,7 +284,7 @@ struct OperationConfig {
                                      0 is valid. default: CURL_HET_DEFAULT. */
   bool haproxy_protocol;          /* whether to send HAProxy protocol v1 */
   bool disallow_username_in_url;  /* disallow usernames in URLs */
-  char *aws_sigv4_provider;
+  char *aws_sigv4;
   struct GlobalConfig *global;
   struct OperationConfig *prev;
   struct OperationConfig *next;   /* Always last in the struct */

+ 3 - 2
src/tool_getparam.c

@@ -113,7 +113,7 @@ static const struct LongShort aliases[]= {
   {"*t", "proxy-ntlm",               ARG_BOOL},
   {"*u", "crlf",                     ARG_BOOL},
   {"*v", "stderr",                   ARG_FILENAME},
-  {"*V", "aws-sigv4",             ARG_STRING},
+  {"*V", "aws-sigv4",                ARG_STRING},
   {"*w", "interface",                ARG_STRING},
   {"*x", "krb",                      ARG_STRING},
   {"*x", "krb4",                     ARG_STRING},
@@ -806,8 +806,9 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
 
       case 'V': /* --aws-sigv4 */
         config->authtype |= CURLAUTH_AWS_SIGV4;
-        GetStr(&config->aws_sigv4_provider, nextarg);
+        GetStr(&config->aws_sigv4, nextarg);
         break;
+
       case 'v': /* --stderr */
         if(strcmp(nextarg, "-")) {
           FILE *newfile = fopen(nextarg, FOPEN_WRITETEXT);

+ 1 - 1
src/tool_help.c

@@ -133,7 +133,7 @@ static const struct helptxt helptext[] = {
   {"-a, --append",
    "Append to target file when uploading",
    CURLHELP_FTP | CURLHELP_SFTP},
-  {"    --aws-sigv4 <provider1[:provider2]>",
+  {"    --aws-sigv4 <provider1[:provider2[:region[:service]]]>",
    "Use AWS V4 signature authentication",
    CURLHELP_AUTH | CURLHELP_HTTP},
   {"    --basic",

+ 1 - 1
src/tool_operate.c

@@ -1662,7 +1662,7 @@ static CURLcode single_transfer(struct GlobalConfig *global,
           my_setopt_str(curl, CURLOPT_PROXY_SSLKEYTYPE,
                         config->proxy_key_type);
           my_setopt_str(curl, CURLOPT_AWS_SIGV4,
-                        config->aws_sigv4_provider);
+                        config->aws_sigv4);
 
           if(config->insecure_ok) {
             my_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);

+ 1 - 1
tests/data/Makefile.inc

@@ -209,7 +209,7 @@ test1800 test1801 \
 test1908 test1909 test1910 test1911 test1912 test1913 test1914 test1915 \
 test1916 test1917 test1918 \
 \
-test1933 \
+test1933 test1934 test1935 test1936 \
 \
 test2000 test2001 test2002 test2003 test2004 test2005 test2006 test2007 \
 test2008 test2009 test2010 test2011 test2012 test2013 test2014 test2015 \

+ 4 - 4
tests/data/test1933

@@ -40,14 +40,14 @@ crypto
 </features>
 
 <name>
-HTTP AWS_SIGV4
+HTTP AWS_SIGV4 with one provider and auth cred via URL
 </name>
 <tool>
 lib1933
 </tool>
 
 <command>
-http://%HOSTIP:%HTTPPORT/1933/testapi/test
+http://xxx:yyy@%HOSTIP:%HTTPPORT/1933/testapi/test
 </command>
 </client>
 
@@ -61,8 +61,8 @@ http://%HOSTIP:%HTTPPORT/1933/testapi/test
 <protocol>
 GET /1933/testapi/test HTTP/1.1
 Host: %HOSTIP:%HTTPPORT
-X-yyy-Date: 19700101T000000Z
-Authorization: XXX4-HMAC-SHA256 Credential=xxx/19700101/0/127/xxx4_request, SignedHeaders=content-type;host;x-yyy-date, Signature=b125a904e7f5cc1f553d8f3682947eb003c4ca6c504097c0dc2d9323289bfcdd
+Authorization: XXX4-HMAC-SHA256 Credential=xxx/19700101/0/127/xxx4_request, SignedHeaders=content-type;host;x-xxx-date, Signature=d2c2dff48c59ec49dc31ef94f18c5dc1ac3eae2a70d51633a4342dadc0683664
+X-Xxx-Date: 19700101T000000Z
 
 </protocol>
 </verify>

+ 69 - 0
tests/data/test1934

@@ -0,0 +1,69 @@
+<testcase>
+<info>
+<keywords>
+HTTP
+CURLOPT_AWS_SIGV4
+</keywords>
+</info>
+
+# Server-side
+<reply>
+<data nocheck="yes">
+HTTP/1.1 302 OK
+Date: Thu, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Content-Type: text/html
+Content-Length: 0
+Location: /19340002
+
+</data>
+<data2>
+HTTP/1.1 200 OK
+Date: Thu, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Content-Type: text/html
+Content-Length: 0
+
+</data>
+</reply>
+
+# Client-side
+<client>
+<server>
+http
+</server>
+# this relies on the debug feature which allow to set the time
+<features>
+SSL
+debug
+crypto
+</features>
+
+<name>
+HTTP AWS_SIGV4 with two providers
+</name>
+<tool>
+lib1934
+</tool>
+
+<command>
+http://%HOSTIP:%HTTPPORT/1934/testapi/test
+</command>
+</client>
+
+# Verify data after the test has been "shot"
+<verify>
+<strip>
+^User-Agent:.*
+^Content-Type:.*
+^Accept:.*
+</strip>
+<protocol>
+GET /1934/testapi/test HTTP/1.1
+Host: %HOSTIP:%HTTPPORT
+Authorization: XXX4-HMAC-SHA256 Credential=xxx/19700101/0/127/xxx4_request, SignedHeaders=content-type;host;x-yyy-date, Signature=938937ca7da6bb3dbf15e30928265ec6f061532d035d2afda92fa7cb10feb196
+X-Yyy-Date: 19700101T000000Z
+
+</protocol>
+</verify>
+</testcase>

+ 69 - 0
tests/data/test1935

@@ -0,0 +1,69 @@
+<testcase>
+<info>
+<keywords>
+HTTP
+CURLOPT_AWS_SIGV4
+</keywords>
+</info>
+
+# Server-side
+<reply>
+<data nocheck="yes">
+HTTP/1.1 302 OK
+Date: Thu, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Content-Type: text/html
+Content-Length: 0
+Location: /19350002
+
+</data>
+<data2>
+HTTP/1.1 200 OK
+Date: Thu, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Content-Type: text/html
+Content-Length: 0
+
+</data>
+</reply>
+
+# Client-side
+<client>
+<server>
+http
+</server>
+# this relies on the debug feature which allow to set the time
+<features>
+SSL
+debug
+crypto
+</features>
+
+<name>
+HTTP AWS_SIGV4 with two providers and region
+</name>
+<tool>
+lib1935
+</tool>
+
+<command>
+http://%HOSTIP:%HTTPPORT/1935/testapi/test
+</command>
+</client>
+
+# Verify data after the test has been "shot"
+<verify>
+<strip>
+^User-Agent:.*
+^Content-Type:.*
+^Accept:.*
+</strip>
+<protocol>
+GET /1935/testapi/test HTTP/1.1
+Host: %HOSTIP:%HTTPPORT
+Authorization: XXX4-HMAC-SHA256 Credential=xxx/19700101/rrr/127/xxx4_request, SignedHeaders=content-type;host;x-yyy-date, Signature=240750deb9263d4c8ece71c016f3919b56e518249390ef075740f94ef8df846f
+X-Yyy-Date: 19700101T000000Z
+
+</protocol>
+</verify>
+</testcase>

+ 69 - 0
tests/data/test1936

@@ -0,0 +1,69 @@
+<testcase>
+<info>
+<keywords>
+HTTP
+CURLOPT_AWS_SIGV4
+</keywords>
+</info>
+
+# Server-side
+<reply>
+<data nocheck="yes">
+HTTP/1.1 302 OK
+Date: Thu, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Content-Type: text/html
+Content-Length: 0
+Location: /19360002
+
+</data>
+<data2>
+HTTP/1.1 200 OK
+Date: Thu, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Content-Type: text/html
+Content-Length: 0
+
+</data>
+</reply>
+
+# Client-side
+<client>
+<server>
+http
+</server>
+# this relies on the debug feature which allow to set the time
+<features>
+SSL
+debug
+crypto
+</features>
+
+<name>
+HTTP AWS_SIGV4 with two providers, region and service
+</name>
+<tool>
+lib1936
+</tool>
+
+<command>
+http://%HOSTIP:%HTTPPORT/1936/testapi/test
+</command>
+</client>
+
+# Verify data after the test has been "shot"
+<verify>
+<strip>
+^User-Agent:.*
+^Content-Type:.*
+^Accept:.*
+</strip>
+<protocol>
+GET /1936/testapi/test HTTP/1.1
+Host: %HOSTIP:%HTTPPORT
+Authorization: XXX4-HMAC-SHA256 Credential=xxx/19700101/rrr/sss/xxx4_request, SignedHeaders=content-type;host;x-yyy-date, Signature=f32cf87977cea5d3274b524b53e5d28f4aac54c372f710ae0cc3a9ececaf169f
+X-Yyy-Date: 19700101T000000Z
+
+</protocol>
+</verify>
+</testcase>

+ 13 - 1
tests/libtest/Makefile.inc

@@ -60,7 +60,7 @@ noinst_PROGRAMS = chkhostname libauthretry libntlmconnect                \
  lib1558 lib1559 lib1560 lib1564 lib1565 lib1567 lib1568 \
  lib1591 lib1592 lib1593 lib1594 lib1596 \
          lib1905 lib1906 lib1907 lib1908 lib1910 lib1911 lib1912 lib1913 \
-         lib1915 lib1916 lib1917 lib1918 lib1933 \
+         lib1915 lib1916 lib1917 lib1918 lib1933 lib1934 lib1935 lib1936 \
          lib3010
 
 chkdecimalpoint_SOURCES = chkdecimalpoint.c ../../lib/mprintf.c \
@@ -675,6 +675,18 @@ lib1933_SOURCES = lib1933.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
 lib1933_LDADD = $(TESTUTIL_LIBS)
 lib1933_CPPFLAGS = $(AM_CPPFLAGS)
 
+lib1934_SOURCES = lib1934.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
+lib1934_LDADD = $(TESTUTIL_LIBS)
+lib1934_CPPFLAGS = $(AM_CPPFLAGS)
+
+lib1935_SOURCES = lib1935.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
+lib1935_LDADD = $(TESTUTIL_LIBS)
+lib1935_CPPFLAGS = $(AM_CPPFLAGS)
+
+lib1936_SOURCES = lib1936.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
+lib1936_LDADD = $(TESTUTIL_LIBS)
+lib1936_CPPFLAGS = $(AM_CPPFLAGS)
+
 lib3010_SOURCES = lib3010.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
 lib3010_LDADD = $(TESTUTIL_LIBS)
 lib3010_CPPFLAGS = $(AM_CPPFLAGS)

+ 1 - 2
tests/libtest/lib1933.c

@@ -42,8 +42,7 @@ int test(char *URL)
   }
 
   test_setopt(curl, CURLOPT_VERBOSE, 1L);
-  test_setopt(curl, CURLOPT_AWS_SIGV4, "xxx:yyy");
-  test_setopt(curl, CURLOPT_USERPWD, "xxx:yyy");
+  test_setopt(curl, CURLOPT_AWS_SIGV4, "xxx");
   test_setopt(curl, CURLOPT_HEADER, 0L);
   test_setopt(curl, CURLOPT_URL, URL);
   list = curl_slist_append(list, "Content-Type: application/json");

+ 61 - 0
tests/libtest/lib1934.c

@@ -0,0 +1,61 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.haxx.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ***************************************************************************/
+#include "test.h"
+
+#include "memdebug.h"
+
+int test(char *URL)
+{
+  CURL *curl;
+  CURLcode res = TEST_ERR_MAJOR_BAD;
+  struct curl_slist *list = NULL;
+
+  if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
+    fprintf(stderr, "curl_global_init() failed\n");
+    return TEST_ERR_MAJOR_BAD;
+  }
+
+  curl = curl_easy_init();
+  if(!curl) {
+    fprintf(stderr, "curl_easy_init() failed\n");
+    curl_global_cleanup();
+    return TEST_ERR_MAJOR_BAD;
+  }
+
+  test_setopt(curl, CURLOPT_VERBOSE, 1L);
+  test_setopt(curl, CURLOPT_AWS_SIGV4, "xxx:yyy");
+  test_setopt(curl, CURLOPT_USERPWD, "xxx:yyy");
+  test_setopt(curl, CURLOPT_HEADER, 0L);
+  test_setopt(curl, CURLOPT_URL, URL);
+  list = curl_slist_append(list, "Content-Type: application/json");
+  test_setopt(curl, CURLOPT_HTTPHEADER, list);
+
+  res = curl_easy_perform(curl);
+
+test_cleanup:
+
+  curl_slist_free_all(list);
+  curl_easy_cleanup(curl);
+  curl_global_cleanup();
+
+  return res;
+}

+ 61 - 0
tests/libtest/lib1935.c

@@ -0,0 +1,61 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.haxx.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ***************************************************************************/
+#include "test.h"
+
+#include "memdebug.h"
+
+int test(char *URL)
+{
+  CURL *curl;
+  CURLcode res = TEST_ERR_MAJOR_BAD;
+  struct curl_slist *list = NULL;
+
+  if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
+    fprintf(stderr, "curl_global_init() failed\n");
+    return TEST_ERR_MAJOR_BAD;
+  }
+
+  curl = curl_easy_init();
+  if(!curl) {
+    fprintf(stderr, "curl_easy_init() failed\n");
+    curl_global_cleanup();
+    return TEST_ERR_MAJOR_BAD;
+  }
+
+  test_setopt(curl, CURLOPT_VERBOSE, 1L);
+  test_setopt(curl, CURLOPT_AWS_SIGV4, "xxx:yyy:rrr");
+  test_setopt(curl, CURLOPT_USERPWD, "xxx:yyy");
+  test_setopt(curl, CURLOPT_HEADER, 0L);
+  test_setopt(curl, CURLOPT_URL, URL);
+  list = curl_slist_append(list, "Content-Type: application/json");
+  test_setopt(curl, CURLOPT_HTTPHEADER, list);
+
+  res = curl_easy_perform(curl);
+
+test_cleanup:
+
+  curl_slist_free_all(list);
+  curl_easy_cleanup(curl);
+  curl_global_cleanup();
+
+  return res;
+}

+ 61 - 0
tests/libtest/lib1936.c

@@ -0,0 +1,61 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2020, Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.haxx.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ***************************************************************************/
+#include "test.h"
+
+#include "memdebug.h"
+
+int test(char *URL)
+{
+  CURL *curl;
+  CURLcode res = TEST_ERR_MAJOR_BAD;
+  struct curl_slist *list = NULL;
+
+  if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
+    fprintf(stderr, "curl_global_init() failed\n");
+    return TEST_ERR_MAJOR_BAD;
+  }
+
+  curl = curl_easy_init();
+  if(!curl) {
+    fprintf(stderr, "curl_easy_init() failed\n");
+    curl_global_cleanup();
+    return TEST_ERR_MAJOR_BAD;
+  }
+
+  test_setopt(curl, CURLOPT_VERBOSE, 1L);
+  test_setopt(curl, CURLOPT_AWS_SIGV4, "xxx:yyy:rrr:sss");
+  test_setopt(curl, CURLOPT_USERPWD, "xxx:yyy");
+  test_setopt(curl, CURLOPT_HEADER, 0L);
+  test_setopt(curl, CURLOPT_URL, URL);
+  list = curl_slist_append(list, "Content-Type: application/json");
+  test_setopt(curl, CURLOPT_HTTPHEADER, list);
+
+  res = curl_easy_perform(curl);
+
+test_cleanup:
+
+  curl_slist_free_all(list);
+  curl_easy_cleanup(curl);
+  curl_global_cleanup();
+
+  return res;
+}