libqtaws  0.1.0
UnofficialAWSlibraryforQt-InternalDocumentation
awssignaturev3.cpp
1 /*
2  Copyright 2013-2015 Paul Colby
3 
4  This file is part of libqtaws.
5 
6  Libqtaws is free software: you can redistribute it and/or modify
7  it under the terms of the GNU Lesser General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  Libqtaws is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU Lesser General Public License for more details.
15 
16  You should have received a copy of the GNU Lesser General Public License
17  along with libqtaws. If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "awssignaturev3.h"
21 #include "awssignaturev3_p.h"
22 
23 #include "awsendpoint.h"
24 
25 #include <QDebug>
26 #include <QUuid>
27 
28 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) && QT_VERSION < QT_VERSION_CHECK(5, 1, 0)
29 #include "qmessageauthenticationcode.h"
30 #else
31 #include <QMessageAuthenticationCode>
32 #endif
33 
34 QTAWS_BEGIN_NAMESPACE
35 
36 /**
37  * @class AwsSignatureV3
38  *
39  * @brief Implements AWS Signature Version 3.
40  *
41  * This class implements both `AWS3` and `AWS3-HTTPS` varieties.
42  *
43  * @see http://docs.aws.amazon.com/amazonswf/latest/developerguide/HMACAuth-swf.html (AWS3)
44  * @see http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html (AWS3-HTTPS)
45  */
46 
47 /**
48  * @brief Constructs a new AwsSignatureV3 object.
49  *
50  * Use instances of this object to provide Version 3 signatures for AWS services.
51  *
52  * @param hashAlgorithm Hash algorithm for signatures. Must be either QCryptographicHash::Sha1
53  * or QCryptographicHash::Sha256 (default, recommended).
54  */
55 AwsSignatureV3::AwsSignatureV3(const QCryptographicHash::Algorithm hashAlgorithm)
56  : AwsAbstractSignature(new AwsSignatureV3Private(hashAlgorithm, this))
57 {
58 
59 }
60 
62  const QNetworkAccessManager::Operation operation,
63  QNetworkRequest &request, const QByteArray &data) const
64 {
65  Q_D(const AwsSignatureV3);
66 
67  // Note, the use of a nonce value with AWS3-HTTPS is undocumented, but done by the
68  // official Java SDK, and worth copying for additional security.
69  if ((d->isHttps(request)) && (!request.hasRawHeader("x-amz-nonce"))) {
70  request.setRawHeader("x-amz-nonce", QUuid::createUuid().toByteArray().mid(1,36));
71  }
72 
73  d->setDateHeader(request);
74  d->setAuthorizationHeader(credentials, operation, request, data);
75 }
76 
78 {
79  return 3;
80 }
81 
82 /**
83  * @internal
84  *
85  * @class AwsSignatureV3Private
86  *
87  * @brief Private implementation for AwsSignatureV3.
88  *
89  * @see http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
90  */
91 
92 /**
93  * @brief Constructs a new AwsSignatureV3Private object.
94  *
95  * @param hashAlgorithm The algorithm to use during various stages of signing.
96  * @param q Pointer to this object's public AwsSignatureV3 instance.
97  */
98 AwsSignatureV3Private::AwsSignatureV3Private(const QCryptographicHash::Algorithm hashAlgorithm,
99  AwsSignatureV3 * const q)
100  : AwsAbstractSignaturePrivate(q), hashAlgorithm(hashAlgorithm)
101 {
102 
103 }
104 
105 /**
106  * @brief Create an AWS V3 Signature algorithm designation.
107  *
108  * This function returns an algorithm designation, as defined by Amazon, for use with
109  * V3 signatures.
110  *
111  * For example, if the algorith is `QCryptographicHash::Sha256`, this function will
112  * return `HmacSHA256`.
113  *
114  * @param algorithm The hash algorithm to get the canonical designation for.
115  *
116  * @return An AWS V3 Signature algorithm designation.
117  *
118  * @see http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html
119  */
120 QByteArray AwsSignatureV3Private::algorithmDesignation(const QCryptographicHash::Algorithm algorithm) const
121 {
122  switch (algorithm) {
123  case QCryptographicHash::Sha1: return "HmacSHA1";
124  case QCryptographicHash::Sha256: return "HmacSHA256";
125  default:
126  Q_ASSERT_X(false, Q_FUNC_INFO, "invalid algorithm");
127  return "invalid-algorithm";
128  }
129 }
130 
131 /**
132  * @brief Create an AWS V3 Signature authorization header value.
133  *
134  * This function builds a V3 signature, and returns it to the caller. The returned
135  * header value is then suitable for adding as an `Authorization` header in the HTTP
136  * request, to be accepted by Amazon.
137  *
138  * @param credentials The AWS credentials to use to sign the request.
139  * @param operation The HTTP method being used for the request.
140  * @param request The network request to generate a signature for.
141  * @param payload Optional data being submitted in the request (eg for `PUT` and `POST` operations).
142  *
143  * @return An AWS V3 Signature authorization header value.
144  *
145  * @see http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html
146  * @see setAuthorizationHeader
147  */
149  const QNetworkAccessManager::Operation operation,
150  QNetworkRequest &request, const QByteArray &payload) const
151 {
152  // Calculate the signature.
153  QByteArray signedHeaders;
154  QByteArray stringToSign = canonicalRequest(operation, request, payload, &signedHeaders);
155  if (!isHttps(request)) {
156  stringToSign = QCryptographicHash::hash(stringToSign, hashAlgorithm);
157  }
158  const QByteArray signature = QMessageAuthenticationCode::hash(
159  stringToSign, credentials.secretKey().toUtf8(), hashAlgorithm);
160 
161  // Build and return the authorization header value.
162  return
163  QByteArray((isHttps(request)) ? "AWS3-HTTPS " : "AWS3 ") +
164  "AWSAccessKeyId=" + credentials.accessKeyId().toUtf8() + ","
165  "Algorithm=" + algorithmDesignation(hashAlgorithm) + "," +
166  ((!isHttps(request)) ? "SignedHeaders=" + signedHeaders + ',' : "") +
167  "Signature=" + signature.toBase64();
168 }
169 
170 /**
171  * @brief Create an AWS V3 Signature canonical header string.
172  *
173  * @note Amazon documentation does not specify how to handle whitespace within
174  * quotes for V3 signatures, so here we use the same approach as V3
175  * signatures. That is:
176  *
177  * In canonical form, header name and value are combined with a single semi-colon
178  * separator, with all whitespace removed from both, _except_ for whitespace within
179  * double-quotes.
180  *
181  * @note This function is only applicable to the `AWS3` format, not `AWS3-HTTPS`.
182  *
183  * @param headerName Name of the HTTP header to convert to canonical form.
184  * @param headerValue Value of the HTTP header to convert to canonical form.
185  *
186  * @return An AWS V3 Signature canonical header string.
187  *
188  * @see http://docs.aws.amazon.com/amazonswf/latest/developerguide/HMACAuth-swf.html
189  * @see http://docs.aws.amazon.com/general/latest/gr/sigV4-create-canonical-request.html
190  * @see canonicalHeaders
191  */
192 QByteArray AwsSignatureV3Private::canonicalHeader(const QByteArray &headerName, const QByteArray &headerValue) const
193 {
194  QByteArray header = headerName.toLower() + ':';
195  const QByteArray trimmedHeaderValue = headerValue.trimmed();
196  bool isInQuotes = false;
197  char previousChar = '\0';
198  for (int index = 0; index < trimmedHeaderValue.size(); ++index) {
199  char thisChar = trimmedHeaderValue.at(index);
200  header += thisChar;
201  if (isInQuotes) {
202  if ((thisChar == '"') && (previousChar != '\\'))
203  isInQuotes = false;
204  } else {
205  if ((thisChar == '"') && (previousChar != '\\')) {
206  isInQuotes = true;
207  } else if (isspace(thisChar)) {
208  while ((index < trimmedHeaderValue.size()-1) &&
209  (isspace(trimmedHeaderValue.at(index+1))))
210  ++index;
211  }
212  }
213  previousChar = thisChar;
214  }
215  return header;
216 }
217 
218 /**
219  * @brief Create an AWS V3 Signature canonical headers string.
220  *
221  * This function constructs a canonical string containing all of the headers
222  * in the given request.
223  *
224  * @note \p request will typically not include a `Host` header at this stage,
225  * however Qt will add an appropriate `Host` header when the request is
226  * performed. So, if \p request does not include a `Host` header yet,
227  * this function will include a derived `Host` header in the canonical
228  * headers to allow for it.
229  *
230  * @note This function is only applicable to the `AWS3` format, not `AWS3-HTTPS`.
231  *
232  * @param[in] request The network request to fetch the canonical headers from.
233  * @param[out] signedHeaders A semi-colon separated list of the names of all headers
234  * included in the result.
235  *
236  * @return An AWS V3 Signature canonical headers string.
237  *
238  * @see http://docs.aws.amazon.com/general/latest/gr/sigV3-create-canonical-request.html
239  * @see canonicalHeader
240  */
241 QByteArray AwsSignatureV3Private::canonicalHeaders(const QNetworkRequest &request, QByteArray * const signedHeaders) const
242 {
243  Q_CHECK_PTR(signedHeaders);
244  signedHeaders->clear();
245 
246  /* Note, Amazon says we should combine duplicate headers with comma separators...
247  * conveniently for us, QNetworkRequest requires that to have been done already.
248  * See note in QNetworkRequest::setRawHeader.
249  */
250 
251  // Convert the raw headers list to a map to sort on (lowercased) header names only.
252  QMap<QByteArray,QByteArray> headers;
253  foreach (const QByteArray &rawHeader, request.rawHeaderList()) {
254  headers.insert(rawHeader.toLower(), request.rawHeader(rawHeader));
255  }
256  // The "host" header is not included in QNetworkRequest::rawHeaderList, but will be sent by Qt.
257  headers.insert("host", request.url().host().toUtf8());
258 
259  // Convert the headers map to a canonical string, keeping track of which headers we've included too.
260  QByteArray canonicalHeaders;
261  for (QMap<QByteArray,QByteArray>::const_iterator iter = headers.constBegin(); iter != headers.constEnd(); ++iter) {
262  // Only include "host" and "x-amz-*" headers. Note, Amazon documentation states that latter as
263  // "x-amz-" (ie with the trailing '-'), yet the official Amazon Java SDK tests for a "x-amz"
264  // prefix. Thus the Java SDK would include headers with keys like "x-amzfoo", but here we do not
265  // since that would disagree with the official documentation.
266  if ((iter.key() == "host") || (iter.key().startsWith("x-amz-"))) {
267  canonicalHeaders += canonicalHeader(iter.key(), iter.value()) + '\n';
268  if (!signedHeaders->isEmpty()) *signedHeaders += ';';
269  *signedHeaders += iter.key();
270  }
271  }
272  return canonicalHeaders;
273 }
274 
275 /**
276  * @brief Create an AWS V3 Signature canonical request.
277  *
278  * Note, this function implments both `AWS3` and `AWS3-HTTPS` variants of the
279  * AWS Signature version 3 - which are quite different.
280  *
281  * @param[in] operation The HTTP method being used for the request.
282  * @param[in] request The network request to generate a canonical request for.
283  * @param[in] payload Optional data being submitted in the request (eg for `PUT` and `POST` operations).
284  * @param[out] signedHeaders A semi-colon separated list of the names of all headers
285  * included in the result.
286  *
287  * @return An AWS V3 Signature canonical request.
288  *
289  * @see http://docs.aws.amazon.com/amazonswf/latest/developerguide/HMACAuth-swf.html (AWS3)
290  * @see http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html (AWS3-HTTPS)
291  */
292 QByteArray AwsSignatureV3Private::canonicalRequest(const QNetworkAccessManager::Operation operation,
293  const QNetworkRequest &request, const QByteArray &payload,
294  QByteArray * const signedHeaders) const
295 {
296  // AWS3-HTTPS
297  if (isHttps(request)) {
298  Q_ASSERT((request.hasRawHeader("x-amz-date")) || (request.hasRawHeader("Date")));
299  QByteArray canonicalRequest = request.rawHeader(request.hasRawHeader("x-amz-date") ? "x-amz-date" : "Date");
300  if (request.hasRawHeader("x-amz-nonce")) {
301  canonicalRequest += request.rawHeader("x-amz-nonce");
302  }
303  return canonicalRequest;
304  }
305 
306  // AWS3
307  return httpMethod(operation).toUtf8() + '\n' +
308  canonicalPath(request.url()).toUtf8() + '\n' +
309  canonicalQuery(QUrlQuery(request.url())) + '\n' +
310  canonicalHeaders(request, signedHeaders) + '\n' +
311  payload;
312 }
313 
314 /**
315  * @brief Does a request use the HTTPS scheme?
316  *
317  * @param request The network request to evaluate.
318  *
319  * @return `true` if \a request uses the HTTPS scheme, `false` otherwise.
320  */
321 bool AwsSignatureV3Private::isHttps(const QNetworkRequest &request)
322 {
323  return (request.url().scheme() == QLatin1String("https"));
324 }
325 
326 /**
327  * @brief Set authorization header on a network request.
328  *
329  * This function will calculate the authorization header value and set it as the `Authorization`
330  * HTTP header on \p request.
331  *
332  * @param[in] credentials The AWS credentials to use to sign the request.
333  * @param[in] operation The HTTP method being used for the request.
334  * @param[in,out] request The network request to add the authorization header to.
335  * @param[in] payload Optional data being submitted in the request (eg for `PUT` and `POST` operations).
336  *
337  * @see http://docs.aws.amazon.com/general/latest/gr/sigV3-signed-request-examples.html
338  * @see authorizationHeaderValue
339  */
341  const QNetworkAccessManager::Operation operation,
342  QNetworkRequest &request, const QByteArray &payload) const
343 {
344  Q_ASSERT(!request.hasRawHeader("Authorization"));
345  request.setRawHeader("Authorization", authorizationHeaderValue(credentials, operation, request, payload));
346 }
347 
348 /**
349  * @brief Set the AWS custom date header.
350  *
351  * If \a request does not already contain an `x-amz-date` header, then this function
352  * will set a custom `x-amz-date` header to the value of \p dateTime formatted like
353  * "Fri, 09 Sep 2011 23:36:00 GMT".
354  *
355  * @param request The network request to add the date header to.
356  * @param dateTime The timestamp (in UTC) to set the date header's value to.
357  */
358 void AwsSignatureV3Private::setDateHeader(QNetworkRequest &request, const QDateTime &dateTime) const
359 {
360  Q_ASSERT(dateTime.timeSpec() == Qt::UTC);
361  if (!request.hasRawHeader("x-amz-date")) {
362  request.setRawHeader("x-amz-date", dateTime.toString(QLatin1String("ddd, dd MMM yyyy hh:mm:ss 'GMT'")).toUtf8());
363  }
364 }
365 
366 QTAWS_END_NAMESPACE
virtual void sign(const AwsAbstractCredentials &credentials, const QNetworkAccessManager::Operation operation, QNetworkRequest &request, const QByteArray &data=QByteArray()) const
Sign an AWS request.
void setAuthorizationHeader(const AwsAbstractCredentials &credentials, const QNetworkAccessManager::Operation operation, QNetworkRequest &request, const QByteArray &payload) const
Set authorization header on a network request.
AwsSignatureV3(const QCryptographicHash::Algorithm hashAlgorithm=QCryptographicHash::Sha256)
Constructs a new AwsSignatureV3 object.
QByteArray authorizationHeaderValue(const AwsAbstractCredentials &credentials, const QNetworkAccessManager::Operation operation, QNetworkRequest &request, const QByteArray &payload) const
Create an AWS V3 Signature authorization header value.
Private implementation for AwsAbstractSignature.
QByteArray canonicalQuery(const QUrlQuery &query) const
Create an AWS Signature canonical query.
virtual QString secretKey() const =0
AWS secret access key for this credentials object.
AwsSignatureV3Private(const QCryptographicHash::Algorithm hashAlgorithm, AwsSignatureV3 *const q)
Constructs a new AwsSignatureV3Private object.
Interface class for providing AWS credentials.
QByteArray canonicalHeader(const QByteArray &headerName, const QByteArray &headerValue) const
Create an AWS V3 Signature canonical header string.
Private implementation for AwsSignatureV3.
QByteArray algorithmDesignation(const QCryptographicHash::Algorithm algorithm) const
Create an AWS V3 Signature algorithm designation.
QByteArray canonicalHeaders(const QNetworkRequest &request, QByteArray *const signedHeaders) const
Create an AWS V3 Signature canonical headers string.
virtual QString accessKeyId() const =0
AWS access key ID for this credentials object.
void setDateHeader(QNetworkRequest &request, const QDateTime &dateTime=QDateTime::currentDateTimeUtc()) const
Set the AWS custom date header.
QByteArray canonicalRequest(const QNetworkAccessManager::Operation operation, const QNetworkRequest &request, const QByteArray &payload, QByteArray *const signedHeaders) const
Create an AWS V3 Signature canonical request.
Implements AWS Signature Version 3.
virtual int version() const
AWS Signature version implemented by this class.
static bool isHttps(const QNetworkRequest &request)
Does a request use the HTTPS scheme?
const QCryptographicHash::Algorithm hashAlgorithm
Hash algorithm to use when signing.
QString httpMethod(const QNetworkAccessManager::Operation operation) const
Create an AWS Signature request method string.
Interface class for providing AWS signatures.
QString canonicalPath(const QUrl &url) const
Create an AWS Signature canonical path.