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 "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 144 : AwsSignatureV4::AwsSignatureV4(const QCryptographicHash::Algorithm hashAlgorithm)
55 144 : : AwsAbstractSignature(new AwsSignatureV4Private(hashAlgorithm, this))
56 : {
57 :
58 144 : }
59 :
60 1 : void AwsSignatureV4::sign(const AwsAbstractCredentials &credentials,
61 : const QNetworkAccessManager::Operation operation,
62 : QNetworkRequest &request, const QByteArray &data) const
63 : {
64 1 : Q_D(const AwsSignatureV4);
65 1 : d->setAuthorizationHeader(credentials, operation, request, data, d->setDateHeader(request));
66 1 : }
67 :
68 1 : int AwsSignatureV4::version() const
69 : {
70 1 : 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 1 : const QLatin1String AwsSignatureV4Private::DateFormat("yyyyMMdd");
85 :
86 : /// Format V4 signatures use to represent timestamps in canonical form.
87 1 : 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 144 : AwsSignatureV4Private::AwsSignatureV4Private(const QCryptographicHash::Algorithm hashAlgorithm,
96 : AwsSignatureV4 * const q)
97 144 : : AwsAbstractSignaturePrivate(q), hashAlgorithm(hashAlgorithm)
98 : {
99 :
100 144 : }
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 70 : QByteArray AwsSignatureV4Private::algorithmDesignation(const QCryptographicHash::Algorithm algorithm) const
118 : {
119 70 : switch (algorithm) {
120 1 : case QCryptographicHash::Md4: return "AWS4-HMAC-MD4";
121 1 : case QCryptographicHash::Md5: return "AWS4-HMAC-MD5";
122 1 : case QCryptographicHash::Sha1: return "AWS4-HMAC-SHA1";
123 1 : case QCryptographicHash::Sha224: return "AWS4-HMAC-SHA224";
124 58 : case QCryptographicHash::Sha256: return "AWS4-HMAC-SHA256";
125 1 : case QCryptographicHash::Sha384: return "AWS4-HMAC-SHA384";
126 1 : case QCryptographicHash::Sha512: return "AWS4-HMAC-SHA512";
127 : default:
128 6 : Q_ASSERT_X(false, Q_FUNC_INFO, "invalid algorithm");
129 6 : 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 : */
151 57 : QByteArray AwsSignatureV4Private::authorizationHeaderValue(const AwsAbstractCredentials &credentials,
152 : const QNetworkAccessManager::Operation operation,
153 : QNetworkRequest &request, const QByteArray &payload,
154 : const QDateTime ×tamp) const
155 : {
156 57 : const QByteArray algorithmDesignation = this->algorithmDesignation(hashAlgorithm);
157 114 : const AwsEndpoint endpoint(request.url().host());
158 :
159 114 : const QByteArray credentialScope = this->credentialScope(timestamp.date(), endpoint.regionName(), endpoint.serviceName());
160 114 : QByteArray signedHeaders;
161 114 : const QByteArray canonicalRequest = this->canonicalRequest(operation, request, payload, &signedHeaders);
162 :
163 114 : const QByteArray stringToSign = this->stringToSign(algorithmDesignation, timestamp, credentialScope, canonicalRequest);
164 114 : const QByteArray signingKey = this->signingKey(credentials, timestamp.date(), endpoint.regionName(), endpoint.serviceName());
165 114 : const QByteArray signature = QMessageAuthenticationCode::hash(stringToSign, signingKey, hashAlgorithm);
166 :
167 114 : return algorithmDesignation + " Credential=" + credentials.accessKeyId().toUtf8() + '/' + credentialScope +
168 228 : ", 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 200 : QByteArray AwsSignatureV4Private::canonicalHeader(const QByteArray &headerName, const QByteArray &headerValue) const
187 : {
188 200 : QByteArray header = headerName.toLower() + ':';
189 400 : const QByteArray trimmedHeaderValue = headerValue.trimmed();
190 200 : bool isInQuotes = false;
191 200 : char previousChar = '\0';
192 4297 : for (int index = 0; index < trimmedHeaderValue.size(); ++index) {
193 4097 : char thisChar = trimmedHeaderValue.at(index);
194 4097 : header += thisChar;
195 4097 : if (isInQuotes) {
196 43 : if ((thisChar == '"') && (previousChar != '\\'))
197 3 : isInQuotes = false;
198 : } else {
199 4054 : if ((thisChar == '"') && (previousChar != '\\')) {
200 3 : isInQuotes = true;
201 4051 : } else if (isspace(thisChar)) {
202 1302 : while ((index < trimmedHeaderValue.size()-1) &&
203 440 : (isspace(trimmedHeaderValue.at(index+1))))
204 18 : ++index;
205 : }
206 : }
207 4097 : previousChar = thisChar;
208 : }
209 400 : 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 88 : QByteArray AwsSignatureV4Private::canonicalHeaders(const QNetworkRequest &request, QByteArray * const signedHeaders) const
234 : {
235 88 : Q_CHECK_PTR(signedHeaders);
236 88 : 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 88 : QMap<QByteArray,QByteArray> headers;
245 193 : foreach (const QByteArray &rawHeader, request.rawHeaderList()) {
246 105 : headers.insert(rawHeader.toLower(), request.rawHeader(rawHeader));
247 88 : }
248 : // The "host" header is not included in QNetworkRequest::rawHeaderList, but will be sent by Qt.
249 88 : 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 88 : QByteArray canonicalHeaders;
253 281 : for (QMap<QByteArray,QByteArray>::const_iterator iter = headers.constBegin(); iter != headers.constEnd(); ++iter) {
254 193 : canonicalHeaders += canonicalHeader(iter.key(), iter.value()) + '\n';
255 193 : if (!signedHeaders->isEmpty()) *signedHeaders += ';';
256 193 : *signedHeaders += iter.key();
257 : }
258 88 : 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 86 : QByteArray AwsSignatureV4Private::canonicalRequest(const QNetworkAccessManager::Operation operation,
275 : const QNetworkRequest &request, const QByteArray &payload,
276 : QByteArray * const signedHeaders) const
277 : {
278 172 : return httpMethod(operation).toUtf8() + '\n' +
279 344 : canonicalPath(request.url()).toUtf8() + '\n' +
280 344 : canonicalQuery(QUrlQuery(request.url())) + '\n' +
281 172 : canonicalHeaders(request, signedHeaders) + '\n' +
282 172 : *signedHeaders + '\n' +
283 258 : 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 59 : QByteArray AwsSignatureV4Private::credentialScope(const QDate &date, const QString ®ion, const QString &service) const
298 : {
299 59 : 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 : */
317 29 : void AwsSignatureV4Private::setAuthorizationHeader(const AwsAbstractCredentials &credentials,
318 : const QNetworkAccessManager::Operation operation,
319 : QNetworkRequest &request, const QByteArray &payload,
320 : const QDateTime ×tamp) const
321 : {
322 29 : Q_ASSERT(!request.hasRawHeader("Authorization"));
323 29 : request.setRawHeader("Authorization", authorizationHeaderValue(credentials, operation, request, payload, timestamp));
324 29 : }
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 4 : QDateTime AwsSignatureV4Private::setDateHeader(QNetworkRequest &request, const QDateTime &dateTime) const
340 : {
341 4 : Q_ASSERT(!request.hasRawHeader("x-amz-date"));
342 4 : request.setRawHeader("x-amz-date", dateTime.toString(DateTimeFormat).toUtf8());
343 4 : 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 58 : QByteArray AwsSignatureV4Private::signingKey(const AwsAbstractCredentials &credentials, const QDate &date,
359 : const QString ®ion, const QString &service) const
360 : {
361 : return QMessageAuthenticationCode::hash("aws4_request",
362 : QMessageAuthenticationCode::hash(service.toUtf8(),
363 : QMessageAuthenticationCode::hash(region.toUtf8(),
364 116 : QMessageAuthenticationCode::hash(date.toString(DateFormat).toUtf8(), "AWS4"+credentials.secretKey().toUtf8(),
365 174 : hashAlgorithm), hashAlgorithm), hashAlgorithm), hashAlgorithm);
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 86 : QByteArray AwsSignatureV4Private::stringToSign(const QByteArray &algorithmDesignation, const QDateTime &requestDate,
384 : const QByteArray &credentialScope, const QByteArray &canonicalRequest) const
385 : {
386 172 : return algorithmDesignation + '\n' +
387 172 : requestDate.toString(DateTimeFormat).toUtf8() + '\n' +
388 172 : credentialScope + '\n' +
389 258 : QCryptographicHash::hash(canonicalRequest, hashAlgorithm).toHex();
390 3 : }
391 :
392 : QTAWS_END_NAMESPACE
|