Skip to content

Commit

Permalink
feat: implement clock skew interceptor (#972)
Browse files Browse the repository at this point in the history
  • Loading branch information
lauzadis authored Oct 17, 2023
1 parent fd6a340 commit 4a0dda6
Show file tree
Hide file tree
Showing 12 changed files with 414 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changes/6de10487-c3a0-4c63-929a-ba11a415ea8f.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "6de10487-c3a0-4c63-929a-ba11a415ea8f",
"type": "feature",
"description": "Detect and automatically correct clock skew to prevent signing errors"
}
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ object RuntimeTypes {
val AwsQueryCompatibleErrorDetails = symbol("AwsQueryCompatibleErrorDetails")
val setAwsQueryCompatibleErrorMetadata = symbol("setAwsQueryCompatibleErrorMetadata")
val XAmznQueryErrorHeader = symbol("X_AMZN_QUERY_ERROR_HEADER")
val ClockSkewInterceptor = symbol("ClockSkewInterceptor")
}

object AwsJsonProtocols : RuntimeTypePackage(KotlinDependency.AWS_JSON_PROTOCOLS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class ServiceClientGenerator(private val ctx: RenderingContext<ServiceShape>) {
*/
val RenderingContext: SectionKey<RenderingContext<ServiceShape>> = SectionKey("RenderingContext")
}

/**
* [SectionId] used when rendering the finalizeConfig block of a service client
*/
object FinalizeConfig : SectionId
}

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import aws.smithy.kotlin.runtime.auth.awssigning.internal.setAwsChunkedBody
import aws.smithy.kotlin.runtime.auth.awssigning.internal.setAwsChunkedHeaders
import aws.smithy.kotlin.runtime.auth.awssigning.internal.useAwsChunkedEncoding
import aws.smithy.kotlin.runtime.http.HttpBody
import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.util.get
import kotlin.time.Duration

