Line data Source code
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 58 : AwsSignatureV3::AwsSignatureV3(const QCryptographicHash::Algorithm hashAlgorithm)
56 58 : : AwsAbstractSignature(new AwsSignatureV3Private(hashAlgorithm, this))
57 : {
58 :
59 58 : }
60 :
61 3 : void AwsSignatureV3::sign(const AwsAbstractCredentials &credentials,
62 : const QNetworkAccessManager::Operation operation,
63 : QNetworkRequest &request, const QByteArray &data) const
64 : {
65 3 : 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 3 : if ((d->isHttps(request)) && (!request.hasRawHeader("x-amz-nonce"))) {
70 1 : request.setRawHeader("x-amz-nonce", QUuid::createUuid().toByteArray().mid(1,36));
71 : }
72 :
73 3 : d->setDateHeader(request);
74 3 : d->setAuthorizationHeader(credentials, operation, request, data);
75 3 : }
76 :
77 1 : int AwsSignatureV3::version() const
78 : {
79 1 : 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 58 : AwsSignatureV3Private::AwsSignatureV3Private(const QCryptographicHash::Algorithm hashAlgorithm,
99 : AwsSignatureV3 * const q)
100 58 : : AwsAbstractSignaturePrivate(q), hashAlgorithm(hashAlgorithm)
101 : {
102 :
103 58 : }
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 30 : QByteArray AwsSignatureV3Private::algorithmDesignation(const QCryptographicHash::Algorithm algorithm) const
121 : {
122 30 : switch (algorithm) {
123 1 : case QCryptographicHash::Sha1: return "HmacSHA1";
124 20 : case QCryptographicHash::Sha256: return "HmacSHA256";
125 : default:
126 9 : Q_ASSERT_X(false, Q_FUNC_INFO, "invalid algorithm");
127 9 : 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 : */
148 19 : QByteArray AwsSignatureV3Private::authorizationHeaderValue(const AwsAbstractCredentials &credentials,
149 : const QNetworkAccessManager::Operation operation,
150 : QNetworkRequest &request, const QByteArray &payload) const
151 : {
152 : // Calculate the signature.
153 19 : QByteArray signedHeaders;
154 38 : QByteArray stringToSign = canonicalRequest(operation, request, payload, &signedHeaders);
155 19 : if (!isHttps(request)) {
156 14 : stringToSign = QCryptographicHash::hash(stringToSign, hashAlgorithm);
157 : }
158 : const QByteArray signature = QMessageAuthenticationCode::hash(
159 38 : stringToSign, credentials.secretKey().toUtf8(), hashAlgorithm);
160 :
161 : // Build and return the authorization header value.
162 : return
163 38 : QByteArray((isHttps(request)) ? "AWS3-HTTPS " : "AWS3 ") +
164 76 : "AWSAccessKeyId=" + credentials.accessKeyId().toUtf8() + ","
165 90 : "Algorithm=" + algorithmDesignation(hashAlgorithm) + "," +
166 90 : ((!isHttps(request)) ? "SignedHeaders=" + signedHeaders + ',' : "") +
167 76 : "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 64 : QByteArray AwsSignatureV3Private::canonicalHeader(const QByteArray &headerName, const QByteArray &headerValue) const
193 : {
194 64 : QByteArray header = headerName.toLower() + ':';
195 128 : const QByteArray trimmedHeaderValue = headerValue.trimmed();
196 64 : bool isInQuotes = false;
197 64 : char previousChar = '\0';
198 1250 : for (int index = 0; index < trimmedHeaderValue.size(); ++index) {
199 1186 : char thisChar = trimmedHeaderValue.at(index);
200 1186 : header += thisChar;
201 1186 : if (isInQuotes) {
202 43 : if ((thisChar == '"') && (previousChar != '\\'))
203 3 : isInQuotes = false;
204 : } else {
205 1143 : if ((thisChar == '"') && (previousChar != '\\')) {
206 3 : isInQuotes = true;
207 1140 : } else if (isspace(thisChar)) {
208 456 : while ((index < trimmedHeaderValue.size()-1) &&
209 158 : (isspace(trimmedHeaderValue.at(index+1))))
210 18 : ++index;
211 : }
212 : }
213 1186 : previousChar = thisChar;
214 : }
215 128 : 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 27 : QByteArray AwsSignatureV3Private::canonicalHeaders(const QNetworkRequest &request, QByteArray * const signedHeaders) const
242 : {
243 27 : Q_CHECK_PTR(signedHeaders);
244 27 : 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 27 : QMap<QByteArray,QByteArray> headers;
253 57 : foreach (const QByteArray &rawHeader, request.rawHeaderList()) {
254 30 : headers.insert(rawHeader.toLower(), request.rawHeader(rawHeader));
255 27 : }
256 : // The "host" header is not included in QNetworkRequest::rawHeaderList, but will be sent by Qt.
257 27 : 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 27 : QByteArray canonicalHeaders;
261 84 : 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 57 : if ((iter.key() == "host") || (iter.key().startsWith("x-amz-"))) {
267 57 : canonicalHeaders += canonicalHeader(iter.key(), iter.value()) + '\n';
268 57 : if (!signedHeaders->isEmpty()) *signedHeaders += ';';
269 57 : *signedHeaders += iter.key();
270 : }
271 : }
272 27 : 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 29 : QByteArray AwsSignatureV3Private::canonicalRequest(const QNetworkAccessManager::Operation operation,
293 : const QNetworkRequest &request, const QByteArray &payload,
294 : QByteArray * const signedHeaders) const
295 : {
296 : // AWS3-HTTPS
297 29 : if (isHttps(request)) {
298 9 : Q_ASSERT((request.hasRawHeader("x-amz-date")) || (request.hasRawHeader("Date")));
299 9 : QByteArray canonicalRequest = request.rawHeader(request.hasRawHeader("x-amz-date") ? "x-amz-date" : "Date");
300 9 : if (request.hasRawHeader("x-amz-nonce")) {
301 3 : canonicalRequest += request.rawHeader("x-amz-nonce");
302 : }
303 9 : return canonicalRequest;
304 : }
305 :
306 : // AWS3
307 40 : return httpMethod(operation).toUtf8() + '\n' +
308 80 : canonicalPath(request.url()).toUtf8() + '\n' +
309 80 : canonicalQuery(QUrlQuery(request.url())) + '\n' +
310 40 : canonicalHeaders(request, signedHeaders) + '\n' +
311 20 : 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 89 : bool AwsSignatureV3Private::isHttps(const QNetworkRequest &request)
322 : {
323 89 : 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 : */
340 11 : void AwsSignatureV3Private::setAuthorizationHeader(const AwsAbstractCredentials &credentials,
341 : const QNetworkAccessManager::Operation operation,
342 : QNetworkRequest &request, const QByteArray &payload) const
343 : {
344 11 : Q_ASSERT(!request.hasRawHeader("Authorization"));
345 11 : request.setRawHeader("Authorization", authorizationHeaderValue(credentials, operation, request, payload));
346 11 : }
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 6 : void AwsSignatureV3Private::setDateHeader(QNetworkRequest &request, const QDateTime &dateTime) const
359 : {
360 6 : Q_ASSERT(dateTime.timeSpec() == Qt::UTC);
361 6 : if (!request.hasRawHeader("x-amz-date")) {
362 6 : request.setRawHeader("x-amz-date", dateTime.toString(QLatin1String("ddd, dd MMM yyyy hh:mm:ss 'GMT'")).toUtf8());
363 : }
364 6 : }
365 :
366 : QTAWS_END_NAMESPACE
|