Skip to content
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

feat: implement clock skew interceptor #972

Merged
merged 54 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5b7e337
Add `until` convenience function
lauzadis Oct 5, 2023
023c845
ClockSkew integration / interceptor
lauzadis Oct 6, 2023
371e993
Add Skew RetryErrorType
lauzadis Oct 6, 2023
e2bbde5
Use skew from execution context
lauzadis Oct 6, 2023
5a42d82
Final
lauzadis Oct 6, 2023
10c8013
ktlint
lauzadis Oct 6, 2023
b21ae43
ktlint
lauzadis Oct 6, 2023
dd94acc
ktlint
lauzadis Oct 6, 2023
e83af28
Add tests
lauzadis Oct 6, 2023
3c8ca1a
Store clock skew and apply to all requests
lauzadis Oct 6, 2023
6234df5
Duration.ZERO
lauzadis Oct 6, 2023
ee15721
ktlintFormat
lauzadis Oct 6, 2023
be94c7c
Don't need Duration/nanoseconds
lauzadis Oct 6, 2023
f2cfa77
KDocs
lauzadis Oct 6, 2023
a22de25
Add additional assertions
lauzadis Oct 6, 2023
4c50705
Actually update the interceptor state
lauzadis Oct 6, 2023
ad55bd8
Changelog
lauzadis Oct 6, 2023
cfc0ce5
Merge branch 'main' of github.com:awslabs/smithy-kotlin into feat-clo…
lauzadis Oct 6, 2023
5cfcb17
Remove usage of ByteArrayContent
lauzadis Oct 6, 2023
c37e2e8
Add `until` tests
lauzadis Oct 6, 2023
d3520c8
Make getSkew internal
lauzadis Oct 6, 2023
bb57855
Refactor clock skew application
lauzadis Oct 6, 2023
fd11afd
Remove `Skew` error type
lauzadis Oct 6, 2023
1230226
Use `Instant.now()` for client time, lower some logs to debug level, …
lauzadis Oct 6, 2023
2849f66
use atomic
lauzadis Oct 6, 2023
bb2bce8
apiDump
lauzadis Oct 6, 2023
c4aa35b
Use atomics
lauzadis Oct 6, 2023
0007895
apiDump
lauzadis Oct 6, 2023
52c5d3b
Refactor to extension function
lauzadis Oct 6, 2023
3fd3f9f
Proper skew logic
lauzadis Oct 6, 2023
f44477d
Unused import
lauzadis Oct 6, 2023
b822870
ktlint
lauzadis Oct 6, 2023
7c9c012
Remove unnecessary `Duration.ZERO -`
lauzadis Oct 11, 2023
6c01e69
Honest KDocs
lauzadis Oct 11, 2023
672740e
`var` -> `val`
lauzadis Oct 11, 2023
b139e49
Remove `getSkew` in favor of `Instant.until`
lauzadis Oct 11, 2023
cf58ca7
Use `Instant.until`
lauzadis Oct 11, 2023
839ef98
Remove import too
lauzadis Oct 11, 2023
ae609bf
Changelog
lauzadis Oct 11, 2023
3b5d2e1
Merge branch 'main' of github.com:awslabs/smithy-kotlin into feat-clo…
lauzadis Oct 11, 2023
3924710
Use client's approximate signing time instead of `Instant.now()`
lauzadis Oct 11, 2023
928d58f
Add import
lauzadis Oct 11, 2023
34c1b3b
Use service exceptions to determine skew
lauzadis Oct 11, 2023
2a85575
ktlint
lauzadis Oct 11, 2023
343ae26
Fix exceptions handling
lauzadis Oct 12, 2023
e590d52
Relocate to aws-protocol-core
lauzadis Oct 12, 2023
5ffebe9
Relocate to aws-protocol-core
lauzadis Oct 12, 2023
19a9ea9
Refactor to section writer
lauzadis Oct 12, 2023
6c4e3bf
Merge branch 'main' of github.com:awslabs/smithy-kotlin into feat-clo…
lauzadis Oct 12, 2023
badf986
ktlint
lauzadis Oct 12, 2023
7becf58
correct possible skew error codes
lauzadis Oct 12, 2023
8d52429
correct code in test
lauzadis Oct 12, 2023
7582dd0
Rename `responseCodeDescription` to `errorCode` and make it required
lauzadis Oct 17, 2023
66fa578
Merge branch 'main' of github.com:awslabs/smithy-kotlin into feat-clo…
lauzadis Oct 17, 2023
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
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,119 @@
/*
* 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 responseCodeDescription the server's response code description
* @param serverTime the server's time
*/
internal fun Instant.isSkewed(serverTime: Instant, responseCodeDescription: String?): Boolean =
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: responseCodeDescription -> errorCode

Also responseCodeDescription should probably be non-nullable since this always returns false otherwise which means it's really a required parameter for determining if a clock is skewed.

responseCodeDescription?.let {
CLOCK_SKEW_ERROR_CODES.contains(it) || (POSSIBLE_CLOCK_SKEW_ERROR_CODES.contains(it) && until(serverTime).absoluteValue >= CLOCK_SKEW_THRESHOLD)
} ?: false

// 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(
"PriorRequestNotComplete",
"RequestTimeout",
"RequestTimeoutException",
"InternalError",
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Correctness: These don't appear to be related to clock skew. Looks like Java considers InvalidSignatureException, SignatureDoesNotMatch, and AuthFailure as possible errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤦 Good catch. I took the wrong list of exceptions from Java and then convinced myself they could be caused by clock skew.

}

// 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)

if (clientTime.isSkewed(serverTime, errorCode)) {
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
Loading