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 ¶mName : 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 ®ionMap, 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 ®ionMap)
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