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

software.amazon.awssdk.auth.signer.internal.BaseEventStreamAsyncAws4Signer Maven / Gradle / Ivy

Go to download

The AWS SDK for Java - Auth module holds the classes that are used for authentication with services

There is a newer version: 2.29.15
Show newest version
/*
 * Copyright 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 software.amazon.awssdk.auth.signer.internal;

import static software.amazon.awssdk.auth.signer.internal.SignerConstant.X_AMZ_CONTENT_SHA256;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.SdkPublisher;
import software.amazon.awssdk.core.checksums.SdkChecksum;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.utils.BinaryUtils;
import software.amazon.awssdk.utils.Logger;
import software.amazon.eventstream.HeaderValue;
import software.amazon.eventstream.Message;


@SdkInternalApi
public abstract class BaseEventStreamAsyncAws4Signer extends BaseAsyncAws4Signer {
    //Constants for event stream headers
    public static final String EVENT_STREAM_SIGNATURE = ":chunk-signature";
    public static final String EVENT_STREAM_DATE = ":date";

    private static final Logger LOG = Logger.loggerFor(BaseEventStreamAsyncAws4Signer.class);
    private static final String HTTP_CONTENT_SHA_256 = "STREAMING-AWS4-HMAC-SHA256-EVENTS";
    private static final String EVENT_STREAM_PAYLOAD = "AWS4-HMAC-SHA256-PAYLOAD";

    private static final int PAYLOAD_TRUNCATE_LENGTH = 32;


    protected BaseEventStreamAsyncAws4Signer() {
    }

    @Override
    public SdkHttpFullRequest sign(SdkHttpFullRequest request, ExecutionAttributes executionAttributes) {
        request = addContentSha256Header(request);
        return super.sign(request, executionAttributes);
    }

    @Override
    public SdkHttpFullRequest sign(SdkHttpFullRequest request, Aws4SignerParams signingParams) {
        request = addContentSha256Header(request);
        return super.sign(request, signingParams);
    }

    @Override
    protected AsyncRequestBody transformRequestProvider(String headerSignature,
                                                        Aws4SignerRequestParams signerRequestParams,
                                                        Aws4SignerParams signerParams,
                                                        AsyncRequestBody asyncRequestBody) {
        /**
         * Concat trailing empty frame to publisher
         */
        Publisher publisherWithTrailingEmptyFrame = appendEmptyFrame(asyncRequestBody);

        /**
         * Map publisher with signing function
         */
        Publisher publisherWithSignedFrame =
            transformRequestBodyPublisher(publisherWithTrailingEmptyFrame, headerSignature,
                                          signerParams.awsCredentials(), signerRequestParams);

        AsyncRequestBody transformedRequestBody = AsyncRequestBody.fromPublisher(publisherWithSignedFrame);

        return new SigningRequestBodyProvider(transformedRequestBody);
    }

    /**
     * Returns the pre-defined header value and set other necessary headers if
     * the request needs to be chunk-encoded. Otherwise calls the superclass
     * method which calculates the hash of the whole content for signing.
     */
    @Override
    protected String calculateContentHash(SdkHttpFullRequest.Builder mutableRequest, Aws4SignerParams signerParams,
                                          SdkChecksum contentFlexibleChecksum) {
        return HTTP_CONTENT_SHA_256;
    }

    private static Publisher appendEmptyFrame(Publisher publisher) {
        return s -> {
            Subscriber adaptedSubscriber = new AsyncSigV4SubscriberAdapter(s);
            publisher.subscribe(adaptedSubscriber);
        };
    }

    private Publisher transformRequestBodyPublisher(Publisher publisher, String headerSignature,
                                                                AwsCredentials credentials,
                                                                Aws4SignerRequestParams signerRequestParams) {
        return SdkPublisher.adapt(publisher)
                           .map(getDataFrameSigner(headerSignature, credentials, signerRequestParams));
    }

    private Function getDataFrameSigner(String headerSignature,
                                                                AwsCredentials credentials,
                                                                Aws4SignerRequestParams signerRequestParams) {
        return new Function() {
            final Aws4SignerRequestParams requestParams = signerRequestParams;

            /**
             * Initiate rolling signature with header signature
             */
            String priorSignature = headerSignature;

            @Override
            public ByteBuffer apply(ByteBuffer byteBuffer) {
                /**
                 * Signing Date
                 */
                Map nonSignatureHeaders = new HashMap<>();
                Instant signingInstant = requestParams.getSigningClock().instant();
                nonSignatureHeaders.put(EVENT_STREAM_DATE, HeaderValue.fromTimestamp(signingInstant));

                /**
                 * Derive Signing Key
                 */
                AwsCredentials sanitizedCredentials = sanitizeCredentials(credentials);
                byte[] signingKey = deriveSigningKey(sanitizedCredentials,
                                                     signingInstant,
                                                     requestParams.getRegionName(),
                                                     requestParams.getServiceSigningName());
                /**
                 * Calculate rolling signature
                 */
                byte[] payload = byteBuffer.array();
                byte[] signatureBytes = signEventStream(priorSignature, signingKey, signingInstant, requestParams,
                                                        nonSignatureHeaders, payload);
                priorSignature = BinaryUtils.toHex(signatureBytes);

                /**
                 * Add signing layer event-stream headers
                 */
                Map headers = new HashMap<>(nonSignatureHeaders);
                //Signature headers
                headers.put(EVENT_STREAM_SIGNATURE, HeaderValue.fromByteArray(signatureBytes));

                /**
                 * Encode signed event to byte
                 */
                Message signedMessage = new Message(sortHeaders(headers), payload);

                if (LOG.isLoggingLevelEnabled("trace")) {
                    LOG.trace(() -> "Signed message: " + toDebugString(signedMessage, false));
                } else {
                    LOG.debug(() -> "Signed message: " + toDebugString(signedMessage, true));
                }

                return signedMessage.toByteBuffer();
            }
        };
    }


    /**
     * Sign event stream with SigV4 signature
     *
     * @param priorSignature signature of previous frame (Header frame is the 0th frame)
     * @param signingKey derived signing key
     * @param signingInstant the instant at which this message is being signed
     * @param requestParams request parameters
     * @param nonSignatureHeaders non-signature headers
     * @param payload event stream payload
     * @return encoded event with signature
     */
    private byte[] signEventStream(
        String priorSignature,
        byte[] signingKey,
        Instant signingInstant,
        Aws4SignerRequestParams requestParams,
        Map nonSignatureHeaders,
        byte[] payload) {

        // String to sign
        String stringToSign =
            EVENT_STREAM_PAYLOAD +
            SignerConstant.LINE_SEPARATOR +
            Aws4SignerUtils.formatTimestamp(signingInstant) +
            SignerConstant.LINE_SEPARATOR +
            computeScope(signingInstant, requestParams) +
            SignerConstant.LINE_SEPARATOR +
            priorSignature +
            SignerConstant.LINE_SEPARATOR +
            BinaryUtils.toHex(hash(Message.encodeHeaders(sortHeaders(nonSignatureHeaders).entrySet()))) +
            SignerConstant.LINE_SEPARATOR +
            BinaryUtils.toHex(hash(payload));

        // calculate signature
        return sign(stringToSign.getBytes(StandardCharsets.UTF_8), signingKey,
                    SigningAlgorithm.HmacSHA256);
    }

    private String computeScope(Instant signingInstant, Aws4SignerRequestParams requestParams) {
        return Aws4SignerUtils.formatDateStamp(signingInstant) + "/" +
               requestParams.getRegionName() + "/" +
               requestParams.getServiceSigningName() + "/" +
               SignerConstant.AWS4_TERMINATOR;
    }

    /**
     * Sort headers in alphabetic order, with exception that EVENT_STREAM_SIGNATURE header always at last
     *
     * @param headers unsorted headers
     * @return sorted headers
     */
    private TreeMap sortHeaders(Map headers) {
        TreeMap sortedHeaders = new TreeMap<>((header1, header2) -> {
            // signature header should always be the last header
            if (header1.equals(EVENT_STREAM_SIGNATURE)) {
                return 1; // put header1 at last
            } else if (header2.equals(EVENT_STREAM_SIGNATURE)) {
                return -1; // put header2 at last
            } else {
                return header1.compareTo(header2);
            }
        });
        sortedHeaders.putAll(headers);
        return sortedHeaders;
    }

    private SdkHttpFullRequest addContentSha256Header(SdkHttpFullRequest request) {
        return request.toBuilder()
                      .putHeader(X_AMZ_CONTENT_SHA256, HTTP_CONTENT_SHA_256).build();
    }

    /**
     * {@link AsyncRequestBody} implementation that use the provider that signs the events.
     * Using anonymous class raises spot bug violation
     */
    private static class SigningRequestBodyProvider implements AsyncRequestBody {

        private AsyncRequestBody transformedRequestBody;

        SigningRequestBodyProvider(AsyncRequestBody transformedRequestBody) {
            this.transformedRequestBody = transformedRequestBody;
        }

        @Override
        public void subscribe(Subscriber s) {
            transformedRequestBody.subscribe(s);
        }

        @Override
        public Optional contentLength() {
            return transformedRequestBody.contentLength();
        }

        @Override
        public String contentType() {
            return transformedRequestBody.contentType();
        }
    }

    static String toDebugString(Message m, boolean truncatePayload) {
        StringBuilder sb = new StringBuilder("Message = {headers={");
        Map headers = m.getHeaders();

        Iterator> headersIter = headers.entrySet().iterator();

        while (headersIter.hasNext()) {
            Map.Entry h = headersIter.next();

            sb.append(h.getKey()).append("={").append(h.getValue().toString()).append("}");

            if (headersIter.hasNext()) {
                sb.append(", ");
            }
        }

        sb.append("}, payload=");

        byte[] payload = m.getPayload();
        byte[] payloadToLog;

        // We don't actually need to truncate if the payload length is already within the truncate limit
        truncatePayload = truncatePayload && payload.length > PAYLOAD_TRUNCATE_LENGTH;

        if (truncatePayload) {
            // Would be nice if BinaryUtils.toHex() could take an array index range instead so we don't need to copy
            payloadToLog = Arrays.copyOf(payload, PAYLOAD_TRUNCATE_LENGTH);
        } else {
            payloadToLog = payload;
        }

        sb.append(BinaryUtils.toHex(payloadToLog));

        if (truncatePayload) {
            sb.append("...");
        }

        sb.append("}");

        return sb.toString();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy