libqtaws  0.1.0
UnofficialAWSlibraryforQt-InternalDocumentation
awssignaturev4.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 "awssignaturev4.h"
21 #include "awssignaturev4_p.h"
22 
23 #include "awsendpoint.h"
24 
25 #include <QDebug>
26 
27 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) && QT_VERSION < QT_VERSION_CHECK(5, 1, 0)
28 #include "qmessageauthenticationcode.h"
29 #else
30 #include <QMessageAuthenticationCode>
31 #endif
32 
33 QTAWS_BEGIN_NAMESPACE
34 
35 /**
36  * @class AwsSignatureV4
37  *
38  * @brief Implements AWS Signature Version 4.
39  *
40  * @see http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
41  */
42 
43 /**
44  * @brief Constructs a new AwsSignatureV4 object.
45  *
46  * Use instances of this object to provide Version 4 signatures for AWS services.
47  *
48  * @param hashAlgorithm The algorithm to use during various stages of signing.
49  *
50  * @note The AWS Signature Version 4 documentation is not explcit about which hash
51  * algorithms are supported by Amazon, however all documented examples use
52  * SHA256.
53  */
54 AwsSignatureV4::AwsSignatureV4(const QCryptographicHash::Algorithm hashAlgorithm)
55  : AwsAbstractSignature(new AwsSignatureV4Private(hashAlgorithm, this))
56 {
57 
58 }
59 
61  const QNetworkAccessManager::Operation operation,
62  QNetworkRequest &request, const QByteArray &data) const
63 {
64  Q_D(const AwsSignatureV4);
65  d->setAuthorizationHeader(credentials, operation, request, data, d->setDateHeader(request));
66 }
67 
69 {
70  return 4;
71 }
72 
73 /**
74  * @internal
75  *
76  * @class AwsSignatureV4Private
77  *
78  * @brief Private implementation for AwsSignatureV4.
79  *
80  * @see http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
81  */
82 
83 /// Format V4 signatures use to represent dates in canonical form.
84 const QLatin1String AwsSignatureV4Private::DateFormat("yyyyMMdd");
85 
86 /// Format V4 signatures use to represent timestamps in canonical form.
87 const QLatin1String AwsSignatureV4Private::DateTimeFormat("yyyyMMddThhmmssZ");
88 
89 /**
90  * @brief Constructs a new AwsSignatureV4Private object.
91  *
92  * @param hashAlgorithm The algorithm to use during various stages of signing.
93  * @param q Pointer to this object's public AwsSignatureV4 instance.
94  */
95 AwsSignatureV4Private::AwsSignatureV4Private(const QCryptographicHash::Algorithm hashAlgorithm,
96  AwsSignatureV4 * const q)
97  : AwsAbstractSignaturePrivate(q), hashAlgorithm(hashAlgorithm)
98 {
99 
100 }
101 
102 /**
103  * @brief Create an AWS V4 Signature algorithm designation.
104  *
105  * This function returns an algorithm designation, as defined by Amazon, for use with
106  * V4 signatures.
107  *
108  * For example, if the algorith is `QCryptographicHash::Sha256`, this function will
109  * return `AWS4-HMAC-SHA256`.
110  *
111  * @param algorithm The hash algorithm to get the canonical designation for.
112  *
113  * @return An AWS V4 Signature algorithm designation.
114  *
115  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
116  */
117 QByteArray AwsSignatureV4Private::algorithmDesignation(const QCryptographicHash::Algorithm algorithm) const
118 {
119  switch (algorithm) {
120  case QCryptographicHash::Md4: return "AWS4-HMAC-MD4";
121  case QCryptographicHash::Md5: return "AWS4-HMAC-MD5";
122  case QCryptographicHash::Sha1: return "AWS4-HMAC-SHA1";
123  case QCryptographicHash::Sha224: return "AWS4-HMAC-SHA224";
124  case QCryptographicHash::Sha256: return "AWS4-HMAC-SHA256";
125  case QCryptographicHash::Sha384: return "AWS4-HMAC-SHA384";
126  case QCryptographicHash::Sha512: return "AWS4-HMAC-SHA512";
127  default:
128  Q_ASSERT_X(false, Q_FUNC_INFO, "invalid algorithm");
129  return "invalid-algorithm";
130  }
131 }
132 
133 /**
134  * @brief Create an AWS V4 Signature authorization header value.
135  *
136  * This function builds an V4 signature, and returns it to the caller. The returned
137  * header value is then suitable for adding as a `Authorization` header in the HTTP
138  * request, to be accepted by Amazon.
139  *
140  * @param credentials The AWS credentials to use to sign the request.
141  * @param operation The HTTP method being used for the request.
142  * @param request The network request to generate a signature for.
143  * @param payload Optional data being submitted in the request (eg for `PUT` and `POST` operations).
144  * @param timestamp The timestamp to use when signing the request.
145  *
146  * @return An AWS V4 Signature authorization header value.
147  *
148  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
149  * @see setAuthorizationHeader
150  */
152  const QNetworkAccessManager::Operation operation,
153  QNetworkRequest &request, const QByteArray &payload,
154  const QDateTime &timestamp) const
155 {
156  const QByteArray algorithmDesignation = this->algorithmDesignation(hashAlgorithm);
157  const AwsEndpoint endpoint(request.url().host());
158 
159  const QByteArray credentialScope = this->credentialScope(timestamp.date(), endpoint.regionName(), endpoint.serviceName());
160  QByteArray signedHeaders;
161  const QByteArray canonicalRequest = this->canonicalRequest(operation, request, payload, &signedHeaders);
162 
163  const QByteArray stringToSign = this->stringToSign(algorithmDesignation, timestamp, credentialScope, canonicalRequest);
164  const QByteArray signingKey = this->signingKey(credentials, timestamp.date(), endpoint.regionName(), endpoint.serviceName());
165  const QByteArray signature = QMessageAuthenticationCode::hash(stringToSign, signingKey, hashAlgorithm);
166 
167  return algorithmDesignation + " Credential=" + credentials.accessKeyId().toUtf8() + '/' + credentialScope +
168  ", SignedHeaders=" + signedHeaders + ", Signature=" + signature.toHex();
169 }
170 
171 /**
172  * @brief Create an AWS V4 Signature canonical header string.
173  *
174  * In canonical form, header name and value are combined with a single semi-colon
175  * separator, with all whitespace removed from both, _except_ for whitespace within
176  * double-quotes.
177  *
178  * @param headerName Name of the HTTP header to convert to canonical form.
179  * @param headerValue Value of the HTTP header to convert to canonical form.
180  *
181  * @return An AWS V4 Signature canonical header string.
182  *
183  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
184  * @see canonicalHeaders
185  */
186 QByteArray AwsSignatureV4Private::canonicalHeader(const QByteArray &headerName, const QByteArray &headerValue) const
187 {
188  QByteArray header = headerName.toLower() + ':';
189  const QByteArray trimmedHeaderValue = headerValue.trimmed();
190  bool isInQuotes = false;
191  char previousChar = '\0';
192  for (int index = 0; index < trimmedHeaderValue.size(); ++index) {
193  char thisChar = trimmedHeaderValue.at(index);
194  header += thisChar;
195  if (isInQuotes) {
196  if ((thisChar == '"') && (previousChar != '\\'))
197  isInQuotes = false;
198  } else {
199  if ((thisChar == '"') && (previousChar != '\\')) {
200  isInQuotes = true;
201  } else if (isspace(thisChar)) {
202  while ((index < trimmedHeaderValue.size()-1) &&
203  (isspace(trimmedHeaderValue.at(index+1))))
204  ++index;
205  }
206  }
207  previousChar = thisChar;
208  }
209  return header;
210 }
211 
212 /**
213  * @brief Create an AWS V4 Signature canonical headers string.
214  *
215  * This function constructs a canonical string containing all of the headers
216  * in the given request.
217  *
218  * @note \p request will typically not include a `Host` header at this stage,
219  * however Qt will add an appropriate `Host` header when the request is
220  * performed. So, if \p request does not include a `Host` header yet,
221  * this function will include a derived `Host` header in the canonical
222  * headers to allow for it.
223  *
224  * @param[in] request The network request to fetch the canonical headers from.
225  * @param[out] signedHeaders A semi-colon separated list of the names of all headers
226  * included in the result.
227  *
228  * @return An AWS V4 Signature canonical headers string.
229  *
230  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
231  * @see canonicalHeader
232  */
233 QByteArray AwsSignatureV4Private::canonicalHeaders(const QNetworkRequest &request, QByteArray * const signedHeaders) const
234 {
235  Q_CHECK_PTR(signedHeaders);
236  signedHeaders->clear();
237 
238  /* Note, Amazon says we should combine duplicate headers with comma separators...
239  * conveniently for us, QNetworkRequest requires that to have been done already.
240  * See note in QNetworkRequest::setRawHeader.
241  */
242 
243  // Convert the raw headers list to a map to sort on (lowercased) header names only.
244  QMap<QByteArray,QByteArray> headers;
245  foreach (const QByteArray &rawHeader, request.rawHeaderList()) {
246  headers.insert(rawHeader.toLower(), request.rawHeader(rawHeader));
247  }
248  // The "host" header is not included in QNetworkRequest::rawHeaderList, but will be sent by Qt.
249  headers.insert("host", request.url().host().toUtf8());
250 
251  // Convert the headers map to a canonical string, keeping track of which headers we've included too.
252  QByteArray canonicalHeaders;
253  for (QMap<QByteArray,QByteArray>::const_iterator iter = headers.constBegin(); iter != headers.constEnd(); ++iter) {
254  canonicalHeaders += canonicalHeader(iter.key(), iter.value()) + '\n';
255  if (!signedHeaders->isEmpty()) *signedHeaders += ';';
256  *signedHeaders += iter.key();
257  }
258  return canonicalHeaders;
259 }
260 
261 /**
262  * @brief Create an AWS V4 Signature canonical request.
263  *
264  * @param[in] operation The HTTP method being used for the request.
265  * @param[in] request The network request to generate a canonical request for.
266  * @param[in] payload Optional data being submitted in the request (eg for `PUT` and `POST` operations).
267  * @param[out] signedHeaders A semi-colon separated list of the names of all headers
268  * included in the result.
269  *
270  * @return An AWS V4 Signature canonical request.
271  *
272  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
273  */
274 QByteArray AwsSignatureV4Private::canonicalRequest(const QNetworkAccessManager::Operation operation,
275  const QNetworkRequest &request, const QByteArray &payload,
276  QByteArray * const signedHeaders) const
277 {
278  return httpMethod(operation).toUtf8() + '\n' +
279  canonicalPath(request.url()).toUtf8() + '\n' +
280  canonicalQuery(QUrlQuery(request.url())) + '\n' +
281  canonicalHeaders(request, signedHeaders) + '\n' +
282  *signedHeaders + '\n' +
283  QCryptographicHash::hash(payload, hashAlgorithm).toHex();
284 }
285 
286 /**
287  * @brief Create an AWS V4 Signature credential scope.
288  *
289  * @param date Date to include in the credential scope.
290  * @param region Region name to include in the credential scope.
291  * @param service Service name to include in the credential scope.
292  *
293  * @return An AWS V4 Signature credential scope.
294  *
295  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
296  */
297 QByteArray AwsSignatureV4Private::credentialScope(const QDate &date, const QString &region, const QString &service) const
298 {
299  return date.toString(DateFormat).toUtf8() + '/' + region.toUtf8() + '/' + service.toUtf8() + "/aws4_request";
300 }
301 
302 /**
303  * @brief Set authorization header on a network request.
304  *
305  * This function will calculate the authorization header value and set it as the `Authorization`
306  * HTTP header on \p request.
307  *
308  * @param[in] credentials The AWS credentials to use to sign the request.
309  * @param[in] operation The HTTP method being used for the request.
310  * @param[in,out] request The network request to add the authorization header to.
311  * @param[in] payload Optional data being submitted in the request (eg for `PUT` and `POST` operations).
312  * @param[in] timestamp The timestamp to use when signing the request.
313  *
314  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
315  * @see authorizationHeaderValue
316  */
318  const QNetworkAccessManager::Operation operation,
319  QNetworkRequest &request, const QByteArray &payload,
320  const QDateTime &timestamp) const
321 {
322  Q_ASSERT(!request.hasRawHeader("Authorization"));
323  request.setRawHeader("Authorization", authorizationHeaderValue(credentials, operation, request, payload, timestamp));
324 }
325 
326 /**
327  * @brief Set the AWS custom date header.
328  *
329  * This function will set a custom `x-amz-date` header to the value of \p dateTime
330  * formatted to AwsSignatureV4Private::DateTimeFormat.
331  *
332  * @note Although Amazon labels this as a "date", it is in fact a full timestamp.
333  *
334  * @param request The network request to add the date header to.
335  * @param dateTime The timestamp to set the date header's value to.
336  *
337  * @return \p dateTime verbatim (just a convenience for some callers).
338  */
339 QDateTime AwsSignatureV4Private::setDateHeader(QNetworkRequest &request, const QDateTime &dateTime) const
340 {
341  Q_ASSERT(!request.hasRawHeader("x-amz-date"));
342  request.setRawHeader("x-amz-date", dateTime.toString(DateTimeFormat).toUtf8());
343  return dateTime;
344 }
345 
346 /**
347  * @brief Create an AWS V4 Signature signing key.
348  *
349  * @param credentials AWS credentials to use when generating the signing key.
350  * @param date Date to include in the signing key.
351  * @param region Region name to include in the signing key.
352  * @param service Service name to include in the signing key.
353  *
354  * @return An AWS V4 Signature signing key.
355  *
356  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
357  */
358 QByteArray AwsSignatureV4Private::signingKey(const AwsAbstractCredentials &credentials, const QDate &date,
359  const QString &region, const QString &service) const
360 {
361  return QMessageAuthenticationCode::hash("aws4_request",
362  QMessageAuthenticationCode::hash(service.toUtf8(),
363  QMessageAuthenticationCode::hash(region.toUtf8(),
364  QMessageAuthenticationCode::hash(date.toString(DateFormat).toUtf8(), "AWS4"+credentials.secretKey().toUtf8(),
366 }
367 
368 /**
369  * @brief Create an AWS V4 Signature string to sign.
370  *
371  * @param algorithmDesignation AWS designation for the hash algorithm used to sign the request.
372  * @param requestDate AWS request timestamp.
373  * @param credentialScope Aws credential scope used to sign the request.
374  * @param canonicalRequest AWS request in canonical form.
375  *
376  * @return An AWS V4 Signature string to sign.
377  *
378  * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
379  * @see algorithmDesignation
380  * @see canonicalRequest
381  * @see credentialScope
382  */
383 QByteArray AwsSignatureV4Private::stringToSign(const QByteArray &algorithmDesignation, const QDateTime &requestDate,
384  const QByteArray &credentialScope, const QByteArray &canonicalRequest) const
385 {
386  return algorithmDesignation + '\n' +
387  requestDate.toString(DateTimeFormat).toUtf8() + '\n' +
388  credentialScope + '\n' +
389  QCryptographicHash::hash(canonicalRequest, hashAlgorithm).toHex();
390 }
391 
392 QTAWS_END_NAMESPACE
QDateTime setDateHeader(QNetworkRequest &request, const QDateTime &dateTime=QDateTime::currentDateTimeUtc()) const
Set the AWS custom date header.
static const QLatin1String DateFormat
Format V4 signatures use to represent dates in canonical form.
const QCryptographicHash::Algorithm hashAlgorithm
Hash algorithm to use when signing.
Private implementation for AwsSignatureV4.
QByteArray authorizationHeaderValue(const AwsAbstractCredentials &credentials, const QNetworkAccessManager::Operation operation, QNetworkRequest &request, const QByteArray &payload, const QDateTime &timestamp) const
Create an AWS V4 Signature authorization header value.
virtual void sign(const AwsAbstractCredentials &credentials, const QNetworkAccessManager::Operation operation, QNetworkRequest &request, const QByteArray &data=QByteArray()) const
Sign an AWS request.
QByteArray algorithmDesignation(const QCryptographicHash::Algorithm algorithm) const
Create an AWS V4 Signature algorithm designation.
void setAuthorizationHeader(const AwsAbstractCredentials &credentials, const QNetworkAccessManager::Operation operation, QNetworkRequest &request, const QByteArray &payload, const QDateTime &timestamp) const
Set authorization header on a network request.
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.
virtual int version() const
AWS Signature version implemented by this class.
Interface class for providing AWS credentials.
QByteArray canonicalHeaders(const QNetworkRequest &request, QByteArray *const signedHeaders) const
Create an AWS V4 Signature canonical headers string.
Provides AWS endpoint information.
Definition: awsendpoint.h:33
AwsSignatureV4Private(const QCryptographicHash::Algorithm hashAlgorithm, AwsSignatureV4 *const q)
Constructs a new AwsSignatureV4Private object.
Implements AWS Signature Version 4.
virtual QString accessKeyId() const =0
AWS access key ID for this credentials object.
QByteArray canonicalRequest(const QNetworkAccessManager::Operation operation, const QNetworkRequest &request, const QByteArray &payload, QByteArray *const signedHeaders) const
Create an AWS V4 Signature canonical request.
static const QLatin1String DateTimeFormat
Format V4 signatures use to represent timestamps in canonical form.
QByteArray stringToSign(const QByteArray &algorithmDesignation, const QDateTime &requestDate, const QByteArray &credentialScope, const QByteArray &canonicalRequest) const
Create an AWS V4 Signature string to sign.
QByteArray credentialScope(const QDate &date, const QString &region, const QString &service) const
Create an AWS V4 Signature credential scope.
AwsSignatureV4(const QCryptographicHash::Algorithm hashAlgorithm=QCryptographicHash::Sha256)
Constructs a new AwsSignatureV4 object.
QByteArray signingKey(const AwsAbstractCredentials &credentials, const QDate &date, const QString &region, const QString &service) const
Create an AWS V4 Signature signing key.
QByteArray canonicalHeader(const QByteArray &headerName, const QByteArray &headerValue) const
Create an AWS V4 Signature canonical header string.
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.