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

com.google.auth.oauth2.AwsRequestSigner Maven / Gradle / Ivy

There is a newer version: 1.30.0
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *
 *    * Neither the name of Google LLC nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.google.auth.oauth2;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.auth.ServiceAccountSigner.SigningException;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.io.BaseEncoding;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * Internal utility that signs AWS API requests based on the AWS Signature Version 4 signing
 * process.
 *
 * @see AWS
 *     Signature V4
 */
class AwsRequestSigner {

  // AWS Signature Version 4 signing algorithm identifier.
  private static final String HASHING_ALGORITHM = "AWS4-HMAC-SHA256";

  // The termination string for the AWS credential scope value as defined in
  // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
  private static final String AWS_REQUEST_TYPE = "aws4_request";

  private final AwsSecurityCredentials awsSecurityCredentials;
  private final Map additionalHeaders;
  private final String httpMethod;
  private final String region;
  private final String requestPayload;
  private final URI uri;
  private final AwsDates dates;

  /**
   * Internal constructor.
   *
   * @param awsSecurityCredentials AWS security credentials
   * @param httpMethod the HTTP request method
   * @param url the request URL
   * @param region the targeted region
   * @param requestPayload the request payload
   * @param additionalHeaders a map of additional HTTP headers to be included with the signed
   *     request
   */
  private AwsRequestSigner(
      AwsSecurityCredentials awsSecurityCredentials,
      String httpMethod,
      String url,
      String region,
      @Nullable String requestPayload,
      @Nullable Map additionalHeaders,
      @Nullable AwsDates awsDates) {
    this.awsSecurityCredentials = checkNotNull(awsSecurityCredentials);
    this.httpMethod = checkNotNull(httpMethod);
    this.uri = URI.create(url).normalize();
    this.region = checkNotNull(region);
    this.requestPayload = requestPayload == null ? "" : requestPayload;
    this.additionalHeaders =
        (additionalHeaders != null)
            ? new HashMap<>(additionalHeaders)
            : new HashMap();
    this.dates = awsDates == null ? AwsDates.generateXAmzDate() : awsDates;
  }

  /**
   * Signs the specified AWS API request.
   *
   * @return the {@link AwsRequestSignature}
   */
  AwsRequestSignature sign() {
    // Retrieve the service name. For example: iam.amazonaws.com host => iam service.
    String serviceName = Splitter.on(".").split(uri.getHost()).iterator().next();

    Map canonicalHeaders = getCanonicalHeaders(dates.getOriginalDate());
    // Headers must be sorted.
    List sortedHeaderNames = new ArrayList<>();
    for (String headerName : canonicalHeaders.keySet()) {
      sortedHeaderNames.add(headerName.toLowerCase(Locale.US));
    }
    Collections.sort(sortedHeaderNames);

    String canonicalRequestHash = createCanonicalRequestHash(canonicalHeaders, sortedHeaderNames);
    String credentialScope =
        dates.getFormattedDate() + "/" + region + "/" + serviceName + "/" + AWS_REQUEST_TYPE;
    String stringToSign =
        createStringToSign(canonicalRequestHash, dates.getXAmzDate(), credentialScope);
    String signature =
        calculateAwsV4Signature(
            serviceName,
            awsSecurityCredentials.getSecretAccessKey(),
            dates.getFormattedDate(),
            region,
            stringToSign);

    String authorizationHeader =
        generateAuthorizationHeader(
            sortedHeaderNames, awsSecurityCredentials.getAccessKeyId(), credentialScope, signature);

    return new AwsRequestSignature.Builder()
        .setSignature(signature)
        .setCanonicalHeaders(canonicalHeaders)
        .setHttpMethod(httpMethod)
        .setSecurityCredentials(awsSecurityCredentials)
        .setCredentialScope(credentialScope)
        .setUrl(uri.toString())
        .setDate(dates.getOriginalDate())
        .setRegion(region)
        .setAuthorizationHeader(authorizationHeader)
        .build();
  }

  /** Task 1: Create a canonical request for Signature Version 4. */
  private String createCanonicalRequestHash(
      Map headers, List sortedHeaderNames) {
    // Append the HTTP request method.
    StringBuilder canonicalRequest = new StringBuilder(httpMethod).append("\n");

    // Append the path.
    String urlPath = uri.getRawPath().isEmpty() ? "/" : uri.getRawPath();
    canonicalRequest.append(urlPath).append("\n");

    // Append the canonical query string.
    String actionQueryString = uri.getRawQuery() != null ? uri.getRawQuery() : "";
    canonicalRequest.append(actionQueryString).append("\n");

    // Append the canonical headers.
    StringBuilder canonicalHeaders = new StringBuilder();
    for (String headerName : sortedHeaderNames) {
      canonicalHeaders.append(headerName).append(":").append(headers.get(headerName)).append("\n");
    }
    canonicalRequest.append(canonicalHeaders).append("\n");

    // Append the signed headers.
    canonicalRequest.append(Joiner.on(';').join(sortedHeaderNames)).append("\n");

    // Append the hashed request payload.
    canonicalRequest.append(getHexEncodedSha256Hash(requestPayload.getBytes(UTF_8)));

    // Return the hashed canonical request.
    return getHexEncodedSha256Hash(canonicalRequest.toString().getBytes(UTF_8));
  }

