Skip to content

Commit

Permalink
feat: initial implementation of standard AWS signer (#635)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianbotsf authored May 9, 2022
1 parent 0f9311a commit 5be175d
Show file tree
Hide file tree
Showing 38 changed files with 1,256 additions and 122 deletions.
3 changes: 3 additions & 0 deletions gradle/jvm.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jvmTest {
testLogging {
events("passed", "skipped", "failed")
showStandardStreams = true
showStackTraces = true
showExceptions = true
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}

useJUnitPlatform()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ object AwsSigningAttributes {
val CredentialsProvider: ClientOption<CredentialsProvider> = ClientOption("CredentialsProvider")

/**
* The source for the body hash.
* The specification for determining the hash value for the request.
*
* **Note**: This is an advanced configuration option that does not normally need to be set manually.
*/
val BodyHash: ClientOption<BodyHash> = ClientOption("BodyHash")
val HashSpecification: ClientOption<HashSpecification> = ClientOption("HashSpecification")

/**
* The signed body header type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,41 +51,6 @@ enum class AwsSignatureType {
HTTP_REQUEST_EVENT,
}

/**
* Identifies a source for calculating the body hash value
*/
sealed class BodyHash(open val hash: String?) {
/**
* The hash value should be calculated from the body payload
*/
object CalculateFromPayload : BodyHash(null)

/**
* The hash value should indicate an unsigned payload
*/
object UnsignedPayload : BodyHash("UNSIGNED-PAYLOAD")

/**
* The hash value should indicate an empty body
*/
object EmptyBody : BodyHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") // hash of ""

/**
* The hash value should indicate that signature covers only headers and that there is no payload
*/
object StreamingAws4HmacSha256Payload : BodyHash("STREAMING-AWS4-HMAC-SHA256-PAYLOAD")

/**
* The hash value should indicate ???
*/
object StreamingAws4HmacSha256Events : BodyHash("STREAMING-AWS4-HMAC-SHA256-EVENTS")

/**
* Use an explicit, precalculated value for the hash
*/
data class Precalculated(override val hash: String) : BodyHash(hash)
}

enum class AwsSignedBodyHeader {
/**
* Do not add a header.
Expand Down Expand Up @@ -157,18 +122,21 @@ class AwsSigningConfig(builder: Builder) {
val normalizeUriPath: Boolean = builder.normalizeUriPath

/**
* Determines wheter the `X-Amz-Security-Token` query param should be omitted. Normally, this parameter is added
* during signing if the credentials have a session token. The only known case where this should be true is when
* signing a websocket handshake to IoT Core.
* Determines whether the `X-Amz-Security-Token` query param should be omitted from the canonical signing
* calculation. Normally, this parameter is added during signing if the credentials have a session token. The only
* known case where this should be true is when signing a websocket handshake to IoT Core.
*
* If this value is false, a non-null security token is _still added to the request_ but it is not used in signature
* calculation.
*/
val omitSessionToken: Boolean = builder.omitSessionToken

/**
* Determines the source of the canonical request's body public value. The default is
* [BodyHash.CalculateFromPayload], indicating that a public value will be calculated from the payload during
* signing.
* [HashSpecification.CalculateFromPayload], indicating that a public value will be calculated from the payload
* during signing.
*/
val bodyHash: BodyHash = builder.bodyHash ?: BodyHash.CalculateFromPayload
val hashSpecification: HashSpecification = builder.hashSpecification ?: HashSpecification.CalculateFromPayload

/**
* Determines which body "hash" header, if any, should be added to the canonical request and the signed request.
Expand Down Expand Up @@ -200,7 +168,7 @@ class AwsSigningConfig(builder: Builder) {
it.useDoubleUriEncode = useDoubleUriEncode
it.normalizeUriPath = normalizeUriPath
it.omitSessionToken = omitSessionToken
it.bodyHash = bodyHash
it.hashSpecification = hashSpecification
it.signedBodyHeader = signedBodyHeader
it.credentialsProvider = credentialsProvider
it.expiresAfter = expiresAfter
Expand All @@ -216,7 +184,7 @@ class AwsSigningConfig(builder: Builder) {
var useDoubleUriEncode: Boolean = true
var normalizeUriPath: Boolean = true
var omitSessionToken: Boolean = false
var bodyHash: BodyHash? = null
var hashSpecification: HashSpecification? = null
var signedBodyHeader: AwsSignedBodyHeader = AwsSignedBodyHeader.NONE
var credentialsProvider: CredentialsProvider? = null
var expiresAfter: Duration? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
package aws.smithy.kotlin.runtime.auth.awssigning

/**
* Specifies a hash for a signable request
*/
sealed class HashSpecification {
/**
* Indicates that the hash value should be calculated from the body payload of the request
*/
object CalculateFromPayload : HashSpecification()

/**
* Specifies a literal value to use as a hash
*/
sealed class HashLiteral(open val hash: String) : HashSpecification()

/**
* The hash value should indicate an unsigned payload
*/
object UnsignedPayload : HashLiteral("UNSIGNED-PAYLOAD")

/**
* The hash value should indicate an empty body
*/
object EmptyBody : HashLiteral("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") // hash of ""

/**
* The hash value should indicate that signature covers only headers and that there is no payload
*/
object StreamingAws4HmacSha256Payload : HashLiteral("STREAMING-AWS4-HMAC-SHA256-PAYLOAD")

/**
* The hash value should indicate ???
*/
object StreamingAws4HmacSha256Events : HashLiteral("STREAMING-AWS4-HMAC-SHA256-EVENTS")

/**
* Use an explicit, precalculated value for the hash
*/
data class Precalculated(override val hash: String) : HashLiteral(hash)
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,16 @@ suspend fun createPresignedRequest(
PresigningLocation.HEADER -> AwsSignatureType.HTTP_REQUEST_VIA_HEADERS
PresigningLocation.QUERY_STRING -> AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS
}
val bodyHash = if (requestConfig.signBody) BodyHash.CalculateFromPayload else BodyHash.UnsignedPayload
val hashSpecification =
if (requestConfig.signBody) HashSpecification.CalculateFromPayload else HashSpecification.UnsignedPayload

val signingConfig = AwsSigningConfig {
region = endpoint.context?.region ?: serviceConfig.region
service = endpoint.context?.service ?: serviceConfig.signingName
credentialsProvider = serviceConfig.credentialsProvider
this.signatureType = signatureType
signedBodyHeader = AwsSignedBodyHeader.X_AMZ_CONTENT_SHA256
this.bodyHash = bodyHash
this.hashSpecification = hashSpecification
expiresAfter = requestConfig.expiresAfter
useDoubleUriEncode = serviceConfig.useDoubleUriEncode
normalizeUriPath = serviceConfig.normalizeUriPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class AwsSigningMiddleware(private val config: Config) : ModifyRequestMiddleware
val body = req.subject.body

// favor attributes from the current request context
val contextBodyHash = req.context.getOrNull(AwsSigningAttributes.BodyHash)
val contextHashSpecification = req.context.getOrNull(AwsSigningAttributes.HashSpecification)
val contextSignedBodyHeader = req.context.getOrNull(AwsSigningAttributes.SignedBodyHeader)

// operation signing config is baseConfig + operation specific config/overrides
Expand Down Expand Up @@ -142,16 +142,16 @@ class AwsSigningMiddleware(private val config: Config) : ModifyRequestMiddleware
// FIXME - see: https://github.com/awslabs/smithy-kotlin/issues/296
// if we know we have a (streaming) body and toSignableRequest() fails to convert it to a CRT equivalent
// then we must decide how to compute the payload hash ourselves (defaults to unsigned payload)
bodyHash = when {
contextBodyHash != null -> contextBodyHash
config.isUnsignedPayload -> BodyHash.UnsignedPayload
body is HttpBody.Empty -> BodyHash.EmptyBody
hashSpecification = when {
contextHashSpecification != null -> contextHashSpecification
config.isUnsignedPayload -> HashSpecification.UnsignedPayload
body is HttpBody.Empty -> HashSpecification.EmptyBody
body is HttpBody.Streaming && !body.isReplayable -> {
logger.warn { "unable to compute hash for unbounded stream; defaulting to unsigned payload" }
BodyHash.UnsignedPayload
HashSpecification.UnsignedPayload
}
// use the payload to compute the hash
else -> BodyHash.CalculateFromPayload
else -> HashSpecification.CalculateFromPayload
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private suspend fun AwsSigningConfig.toCrtSigningConfig(): CrtSigningConfig {
useDoubleUriEncode = src.useDoubleUriEncode
normalizeUriPath = src.normalizeUriPath
omitSessionToken = src.omitSessionToken
signedBodyValue = src.bodyHash.hash
signedBodyValue = src.hashSpecification.toCrtSignedBodyValue()
signedBodyHeader = src.signedBodyHeader.toCrtSignedBodyHeaderType()
credentials = srcCredentials.toCrtCredentials()
expirationInSeconds = src.expiresAfter?.inWholeSeconds ?: 0
Expand All @@ -88,6 +88,11 @@ private suspend fun AwsSigningConfig.toCrtSigningConfig(): CrtSigningConfig {

private fun Credentials.toCrtCredentials() = CrtCredentials(accessKeyId, secretAccessKey, sessionToken)

private fun HashSpecification.toCrtSignedBodyValue(): String? = when (this) {
is HashSpecification.CalculateFromPayload -> null
is HashSpecification.HashLiteral -> hash
}

private fun Headers.toCrtHeaders(): CrtHeaders {
val headersBuilder = aws.sdk.kotlin.crt.http.HeadersBuilder()
forEach(headersBuilder::appendAll)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,7 @@ import aws.smithy.kotlin.runtime.auth.awssigning.tests.SigningSuiteTestBase
class CrtSigningSuiteTest : SigningSuiteTestBase() {
override val signer: AwsSigner = CrtAwsSigner

override val disabledTests = setOf(
// ktor-http-cio parser doesn't support parsing multiline headers since they are deprecated in RFC7230
"get-header-value-multiline",
// ktor fails to parse with space in it (expects it to be a valid request already encoded)
"get-space-normalized",
"get-space-unnormalized",

// no signed request to test against
"get-vanilla-query-order-key",
"get-vanilla-query-order-value",

override val disabledTests = super.disabledTests + setOf(
// FIXME - Signature mismatch possibly related to https://github.com/awslabs/aws-crt-java/pull/419. Needs
// investigation.
"get-utf8",
Expand Down
32 changes: 32 additions & 0 deletions runtime/auth/aws-signing-default/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
description = "AWS Signer default implementation"
extra["displayName"] = "Smithy :: Kotlin :: Standard AWS Signer"
extra["moduleName"] = "aws.smithy.kotlin.runtime.auth.awssigning"

val coroutinesVersion: String by project

kotlin {
sourceSets {
commonMain {
dependencies {
api(project(":runtime:auth:aws-signing-common"))
implementation(project(":runtime:hashing"))
implementation(project(":runtime:logging"))
}
}

commonTest {
dependencies {
implementation(project(":runtime:auth:aws-signing-tests"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
}
}

all {
languageSettings.optIn("aws.smithy.kotlin.runtime.util.InternalApi")
}
}
}
Loading

0 comments on commit 5be175d

Please sign in to comment.