All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.auth.AWS4Signer Maven / Gradle / Ivy

Go to download

The AWS SDK for Java with support for OSGi. The AWS SDK for Java provides Java APIs for building software on AWS' cost-effective, scalable, and reliable infrastructure products. The AWS Java SDK allows developers to code against APIs for all of Amazon's infrastructure web services (Amazon S3, Amazon EC2, Amazon SQS, Amazon Relational Database Service, Amazon AutoScaling, etc).

There is a newer version: 1.11.60
Show newest version
/*
 * Copyright 2013-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package com.amazonaws.auth;

import com.amazonaws.AmazonClientException;
import com.amazonaws.ReadLimitInfo;
import com.amazonaws.SignableRequest;
import com.amazonaws.auth.internal.AWS4SignerRequestParams;
import com.amazonaws.auth.internal.AWS4SignerUtils;
import com.amazonaws.auth.internal.SignerKey;
import com.amazonaws.internal.FIFOCache;
import com.amazonaws.log.InternalLogApi;
import com.amazonaws.log.InternalLogFactory;
import com.amazonaws.util.BinaryUtils;
import com.amazonaws.util.DateUtils;
import com.amazonaws.util.SdkHttpUtils;
import com.amazonaws.util.StringUtils;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.amazonaws.auth.internal.SignerConstants.AUTHORIZATION;
import static com.amazonaws.auth.internal.SignerConstants.AWS4_SIGNING_ALGORITHM;
import static com.amazonaws.auth.internal.SignerConstants.AWS4_TERMINATOR;
import static com.amazonaws.auth.internal.SignerConstants.HOST;
import static com.amazonaws.auth.internal.SignerConstants.LINE_SEPARATOR;
import static com.amazonaws.auth.internal.SignerConstants.PRESIGN_URL_MAX_EXPIRATION_SECONDS;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_ALGORITHM;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_CONTENT_SHA256;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_CREDENTIAL;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_DATE;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_EXPIRES;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_SECURITY_TOKEN;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_SIGNATURE;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_SIGNED_HEADER;

/**
 * Signer implementation that signs requests with the AWS4 signing protocol.
 */
public class AWS4Signer extends AbstractAWSSigner implements
        ServiceAwareSigner, RegionAwareSigner, Presigner {

    protected static final InternalLogApi log = InternalLogFactory.getLog(AWS4Signer.class);
    private static final int SIGNER_CACHE_MAX_SIZE = 300;
    private static final FIFOCache signerCache = new FIFOCache(SIGNER_CACHE_MAX_SIZE);
    private static final List listOfHeadersToIgnoreInLowerCase = Arrays.asList("connection");

    /**
     * Service name override for use when the endpoint can't be used to
     * determine the service name.
     */
    protected String serviceName;

    /**
     * Region name override for use when the endpoint can't be used to determine
     * the region name.
     */
    protected String regionName;

    /** Date override for testing only */
    protected Date overriddenDate;

    /**
     * Whether double url-encode the resource path when constructing the
     * canonical request. By default, we enable double url-encoding.
     *
     * TODO: Different sigv4 services seem to be inconsistent on this. So for
     * services that want to suppress this, they should use new
     * AWS4Signer(false).
     */
    protected boolean doubleUrlEncode;

    /**
     * Construct a new AWS4 signer instance. By default, enable double
     * url-encoding.
     */
    public AWS4Signer() {
        this(true);
    }

    /**
     * Construct a new AWS4 signer instance.
     *
     * @param doubleUrlEncoding
     *            Whether double url-encode the resource path when constructing
     *            the canonical request.
     */
    public AWS4Signer(boolean doubleUrlEncoding) {
        this.doubleUrlEncode = doubleUrlEncoding;
    }

    /**
     * Sets the service name that this signer should use when calculating
     * request signatures. This can almost always be determined directly from
     * the request's end point, so you shouldn't need this method, but it's
     * provided for the edge case where the information is not in the endpoint.
     *
     * @param serviceName
     *            The service name to use when calculating signatures in this
     *            signer.
     */
    @Override
    public void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }

    /**
     * Sets the region name that this signer should use when calculating request
     * signatures. This can almost always be determined directly from the
     * request's end point, so you shouldn't need this method, but it's provided
     * for the edge case where the information is not in the endpoint.
     *
     * @param regionName
     *            The region name to use when calculating signatures in this
     *            signer.
     */
    @Override
    public void setRegionName(String regionName) {
        this.regionName = regionName;
    }

    /**
     * Sets the date that overrides the signing date in the request. This method
     * is internal and should be used only for testing purposes.
     */
    void setOverrideDate(Date overriddenDate) {
        this.overriddenDate = overriddenDate;
    }

    /**
     * Returns the region name that is used when calculating the signature.
     */
    public String getRegionName() {
        return regionName;
    }

    /**
     * Returns the service name that is used when calculating the signature.
     */
    public String getServiceName() {
        return serviceName;
    }

    /**
     * Returns a copy of date that overrides the signing date in the request.
     * Return null by default.
     */
    public Date getOverriddenDate() {
        return overriddenDate == null ? null : new Date(
                overriddenDate.getTime());
    }

    @Override
    public void sign(SignableRequest request, AWSCredentials credentials) {
        // anonymous credentials, don't sign
        if (isAnonymous(credentials)) {
            return;
        }

        AWSCredentials sanitizedCredentials = sanitizeCredentials(credentials);
        if (sanitizedCredentials instanceof AWSSessionCredentials) {
            addSessionCredentials(request,
                    (AWSSessionCredentials) sanitizedCredentials);
        }

        final AWS4SignerRequestParams signerParams = new AWS4SignerRequestParams(
                request, overriddenDate, regionName, serviceName,
                AWS4_SIGNING_ALGORITHM);

        addHostHeader(request);
        request.addHeader(X_AMZ_DATE,
                signerParams.getFormattedSigningDateTime());

        String contentSha256 = calculateContentHash(request);

        if ("required".equals(request.getHeaders().get(X_AMZ_CONTENT_SHA256))) {
            request.addHeader(X_AMZ_CONTENT_SHA256, contentSha256);
        }

        final String canonicalRequest = createCanonicalRequest(request,
                contentSha256);

        final String stringToSign = createStringToSign(canonicalRequest,
                signerParams);

        final byte[] signingKey = deriveSigningKey(sanitizedCredentials,
                signerParams);

        final byte[] signature = computeSignature(stringToSign, signingKey,
                signerParams);

        request.addHeader(
                AUTHORIZATION,
                buildAuthorizationHeader(request, signature,
                        sanitizedCredentials, signerParams));

        processRequestPayload(request, signature, signingKey,
                signerParams);
    }

    @Override
    public void presignRequest(SignableRequest request, AWSCredentials credentials,
            Date userSpecifiedExpirationDate) {

        // anonymous credentials, don't sign
        if (isAnonymous(credentials)) {
            return;
        }

        long expirationInSeconds = generateExpirationDate(userSpecifiedExpirationDate);

        addHostHeader(request);

        AWSCredentials sanitizedCredentials = sanitizeCredentials(credentials);
        if (sanitizedCredentials instanceof AWSSessionCredentials) {
            // For SigV4 pre-signing URL, we need to add "X-Amz-Security-Token"
            // as a query string parameter, before constructing the canonical
            // request.
            request.addParameter(X_AMZ_SECURITY_TOKEN,
                    ((AWSSessionCredentials) sanitizedCredentials)
                            .getSessionToken());
        }

        final AWS4SignerRequestParams signerRequestParams = new AWS4SignerRequestParams(
                request, overriddenDate, regionName, serviceName,
                AWS4_SIGNING_ALGORITHM);

        // Add the important parameters for v4 signing
        final String timeStamp = signerRequestParams.getFormattedSigningDateTime();

        addPreSignInformationToRequest(request, sanitizedCredentials,
                signerRequestParams, timeStamp, expirationInSeconds);

        final String contentSha256 = calculateContentHashPresign(request);

        final String canonicalRequest = createCanonicalRequest(request,
                contentSha256);

        final String stringToSign = createStringToSign(canonicalRequest,
                signerRequestParams);

        final byte[] signingKey = deriveSigningKey(sanitizedCredentials,
                signerRequestParams);

        final byte[] signature = computeSignature(stringToSign, signingKey,
                signerRequestParams);

        request.addParameter(X_AMZ_SIGNATURE, BinaryUtils.toHex(signature));
    }

    /**
     * Step 1 of the AWS Signature version 4 calculation. Refer to
     * http://docs.aws
     * .amazon.com/general/latest/gr/sigv4-create-canonical-request.html to
     * generate the canonical request.
     */
    protected String createCanonicalRequest(SignableRequest request,
            String contentSha256) {
        /* This would url-encode the resource path for the first time. */
        final String path = SdkHttpUtils.appendUri(
                request.getEndpoint().getPath(), request.getResourcePath());

        final StringBuilder canonicalRequestBuilder = new StringBuilder(request
                .getHttpMethod().toString());

        canonicalRequestBuilder.append(LINE_SEPARATOR)
                // This would optionally double url-encode the resource path
                .append(getCanonicalizedResourcePath(path, doubleUrlEncode))
                .append(LINE_SEPARATOR)
                .append(getCanonicalizedQueryString(request))
                .append(LINE_SEPARATOR)
                .append(getCanonicalizedHeaderString(request))
                .append(LINE_SEPARATOR)
                .append(getSignedHeadersString(request)).append(LINE_SEPARATOR)
                .append(contentSha256);

        final String canonicalRequest = canonicalRequestBuilder.toString();

        if (log.isDebugEnabled())
            log.debug("AWS4 Canonical Request: '\"" + canonicalRequest + "\"");

        return canonicalRequest;
    }

    /**
     * Step 2 of the AWS Signature version 4 calculation. Refer to
     * http://docs.aws
     * .amazon.com/general/latest/gr/sigv4-create-string-to-sign.html.
     */
    protected String createStringToSign(String canonicalRequest,
            AWS4SignerRequestParams signerParams) {

        final StringBuilder stringToSignBuilder = new StringBuilder(
                signerParams.getSigningAlgorithm());
        stringToSignBuilder.append(LINE_SEPARATOR)
                .append(signerParams.getFormattedSigningDateTime())
                .append(LINE_SEPARATOR)
                .append(signerParams.getScope())
                .append(LINE_SEPARATOR)
                .append(BinaryUtils.toHex(hash(canonicalRequest)));

        final String stringToSign = stringToSignBuilder.toString();

        if (log.isDebugEnabled())
            log.debug("AWS4 String to Sign: '\"" + stringToSign + "\"");

        return stringToSign;
    }

    /**
     * Step 3 of the AWS Signature version 4 calculation. It involves deriving
     * the signing key and computing the signature. Refer to
     * http://docs.aws.amazon
     * .com/general/latest/gr/sigv4-calculate-signature.html
     */
    private final byte[] deriveSigningKey(AWSCredentials credentials,
            AWS4SignerRequestParams signerRequestParams) {

        final String cacheKey = computeSigningCacheKeyName(credentials,
                signerRequestParams);
        final long daysSinceEpochSigningDate = DateUtils
                .numberOfDaysSinceEpoch(signerRequestParams
                        .getSigningDateTimeMilli());

        SignerKey signerKey = signerCache.get(cacheKey);

        if (signerKey != null) {
            if (daysSinceEpochSigningDate == signerKey
                    .getNumberOfDaysSinceEpoch()) {
                return signerKey.getSigningKey();
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Generating a new signing key as the signing key not available in the cache for the date "
                    + TimeUnit.DAYS.toMillis(daysSinceEpochSigningDate));
        }
        byte[] signingKey = newSigningKey(credentials,
                signerRequestParams.getFormattedSigningDate(),
                signerRequestParams.getRegionName(),
                signerRequestParams.getServiceName());
        signerCache.add(cacheKey, new SignerKey(
                daysSinceEpochSigningDate, signingKey));
        return signingKey;
    }

    /**
     * Computes the name to be used to reference the signing key in the cache.
     */
    private final String computeSigningCacheKeyName(AWSCredentials credentials,
            AWS4SignerRequestParams signerRequestParams) {
        final StringBuilder hashKeyBuilder = new StringBuilder(
                credentials.getAWSSecretKey());

        return hashKeyBuilder.append("-")
                .append(signerRequestParams.getRegionName())
                .append("-")
                .append(signerRequestParams.getServiceName()).toString();
    }

    /**
     * Step 3 of the AWS Signature version 4 calculation. It involves deriving
     * the signing key and computing the signature. Refer to
     * http://docs.aws.amazon
     * .com/general/latest/gr/sigv4-calculate-signature.html
     */
    protected final byte[] computeSignature(String stringToSign,
            byte[] signingKey, AWS4SignerRequestParams signerRequestParams) {
        return sign(stringToSign.getBytes(Charset.forName("UTF-8")), signingKey,
                SigningAlgorithm.HmacSHA256);
    }

    /**
     * Creates the authorization header to be included in the request.
     */
    private String buildAuthorizationHeader(SignableRequest request,
            byte[] signature, AWSCredentials credentials,
            AWS4SignerRequestParams signerParams) {
        final String signingCredentials = credentials.getAWSAccessKeyId() + "/"
                + signerParams.getScope();

        final String credential = "Credential="
                + signingCredentials;
        final String signerHeaders = "SignedHeaders="
                + getSignedHeadersString(request);
        final String signatureHeader = "Signature="
                + BinaryUtils.toHex(signature);

        final StringBuilder authHeaderBuilder = new StringBuilder();

        authHeaderBuilder.append(AWS4_SIGNING_ALGORITHM)
                         .append(" ")
                         .append(credential)
                         .append(", ")
                         .append(signerHeaders)
                         .append(", ")
                         .append(signatureHeader);

        return authHeaderBuilder.toString();
    }

    /**
     * Includes all the signing headers as request parameters for pre-signing.
     */
    private void addPreSignInformationToRequest(SignableRequest request,
            AWSCredentials credentials, AWS4SignerRequestParams signerParams,
            String timeStamp, long expirationInSeconds) {

        String signingCredentials = credentials.getAWSAccessKeyId() + "/"
                + signerParams.getScope();

        request.addParameter(X_AMZ_ALGORITHM, AWS4_SIGNING_ALGORITHM);
        request.addParameter(X_AMZ_DATE, timeStamp);
        request.addParameter(X_AMZ_SIGNED_HEADER,
                getSignedHeadersString(request));
        request.addParameter(X_AMZ_EXPIRES,
                Long.toString(expirationInSeconds));
        request.addParameter(X_AMZ_CREDENTIAL, signingCredentials);
    }

    @Override
    protected void addSessionCredentials(SignableRequest request,
            AWSSessionCredentials credentials) {
        request.addHeader(X_AMZ_SECURITY_TOKEN, credentials.getSessionToken());
    }

    protected String getCanonicalizedHeaderString(SignableRequest request) {
        final List sortedHeaders = new ArrayList(request.getHeaders()
                .keySet());
        Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

        final Map requestHeaders = request.getHeaders();
        StringBuilder buffer = new StringBuilder();
        for (String header : sortedHeaders) {
            if (shouldExcludeHeaderFromSigning(header)) {
                continue;
            }
            String key = StringUtils.lowerCase(header);
            String value = requestHeaders.get(header);

            StringUtils.appendCompactedString(buffer, key);
            buffer.append(":");
            if (value != null) {
                StringUtils.appendCompactedString(buffer, value);
            }

            buffer.append("\n");
        }

        return buffer.toString();
    }

    protected String getSignedHeadersString(SignableRequest request) {
        final List sortedHeaders = new ArrayList(request
                .getHeaders().keySet());
        Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

        StringBuilder buffer = new StringBuilder();
        for (String header : sortedHeaders) {
            if (shouldExcludeHeaderFromSigning(header)) {
                continue;
            }
            if (buffer.length() > 0)
                buffer.append(";");
            buffer.append(StringUtils.lowerCase(header));
        }

        return buffer.toString();
    }

    protected boolean shouldExcludeHeaderFromSigning(String header) {
        return listOfHeadersToIgnoreInLowerCase.contains(header.toLowerCase());
    }

    protected void addHostHeader(SignableRequest request) {
        // AWS4 requires that we sign the Host header so we
        // have to have it in the request by the time we sign.

        final URI endpoint = request.getEndpoint();
        final StringBuilder hostHeaderBuilder = new StringBuilder(
                endpoint.getHost());
        if (SdkHttpUtils.isUsingNonDefaultPort(endpoint)) {
            hostHeaderBuilder.append(":").append(endpoint.getPort());
        }

        request.addHeader(HOST, hostHeaderBuilder.toString());
    }

    /**
     * Calculate the hash of the request's payload. Subclass could override this
     * method to provide different values for "x-amz-content-sha256" header or
     * do any other necessary set-ups on the request headers. (e.g. aws-chunked
     * uses a pre-defined header value, and needs to change some headers
     * relating to content-encoding and content-length.)
     */
    protected String calculateContentHash(SignableRequest request) {
        InputStream payloadStream = getBinaryRequestPayloadStream(request);
        ReadLimitInfo info = request.getReadLimitInfo();
        payloadStream.mark(info == null ? -1 : info.getReadLimit());
        String contentSha256 = BinaryUtils.toHex(hash(payloadStream));
        try {
            payloadStream.reset();
        } catch (IOException e) {
            throw new AmazonClientException(
                    "Unable to reset stream after calculating AWS4 signature",
                    e);
        }
        return contentSha256;
    }

    /**
     * Subclass could override this method to perform any additional procedure
     * on the request payload, with access to the result from signing the
     * header. (e.g. Signing the payload by chunk-encoding). The default
     * implementation doesn't need to do anything.
     */
    protected void processRequestPayload(SignableRequest request, byte[] signature,
            byte[] signingKey, AWS4SignerRequestParams signerRequestParams) {
        return;
    }

    /**
     * Calculate the hash of the request's payload. In case of pre-sign, the
     * existing code would generate the hash of an empty byte array and returns
     * it. This method can be overridden by sub classes to provide different
     * values (e.g) For S3 pre-signing, the content hash calculation is
     * different from the general implementation.
     *
     */
    protected String calculateContentHashPresign(SignableRequest request) {
        return calculateContentHash(request);
    }

    /**
     * Checks if the credentials is an instance of
     * AnonymousAWSCredentials
     */
    private boolean isAnonymous(AWSCredentials credentials) {
        return credentials instanceof AnonymousAWSCredentials;
    }

    /**
     * Generates an expiration date for the presigned url. If user has specified
     * an expiration date, check if it is in the given limit.
     */
    private long generateExpirationDate(Date expirationDate) {

        long expirationInSeconds = expirationDate != null ? ((expirationDate
                .getTime() - System.currentTimeMillis()) / 1000L)
                : PRESIGN_URL_MAX_EXPIRATION_SECONDS;

        if (expirationInSeconds > PRESIGN_URL_MAX_EXPIRATION_SECONDS) {
            throw new AmazonClientException(
                    "Requests that are pre-signed by SigV4 algorithm are valid for at most 7 days. "
                            + "The expiration date set on the current request ["
                            + AWS4SignerUtils.formatTimestamp(expirationDate
                                    .getTime()) + "] has exceeded this limit.");
        }
        return expirationInSeconds;
    }

    /**
     * Generates a new signing key from the given parameters and returns it.
     */
    private byte[] newSigningKey(AWSCredentials credentials,
            String dateStamp, String regionName, String serviceName) {
        byte[] kSecret = ("AWS4" + credentials.getAWSSecretKey())
                .getBytes(Charset.forName("UTF-8"));
        byte[] kDate = sign(dateStamp, kSecret, SigningAlgorithm.HmacSHA256);
        byte[] kRegion = sign(regionName, kDate, SigningAlgorithm.HmacSHA256);
        byte[] kService = sign(serviceName, kRegion,
                SigningAlgorithm.HmacSHA256);
        return sign(AWS4_TERMINATOR, kService, SigningAlgorithm.HmacSHA256);
    }
}