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