Skip to content

Commit 82b9ffe

Browse files
authored
feat: Kotlin implementation of SigV4a signing (#1246)
1 parent 283ca31 commit 82b9ffe

File tree

22 files changed

+571
-129
lines changed

22 files changed

+571
-129
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "4b6debe1-7706-484a-8599-ef8c14cecde2",
3+
"type": "feature",
4+
"description": "Add SigV4a support to the default AWS signer"
5+
}

runtime/auth/aws-signing-common/api/aws-signing-common.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm
5555
public static final field SIGV4 Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
5656
public static final field SIGV4_ASYMMETRIC Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
5757
public static fun getEntries ()Lkotlin/enums/EnumEntries;
58+
public final fun getSigningName ()Ljava/lang/String;
5859
public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
5960
public static fun values ()[Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
6061
}

runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ public typealias ShouldSignHeaderPredicate = (String) -> Boolean
1212

1313
/**
1414
* Defines the AWS signature version to use
15+
* @param signingName The name of this algorithm to use when signing requests.
1516
*/
16-
public enum class AwsSigningAlgorithm {
17+
public enum class AwsSigningAlgorithm(public val signingName: String) {
1718
/**
1819
* AWS Signature Version 4
1920
* see: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
2021
*/
21-
SIGV4,
22+
SIGV4("AWS4-HMAC-SHA256"),
2223

2324
/**
2425
* AWS Signature Version 4 Asymmetric
2526
*/
26-
SIGV4_ASYMMETRIC,
27+
SIGV4_ASYMMETRIC("AWS4-ECDSA-P256-SHA256"),
2728
}
2829

2930
/**

runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import aws.smithy.kotlin.runtime.InternalApi
1717
* @param cause The cause of the exception
1818
*/
1919
@InternalApi
20+
@Deprecated("This exception is no longer thrown. It will be removed in the next minor version, v1.5.x.")
2021
public class UnsupportedSigningAlgorithmException(
2122
message: String,
2223
public val signingAlgorithm: AwsSigningAlgorithm,

runtime/auth/aws-signing-default/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ kotlin {
1818
dependencies {
1919
implementation(project(":runtime:auth:aws-signing-tests"))
2020
implementation(libs.kotlinx.coroutines.test)
21+
implementation(libs.kotlinx.serialization.json)
2122
}
2223
}
2324

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.auth.awssigning
6+
7+
import aws.smithy.kotlin.runtime.hashing.HashSupplier
8+
import aws.smithy.kotlin.runtime.hashing.Sha256
9+
import aws.smithy.kotlin.runtime.hashing.hash
10+
import aws.smithy.kotlin.runtime.hashing.sha256
11+
import aws.smithy.kotlin.runtime.text.encoding.encodeToHex
12+
import aws.smithy.kotlin.runtime.time.Instant
13+
import aws.smithy.kotlin.runtime.time.TimestampFormat
14+
import aws.smithy.kotlin.runtime.time.epochMilliseconds
15+
16+
/**
17+
* Common signature implementation used for SigV4 and SigV4a, primarily for forming the strings-to-sign which don't differ
18+
* between the two signing algorithms (besides their names).
19+
*/
20+
internal abstract class BaseSigV4SignatureCalculator(
21+
val algorithm: AwsSigningAlgorithm,
22+
open val sha256Provider: HashSupplier = ::Sha256,
23+
) : SignatureCalculator {
24+
private val supportedAlgorithms = setOf(AwsSigningAlgorithm.SIGV4, AwsSigningAlgorithm.SIGV4_ASYMMETRIC)
25+
26+
init {
27+
check(algorithm in supportedAlgorithms) {
28+
"This class should only be used for ${supportedAlgorithms.joinToString()}, got $algorithm"
29+
}
30+
}
31+
32+
override fun stringToSign(canonicalRequest: String, config: AwsSigningConfig): String = buildString {
33+
appendLine(algorithm.signingName)
34+
appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED))
35+
appendLine(config.credentialScope)
36+
append(canonicalRequest.encodeToByteArray().hash(sha256Provider).encodeToHex())
37+
}
38+
39+
override fun chunkStringToSign(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String = buildString {
40+
appendLine("${algorithm.signingName}-PAYLOAD")
41+
appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED))
42+
appendLine(config.credentialScope)
43+
appendLine(prevSignature.decodeToString()) // Should already be a byte array of ASCII hex chars
44+
45+
val nonSignatureHeadersHash = when (config.signatureType) {
46+
AwsSignatureType.HTTP_REQUEST_EVENT -> eventStreamNonSignatureHeaders(config.signingDate)
47+
else -> HashSpecification.EmptyBody.hash
48+
}
49+
50+
appendLine(nonSignatureHeadersHash)
51+
append(chunkBody.hash(sha256Provider).encodeToHex())
52+
}
53+
54+
override fun chunkTrailerStringToSign(trailingHeaders: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String =
55+
buildString {
56+
appendLine("${algorithm.signingName}-TRAILER")
57+
appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED))
58+
appendLine(config.credentialScope)
59+
appendLine(prevSignature.decodeToString())
60+
append(trailingHeaders.hash(sha256Provider).encodeToHex())
61+
}
62+
}
63+
64+
private const val HEADER_TIMESTAMP_TYPE: Byte = 8
65+
66+
/**
67+
* Return the sha256 hex representation of the encoded event stream date header
68+
*
69+
* ```
70+
* sha256Hex( Header(":date", HeaderValue::Timestamp(date)).encodeToByteArray() )
71+
* ```
72+
*
73+
* NOTE: This duplicates parts of the event stream encoding implementation here to avoid a direct dependency.
74+
* Should this become more involved than encoding a single date header we should reconsider this choice.
75+
*
76+
* see [Event Stream implementation](https://github.com/smithy-lang/smithy-kotlin/blob/612c39ba446e6403ea2bd9a51c4d1db111b6e26f/runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/Header.kt#L52)
77+
*/
78+
private fun eventStreamNonSignatureHeaders(date: Instant): String {
79+
val bytes = ByteArray(15)
80+
// encode header name
81+
val name = ":date".encodeToByteArray()
82+
var offset = 0
83+
bytes[offset++] = name.size.toByte()
84+
name.copyInto(bytes, destinationOffset = offset)
85+
offset += name.size
86+
87+
// encode header value
88+
bytes[offset++] = HEADER_TIMESTAMP_TYPE
89+
writeLongBE(bytes, offset, date.epochMilliseconds)
90+
return bytes.sha256().encodeToHex()
91+
}
92+
93+
private fun writeLongBE(dest: ByteArray, offset: Int, x: Long) {
94+
var idx = offset
95+
for (i in 7 downTo 0) {
96+
dest[idx++] = ((x ushr (i * 8)) and 0xff).toByte()
97+
}
98+
}

runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,13 @@ internal class DefaultCanonicalizer(private val sha256Supplier: HashSupplier = :
124124
}
125125

126126
param("Host", builder.url.hostAndPort, !signViaQueryParams, overwrite = false)
127-
param("X-Amz-Algorithm", ALGORITHM_NAME, signViaQueryParams)
127+
param("X-Amz-Algorithm", config.algorithm.signingName, signViaQueryParams)
128128
param("X-Amz-Credential", credentialValue(config), signViaQueryParams)
129129
param("X-Amz-Content-Sha256", hash, addHashHeader)
130130
param("X-Amz-Date", config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED))
131131
param("X-Amz-Expires", config.expiresAfter?.inWholeSeconds?.toString(), signViaQueryParams)
132132
param("X-Amz-Security-Token", sessionToken, !config.omitSessionToken) // Add pre-sig if omitSessionToken=false
133+
param("X-Amz-Region-Set", config.region, config.algorithm == AwsSigningAlgorithm.SIGV4_ASYMMETRIC)
133134

134135
val headers = builder
135136
.headers

runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,29 @@ public class DefaultAwsSignerBuilder {
2929
)
3030
}
3131

32+
private val AwsSigningAlgorithm.signatureCalculator
33+
get() = when (this) {
34+
AwsSigningAlgorithm.SIGV4 -> SignatureCalculator.SigV4
35+
AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> SignatureCalculator.SigV4a
36+
}
37+
3238
@OptIn(ExperimentalApi::class)
3339
internal class DefaultAwsSignerImpl(
3440
private val canonicalizer: Canonicalizer = Canonicalizer.Default,
35-
private val signatureCalculator: SignatureCalculator = SignatureCalculator.Default,
3641
private val requestMutator: RequestMutator = RequestMutator.Default,
3742
private val telemetryProvider: TelemetryProvider? = null,
3843
) : AwsSigner {
3944
override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult<HttpRequest> {
4045
val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner")
4146
?: coroutineContext.logger<DefaultAwsSignerImpl>()
4247

43-
// TODO: implement SigV4a
44-
if (config.algorithm != AwsSigningAlgorithm.SIGV4) {
45-
throw UnsupportedSigningAlgorithmException(
46-
"${config.algorithm} support is not yet implemented for the default signer.",
47-
config.algorithm,
48-
)
49-
}
50-
5148
val canonical = canonicalizer.canonicalRequest(request, config)
5249
if (config.logRequest) {
5350
logger.trace { "Canonical request:\n${canonical.requestString}" }
5451
}
5552

53+
val signatureCalculator = config.algorithm.signatureCalculator
54+
5655
val stringToSign = signatureCalculator.stringToSign(canonical.requestString, config)
5756
logger.trace { "String to sign:\n$stringToSign" }
5857

@@ -74,6 +73,8 @@ internal class DefaultAwsSignerImpl(
7473
val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner")
7574
?: coroutineContext.logger<DefaultAwsSignerImpl>()
7675

76+
val signatureCalculator = config.algorithm.signatureCalculator
77+
7778
val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config)
7879
logger.trace { "Chunk string to sign:\n$stringToSign" }
7980

@@ -93,6 +94,8 @@ internal class DefaultAwsSignerImpl(
9394
val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner")
9495
?: coroutineContext.logger<DefaultAwsSignerImpl>()
9596

97+
val signatureCalculator = config.algorithm.signatureCalculator
98+
9699
// FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient.
97100
// canonicalize the headers
98101
val trailingHeadersBytes = trailingHeaders.entries().sortedBy { e -> e.key.lowercase() }
@@ -117,16 +120,16 @@ internal class DefaultAwsSignerImpl(
117120
}
118121
}
119122

120-
/** The name of the SigV4 algorithm. */
121-
internal const val ALGORITHM_NAME = "AWS4-HMAC-SHA256"
122-
123123
/**
124-
* Formats a credential scope consisting of a signing date, region, service, and a signature type
124+
* Formats a credential scope consisting of a signing date, region (SigV4 only), service, and a signature type
125125
*/
126126
internal val AwsSigningConfig.credentialScope: String
127-
get() {
127+
get() = run {
128128
val signingDate = signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE)
129-
return "$signingDate/$region/$service/aws4_request"
129+
return when (algorithm) {
130+
AwsSigningAlgorithm.SIGV4 -> "$signingDate/$region/$service/aws4_request"
131+
AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> "$signingDate/$service/aws4_request"
132+
}
130133
}
131134

132135
/**

runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ internal class DefaultRequestMutator : RequestMutator {
4343
val credential = "Credential=${credentialValue(config)}"
4444
val signedHeaders = "SignedHeaders=${canonical.signedHeaders}"
4545
val signature = "Signature=$signatureHex"
46-
canonical.request.headers["Authorization"] = "$ALGORITHM_NAME $credential, $signedHeaders, $signature"
46+
canonical.request.headers["Authorization"] = "${config.algorithm.signingName} $credential, $signedHeaders, $signature"
4747
}
4848

4949
AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS ->
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.auth.awssigning
6+
7+
import aws.smithy.kotlin.runtime.hashing.HashSupplier
8+
import aws.smithy.kotlin.runtime.hashing.Sha256
9+
import aws.smithy.kotlin.runtime.hashing.hmac
10+
import aws.smithy.kotlin.runtime.text.encoding.encodeToHex
11+
import aws.smithy.kotlin.runtime.time.TimestampFormat
12+
13+
/**
14+
* [SignatureCalculator] for the SigV4 ("AWS4-HMAC-SHA256") algorithm
15+
* @param sha256Provider the [HashSupplier] to use for computing SHA-256 hashes
16+
*/
17+
internal class SigV4SignatureCalculator(override val sha256Provider: HashSupplier = ::Sha256) : BaseSigV4SignatureCalculator(AwsSigningAlgorithm.SIGV4, sha256Provider) {
18+
override fun calculate(signingKey: ByteArray, stringToSign: String): String =
19+
hmac(signingKey, stringToSign.encodeToByteArray(), sha256Provider).encodeToHex()
20+
21+
override fun signingKey(config: AwsSigningConfig): ByteArray {
22+
fun hmac(key: ByteArray, message: String) = hmac(key, message.encodeToByteArray(), sha256Provider)
23+
24+
val initialKey = ("AWS4" + config.credentials.secretAccessKey).encodeToByteArray()
25+
val kDate = hmac(initialKey, config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE))
26+
val kRegion = hmac(kDate, config.region)
27+
val kService = hmac(kRegion, config.service)
28+
return hmac(kService, "aws4_request")
29+
}
30+
}

0 commit comments

Comments
 (0)