package software.aws.mcs.auth; /*- * #%L * AWS SigV4 Auth Java Driver 4.x Plugin * %% * Copyright (C) 2020-2021 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. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License 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. * #L% */ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.validation.constraints.NotNull; import org.apache.commons.codec.binary.Hex; import com.datastax.oss.driver.api.core.auth.AuthProvider; import com.datastax.oss.driver.api.core.auth.AuthenticationException; import com.datastax.oss.driver.api.core.auth.Authenticator; import com.datastax.oss.driver.api.core.config.DriverOption; import com.datastax.oss.driver.api.core.context.DriverContext; import com.datastax.oss.driver.api.core.metadata.EndPoint; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; import software.amazon.awssdk.auth.signer.internal.Aws4SignerUtils; import software.amazon.awssdk.auth.signer.internal.SignerConstant; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; import static software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider.create; /** * This auth provider can be used with the Amazon MCS service to * authenticate with SigV4. It uses the AWSCredentialsProvider * interface provided by the official AWS Java SDK to provide * credentials for signing. */ public class SigV4AuthProvider implements AuthProvider { private static final byte[] SIGV4_INITIAL_RESPONSE_BYTES = "SigV4\0\0".getBytes(StandardCharsets.UTF_8); private static final ByteBuffer SIGV4_INITIAL_RESPONSE; static { ByteBuffer initialResponse = ByteBuffer.allocate(SIGV4_INITIAL_RESPONSE_BYTES.length); initialResponse.put(SIGV4_INITIAL_RESPONSE_BYTES); initialResponse.flip(); // According to the driver docs, it's safe to reuse a // read-only buffer, and in our case, the initial response has // no sensitive information SIGV4_INITIAL_RESPONSE = initialResponse.asReadOnlyBuffer(); } private static final int AWS_FRACTIONAL_TIMESTAMP_DIGITS = 3; // SigV4 expects three digits of nanoseconds for timestamps private static final DateTimeFormatter timestampFormatter = (new DateTimeFormatterBuilder()).appendInstant(AWS_FRACTIONAL_TIMESTAMP_DIGITS).toFormatter(); private static final byte[] NONCE_KEY = "nonce=".getBytes(StandardCharsets.UTF_8); private static final int EXPECTED_NONCE_LENGTH = 32; // These are static values because we don't need HTTP, but SigV4 assumes some amount of HTTP metadata private static final String CANONICAL_SERVICE = "cassandra"; private final AwsCredentialsProvider credentialsProvider; private final String signingRegion; /** * Create a new Provider, using the * DefaultAWSCredentialsProviderChain as its credentials provider. * The signing region is taking from the AWS_DEFAULT_REGION * environment variable or the "aws.region" system property. */ public SigV4AuthProvider() { this(create(), null); } private final static DriverOption REGION_OPTION = () -> "advanced.auth-provider.aws-region"; private final static DriverOption ROLE_OPTION = () -> "advanced.auth-provider.aws-role-arn"; /** * This constructor is provided so that the driver can create * instances of this class based on configuration. For example: * *
* datastax-java-driver.advanced.auth-provider = { * aws-region = us-east-2 * class = software.aws.mcs.auth.SigV4AuthProvider * } ** * The signing region is taken from the * datastax-java-driver.advanced.auth-provider.aws-region * property, from the "aws.region" system property, or the * AWS_DEFAULT_REGION environment variable, in that order of * preference. * * For programmatic construction, use {@link #SigV4AuthProvider()} * or {@link #SigV4AuthProvider(AwsCredentialsProvider, String)}. * * @param driverContext the driver context for instance creation. * Unused for this plugin. */ public SigV4AuthProvider(DriverContext driverContext) { this(driverContext.getConfig().getDefaultProfile().getString(REGION_OPTION, getDefaultRegion()), driverContext.getConfig().getDefaultProfile().getString(ROLE_OPTION, null)); } /** * Create a new Provider, using the specified region. * @param region the region (e.g. us-east-1) to use for signing. A * null value indicates to use the AWS_REGION environment * variable, or the "aws.region" system property to configure it. */ public SigV4AuthProvider(final String region) { this(create(), region); } /** * Create a new Provider, using the specified region and IAM role to assume. * @param region the region (e.g. us-east-1) to use for signing. A * null value indicates to use the AWS_REGION environment * variable, or the "aws.region" system property to configure it. * @param roleArn The IAM Role ARN which the connecting client should assume before connecting with Amazon Keyspaces. */ public SigV4AuthProvider(final String region,final String roleArn) { this(Optional.ofNullable(roleArn).map(r->(AwsCredentialsProvider)createSTSRoleCredentialProvider(r,region)).orElse(create()), region); } /** * Create a new Provider, using the specified AWSCredentialsProvider and region. * @param credentialsProvider the credentials provider used to obtain signature material * @param region the region (e.g. us-east-1) to use for signing. A * null value indicates to use the AWS_REGION environment * variable, or the "aws.region" system property to configure it. */ public SigV4AuthProvider(@NotNull AwsCredentialsProvider credentialsProvider, final String region) { this.credentialsProvider = credentialsProvider; if (region == null) { this.signingRegion = getDefaultRegion(); } else { this.signingRegion = region.toLowerCase(); } if (this.signingRegion == null) { throw new IllegalStateException( "A region must be specified by constructor, AWS_REGION env variable, or aws.region system property" ); } } @Override public Authenticator newAuthenticator(EndPoint endPoint, String authenticator) throws AuthenticationException { return new SigV4Authenticator(); } @Override public void onMissingChallenge(EndPoint endPoint) { throw new AuthenticationException(endPoint, "SigV4 requires a challenge from the endpoint. None was sent"); } @Override public void close() { // We do not open any resources, so this is a NOOP } /** * This authenticator performs SigV4 MCS authentication. */ public class SigV4Authenticator implements Authenticator { @Override public CompletionStage