1/*
2 * Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License").
5 * You may not use this file except in compliance with the License.
6 * A copy of the License is located at
7 *
8 * http://aws.amazon.com/apache2.0
9 *
10 * or in the "license" file accompanying this file. This file is distributed
11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 * express or implied. See the License for the specific language governing
13 * permissions and limitations under the License.
14 */
15
16#include <aws/core/auth/AWSAuthSigner.h>
17
18#include <aws/core/auth/AWSCredentialsProvider.h>
19#include <aws/core/client/ClientConfiguration.h>
20#include <aws/core/http/HttpRequest.h>
21#include <aws/core/http/HttpResponse.h>
22#include <aws/core/utils/DateTime.h>
23#include <aws/core/utils/HashingUtils.h>
24#include <aws/core/utils/Outcome.h>
25#include <aws/core/utils/StringUtils.h>
26#include <aws/core/utils/logging/LogMacros.h>
27#include <aws/core/utils/memory/AWSMemory.h>
28#include <aws/core/utils/crypto/Sha256.h>
29#include <aws/core/utils/crypto/Sha256HMAC.h>
30#include <aws/core/utils/stream/PreallocatedStreamBuf.h>
31#include <aws/core/utils/event/EventMessage.h>
32#include <aws/core/utils/event/EventHeader.h>
33
34#include <cstdio>
35#include <iomanip>
36#include <math.h>
37#include <cstring>
38
39using namespace Aws;
40using namespace Aws::Client;
41using namespace Aws::Auth;
42using namespace Aws::Http;
43using namespace Aws::Utils;
44using namespace Aws::Utils::Logging;
45
46static const char* EQ = "=";
47static const char* AWS_HMAC_SHA256 = "AWS4-HMAC-SHA256";
48static const char* EVENT_STREAM_CONTENT_SHA256 = "STREAMING-AWS4-HMAC-SHA256-EVENTS";
49static const char* EVENT_STREAM_PAYLOAD = "AWS4-HMAC-SHA256-PAYLOAD";
50static const char* AWS4_REQUEST = "aws4_request";
51static const char* SIGNED_HEADERS = "SignedHeaders";
52static const char* CREDENTIAL = "Credential";
53static const char* NEWLINE = "\n";
54static const char* X_AMZ_SIGNED_HEADERS = "X-Amz-SignedHeaders";
55static const char* X_AMZ_ALGORITHM = "X-Amz-Algorithm";
56static const char* X_AMZ_CREDENTIAL = "X-Amz-Credential";
57static const char* UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
58static const char* X_AMZ_SIGNATURE = "X-Amz-Signature";
59static const char* X_AMZN_TRACE_ID = "x-amzn-trace-id";
60static const char* X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256";
61static const char* USER_AGENT = "user-agent";
62static const char* SIGNING_KEY = "AWS4";
63static const char* LONG_DATE_FORMAT_STR = "%Y%m%dT%H%M%SZ";
64static const char* SIMPLE_DATE_FORMAT_STR = "%Y%m%d";
65static const char* EMPTY_STRING_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
66
67static const char v4LogTag[] = "AWSAuthV4Signer";
68static const char v4StreamingLogTag[] = "AWSAuthEventStreamV4Signer";
69
70namespace Aws
71{
72 namespace Auth
73 {
74 const char SIGNATURE[] = "Signature";
75 const char SIGV4_SIGNER[] = "SignatureV4";
76 const char EVENTSTREAM_SIGV4_SIGNER[] = "EventStreamSignatureV4";
77 const char EVENTSTREAM_SIGNATURE_HEADER[] = ":chunk-signature";
78 const char EVENTSTREAM_DATE_HEADER[] = ":date";
79 const char NULL_SIGNER[] = "NullSigner";
80 }
81}
82
83static Aws::String CanonicalizeRequestSigningString(HttpRequest& request, bool urlEscapePath)
84{
85 request.CanonicalizeRequest();
86 Aws::StringStream signingStringStream;
87 signingStringStream << HttpMethodMapper::GetNameForHttpMethod(request.GetMethod());
88
89 URI uriCpy = request.GetUri();
90 // Many AWS services do not decode the URL before calculating SignatureV4 on their end.
91 // This results in the signature getting calculated with a double encoded URL.
92 // That means we have to double encode it here for the signature to match on the service side.
93 if(urlEscapePath)
94 {
95 // RFC3986 is how we encode the URL before sending it on the wire.
96 auto rfc3986EncodedPath = URI::URLEncodePathRFC3986(uriCpy.GetPath());
97 uriCpy.SetPath(rfc3986EncodedPath);
98 // However, SignatureV4 uses this URL encoding scheme
99 signingStringStream << NEWLINE << uriCpy.GetURLEncodedPath() << NEWLINE;
100 }
101 else
102 {
103 // For the services that DO decode the URL first; we don't need to double encode it.
104 uriCpy.SetPath(uriCpy.GetURLEncodedPath());
105 signingStringStream << NEWLINE << uriCpy.GetPath() << NEWLINE;
106 }
107
108 if (request.GetQueryString().find('=') != std::string::npos)
109 {
110 signingStringStream << request.GetQueryString().substr(1) << NEWLINE;
111 }
112 else if (request.GetQueryString().size() > 1)
113 {
114 signingStringStream << request.GetQueryString().substr(1) << "=" << NEWLINE;
115 }
116 else
117 {
118 signingStringStream << NEWLINE;
119 }
120
121 return signingStringStream.str();
122}
123
124static Http::HeaderValueCollection CanonicalizeHeaders(Http::HeaderValueCollection&& headers)
125{
126 Http::HeaderValueCollection canonicalHeaders;
127 for (const auto& header : headers)
128 {
129 auto trimmedHeaderName = StringUtils::Trim(header.first.c_str());
130 auto trimmedHeaderValue = StringUtils::Trim(header.second.c_str());
131
132 //multiline gets converted to line1,line2,etc...
133 auto headerMultiLine = StringUtils::SplitOnLine(trimmedHeaderValue);
134 Aws::String headerValue = headerMultiLine.size() == 0 ? "" : headerMultiLine[0];
135
136 if (headerMultiLine.size() > 1)
137 {
138 for(size_t i = 1; i < headerMultiLine.size(); ++i)
139 {
140 headerValue += ",";
141 headerValue += StringUtils::Trim(headerMultiLine[i].c_str());
142 }
143 }
144
145 //duplicate spaces need to be converted to one.
146 Aws::String::iterator new_end =
147 std::unique(headerValue.begin(), headerValue.end(),
148 [=](char lhs, char rhs) { return (lhs == rhs) && (lhs == ' '); }
149 );
150 headerValue.erase(new_end, headerValue.end());
151
152 canonicalHeaders[trimmedHeaderName] = headerValue;
153 }
154
155 return canonicalHeaders;
156}
157
158AWSAuthV4Signer::AWSAuthV4Signer(const std::shared_ptr<Auth::AWSCredentialsProvider>& credentialsProvider,
159 const char* serviceName, const Aws::String& region, PayloadSigningPolicy signingPolicy, bool urlEscapePath) :
160 m_includeSha256HashHeader(true),
161 m_credentialsProvider(credentialsProvider),
162 m_serviceName(serviceName),
163 m_region(region),
164 m_hash(Aws::MakeUnique<Aws::Utils::Crypto::Sha256>(v4LogTag)),
165 m_HMAC(Aws::MakeUnique<Aws::Utils::Crypto::Sha256HMAC>(v4LogTag)),
166 m_unsignedHeaders({USER_AGENT, X_AMZN_TRACE_ID}),
167 m_payloadSigningPolicy(signingPolicy),
168 m_urlEscapePath(urlEscapePath)
169{
170 //go ahead and warm up the signing cache.
171 ComputeHash(credentialsProvider->GetAWSCredentials().GetAWSSecretKey(), DateTime::CalculateGmtTimestampAsString(SIMPLE_DATE_FORMAT_STR), region, m_serviceName);
172}
173
174AWSAuthV4Signer::~AWSAuthV4Signer()
175{
176 // empty destructor in .cpp file to keep from needing the implementation of (AWSCredentialsProvider, Sha256, Sha256HMAC) in the header file
177}
178
179
180bool AWSAuthV4Signer::ShouldSignHeader(const Aws::String& header) const
181{
182 return m_unsignedHeaders.find(Aws::Utils::StringUtils::ToLower(header.c_str())) == m_unsignedHeaders.cend();
183}
184
185bool AWSAuthV4Signer::SignRequest(Aws::Http::HttpRequest& request, const char* region, bool signBody) const
186{
187 AWSCredentials credentials = m_credentialsProvider->GetAWSCredentials();
188
189 //don't sign anonymous requests
190 if (credentials.GetAWSAccessKeyId().empty() || credentials.GetAWSSecretKey().empty())
191 {
192 return true;
193 }
194
195 if (!credentials.GetSessionToken().empty())
196 {
197 request.SetAwsSessionToken(credentials.GetSessionToken());
198 }
199
200 Aws::String payloadHash(UNSIGNED_PAYLOAD);
201 switch(m_payloadSigningPolicy)
202 {
203 case PayloadSigningPolicy::Always:
204 signBody = true;
205 break;
206 case PayloadSigningPolicy::Never:
207 signBody = false;
208 break;
209 case PayloadSigningPolicy::RequestDependent:
210 // respect the request setting
211 default:
212 break;
213 }
214
215 if(signBody || request.GetUri().GetScheme() != Http::Scheme::HTTPS)
216 {
217 payloadHash = ComputePayloadHash(request);
218 if (payloadHash.empty())
219 {
220 return false;
221 }
222 }
223 else
224 {
225 AWS_LOGSTREAM_DEBUG(v4LogTag, "Note: Http payloads are not being signed. signPayloads=" << signBody
226 << " http scheme=" << Http::SchemeMapper::ToString(request.GetUri().GetScheme()));
227 }
228
229 if(m_includeSha256HashHeader)
230 {
231 request.SetHeaderValue(X_AMZ_CONTENT_SHA256, payloadHash);
232 }
233
234 //calculate date header to use in internal signature (this also goes into date header).
235 DateTime now = GetSigningTimestamp();
236 Aws::String dateHeaderValue = now.ToGmtString(LONG_DATE_FORMAT_STR);
237 request.SetHeaderValue(AWS_DATE_HEADER, dateHeaderValue);
238
239 Aws::StringStream headersStream;
240 Aws::StringStream signedHeadersStream;
241
242 for (const auto& header : CanonicalizeHeaders(request.GetHeaders()))
243 {
244 if(ShouldSignHeader(header.first))
245 {
246 headersStream << header.first.c_str() << ":" << header.second.c_str() << NEWLINE;
247 signedHeadersStream << header.first.c_str() << ";";
248 }
249 }
250
251 Aws::String canonicalHeadersString = headersStream.str();
252 AWS_LOGSTREAM_DEBUG(v4LogTag, "Canonical Header String: " << canonicalHeadersString);
253
254 //calculate signed headers parameter
255 Aws::String signedHeadersValue = signedHeadersStream.str();
256 //remove that last semi-colon
257 if (!signedHeadersValue.empty())
258 {
259 signedHeadersValue.pop_back();
260 }
261
262 AWS_LOGSTREAM_DEBUG(v4LogTag, "Signed Headers value:" << signedHeadersValue);
263
264 //generate generalized canonicalized request string.
265 Aws::String canonicalRequestString = CanonicalizeRequestSigningString(request, m_urlEscapePath);
266
267 //append v4 stuff to the canonical request string.
268 canonicalRequestString.append(canonicalHeadersString);
269 canonicalRequestString.append(NEWLINE);
270 canonicalRequestString.append(signedHeadersValue);
271 canonicalRequestString.append(NEWLINE);
272 canonicalRequestString.append(payloadHash);
273
274 AWS_LOGSTREAM_DEBUG(v4LogTag, "Canonical Request String: " << canonicalRequestString);
275
276 //now compute sha256 on that request string
277 auto hashResult = m_hash->Calculate(canonicalRequestString);
278 if (!hashResult.IsSuccess())
279 {
280 AWS_LOGSTREAM_ERROR(v4LogTag, "Failed to hash (sha256) request string");
281 AWS_LOGSTREAM_DEBUG(v4LogTag, "The request string is: \"" << canonicalRequestString << "\"");
282 return false;
283 }
284
285 auto sha256Digest = hashResult.GetResult();
286 Aws::String cannonicalRequestHash = HashingUtils::HexEncode(sha256Digest);
287 Aws::String simpleDate = now.ToGmtString(SIMPLE_DATE_FORMAT_STR);
288
289 Aws::String signingRegion = region ? region : m_region;
290 Aws::String stringToSign = GenerateStringToSign(dateHeaderValue, simpleDate, cannonicalRequestHash, signingRegion, m_serviceName);
291 auto finalSignature = GenerateSignature(credentials, stringToSign, simpleDate, signingRegion, m_serviceName);
292
293 Aws::StringStream ss;
294 ss << AWS_HMAC_SHA256 << " " << CREDENTIAL << EQ << credentials.GetAWSAccessKeyId() << "/" << simpleDate
295 << "/" << signingRegion << "/" << m_serviceName << "/" << AWS4_REQUEST << ", " << SIGNED_HEADERS << EQ
296 << signedHeadersValue << ", " << SIGNATURE << EQ << finalSignature;
297
298 auto awsAuthString = ss.str();
299 AWS_LOGSTREAM_DEBUG(v4LogTag, "Signing request with: " << awsAuthString);
300 request.SetAwsAuthorization(awsAuthString);
301 request.SetSigningAccessKey(credentials.GetAWSAccessKeyId());
302 request.SetSigningRegion(signingRegion);
303 return true;
304}
305
306bool AWSAuthV4Signer::PresignRequest(Aws::Http::HttpRequest& request, long long expirationTimeInSeconds) const
307{
308 return PresignRequest(request, m_region.c_str(), expirationTimeInSeconds);
309}
310
311bool AWSAuthV4Signer::PresignRequest(Aws::Http::HttpRequest& request, const char* region, long long expirationInSeconds) const
312{
313 return PresignRequest(request, region, m_serviceName.c_str(), expirationInSeconds);
314}
315
316bool AWSAuthV4Signer::PresignRequest(Aws::Http::HttpRequest& request, const char* region, const char* serviceName, long long expirationTimeInSeconds) const
317{
318 AWSCredentials credentials = m_credentialsProvider->GetAWSCredentials();
319
320 //don't sign anonymous requests
321 if (credentials.GetAWSAccessKeyId().empty() || credentials.GetAWSSecretKey().empty())
322 {
323 return true;
324 }
325
326 Aws::StringStream intConversionStream;
327 intConversionStream << expirationTimeInSeconds;
328 request.AddQueryStringParameter(Http::X_AMZ_EXPIRES_HEADER, intConversionStream.str());
329
330 if (!credentials.GetSessionToken().empty())
331 {
332 request.AddQueryStringParameter(Http::AWS_SECURITY_TOKEN, credentials.GetSessionToken());
333 }
334
335 //calculate date header to use in internal signature (this also goes into date header).
336 DateTime now = GetSigningTimestamp();
337 Aws::String dateQueryValue = now.ToGmtString(LONG_DATE_FORMAT_STR);
338 request.AddQueryStringParameter(Http::AWS_DATE_HEADER, dateQueryValue);
339
340 Aws::StringStream headersStream;
341 Aws::StringStream signedHeadersStream;
342 for (const auto& header : CanonicalizeHeaders(request.GetHeaders()))
343 {
344 if(ShouldSignHeader(header.first))
345 {
346 headersStream << header.first.c_str() << ":" << header.second.c_str() << NEWLINE;
347 signedHeadersStream << header.first.c_str() << ";";
348 }
349 }
350
351 Aws::String canonicalHeadersString = headersStream.str();
352 AWS_LOGSTREAM_DEBUG(v4LogTag, "Canonical Header String: " << canonicalHeadersString);
353
354 //calculate signed headers parameter
355 Aws::String signedHeadersValue(signedHeadersStream.str());
356 //remove that last semi-colon
357 if (!signedHeadersValue.empty())
358 {
359 signedHeadersValue.pop_back();
360 }
361
362 request.AddQueryStringParameter(X_AMZ_SIGNED_HEADERS, signedHeadersValue);
363 AWS_LOGSTREAM_DEBUG(v4LogTag, "Signed Headers value: " << signedHeadersValue);
364
365 Aws::StringStream ss;
366 Aws::String signingRegion = region ? region : m_region;
367 Aws::String signingServiceName = serviceName ? serviceName : m_serviceName;
368 Aws::String simpleDate = now.ToGmtString(SIMPLE_DATE_FORMAT_STR);
369 ss << credentials.GetAWSAccessKeyId() << "/" << simpleDate
370 << "/" << signingRegion << "/" << signingServiceName << "/" << AWS4_REQUEST;
371
372 request.AddQueryStringParameter(X_AMZ_ALGORITHM, AWS_HMAC_SHA256);
373 request.AddQueryStringParameter(X_AMZ_CREDENTIAL, ss.str());
374 ss.str("");
375
376 request.SetSigningAccessKey(credentials.GetAWSAccessKeyId());
377 request.SetSigningRegion(signingRegion);
378
379 //generate generalized canonicalized request string.
380 Aws::String canonicalRequestString = CanonicalizeRequestSigningString(request, m_urlEscapePath);
381
382 //append v4 stuff to the canonical request string.
383 canonicalRequestString.append(canonicalHeadersString);
384 canonicalRequestString.append(NEWLINE);
385 canonicalRequestString.append(signedHeadersValue);
386 canonicalRequestString.append(NEWLINE);
387 if (ServiceRequireUnsignedPayload(signingServiceName))
388 {
389 canonicalRequestString.append(UNSIGNED_PAYLOAD);
390 }
391 else
392 {
393 canonicalRequestString.append(EMPTY_STRING_SHA256);
394 }
395 AWS_LOGSTREAM_DEBUG(v4LogTag, "Canonical Request String: " << canonicalRequestString);
396
397 //now compute sha256 on that request string
398 auto hashResult = m_hash->Calculate(canonicalRequestString);
399 if (!hashResult.IsSuccess())
400 {
401 AWS_LOGSTREAM_ERROR(v4LogTag, "Failed to hash (sha256) request string");
402 AWS_LOGSTREAM_DEBUG(v4LogTag, "The request string is: \"" << canonicalRequestString << "\"");
403 return false;
404 }
405
406 auto sha256Digest = hashResult.GetResult();
407 auto cannonicalRequestHash = HashingUtils::HexEncode(sha256Digest);
408
409 auto stringToSign = GenerateStringToSign(dateQueryValue, simpleDate, cannonicalRequestHash, signingRegion, signingServiceName);
410
411 auto finalSigningHash = GenerateSignature(credentials, stringToSign, simpleDate, signingRegion, signingServiceName);
412 if (finalSigningHash.empty())
413 {
414 return false;
415 }
416
417 //add that the signature to the query string
418 request.AddQueryStringParameter(X_AMZ_SIGNATURE, finalSigningHash);
419
420 return true;
421}
422
423bool AWSAuthV4Signer::ServiceRequireUnsignedPayload(const Aws::String& serviceName) const
424{
425 // S3 uses a magic string (instead of the empty string) for its body hash for presigned URLs as outlined here:
426 // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
427 // this is true for PUT, POST, GET, DELETE and HEAD operations.
428 // However, other services (for example RDS) implement the specification as outlined here:
429 // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
430 // which states that body-less requests should use the empty-string SHA256 hash.
431 return "s3" == serviceName;
432}
433
434Aws::String AWSAuthV4Signer::GenerateSignature(const AWSCredentials& credentials, const Aws::String& stringToSign,
435 const Aws::String& simpleDate, const Aws::String& region, const Aws::String& serviceName) const
436{
437 auto key = ComputeHash(credentials.GetAWSSecretKey(), simpleDate, region, serviceName);
438 return GenerateSignature(stringToSign, key);
439}
440
441Aws::String AWSAuthV4Signer::GenerateSignature(const Aws::String& stringToSign, const ByteBuffer& key) const
442{
443 AWS_LOGSTREAM_DEBUG(v4LogTag, "Final String to sign: " << stringToSign);
444
445 Aws::StringStream ss;
446
447 auto hashResult = m_HMAC->Calculate(ByteBuffer((unsigned char*)stringToSign.c_str(), stringToSign.length()), key);
448 if (!hashResult.IsSuccess())
449 {
450 AWS_LOGSTREAM_ERROR(v4LogTag, "Unable to hmac (sha256) final string");
451 AWS_LOGSTREAM_DEBUG(v4LogTag, "The final string is: \"" << stringToSign << "\"");
452 return {};
453 }
454
455 //now we finally sign our request string with our hex encoded derived hash.
456 auto finalSigningDigest = hashResult.GetResult();
457
458 auto finalSigningHash = HashingUtils::HexEncode(finalSigningDigest);
459 AWS_LOGSTREAM_DEBUG(v4LogTag, "Final computed signing hash: " << finalSigningHash);
460
461 return finalSigningHash;
462}
463
464Aws::String AWSAuthV4Signer::ComputePayloadHash(Aws::Http::HttpRequest& request) const
465{
466 if (!request.GetContentBody())
467 {
468 AWS_LOGSTREAM_DEBUG(v4LogTag, "Using cached empty string sha256 " << EMPTY_STRING_SHA256 << " because payload is empty.");
469 return EMPTY_STRING_SHA256;
470 }
471
472 //compute hash on payload if it exists.
473 auto hashResult = m_hash->Calculate(*request.GetContentBody());
474
475 if(request.GetContentBody())
476 {
477 request.GetContentBody()->clear();
478 request.GetContentBody()->seekg(0);
479 }
480
481 if (!hashResult.IsSuccess())
482 {
483 AWS_LOGSTREAM_ERROR(v4LogTag, "Unable to hash (sha256) request body");
484 return {};
485 }
486
487 auto sha256Digest = hashResult.GetResult();
488
489 Aws::String payloadHash(HashingUtils::HexEncode(sha256Digest));
490 AWS_LOGSTREAM_DEBUG(v4LogTag, "Calculated sha256 " << payloadHash << " for payload.");
491 return payloadHash;
492}
493
494Aws::String AWSAuthV4Signer::GenerateStringToSign(const Aws::String& dateValue, const Aws::String& simpleDate,
495 const Aws::String& canonicalRequestHash, const Aws::String& region, const Aws::String& serviceName) const
496{
497 //generate the actual string we will use in signing the final request.
498 Aws::StringStream ss;
499
500 ss << AWS_HMAC_SHA256 << NEWLINE << dateValue << NEWLINE << simpleDate << "/" << region << "/"
501 << serviceName << "/" << AWS4_REQUEST << NEWLINE << canonicalRequestHash;
502
503 return ss.str();
504}
505
506Aws::Utils::ByteBuffer AWSAuthV4Signer::ComputeHash(const Aws::String& secretKey,
507 const Aws::String& simpleDate, const Aws::String& region, const Aws::String& serviceName) const
508{
509 Aws::String signingKey(SIGNING_KEY);
510 signingKey.append(secretKey);
511 auto hashResult = m_HMAC->Calculate(ByteBuffer((unsigned char*)simpleDate.c_str(), simpleDate.length()),
512 ByteBuffer((unsigned char*)signingKey.c_str(), signingKey.length()));
513
514 if (!hashResult.IsSuccess())
515 {
516 AWS_LOGSTREAM_ERROR(v4LogTag, "Failed to HMAC (SHA256) date string \"" << simpleDate << "\"");
517 return {};
518 }
519
520 auto kDate = hashResult.GetResult();
521 hashResult = m_HMAC->Calculate(ByteBuffer((unsigned char*)region.c_str(), region.length()), kDate);
522 if (!hashResult.IsSuccess())
523 {
524 AWS_LOGSTREAM_ERROR(v4LogTag, "Failed to HMAC (SHA256) region string \"" << region << "\"");
525 return {};
526 }
527
528 auto kRegion = hashResult.GetResult();
529 hashResult = m_HMAC->Calculate(ByteBuffer((unsigned char*)serviceName.c_str(), serviceName.length()), kRegion);
530 if (!hashResult.IsSuccess())
531 {
532 AWS_LOGSTREAM_ERROR(v4LogTag, "Failed to HMAC (SHA256) service string \"" << m_serviceName << "\"");
533 return {};
534 }
535
536 auto kService = hashResult.GetResult();
537 hashResult = m_HMAC->Calculate(ByteBuffer((unsigned char*)AWS4_REQUEST, strlen(AWS4_REQUEST)), kService);
538 if (!hashResult.IsSuccess())
539 {
540 AWS_LOGSTREAM_ERROR(v4LogTag, "Unable to HMAC (SHA256) request string");
541 AWS_LOGSTREAM_DEBUG(v4LogTag, "The request string is: \"" << AWS4_REQUEST << "\"");
542 return {};
543 }
544 return hashResult.GetResult();
545}
546
547
548AWSAuthEventStreamV4Signer::AWSAuthEventStreamV4Signer(const std::shared_ptr<Auth::AWSCredentialsProvider>&
549 credentialsProvider, const char* serviceName, const Aws::String& region) :
550 m_serviceName(serviceName),
551 m_region(region),
552 m_credentialsProvider(credentialsProvider)
553{
554
555 m_unsignedHeaders.emplace_back(X_AMZN_TRACE_ID);
556 m_unsignedHeaders.emplace_back(USER_AGENT_HEADER);
557}
558
559bool AWSAuthEventStreamV4Signer::SignRequest(Aws::Http::HttpRequest& request, const char* region, bool /* signBody */) const
560{
561 AWSCredentials credentials = m_credentialsProvider->GetAWSCredentials();
562
563 //don't sign anonymous requests
564 if (credentials.GetAWSAccessKeyId().empty() || credentials.GetAWSSecretKey().empty())
565 {
566 return true;
567 }
568
569 if (!credentials.GetSessionToken().empty())
570 {
571 request.SetAwsSessionToken(credentials.GetSessionToken());
572 }
573
574 request.SetHeaderValue(X_AMZ_CONTENT_SHA256, EVENT_STREAM_CONTENT_SHA256);
575
576 //calculate date header to use in internal signature (this also goes into date header).
577 DateTime now = GetSigningTimestamp();
578 Aws::String dateHeaderValue = now.ToGmtString(LONG_DATE_FORMAT_STR);
579 request.SetHeaderValue(AWS_DATE_HEADER, dateHeaderValue);
580
581 Aws::StringStream headersStream;
582 Aws::StringStream signedHeadersStream;
583
584 for (const auto& header : CanonicalizeHeaders(request.GetHeaders()))
585 {
586 if(ShouldSignHeader(header.first))
587 {
588 headersStream << header.first.c_str() << ":" << header.second.c_str() << NEWLINE;
589 signedHeadersStream << header.first.c_str() << ";";
590 }
591 }
592
593 Aws::String canonicalHeadersString = headersStream.str();
594 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "Canonical Header String: " << canonicalHeadersString);
595
596 //calculate signed headers parameter
597 Aws::String signedHeadersValue = signedHeadersStream.str();
598 //remove that last semi-colon
599 if (!signedHeadersValue.empty())
600 {
601 signedHeadersValue.pop_back();
602 }
603
604 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "Signed Headers value:" << signedHeadersValue);
605
606 //generate generalized canonicalized request string.
607 Aws::String canonicalRequestString = CanonicalizeRequestSigningString(request, true/* m_urlEscapePath */);
608
609 //append v4 stuff to the canonical request string.
610 canonicalRequestString.append(canonicalHeadersString);
611 canonicalRequestString.append(NEWLINE);
612 canonicalRequestString.append(signedHeadersValue);
613 canonicalRequestString.append(NEWLINE);
614 canonicalRequestString.append(EVENT_STREAM_CONTENT_SHA256);
615
616 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "Canonical Request String: " << canonicalRequestString);
617
618 //now compute sha256 on that request string
619 auto hashResult = m_hash.Calculate(canonicalRequestString);
620 if (!hashResult.IsSuccess())
621 {
622 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Failed to hash (sha256) request string");
623 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "The request string is: \"" << canonicalRequestString << "\"");
624 return false;
625 }
626
627 auto sha256Digest = hashResult.GetResult();
628 Aws::String cannonicalRequestHash = HashingUtils::HexEncode(sha256Digest);
629 Aws::String simpleDate = now.ToGmtString(SIMPLE_DATE_FORMAT_STR);
630
631 Aws::String signingRegion = region ? region : m_region;
632 Aws::String stringToSign = GenerateStringToSign(dateHeaderValue, simpleDate, cannonicalRequestHash, signingRegion, m_serviceName);
633 auto finalSignature = GenerateSignature(credentials, stringToSign, simpleDate, signingRegion, m_serviceName);
634
635 Aws::StringStream ss;
636 ss << AWS_HMAC_SHA256 << " " << CREDENTIAL << EQ << credentials.GetAWSAccessKeyId() << "/" << simpleDate
637 << "/" << signingRegion << "/" << m_serviceName << "/" << AWS4_REQUEST << ", " << SIGNED_HEADERS << EQ
638 << signedHeadersValue << ", " << SIGNATURE << EQ << HashingUtils::HexEncode(finalSignature);
639
640 auto awsAuthString = ss.str();
641 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "Signing request with: " << awsAuthString);
642 request.SetAwsAuthorization(awsAuthString);
643 request.SetSigningAccessKey(credentials.GetAWSAccessKeyId());
644 request.SetSigningRegion(signingRegion);
645 return true;
646}
647
648// this works regardless if the current machine is Big/Little Endian
649static void WriteBigEndian(Aws::String& str, uint64_t n)
650{
651 int shift = 56;
652 while(shift >= 0)
653 {
654 str.push_back((n >> shift) & 0xFF);
655 shift -= 8;
656 }
657}
658
659bool AWSAuthEventStreamV4Signer::SignEventMessage(Event::Message& message, Aws::String& priorSignature) const
660{
661 using Event::EventHeaderValue;
662
663 Aws::StringStream stringToSign;
664 stringToSign << EVENT_STREAM_PAYLOAD << NEWLINE;
665 const DateTime now = GetSigningTimestamp();
666 const auto simpleDate = now.ToGmtString(SIMPLE_DATE_FORMAT_STR);
667 stringToSign << now.ToGmtString(LONG_DATE_FORMAT_STR) << NEWLINE
668 << simpleDate << "/" << m_region << "/"
669 << m_serviceName << "/aws4_request" << NEWLINE << priorSignature << NEWLINE;
670
671
672 Aws::String nonSignatureHeaders;
673 nonSignatureHeaders.push_back(char(sizeof(EVENTSTREAM_DATE_HEADER) - 1)); // length of the string
674 nonSignatureHeaders += EVENTSTREAM_DATE_HEADER;
675 nonSignatureHeaders.push_back(static_cast<char>(EventHeaderValue::EventHeaderType::TIMESTAMP)); // type of the value
676 WriteBigEndian(nonSignatureHeaders, static_cast<uint64_t>(now.Millis())); // the value of the timestamp in big-endian
677
678 auto hashOutcome = m_hash.Calculate(nonSignatureHeaders);
679 if (!hashOutcome.IsSuccess())
680 {
681 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Failed to hash (sha256) non-signature headers.");
682 return false;
683 }
684
685 const auto nonSignatureHeadersHash = hashOutcome.GetResult();
686 stringToSign << HashingUtils::HexEncode(nonSignatureHeadersHash) << NEWLINE;
687
688 if (message.GetEventPayload().empty())
689 {
690 AWS_LOGSTREAM_WARN(v4StreamingLogTag, "Attempting to sign an empty message (no payload and no headers). "
691 "It is unlikely that this is the intended behavior.");
692 }
693 else
694 {
695 // use a preallocatedStreamBuf to avoid making a copy.
696 // The Hashing API requires either Aws::String or IStream as input.
697 // TODO: the hashing API should be accept 'unsigned char*' as input.
698 Utils::Stream::PreallocatedStreamBuf streamBuf(message.GetEventPayload().data(), message.GetEventPayload().size());
699 Aws::IOStream payload(&streamBuf);
700 hashOutcome = m_hash.Calculate(payload);
701
702 if (!hashOutcome.IsSuccess())
703 {
704 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Failed to hash (sha256) non-signature headers.");
705 return false;
706 }
707 const auto payloadHash = hashOutcome.GetResult();
708 stringToSign << HashingUtils::HexEncode(payloadHash);
709 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "Payload hash - " << HashingUtils::HexEncode(payloadHash));
710 }
711
712 Utils::ByteBuffer finalSignatureDigest = GenerateSignature(m_credentialsProvider->GetAWSCredentials(), stringToSign.str(), simpleDate, m_region, m_serviceName);
713 const auto finalSignature = HashingUtils::HexEncode(finalSignatureDigest);
714 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "Final computed signing hash: " << finalSignature);
715 priorSignature = finalSignature;
716
717 message.InsertEventHeader(EVENTSTREAM_DATE_HEADER, EventHeaderValue(now.Millis(), EventHeaderValue::EventHeaderType::TIMESTAMP));
718 message.InsertEventHeader(EVENTSTREAM_SIGNATURE_HEADER, std::move(finalSignatureDigest));
719
720 AWS_LOGSTREAM_INFO(v4StreamingLogTag, "Event chunk final signature - " << finalSignature);
721 return true;
722}
723
724bool AWSAuthEventStreamV4Signer::ShouldSignHeader(const Aws::String& header) const
725{
726 return std::find(m_unsignedHeaders.cbegin(), m_unsignedHeaders.cend(), Aws::Utils::StringUtils::ToLower(header.c_str())) == m_unsignedHeaders.cend();
727}
728
729Utils::ByteBuffer AWSAuthEventStreamV4Signer::GenerateSignature(const AWSCredentials& credentials, const Aws::String& stringToSign,
730 const Aws::String& simpleDate, const Aws::String& region, const Aws::String& serviceName) const
731{
732 Utils::Threading::ReaderLockGuard guard(m_derivedKeyLock);
733 const auto& secretKey = credentials.GetAWSSecretKey();
734 if (secretKey != m_currentSecretKey || simpleDate != m_currentDateStr)
735 {
736 guard.UpgradeToWriterLock();
737 // double-checked lock to prevent updating twice
738 if (m_currentDateStr != simpleDate || m_currentSecretKey != secretKey)
739 {
740 m_currentSecretKey = secretKey;
741 m_currentDateStr = simpleDate;
742 m_derivedKey = ComputeHash(m_currentSecretKey, m_currentDateStr, region, serviceName);
743 }
744
745 }
746 return GenerateSignature(stringToSign, m_derivedKey);
747}
748
749Utils::ByteBuffer AWSAuthEventStreamV4Signer::GenerateSignature(const Aws::String& stringToSign, const ByteBuffer& key) const
750{
751 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "Final String to sign: " << stringToSign);
752
753 Aws::StringStream ss;
754
755 auto hashResult = m_HMAC.Calculate(ByteBuffer((unsigned char*)stringToSign.c_str(), stringToSign.length()), key);
756 if (!hashResult.IsSuccess())
757 {
758 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Unable to hmac (sha256) final string");
759 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "The final string is: \"" << stringToSign << "\"");
760 return {};
761 }
762
763 return hashResult.GetResult();
764}
765
766Aws::String AWSAuthEventStreamV4Signer::GenerateStringToSign(const Aws::String& dateValue, const Aws::String& simpleDate,
767 const Aws::String& canonicalRequestHash, const Aws::String& region, const Aws::String& serviceName) const
768{
769 //generate the actual string we will use in signing the final request.
770 Aws::StringStream ss;
771
772 ss << AWS_HMAC_SHA256 << NEWLINE << dateValue << NEWLINE << simpleDate << "/" << region << "/"
773 << serviceName << "/" << AWS4_REQUEST << NEWLINE << canonicalRequestHash;
774
775 return ss.str();
776}
777
778Aws::Utils::ByteBuffer AWSAuthEventStreamV4Signer::ComputeHash(const Aws::String& secretKey,
779 const Aws::String& simpleDate, const Aws::String& region, const Aws::String& serviceName) const
780{
781 Aws::String signingKey(SIGNING_KEY);
782 signingKey.append(secretKey);
783 auto hashResult = m_HMAC.Calculate(ByteBuffer((unsigned char*)simpleDate.c_str(), simpleDate.length()),
784 ByteBuffer((unsigned char*)signingKey.c_str(), signingKey.length()));
785
786 if (!hashResult.IsSuccess())
787 {
788 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Failed to HMAC (SHA256) date string \"" << simpleDate << "\"");
789 return {};
790 }
791
792 auto kDate = hashResult.GetResult();
793 hashResult = m_HMAC.Calculate(ByteBuffer((unsigned char*)region.c_str(), region.length()), kDate);
794 if (!hashResult.IsSuccess())
795 {
796 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Failed to HMAC (SHA256) region string \"" << region << "\"");
797 return {};
798 }
799
800 auto kRegion = hashResult.GetResult();
801 hashResult = m_HMAC.Calculate(ByteBuffer((unsigned char*)serviceName.c_str(), serviceName.length()), kRegion);
802 if (!hashResult.IsSuccess())
803 {
804 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Failed to HMAC (SHA256) service string \"" << m_serviceName << "\"");
805 return {};
806 }
807
808 auto kService = hashResult.GetResult();
809 hashResult = m_HMAC.Calculate(ByteBuffer((unsigned char*)AWS4_REQUEST, strlen(AWS4_REQUEST)), kService);
810 if (!hashResult.IsSuccess())
811 {
812 AWS_LOGSTREAM_ERROR(v4StreamingLogTag, "Unable to HMAC (SHA256) request string");
813 AWS_LOGSTREAM_DEBUG(v4StreamingLogTag, "The request string is: \"" << AWS4_REQUEST << "\"");
814 return {};
815 }
816 return hashResult.GetResult();
817}
818