Expand Down Expand Up @@ -123,7 +125,10 @@ public class AwsHttpSigner(private val config: Config) : HttpSigner {
service = attributes.getOrNull(AwsSigningAttributes.SigningService) ?: checkNotNull(config.service)
credentials = signingRequest.identity as Credentials
algorithm = config.algorithm

// apply clock skew if applicable
signingDate = attributes.getOrNull(AwsSigningAttributes.SigningDate)
?: (Instant.now() + (attributes.getOrNull(HttpOperationContext.ClockSkew) ?: Duration.ZERO))

signatureType = config.signatureType
omitSessionToken = config.omitSessionToken
Expand Down
28 changes: 28 additions & 0 deletions runtime/protocol/aws-protocol-core/api/aws-protocol-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@ public final class aws/smithy/kotlin/runtime/awsprotocol/AwsQueryCompatibleError
public final class aws/smithy/kotlin/runtime/awsprotocol/AwsQueryCompatibleErrorDetailsKt {
}

public final class aws/smithy/kotlin/runtime/awsprotocol/ClockSkewInterceptor : aws/smithy/kotlin/runtime/client/Interceptor {
public static final field Companion Laws/smithy/kotlin/runtime/awsprotocol/ClockSkewInterceptor$Companion;
public fun <init> ()V
public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun modifyBeforeRetryLoop (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun modifyBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun modifyBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun readAfterAttempt (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V
public fun readAfterDeserialization (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V
public fun readAfterExecution (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V
public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V
public fun readAfterSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V
public fun readAfterTransmit (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V
public fun readBeforeAttempt (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V
public fun readBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V
public fun readBeforeExecution (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V
public fun readBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V
public fun readBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V
public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V
}

public final class aws/smithy/kotlin/runtime/awsprotocol/ClockSkewInterceptor$Companion {
public final fun getCLOCK_SKEW_THRESHOLD-UwyO8pc ()J
}

public final class aws/smithy/kotlin/runtime/awsprotocol/ProtocolErrorsKt {
}

Expand Down
12 changes: 12 additions & 0 deletions runtime/protocol/aws-protocol-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@ extra["moduleName"] = "aws.smithy.kotlin.runtime.awsprotocol"

val coroutinesVersion: String by project

apply(plugin = "kotlinx-atomicfu")

kotlin {
sourceSets {
commonMain {
dependencies {
api(project(":runtime:protocol:http"))
api(project(":runtime:runtime-core"))
api(project(":runtime:protocol:http-client"))
api(project(":runtime:smithy-client"))
implementation(libs.kotlinx.atomicfu)
}
}

commonTest {
dependencies {
implementation(libs.kotlinx.coroutines.test)
implementation(project(":runtime:protocol:http-test"))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.awsprotocol

import aws.smithy.kotlin.runtime.ErrorMetadata
import aws.smithy.kotlin.runtime.SdkBaseException
import aws.smithy.kotlin.runtime.ServiceErrorMetadata
import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext
import aws.smithy.kotlin.runtime.client.ResponseInterceptorContext
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.http.response.HttpResponse
import aws.smithy.kotlin.runtime.http.response.header
import aws.smithy.kotlin.runtime.telemetry.logging.logger
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.time.until
import aws.smithy.kotlin.runtime.util.get
import kotlinx.atomicfu.*
import kotlin.coroutines.coroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes

/**
* An interceptor used to detect clock skew (difference between client and server clocks) and apply a correction.
*/
public class ClockSkewInterceptor : HttpInterceptor {
public companion object {
/**
* How much must the clock be skewed before attempting correction
*/
public val CLOCK_SKEW_THRESHOLD: Duration = 4.minutes

/**
* Determine whether the client's clock is skewed relative to the server.
* @return true if the service's response represents a definite clock skew error
* OR a *possible* clock skew error AND the skew exists. false otherwise.
* @param errorCode the server's error code
* @param serverTime the server's time
*/
internal fun Instant.isSkewed(serverTime: Instant, errorCode: String): Boolean =
CLOCK_SKEW_ERROR_CODES.contains(errorCode) || (POSSIBLE_CLOCK_SKEW_ERROR_CODES.contains(errorCode) && until(serverTime).absoluteValue >= CLOCK_SKEW_THRESHOLD)

// Errors definitely caused by clock skew
private val CLOCK_SKEW_ERROR_CODES = listOf(
"RequestTimeTooSkewed",
"RequestExpired",
"RequestInTheFuture",
)

// Errors possibly caused by clock skew
private val POSSIBLE_CLOCK_SKEW_ERROR_CODES = listOf(
"InvalidSignatureException",
"SignatureDoesNotMatch",
"AuthFailure",
)
}

// Clock skew to be applied to all requests
private val _currentSkew: AtomicRef<Duration?> = atomic(null)

/**
* Apply the previously-computed skew, if it's set, to the execution context before signing
*/
public override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): HttpRequest {
val logger = coroutineContext.logger<ClockSkewInterceptor>()

val skew = _currentSkew.value
skew?.let {
logger.info { "applying clock skew $skew to client" }
context.executionContext[HttpOperationContext.ClockSkew] = skew
}

context.executionContext[HttpOperationContext.ClockSkewApproximateSigningTime] = Instant.now()

return context.protocolRequest
}

/**
* After receiving a response, check if the client clock is skewed and apply a correction if necessary.
*/
public override suspend fun modifyBeforeAttemptCompletion(context: ResponseInterceptorContext<Any, Any, HttpRequest, HttpResponse?>): Result<Any> {
val logger = coroutineContext.logger<ClockSkewInterceptor>()

val serverTime = context.protocolResponse?.header("Date")?.let {
Instant.fromRfc5322(it)
} ?: run {
logger.debug { "service did not return \"Date\" header, skipping skew calculation" }
return context.response
}

val clientTime = context.protocolRequest.headers["Date"]?.let {
Instant.fromRfc5322(it)
} ?: context.protocolRequest.headers["x-amz-date"]?.let {
Instant.fromIso8601(it)
} ?: context.executionContext[HttpOperationContext.ClockSkewApproximateSigningTime]

val ex = (context.response.exceptionOrNull() as? SdkBaseException) ?: return context.response
val errorCode = ex.sdkErrorMetadata.attributes.getOrNull(ServiceErrorMetadata.ErrorCode)

errorCode?.let {
if (clientTime.isSkewed(serverTime, it)) {
val skew = clientTime.until(serverTime)
logger.warn { "client clock ($clientTime) is skewed $skew from the server ($serverTime), applying correction" }
_currentSkew.getAndSet(skew)
context.executionContext[HttpOperationContext.ClockSkew] = skew

// Mark the exception as retryable
ex.sdkErrorMetadata.attributes[ErrorMetadata.Retryable] = true
return Result.failure(ex)
}
}

return context.response
}
}
Loading

0 comments on commit 4a0dda6

Please sign in to comment.