Skip to content

Set the signing clock property #4341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.http.auth.aws.internal.signer.DefaultAwsV4HttpSigner;
import software.amazon.awssdk.http.auth.spi.HttpSigner;
import software.amazon.awssdk.http.auth.spi.SharedSignerProperty;
import software.amazon.awssdk.http.auth.spi.SignerProperty;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;

Expand All @@ -46,8 +47,7 @@ public interface AwsV4HttpSigner extends HttpSigner<AwsCredentialsIdentity> {
/**
* A {@link Clock} to be used at the time of signing. This property defaults to the time at which signing occurs.
*/
SignerProperty<Clock> SIGNING_CLOCK =
SignerProperty.create(Clock.class, "SigningClock");
SignerProperty<Clock> SIGNING_CLOCK = SharedSignerProperty.SIGNING_CLOCK;

/**
* A boolean to indicate whether to double url-encode the resource path when constructing the canonical request. This property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.time.Duration;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.http.auth.spi.HttpSigner;
import software.amazon.awssdk.http.auth.spi.SharedSignerProperty;
import software.amazon.awssdk.http.auth.spi.SignerProperty;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;

Expand All @@ -45,8 +46,7 @@ public interface AwsV4aHttpSigner extends HttpSigner<AwsCredentialsIdentity> {
/**
* A {@link Clock} to be used at the time of signing. This property defaults to the time at which signing occurs.
*/
SignerProperty<Clock> SIGNING_CLOCK =
SignerProperty.create(Clock.class, "SigningClock");
SignerProperty<Clock> SIGNING_CLOCK = SharedSignerProperty.SIGNING_CLOCK;

/**
* A boolean to indicate whether to double url-encode the resource path when constructing the canonical request. This property
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.http.auth.spi;

import java.time.Clock;
import software.amazon.awssdk.annotations.SdkPublicApi;

/**
* Container class for the clock signer property that is common to some signing schemes.
*/
@SdkPublicApi
public final class SharedSignerProperty {
/**
* A {@link Clock} to be used at the time of signing. This property defaults to the time at which signing occurs.
*/
public static final SignerProperty<Clock> SIGNING_CLOCK = SignerProperty.create(Clock.class, "SigningClock");

private SharedSignerProperty() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.core.internal.http.pipeline.stages;

import java.nio.ByteBuffer;
import java.time.Clock;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
Expand All @@ -41,6 +42,7 @@
import software.amazon.awssdk.http.auth.spi.AsyncSignedRequest;
import software.amazon.awssdk.http.auth.spi.AuthSchemeOption;
import software.amazon.awssdk.http.auth.spi.HttpSigner;
import software.amazon.awssdk.http.auth.spi.SharedSignerProperty;
import software.amazon.awssdk.http.auth.spi.SignedRequest;
import software.amazon.awssdk.http.auth.spi.SyncSignRequest;
import software.amazon.awssdk.http.auth.spi.SyncSignedRequest;
Expand Down Expand Up @@ -103,6 +105,7 @@ private <T extends Identity> CompletableFuture<SdkHttpFullRequest> doSraSign(Sdk
if (context.requestProvider() == null) {
SyncSignRequest.Builder<T> signRequestBuilder = SyncSignRequest
.builder(identity)
.putProperty(SharedSignerProperty.SIGNING_CLOCK, signingClock())
.request(request)
.payload(request.contentStreamProvider().orElse(null));
authSchemeOption.forEachSignerProperty(signRequestBuilder::putProperty);
Expand All @@ -113,6 +116,7 @@ private <T extends Identity> CompletableFuture<SdkHttpFullRequest> doSraSign(Sdk

AsyncSignRequest.Builder<T> signRequestBuilder = AsyncSignRequest
.builder(identity)
.putProperty(SharedSignerProperty.SIGNING_CLOCK, signingClock())
.request(request)
.payload(context.requestProvider());
authSchemeOption.forEachSignerProperty(signRequestBuilder::putProperty);
Expand Down Expand Up @@ -229,6 +233,14 @@ private boolean shouldDoSraSigning(RequestExecutionContext context) {
&& context.executionAttributes().getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME) != null;
}

/**
* Returns the {@link Clock} used for signing that already accounts for clock skew when detected by the retryable stage.
*/
private Clock signingClock() {
int offsetInSeconds = dependencies.timeOffset();
return Clock.offset(Clock.systemUTC(), Duration.ofSeconds(-offsetInSeconds));
}

/**
* Always use the client level timeOffset.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.core.internal.http.pipeline.stages;

import java.time.Clock;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -37,6 +38,7 @@
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.auth.spi.AuthSchemeOption;
import software.amazon.awssdk.http.auth.spi.HttpSigner;
import software.amazon.awssdk.http.auth.spi.SharedSignerProperty;
import software.amazon.awssdk.http.auth.spi.SyncSignRequest;
import software.amazon.awssdk.http.auth.spi.SyncSignedRequest;
import software.amazon.awssdk.identity.spi.Identity;
Expand Down Expand Up @@ -95,6 +97,7 @@ private <T extends Identity> SdkHttpFullRequest doSraSign(SdkHttpFullRequest req
T identity) {
SyncSignRequest.Builder<T> signRequestBuilder = SyncSignRequest
.builder(identity)
.putProperty(SharedSignerProperty.SIGNING_CLOCK, signingClock())
.request(request)
.payload(request.contentStreamProvider().orElse(null));
AuthSchemeOption authSchemeOption = selectedAuthScheme.authSchemeOption();
Expand Down Expand Up @@ -182,6 +185,14 @@ private boolean shouldDoSraSigning(RequestExecutionContext context) {
&& context.executionAttributes().getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME) != null;
}

/**
* Returns the {@link Clock} used for signing that already accounts for clock skew when detected by the retryable stage.
*/
private Clock signingClock() {
int offsetInSeconds = dependencies.timeOffset();
return Clock.offset(Clock.systemUTC(), Duration.ofSeconds(-offsetInSeconds));
}

/**
* Always use the client level timeOffset.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.core.internal.http.pipeline.stages;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
Expand All @@ -28,6 +29,8 @@
import static software.amazon.awssdk.core.metrics.CoreMetric.SIGNING_DURATION;

import java.nio.ByteBuffer;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.CompletableFuture;
import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -58,6 +61,7 @@
import software.amazon.awssdk.http.auth.spi.AsyncSignedRequest;
import software.amazon.awssdk.http.auth.spi.AuthSchemeOption;
import software.amazon.awssdk.http.auth.spi.HttpSigner;
import software.amazon.awssdk.http.auth.spi.SharedSignerProperty;
import software.amazon.awssdk.http.auth.spi.SignerProperty;
import software.amazon.awssdk.http.auth.spi.SyncSignRequest;
import software.amazon.awssdk.http.auth.spi.SyncSignedRequest;
Expand All @@ -67,7 +71,7 @@

@RunWith(MockitoJUnitRunner.class)
public class AsyncSigningStageTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test (sync and async both) to assert that if the selected auth scheme has a SIGNING_CLOCK it is used over the default SIGNING_CLOCK? LGTM otherwise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call about that use case!


private static final int TEST_TIME_OFFSET = 17;
private static final SignerProperty<String> SIGNER_PROPERTY = SignerProperty.create(String.class, "key");

@Mock
Expand Down Expand Up @@ -134,6 +138,59 @@ public void execute_selectedAuthScheme_doesSraSign() throws Exception {
assertThat(signRequest.request()).isSameAs(request);
assertThat(signRequest.property(SIGNER_PROPERTY)).isEqualTo("value");

// Assert that the time offset set was zero
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK)).isNotNull();
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK).instant())
.isCloseTo(Instant.now(), within(10, ChronoUnit.MILLIS));

// assert that metrics are collected
verify(metricCollector).reportMetric(eq(SIGNING_DURATION), any());

verifyNoInteractions(oldSigner);
}


@Test
public void execute_selectedAuthSchemeAndTimeOffsetSet_doesSraSignAndAdjustTheSigningClock() throws Exception {
// Set up a scheme with a signer property
SelectedAuthScheme<Identity> selectedAuthScheme = new SelectedAuthScheme<>(
CompletableFuture.completedFuture(identity),
signer,
AuthSchemeOption.builder()
.schemeId("my.auth#myAuth")
.putSignerProperty(SIGNER_PROPERTY, "value")
.build());
RequestExecutionContext context = createContext(selectedAuthScheme);

// Setup the timeoffset to test that the clock is setup properly.
httpClientDependencies.updateTimeOffset(TEST_TIME_OFFSET);

SdkHttpRequest signedRequest = ValidSdkObjects.sdkHttpFullRequest().build();
when(signer.sign(Mockito.<SyncSignRequest<? extends Identity>>any()))
.thenReturn(SyncSignedRequest.builder()
.request(signedRequest)
.build());

SdkHttpFullRequest request = ValidSdkObjects.sdkHttpFullRequest().build();
SdkHttpFullRequest result = stage.execute(request, context).join();

assertThat(result).isSameAs(signedRequest);
// assert that interceptor context is updated with result
assertThat(context.executionContext().interceptorContext().httpRequest()).isSameAs(result);

// assert that the input to the signer is as expected, including that signer properties are set
verify(signer).sign(syncSignRequestCaptor.capture());
SyncSignRequest<? extends Identity> signRequest = syncSignRequestCaptor.getValue();
assertThat(signRequest.identity()).isSameAs(identity);
assertThat(signRequest.request()).isSameAs(request);
assertThat(signRequest.property(SIGNER_PROPERTY)).isEqualTo("value");

// Assert that the signing clock is setup properly
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK)).isNotNull();
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK).instant())
.isCloseTo(Instant.now().minusSeconds(17)
, within(10, ChronoUnit.MILLIS));

// assert that metrics are collected
verify(metricCollector).reportMetric(eq(SIGNING_DURATION), any());

Expand Down Expand Up @@ -177,8 +234,13 @@ public void execute_selectedAuthScheme_asyncRequestBody_doesSraSignAsync() throw
AsyncSignRequest<? extends Identity> signRequest = asyncSignRequestCaptor.getValue();
assertThat(signRequest.identity()).isSameAs(identity);
assertThat(signRequest.request()).isSameAs(request);
assertThat(signRequest.property(SIGNER_PROPERTY)).isEqualTo("value");
assertThat(signRequest.payload().get()).isSameAs(asyncPayload);
assertThat(signRequest.property(SIGNER_PROPERTY)).isEqualTo("value");

// Assert that the time offset set was zero
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK)).isNotNull();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think rather not set the property if offset is 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will introduce a branch that is not needed the way it's written now, so I think is better just to have a single code path. Can you please help me understand why you think is better not to set it?

Copy link
Contributor

@gosar gosar Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it is better since with current approach the test is more fragile and can fail sometimes if for some reason Instant.now is actually beyond the within value?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other test is still fragile like that. I think we can have a package-private constructor that takes in a Clock (instead of Clock.systemUTC), so that unit test can use a mock Clock and test exactly, instead of isCloseTo.

Copy link
Contributor

@gosar gosar Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would address my main concern - test fragility. We could still chose to not set or set the clock property if offset is 0. I was thinking that if there is no need for offset, the SigningStage can let the HttpSigner decide whether it needs a clock at all or not. But I don't feel strongly about that, if the test fragility is addressed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather tackle the problem when and if arises, I'm know that what you describing is possible but I don't think is likely, for that to happen it would mean that the machine running the tests will need to park the thread for more than 100ms (~ 300 Million instructions). Were the previous tests brittle like actually failing every so often?

assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK).instant())
.isCloseTo(Instant.now(), within(10, ChronoUnit.MILLIS));

// assert that metrics are collected
verify(metricCollector).reportMetric(eq(SIGNING_DURATION), any());
Expand Down Expand Up @@ -225,6 +287,11 @@ public void execute_selectedNoAuthAuthScheme_doesSraSignAsync() throws Exception
assertThat(signRequest.property(SIGNER_PROPERTY)).isEqualTo("value");
assertThat(signRequest.payload().get()).isSameAs(asyncPayload);

// Assert that the time offset set was zero
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK)).isNotNull();
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK).instant())
.isCloseTo(Instant.now(), within(10, ChronoUnit.MILLIS));

// assert that metrics are collected
verify(metricCollector).reportMetric(eq(SIGNING_DURATION), any());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.core.internal.http.pipeline.stages;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
Expand All @@ -26,6 +27,8 @@
import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME;
import static software.amazon.awssdk.core.metrics.CoreMetric.SIGNING_DURATION;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.CompletableFuture;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -50,6 +53,7 @@
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.auth.spi.AuthSchemeOption;
import software.amazon.awssdk.http.auth.spi.HttpSigner;
import software.amazon.awssdk.http.auth.spi.SharedSignerProperty;
import software.amazon.awssdk.http.auth.spi.SignerProperty;
import software.amazon.awssdk.http.auth.spi.SyncSignRequest;
import software.amazon.awssdk.http.auth.spi.SyncSignedRequest;
Expand All @@ -60,6 +64,7 @@
@RunWith(MockitoJUnitRunner.class)
public class SigningStageTest {

private static final int TEST_TIME_OFFSET = 17;
private static final SignerProperty<String> SIGNER_PROPERTY = SignerProperty.create(String.class, "key");

@Mock
Expand Down Expand Up @@ -122,6 +127,56 @@ public void execute_selectedAuthScheme_doesSraSign() throws Exception {
assertThat(signRequest.identity()).isSameAs(identity);
assertThat(signRequest.request()).isSameAs(request);
assertThat(signRequest.property(SIGNER_PROPERTY)).isEqualTo("value");
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK)).isNotNull();
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK).instant())
.isCloseTo(Instant.now(), within(10, ChronoUnit.MILLIS));

// assert that metrics are collected
verify(metricCollector).reportMetric(eq(SIGNING_DURATION), any());

verifyNoInteractions(oldSigner);
}

@Test
public void execute_selectedAuthSchemeAndTimeOffsetSet_doesSraSignAndAdjustTheSigningClock() throws Exception {
// Set up a scheme with a signer property
SelectedAuthScheme<Identity> selectedAuthScheme = new SelectedAuthScheme<>(
CompletableFuture.completedFuture(identity),
signer,
AuthSchemeOption.builder()
.schemeId("my.auth#myAuth")
.putSignerProperty(SIGNER_PROPERTY, "value")
.build());
RequestExecutionContext context = createContext(selectedAuthScheme);

// Setup the timeoffset to test that the clock is setup properly.
httpClientDependencies.updateTimeOffset(TEST_TIME_OFFSET);

SdkHttpRequest signedRequest = ValidSdkObjects.sdkHttpFullRequest().build();
when(signer.sign(Mockito.<SyncSignRequest<? extends Identity>>any()))
.thenReturn(SyncSignedRequest.builder()
.request(signedRequest)
.build());

SdkHttpFullRequest request = ValidSdkObjects.sdkHttpFullRequest().build();
SdkHttpFullRequest result = stage.execute(request, context);

assertThat(result).isSameAs(signedRequest);
// Assert that interceptor context is updated with result
assertThat(context.executionContext().interceptorContext().httpRequest()).isSameAs(result);

// Assert that the input to the signer is as expected, including that signer properties are set
verify(signer).sign(signRequestCaptor.capture());
SyncSignRequest<? extends Identity> signRequest = signRequestCaptor.getValue();
assertThat(signRequest.identity()).isSameAs(identity);
assertThat(signRequest.request()).isSameAs(request);
assertThat(signRequest.property(SIGNER_PROPERTY)).isEqualTo("value");

// Assert that the signing clock is setup properly
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK)).isNotNull();
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK).instant())
.isCloseTo(Instant.now().minusSeconds(17)
, within(10, ChronoUnit.MILLIS));

// assert that metrics are collected
verify(metricCollector).reportMetric(eq(SIGNING_DURATION), any());
Expand Down Expand Up @@ -160,6 +215,11 @@ public void execute_selectedNoAuthAuthScheme_doesSraSign() throws Exception {
assertThat(signRequest.request()).isSameAs(request);
assertThat(signRequest.property(SIGNER_PROPERTY)).isNull();

// Assert that the time offset set was zero
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK)).isNotNull();
assertThat(signRequest.property(SharedSignerProperty.SIGNING_CLOCK).instant())
.isCloseTo(Instant.now(), within(10, ChronoUnit.MILLIS));

// assert that metrics are collected
verify(metricCollector).reportMetric(eq(SIGNING_DURATION), any());

Expand Down