  /** Task 2: Create a string to sign for Signature Version 4. */
  private String createStringToSign(
      String canonicalRequestHash, String xAmzDate, String credentialScope) {
    return HASHING_ALGORITHM
        + "\n"
        + xAmzDate
        + "\n"
        + credentialScope
        + "\n"
        + canonicalRequestHash;
  }

  /**
   * Task 3: Calculate the signature for AWS Signature Version 4.
   *
   * @param date the date used in the hashing process in YYYYMMDD format
   */
  private String calculateAwsV4Signature(
      String serviceName, String secret, String date, String region, String stringToSign) {
    byte[] kDate = sign(("AWS4" + secret).getBytes(UTF_8), date.getBytes(UTF_8));
    byte[] kRegion = sign(kDate, region.getBytes(UTF_8));
    byte[] kService = sign(kRegion, serviceName.getBytes(UTF_8));
    byte[] kSigning = sign(kService, AWS_REQUEST_TYPE.getBytes(UTF_8));
    return BaseEncoding.base16().lowerCase().encode(sign(kSigning, stringToSign.getBytes(UTF_8)));
  }

  /** Task 4: Format the signature to be added to the HTTP request. */
  private String generateAuthorizationHeader(
      List sortedHeaderNames,
      String accessKeyId,
      String credentialScope,
      String signature) {
    return String.format(
        "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
        HASHING_ALGORITHM,
        accessKeyId,
        credentialScope,
        Joiner.on(';').join(sortedHeaderNames),
        signature);
  }

  private Map getCanonicalHeaders(String defaultDate) {
    Map headers = new HashMap<>();
    headers.put("host", uri.getHost());

    // Only add the date if it hasn't been specified through the "date" header.
    if (!additionalHeaders.containsKey("date")) {
      headers.put("x-amz-date", defaultDate);
    }

    if (awsSecurityCredentials.getSessionToken() != null
        && !awsSecurityCredentials.getSessionToken().isEmpty()) {
      headers.put("x-amz-security-token", awsSecurityCredentials.getSessionToken());
    }

    // Add all additional headers.
    for (String key : additionalHeaders.keySet()) {
      // Header keys need to be lowercase.
      headers.put(key.toLowerCase(Locale.US), additionalHeaders.get(key));
    }
    return headers;
  }

  private static byte[] sign(byte[] key, byte[] value) {
    try {
      String algorithm = "HmacSHA256";
      Mac mac = Mac.getInstance(algorithm);
      mac.init(new SecretKeySpec(key, algorithm));
      return mac.doFinal(value);
    } catch (NoSuchAlgorithmException e) {
      // Will not occur as HmacSHA256 is supported. We may allow other algorithms in the future.
      throw new RuntimeException("HmacSHA256 must be supported by the JVM.", e);
    } catch (InvalidKeyException e) {
      throw new SigningException("Invalid key used when calculating the AWS V4 Signature", e);
    }
  }

  private static String getHexEncodedSha256Hash(byte[] bytes) {
    try {
      MessageDigest digest = MessageDigest.getInstance("SHA-256");
      return BaseEncoding.base16().lowerCase().encode(digest.digest(bytes));
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException("Failed to compute SHA-256 hash.", e);
    }
  }

  static Builder newBuilder(
      AwsSecurityCredentials awsSecurityCredentials, String httpMethod, String url, String region) {
    return new Builder(awsSecurityCredentials, httpMethod, url, region);
  }

  static class Builder {

    private final AwsSecurityCredentials awsSecurityCredentials;
    private final String httpMethod;
    private final String url;
    private final String region;

    @Nullable private String requestPayload;
    @Nullable private Map additionalHeaders;
    @Nullable private AwsDates dates;

    private Builder(
        AwsSecurityCredentials awsSecurityCredentials,
        String httpMethod,
        String url,
        String region) {
      this.awsSecurityCredentials = awsSecurityCredentials;
      this.httpMethod = httpMethod;
      this.url = url;
      this.region = region;
    }

    @CanIgnoreReturnValue
    Builder setRequestPayload(String requestPayload) {
      this.requestPayload = requestPayload;
      return this;
    }

    @CanIgnoreReturnValue
    Builder setAdditionalHeaders(Map additionalHeaders) {
      if (additionalHeaders.containsKey("date") && additionalHeaders.containsKey("x-amz-date")) {
        throw new IllegalArgumentException("One of {date, x-amz-date} can be specified, not both.");
      }
      try {
        if (additionalHeaders.containsKey("date")) {
          this.dates = AwsDates.fromDateHeader(additionalHeaders.get("date"));
        }
        if (additionalHeaders.containsKey("x-amz-date")) {
          this.dates = AwsDates.fromXAmzDate(additionalHeaders.get("x-amz-date"));
        }
      } catch (ParseException e) {
        throw new IllegalArgumentException("The provided date header value is invalid.", e);
      }

      this.additionalHeaders = additionalHeaders;
      return this;
    }

    AwsRequestSigner build() {
      return new AwsRequestSigner(
          awsSecurityCredentials,
          httpMethod,
          url,
          region,
          requestPayload,
          additionalHeaders,
          dates);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy