LCOV - code coverage report
Current view: top level - core - awssignaturev4.cpp (source / functions) Hit Total Coverage
Test: libqtaws 0.1.0 Lines: 95 95 100.0 %
Date: 2015-06-16 07:50:35 Functions: 16 16 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 "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 &timestamp) 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 &region, 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 &timestamp) 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 &region, 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

Generated by: LCOV version 1.11