LCOV - code coverage report
Current view: top level - core - awssignaturev3.cpp (source / functions) Hit Total Coverage
Test: libqtaws 0.1.0 Lines: 89 89 100.0 %
Date: 2015-06-16 07:50:35 Functions: 12 12 100.0 %

          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

Generated by: LCOV version 1.11