1 /*
2   Licensed to the Apache Software Foundation (ASF) under one
3   or more contributor license agreements.  See the NOTICE file
4   distributed with this work for additional information
5   regarding copyright ownership.  The ASF licenses this file
6   to you under the Apache License, Version 2.0 (the
7   "License"); you may not use this file except in compliance
8   with the License.  You may obtain a copy of the License at
9 
10   http://www.apache.org/licenses/LICENSE-2.0
11 
12   Unless required by applicable law or agreed to in writing, software
13   distributed under the License is distributed on an "AS IS" BASIS,
14   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   See the License for the specific language governing permissions and
16   limitations under the License.
17 */
18 
19 /**
20  * @file aws_auth_v4.cc
21  * @brief AWS Auth v4 signing utility.
22  * @see aws_auth_v4.h
23  */
24 
25 #include <cstring>        /* strlen() */
26 #include <string>         /* stoi() */
27 #include <ctime>          /* strftime(), time(), gmtime_r() */
28 #include <iomanip>        /* std::setw */
29 #include <sstream>        /* std::stringstream */
30 #include <openssl/sha.h>  /* SHA(), sha256_Update(), SHA256_Final, etc. */
31 #include <openssl/hmac.h> /* HMAC() */
32 
33 #ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
34 #include <iostream>
35 #endif
36 
37 #include "aws_auth_v4.h"
38 
39 /**
40  * @brief Lower-case Base16 encode a character string (hexadecimal format)
41  *
42  * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
43  * Base16 RFC4648: https://tools.ietf.org/html/rfc4648#section-8
44  *
45  * @param in ptr to an input counted string to be base16 encoded.
46  * @param inLen input character string length
47  * @return base16 encoded string.
48  */
49 String
base16Encode(const char * in,size_t inLen)50 base16Encode(const char *in, size_t inLen)
51 {
52   if (nullptr == in || inLen == 0) {
53     return {};
54   }
55 
56   std::stringstream result;
57 
58   const char *src    = in;
59   const char *srcEnd = in + inLen;
60 
61   while (src < srcEnd) {
62     result << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>((*src) & 0xFF);
63     src++;
64   }
65   return result.str();
66 }
67 
68 /**
69  * @brief URI-encode a character string (AWS specific version, see spec)
70  *
71  * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
72  *
73  * @todo Consider reusing / converting to TSStringPercentEncode() using a custom map to account for the AWS specific rules.
74  *       Currently we don't build a library/archive so we could link with the unit-test binary. Also using
75  *       different sets of encode/decode functions during runtime and unit-testing did not seem as a good idea.
76  * @param in string to be URI encoded
77  * @param isObjectName if true don't encode '/', keep it as it is.
78  * @return encoded string.
79  */
80 String
uriEncode(const String & in,bool isObjectName)81 uriEncode(const String &in, bool isObjectName)
82 {
83   std::stringstream result;
84 
85   for (char i : in) {
86     if (isalnum(i) || i == '-' || i == '_' || i == '.' || i == '~') {
87       /* URI encode every byte except the unreserved characters:
88        * 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'. */
89       result << i;
90     } else if (i == ' ') {
91       /* The space character is a reserved character and must be encoded as "%20" (and not as "+"). */
92       result << "%20";
93     } else if (isObjectName && i == '/') {
94       /* Encode the forward slash character, '/', everywhere except in the object key name. */
95       result << "/";
96     } else {
97       /* Letters in the hexadecimal value must be upper-case, for example "%1A". */
98       result << "%" << std::uppercase << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(i);
99     }
100   }
101 
102   return result.str();
103 }
104 
105 /**
106  * @brief checks if the string is URI-encoded (AWS specific encoding version, see spec)
107  *
108  * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
109  *
110  * @note According to the following RFC if the string is encoded and contains '%' it should
111  *       be followed by 2 hexadecimal symbols otherwise '%' should be encoded with %25:
112  *          https://tools.ietf.org/html/rfc3986#section-2.1
113  *
114  * @param in string to be URI checked
115  * @param isObjectName if true encoding didn't encode '/', kept it as it is.
116  * @return true if encoded, false not encoded.
117  */
118 bool
isUriEncoded(const String & in,bool isObjectName)119 isUriEncoded(const String &in, bool isObjectName)
120 {
121   for (size_t pos = 0; pos < in.length(); pos++) {
122     char c = in[pos];
123 
124     if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
125       /* found a unreserved character which should not have been be encoded regardless
126        * 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'.  */
127       continue;
128     }
129 
130     if (' ' == c) {
131       /* space should have been encoded with %20 if the string was encoded */
132       return false;
133     }
134 
135     if ('/' == c && !isObjectName) {
136       /* if this is not an object name '/' should have been encoded */
137       return false;
138     }
139 
140     if ('%' == c) {
141       if (pos + 2 < in.length() && std::isxdigit(in[pos + 1]) && std::isxdigit(in[pos + 2])) {
142         /* if string was encoded we should have exactly 2 hexadecimal chars following it */
143         return true;
144       } else {
145         /* lonely '%' should have been encoded with %25 according to the RFC so likely not encoded */
146         return false;
147       }
148     }
149   }
150 
151   return false;
152 }
153 
154 String
canonicalEncode(const String & in,bool isObjectName)155 canonicalEncode(const String &in, bool isObjectName)
156 {
157   String canonical;
158   if (!isUriEncoded(in, isObjectName)) {
159     /* Not URI-encoded */
160     canonical = uriEncode(in, isObjectName);
161   } else {
162     /* URI-encoded, then don't encode since AWS does not encode which is not mentioned in the spec,
163      * asked AWS, still waiting for confirmation */
164     canonical = in;
165   }
166 
167   return canonical;
168 }
169 
170 /**
171  * @brief trim the white-space character from the beginning and the end of the string ("in-place", just moving pointers around)
172  *
173  * @param in ptr to an input string
174  * @param inLen input character count
175  * @param newLen trimmed string character count.
176  * @return pointer to the trimmed string.
177  */
178 const char *
trimWhiteSpaces(const char * in,size_t inLen,size_t & newLen)179 trimWhiteSpaces(const char *in, size_t inLen, size_t &newLen)
180 {
181   if (nullptr == in || inLen == 0) {
182     return in;
183   }
184 
185   const char *first = in;
186   while (size_t(first - in) < inLen && isspace(*first)) {
187     first++;
188   }
189 
190   const char *last = in + inLen - 1;
191   while (last > in && isspace(*last)) {
192     last--;
193   }
194 
195   newLen = last - first + 1;
196   return first;
197 }
198 
199 /**
200  * @brief Trim white spaces from beginning and end.
201  * @returns trimmed string
202  */
203 String
trimWhiteSpaces(const String & s)204 trimWhiteSpaces(const String &s)
205 {
206   /* @todo do this better? */
207   static const String whiteSpace = " \t\n\v\f\r";
208   size_t start                   = s.find_first_not_of(whiteSpace);
209   if (String::npos == start) {
210     return String();
211   }
212   size_t stop = s.find_last_not_of(whiteSpace);
213   return s.substr(start, stop - start + 1);
214 }
215 
216 /*
217  * Group of static inline helper function for less error prone parameter handling and unit test logging.
218  */
219 inline static void
sha256Update(SHA256_CTX * ctx,const char * in,size_t inLen)220 sha256Update(SHA256_CTX *ctx, const char *in, size_t inLen)
221 {
222   SHA256_Update(ctx, in, inLen);
223 #ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
224   std::cout << String(in, inLen);
225 #endif
226 }
227 
228 inline static void
sha256Update(SHA256_CTX * ctx,const char * in)229 sha256Update(SHA256_CTX *ctx, const char *in)
230 {
231   sha256Update(ctx, in, strlen(in));
232 }
233 
234 inline static void
sha256Update(SHA256_CTX * ctx,const String & in)235 sha256Update(SHA256_CTX *ctx, const String &in)
236 {
237   sha256Update(ctx, in.c_str(), in.length());
238 }
239 
240 inline static void
sha256Final(unsigned char hex[SHA256_DIGEST_LENGTH],SHA256_CTX * ctx)241 sha256Final(unsigned char hex[SHA256_DIGEST_LENGTH], SHA256_CTX *ctx)
242 {
243   SHA256_Final(hex, ctx);
244 }
245 
246 /**
247  * @brief: Payload SHA 256 = Hex(SHA256Hash(<payload>) (no new-line char at end)
248  *
249  * @todo support for signing of PUSH, POST content / payload
250  * @param signPayload specifies whether the content / payload should be signed
251  * @return signature of the content or "UNSIGNED-PAYLOAD" to mark that the payload is not signed
252  */
253 String
getPayloadSha256(bool signPayload)254 getPayloadSha256(bool signPayload)
255 {
256   static const String UNSIGNED_PAYLOAD("UNSIGNED-PAYLOAD");
257 
258   if (!signPayload) {
259     return UNSIGNED_PAYLOAD;
260   }
261 
262   unsigned char payloadHash[SHA256_DIGEST_LENGTH];
263   SHA256(reinterpret_cast<const unsigned char *>(""), 0, payloadHash); /* empty content */
264 
265   return base16Encode(reinterpret_cast<char *>(payloadHash), SHA256_DIGEST_LENGTH);
266 }
267 
268 /**
269  * @brief Get Canonical Uri SHA256 Hash
270  *
271  * Hex(SHA256Hash(<CanonicalRequest>))
272  * AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
273  *
274  * @param api an TS API wrapper that will provide interface to HTTP request elements (method, path, query, headers, etc).
275  * @param signPayload specifies if the content / payload should be signed.
276  * @param includeHeaders headers that must be signed
277  * @param excludeHeaders headers that must not be signed
278  * @param signedHeaders a reference to a string to which the signed headers names will be appended
279  * @return SHA256 hash of the canonical request.
280  */
281 String
getCanonicalRequestSha256Hash(TsInterface & api,bool signPayload,const StringSet & includeHeaders,const StringSet & excludeHeaders,String & signedHeaders)282 getCanonicalRequestSha256Hash(TsInterface &api, bool signPayload, const StringSet &includeHeaders, const StringSet &excludeHeaders,
283                               String &signedHeaders)
284 {
285   int length;
286   const char *str = nullptr;
287   unsigned char canonicalRequestSha256Hash[SHA256_DIGEST_LENGTH];
288   SHA256_CTX canonicalRequestSha256Ctx;
289 
290   SHA256_Init(&canonicalRequestSha256Ctx);
291 
292 #ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
293   std::cout << "<CanonicalRequest>";
294 #endif
295 
296   /* <HTTPMethod>\n */
297   str = api.getMethod(&length);
298   sha256Update(&canonicalRequestSha256Ctx, str, length);
299   sha256Update(&canonicalRequestSha256Ctx, "\n");
300 
301   /* URI Encoded Canonical URI
302    * <CanonicalURI>\n */
303   str = api.getPath(&length);
304   String path("/");
305   path.append(str, length);
306   String canonicalUri = canonicalEncode(path, /* isObjectName */ true);
307   sha256Update(&canonicalRequestSha256Ctx, canonicalUri);
308   sha256Update(&canonicalRequestSha256Ctx, "\n");
309 
310   /* Sorted Canonical Query String
311    * <CanonicalQueryString>\n */
312   const char *query = api.getQuery(&length);
313 
314   StringSet paramNames;
315   StringMap paramsMap;
316   std::istringstream istr(String(query, length));
317   String token;
318   StringSet container;
319 
320   while (std::getline(istr, token, '&')) {
321     String::size_type pos(token.find_first_of('='));
322     String param(token.substr(0, pos == String::npos ? token.size() : pos));
323     String value(pos == String::npos ? "" : token.substr(pos + 1, token.size()));
324 
325     String encodedParam = canonicalEncode(param, /* isObjectName */ false);
326     paramNames.insert(encodedParam);
327     paramsMap[encodedParam] = canonicalEncode(value, /* isObjectName */ false);
328   }
329 
330   String queryStr;
331   for (const auto &paramName : paramNames) {
332     if (!queryStr.empty()) {
333       queryStr.append("&");
334     }
335     queryStr.append(paramName);
336     queryStr.append("=").append(paramsMap[paramName]);
337   }
338   sha256Update(&canonicalRequestSha256Ctx, queryStr);
339   sha256Update(&canonicalRequestSha256Ctx, "\n");
340 
341   /* Sorted Canonical Headers
342    *  <CanonicalHeaders>\n */
343   StringSet signedHeadersSet;
344   StringMap headersMap;
345 
346   for (HeaderIterator it = api.headerBegin(); it != api.headerEnd(); it++) {
347     int nameLen;
348     int valueLen;
349     const char *name  = it.getName(&nameLen);
350     const char *value = it.getValue(&valueLen);
351 
352     if (nullptr == name || 0 == nameLen) {
353       continue;
354     }
355 
356     String lowercaseName(name, nameLen);
357     std::transform(lowercaseName.begin(), lowercaseName.end(), lowercaseName.begin(), ::tolower);
358 
359     /* Host, content-type and x-amx-* headers are mandatory */
360     bool xAmzHeader        = (lowercaseName.length() >= X_AMZ.length() && 0 == lowercaseName.compare(0, X_AMZ.length(), X_AMZ));
361     bool contentTypeHeader = (0 == CONTENT_TYPE.compare(lowercaseName));
362     bool hostHeader        = (0 == HOST.compare(lowercaseName));
363     if (!xAmzHeader && !contentTypeHeader && !hostHeader) {
364       /* Skip internal headers (starting with '@'*/
365       if ('@' == name[0] /* exclude internal headers */) {
366         continue;
367       }
368 
369       /* @todo do better here, since iterating over the headers in ATS is known to be less efficient,
370        * come up with a better way if include headers set is non-empty */
371       bool include =
372         (!includeHeaders.empty() && includeHeaders.end() != includeHeaders.find(lowercaseName)); /* requested to be included */
373       bool exclude =
374         (!excludeHeaders.empty() && excludeHeaders.end() != excludeHeaders.find(lowercaseName)); /* requested to be excluded */
375 
376       if ((includeHeaders.empty() && exclude) || (!includeHeaders.empty() && (!include || exclude))) {
377 #ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
378         std::cout << "ignore header: " << String(name, nameLen) << std::endl;
379 #endif
380         continue;
381       }
382     }
383 
384     size_t trimValueLen   = 0;
385     const char *trimValue = trimWhiteSpaces(value, valueLen, trimValueLen);
386 
387     signedHeadersSet.insert(lowercaseName);
388     if (headersMap.find(lowercaseName) == headersMap.end()) {
389       headersMap[lowercaseName] = String(trimValue, trimValueLen);
390     } else {
391       headersMap[lowercaseName].append(",").append(String(trimValue, trimValueLen));
392     }
393   }
394 
395   for (const auto &it : signedHeadersSet) {
396     sha256Update(&canonicalRequestSha256Ctx, it);
397     sha256Update(&canonicalRequestSha256Ctx, ":");
398     sha256Update(&canonicalRequestSha256Ctx, headersMap[it]);
399     sha256Update(&canonicalRequestSha256Ctx, "\n");
400   }
401   sha256Update(&canonicalRequestSha256Ctx, "\n");
402 
403   for (const auto &it : signedHeadersSet) {
404     if (!signedHeaders.empty()) {
405       signedHeaders.append(";");
406     }
407     signedHeaders.append(it);
408   }
409 
410   sha256Update(&canonicalRequestSha256Ctx, signedHeaders);
411   sha256Update(&canonicalRequestSha256Ctx, "\n");
412 
413   /* Hex(SHA256Hash(<payload>) (no new-line char at end)
414    * @TODO support non-empty content, i.e. POST */
415   String payloadSha256Hash = getPayloadSha256(signPayload);
416   sha256Update(&canonicalRequestSha256Ctx, payloadSha256Hash);
417 
418   /* Hex(SHA256Hash(<CanonicalRequest>)) */
419   sha256Final(canonicalRequestSha256Hash, &canonicalRequestSha256Ctx);
420 #ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
421   std::cout << "</CanonicalRequest>" << std::endl;
422 #endif
423   return base16Encode(reinterpret_cast<char *>(canonicalRequestSha256Hash), SHA256_DIGEST_LENGTH);
424 }
425 
426 /**
427  * @brief Default AWS entry-point host name to region based on (S3):
428  *
429  * @see http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
430  * it is used to get the region programmatically  w/o configuration
431  * parameters and can (meant to) be overwritten if necessary.
432  * @todo may be if one day AWS naming/mapping becomes 100% consistent
433  * we could just extract (calculate) the right region from hostname.
434  */
435 const StringMap
createDefaultRegionMap()436 createDefaultRegionMap()
437 {
438   StringMap m;
439   /* us-east-2 */
440   m["s3.us-east-2.amazonaws.com"]           = "us-east-2";
441   m["s3-us-east-2.amazonaws.com"]           = "us-east-2";
442   m["s3.dualstack.us-east-2.amazonaws.com"] = "us-east-2";
443   /* "us-east-1" */
444   m["s3.amazonaws.com"]                     = "us-east-1";
445   m["s3.us-east-1.amazonaws.com"]           = "us-east-1";
446   m["s3-external-1.amazonaws.com"]          = "us-east-1";
447   m["s3.dualstack.us-east-1.amazonaws.com"] = "us-east-1";
448   /* us-west-1 */
449   m["s3.us-west-1.amazonaws.com"]           = "us-west-1";
450   m["s3-us-west-1.amazonaws.com"]           = "us-west-1";
451   m["s3.dualstack.us-west-1.amazonaws.com"] = "us-west-1";
452   /* us-west-2 */
453   m["s3.us-west-2.amazonaws.com"]           = "us-west-2";
454   m["s3-us-west-2.amazonaws.com"]           = "us-west-2";
455   m["s3.dualstack.us-west-2.amazonaws.com"] = "us-west-2";
456   /* ap-south-1 */
457   m["s3.ap-south-1.amazonaws.com"]           = "ap-south-1";
458   m["s3-ap-south-1.amazonaws.com"]           = "ap-south-1";
459   m["s3.dualstack.ap-south-1.amazonaws.com"] = "ap-south-1";
460   /* ap-northeast-3 */
461   m["s3.ap-northeast-3.amazonaws.com"]           = "ap-northeast-3";
462   m["s3-ap-northeast-3.amazonaws.com"]           = "ap-northeast-3";
463   m["s3.dualstack.ap-northeast-3.amazonaws.com"] = "ap-northeast-3";
464   /* ap-northeast-2 */
465   m["s3.ap-northeast-2.amazonaws.com"]           = "ap-northeast-2";
466   m["s3-ap-northeast-2.amazonaws.com"]           = "ap-northeast-2";
467   m["s3.dualstack.ap-northeast-2.amazonaws.com"] = "ap-northeast-2";
468   /* ap-southeast-1 */
469   m["s3.ap-southeast-1.amazonaws.com"]           = "ap-southeast-1";
470   m["s3-ap-southeast-1.amazonaws.com"]           = "ap-southeast-1";
471   m["s3.dualstack.ap-southeast-1.amazonaws.com"] = "ap-southeast-1";
472   /* ap-southeast-2 */
473   m["s3.ap-southeast-2.amazonaws.com"]           = "ap-southeast-2";
474   m["s3-ap-southeast-2.amazonaws.com"]           = "ap-southeast-2";
475   m["s3.dualstack.ap-southeast-2.amazonaws.com"] = "ap-southeast-2";
476   /* ap-northeast-1 */
477   m["s3.ap-northeast-1.amazonaws.com"]           = "ap-northeast-1";
478   m["s3-ap-northeast-1.amazonaws.com"]           = "ap-northeast-1";
479   m["s3.dualstack.ap-northeast-1.amazonaws.com"] = "ap-northeast-1";
480   /* ca-central-1 */
481   m["s3.ca-central-1.amazonaws.com"]           = "ca-central-1";
482   m["s3-ca-central-1.amazonaws.com"]           = "ca-central-1";
483   m["s3.dualstack.ca-central-1.amazonaws.com"] = "ca-central-1";
484   /* cn-north-1 */
485   m["s3.cn-north-1.amazonaws.com.cn"] = "cn-north-1";
486   /* cn-northwest-1 */
487   m["s3.cn-northwest-1.amazonaws.com.cn"] = "cn-northwest-1";
488   /* eu-central-1 */
489   m["s3.eu-central-1.amazonaws.com"]           = "eu-central-1";
490   m["s3-eu-central-1.amazonaws.com"]           = "eu-central-1";
491   m["s3.dualstack.eu-central-1.amazonaws.com"] = "eu-central-1";
492   /* eu-west-1 */
493   m["s3.eu-west-1.amazonaws.com"]           = "eu-west-1";
494   m["s3-eu-west-1.amazonaws.com"]           = "eu-west-1";
495   m["s3.dualstack.eu-west-1.amazonaws.com"] = "eu-west-1";
496   /* eu-west-2 */
497   m["s3.eu-west-2.amazonaws.com"]           = "eu-west-2";
498   m["s3-eu-west-2.amazonaws.com"]           = "eu-west-2";
499   m["s3.dualstack.eu-west-2.amazonaws.com"] = "eu-west-2";
500   /* eu-west-3 */
501   m["s3.eu-west-3.amazonaws.com"]           = "eu-west-3";
502   m["s3-eu-west-3.amazonaws.com"]           = "eu-west-3";
503   m["s3.dualstack.eu-west-3.amazonaws.com"] = "eu-west-3";
504   /* sa-east-1 */
505   m["s3.sa-east-1.amazonaws.com"]           = "sa-east-1";
506   m["s3-sa-east-1.amazonaws.com"]           = "sa-east-1";
507   m["s3.dualstack.sa-east-1.amazonaws.com"] = "sa-east-1";
508   /* default "us-east-1" * */
509   m[""] = "us-east-1";
510   return m;
511 }
512 const StringMap defaultDefaultRegionMap = createDefaultRegionMap();
513 
514 /**
515  * @description default list of headers to be excluded from the signing
516  */
517 const StringSet
createDefaultExcludeHeaders()518 createDefaultExcludeHeaders()
519 {
520   StringSet m;
521   /* exclude headers that are meant to be changed */
522   m.insert("x-forwarded-for");
523   m.insert("forwarded");
524   m.insert("via");
525   return m;
526 }
527 const StringSet defaultExcludeHeaders = createDefaultExcludeHeaders();
528 
529 /**
530  * @description default list of headers to be included in the signing
531  */
532 const StringSet
createDefaultIncludeHeaders()533 createDefaultIncludeHeaders()
534 {
535   StringSet m;
536   return m;
537 }
538 const StringSet defaultIncludeHeaders = createDefaultIncludeHeaders();
539 
540 /**
541  * @brief Get AWS (S3) region from the entry-point
542  *
543  * @see Implementation based on the following:
544  *   http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
545  *   http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
546  *
547  * @param regionMap map containing entry-point to region mapping
548  * @param entryPoint entry-point name
549  * @param entryPointLen - entry point string length
550  */
551 String
getRegion(const StringMap & regionMap,const char * entryPoint,size_t entryPointLen)552 getRegion(const StringMap &regionMap, const char *entryPoint, size_t entryPointLen)
553 {
554   String region;
555   size_t dot = String::npos;
556   String hostname(entryPoint, entryPointLen);
557 
558   /* Start looking for a match from the top-level domain backwards to keep the mapping generic
559    * (so we can override it if we need later) */
560   do {
561     String name;
562     dot = hostname.rfind('.', dot - 1);
563     if (String::npos != dot) {
564       name = hostname.substr(dot + 1);
565     } else {
566       name = hostname;
567     }
568     if (regionMap.end() != regionMap.find(name)) {
569       region = regionMap.at(name);
570       break;
571     }
572   } while (String::npos != dot);
573 
574   if (region.empty() && regionMap.end() != regionMap.find("")) {
575     region = regionMap.at(""); /* default region if nothing matches */
576   }
577 
578   return region;
579 }
580 
581 /**
582  * @brief Constructs the string to sign
583  *
584  * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
585 
586  * @param entryPoint  entry-point name
587  * @param entryPointLen entry-point name length
588  * @param dateTime - ISO 8601 time
589  * @param dateTimeLen - ISO 8601 time length
590  * @param region AWS region name
591  * @param region AWS region name length
592  * @param service service name
593  * @param serviceLen service name length
594  * @param sha256Hash canonical request SHA 256 hash
595  * @param sha256HashLen canonical request SHA 256 hash length
596  * @returns the string to sign
597  */
598 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)599 getStringToSign(const char *entryPoint, size_t EntryPointLen, const char *dateTime, size_t dateTimeLen, const char *region,
600                 size_t regionLen, const char *service, size_t serviceLen, const char *sha256Hash, size_t sha256HashLen)
601 {
602   String stringToSign;
603 
604   /* AWS4-HMAC-SHA256\n (hard-coded, other values? */
605   stringToSign.append("AWS4-HMAC-SHA256\n");
606 
607   /* time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>\n */
608   stringToSign.append(dateTime, dateTimeLen);
609   stringToSign.append("\n");
610 
611   /* Scope: date.Format(<YYYYMMDD>) + "/" + <region> + "/" + <service> + "/aws4_request" */
612   stringToSign.append(dateTime, 8); /* Get only the YYYYMMDD */
613   stringToSign.append("/");
614   stringToSign.append(region, regionLen);
615   stringToSign.append("/");
616   stringToSign.append(service, serviceLen);
617   stringToSign.append("/aws4_request\n");
618   stringToSign.append(sha256Hash, sha256HashLen);
619 
620   return stringToSign;
621 }
622 
623 /**
624  * @brief Calculates the final signature based on the following parameters and base16 encodes it.
625  *
626  * signing key = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + "<awsSecret>", <dateTime>),
627  *                   <awsRegion>), <awsService>),"aws4_request")
628  *
629  * @see AWS spec: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
630  *
631  * @param awsSecret AWS secret
632  * @param awsSecretLen AWS secret length
633  * @param awsRegion AWS region
634  * @param awsRegionLen AWS region length
635  * @param awsService AWS Service name
636  * @param awsServiceLen AWS service name length
637  * @param dateTime ISO8601 date/time
638  * @param dateTimeLen ISO8601 date/time length
639  * @param stringToSign string to sign
640  * @param stringToSignLen length of the string to sign
641  * @param base16Signature output buffer where the base16 signature will be stored
642  * @param base16SignatureLen size of the signature buffer = EVP_MAX_MD_SIZE (at least)
643  *
644  * @return number of characters written to the output buffer
645  */
646 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)647 getSignature(const char *awsSecret, size_t awsSecretLen, const char *awsRegion, size_t awsRegionLen, const char *awsService,
648              size_t awsServiceLen, const char *dateTime, size_t dateTimeLen, const char *stringToSign, size_t stringToSignLen,
649              char *signature, size_t signatureLen)
650 {
651   unsigned int dateKeyLen = EVP_MAX_MD_SIZE;
652   unsigned char dateKey[EVP_MAX_MD_SIZE];
653   unsigned int dateRegionKeyLen = EVP_MAX_MD_SIZE;
654   unsigned char dateRegionKey[EVP_MAX_MD_SIZE];
655   unsigned int dateRegionServiceKeyLen = EVP_MAX_MD_SIZE;
656   unsigned char dateRegionServiceKey[EVP_MAX_MD_SIZE];
657   unsigned int signingKeyLen = EVP_MAX_MD_SIZE;
658   unsigned char signingKey[EVP_MAX_MD_SIZE];
659 
660   size_t keyLen = 4 + awsSecretLen;
661   char key[keyLen];
662   memcpy(key, "AWS4", 4);
663   memcpy(key + 4, awsSecret, awsSecretLen);
664 
665   unsigned int len = signatureLen;
666   if (HMAC(EVP_sha256(), key, keyLen, (unsigned char *)dateTime, dateTimeLen, dateKey, &dateKeyLen) &&
667       HMAC(EVP_sha256(), dateKey, dateKeyLen, (unsigned char *)awsRegion, awsRegionLen, dateRegionKey, &dateRegionKeyLen) &&
668       HMAC(EVP_sha256(), dateRegionKey, dateRegionKeyLen, (unsigned char *)awsService, awsServiceLen, dateRegionServiceKey,
669            &dateRegionServiceKeyLen) &&
670       HMAC(EVP_sha256(), dateRegionServiceKey, dateRegionServiceKeyLen, reinterpret_cast<const unsigned char *>("aws4_request"), 12,
671            signingKey, &signingKeyLen) &&
672       HMAC(EVP_sha256(), signingKey, signingKeyLen, (unsigned char *)stringToSign, stringToSignLen,
673            reinterpret_cast<unsigned char *>(signature), &len)) {
674     return len;
675   }
676 
677   return 0;
678 }
679 
680 /**
681  * @brief formats the time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>
682  */
683 size_t
getIso8601Time(time_t * now,char * dateTime,size_t dateTimeLen)684 getIso8601Time(time_t *now, char *dateTime, size_t dateTimeLen)
685 {
686   struct tm tm;
687   return strftime(dateTime, dateTimeLen, "%Y%m%dT%H%M%SZ", gmtime_r(now, &tm));
688 }
689 
690 /**
691  * @brief formats the time stamp in ISO8601 format: <YYYYMMDDTHHMMSSZ>
692  */
693 const char *
getDateTime(size_t * dateTimeLen)694 AwsAuthV4::getDateTime(size_t *dateTimeLen)
695 {
696   *dateTimeLen = sizeof(_dateTime) - 1;
697   return _dateTime;
698 }
699 
700 /**
701  * @brief: HTTP content / payload SHA 256 = Hex(SHA256Hash(<payload>)
702  * @return signature of the content or "UNSIGNED-PAYLOAD" to mark that the payload is not signed
703  */
704 String
getPayloadHash()705 AwsAuthV4::getPayloadHash()
706 {
707   return getPayloadSha256(_signPayload);
708 }
709 
710 /**
711  * @brief Get the value of the Authorization header (AWS authorization) v4
712  * @return the Authorization header value
713  */
714 String
getAuthorizationHeader()715 AwsAuthV4::getAuthorizationHeader()
716 {
717   String signedHeaders;
718   String canonicalReq = getCanonicalRequestSha256Hash(_api, _signPayload, _includedHeaders, _excludedHeaders, signedHeaders);
719 
720   int hostLen      = 0;
721   const char *host = _api.getHost(&hostLen);
722 
723   String awsRegion = getRegion(_regionMap, host, hostLen);
724 
725   String stringToSign = getStringToSign(host, hostLen, _dateTime, sizeof(_dateTime) - 1, awsRegion.c_str(), awsRegion.length(),
726                                         _awsService, _awsServiceLen, canonicalReq.c_str(), canonicalReq.length());
727 #ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
728   std::cout << "<StringToSign>" << stringToSign << "</StringToSign>" << std::endl;
729 #endif
730 
731   char signature[EVP_MAX_MD_SIZE];
732   size_t signatureLen =
733     getSignature(_awsSecretAccessKey, _awsSecretAccessKeyLen, awsRegion.c_str(), awsRegion.length(), _awsService, _awsServiceLen,
734                  _dateTime, 8, stringToSign.c_str(), stringToSign.length(), signature, EVP_MAX_MD_SIZE);
735 
736   String base16Signature = base16Encode(signature, signatureLen);
737 #ifdef AWS_AUTH_V4_DETAILED_DEBUG_OUTPUT
738   std::cout << "<SignatureProvided>" << base16Signature << "</SignatureProvided>" << std::endl;
739 #endif
740 
741   std::stringstream authorizationHeader;
742   authorizationHeader << "AWS4-HMAC-SHA256 ";
743   authorizationHeader << "Credential=" << String(_awsAccessKeyId, _awsAccessKeyIdLen) << "/" << String(_dateTime, 8) << "/"
744                       << awsRegion << "/" << String(_awsService, _awsServiceLen) << "/"
745                       << "aws4_request"
746                       << ",";
747   authorizationHeader << "SignedHeaders=" << signedHeaders << ",";
748   authorizationHeader << "Signature=" << base16Signature;
749 
750   return authorizationHeader.str();
751 }
752 
753 /**
754  * @brief Authorization v4 constructor
755  *
756  * @param api wrapper providing access to HTTP request elements (URI host, path, query, headers, etc.)
757  * @param now current time-stamp
758  * @param signPayload defines if the HTTP content / payload needs to be signed
759  * @param awsAccessKeyId AWS access key ID
760  * @param awsAccessKeyIdLen AWS access key ID length
761  * @param awsSecretAccessKey AWS secret
762  * @param awsSecretAccessKeyLen AWS secret length
763  * @param awsService AWS Service name
764  * @param awsServiceLen AWS service name length
765  * @param includeHeaders set of headers to be signed
766  * @param excludeHeaders set of headers not to be signed
767  * @param regionMap entry-point to AWS region mapping
768  */
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)769 AwsAuthV4::AwsAuthV4(TsInterface &api, time_t *now, bool signPayload, const char *awsAccessKeyId, size_t awsAccessKeyIdLen,
770                      const char *awsSecretAccessKey, size_t awsSecretAccessKeyLen, const char *awsService, size_t awsServiceLen,
771                      const StringSet &includedHeaders, const StringSet &excludedHeaders, const StringMap &regionMap)
772   : _api(api),
773     _signPayload(signPayload),
774     _awsAccessKeyId(awsAccessKeyId),
775     _awsAccessKeyIdLen(awsAccessKeyIdLen),
776     _awsSecretAccessKey(awsSecretAccessKey),
777     _awsSecretAccessKeyLen(awsSecretAccessKeyLen),
778     _awsService(awsService),
779     _awsServiceLen(awsServiceLen),
780     _includedHeaders(includedHeaders.empty() ? defaultIncludeHeaders : includedHeaders),
781     _excludedHeaders(excludedHeaders.empty() ? defaultExcludeHeaders : excludedHeaders),
782     _regionMap(regionMap.empty() ? defaultDefaultRegionMap : regionMap)
783 {
784   getIso8601Time(now, _dateTime, sizeof(_dateTime));
785 }
786