Skip to content

Commit 35b64a3

Browse files
authored
feat(rt): add support for HTTP_REQUEST_EVENT signature type (#670)
1 parent 704d358 commit 35b64a3

File tree

5 files changed

+93
-2
lines changed

5 files changed

+93
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "cd913c8a-f5a8-4bfe-9ab9-d08301c2338f",
3+
"type": "feature",
4+
"description": "Add support for HTTP_REQUEST_EVENT chunked signing"
5+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ data class AwsSigningResult<T>(
2121
* * `HTTP_REQUEST_VIA_QUERY_PARAMS` - hex encoding of the binary signature value
2222
* * `HTTP_REQUEST_CHUNK/SIGV4` - hex encoding of the binary signature value
2323
* * `HTTP_REQUEST_CHUNK/SIGV4_ASYMMETRIC` - '*'-padded hex encoding of the binary signature value
24-
* * `HTTP_REQUEST_EVENT` - binary signature value
24+
* * `HTTP_REQUEST_EVENT` - hex encoding of the binary signature value
2525
*/
2626
val signature: ByteArray,
2727
) {

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package aws.smithy.kotlin.runtime.auth.awssigning
66

77
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
88
import aws.smithy.kotlin.runtime.hashing.*
9+
import aws.smithy.kotlin.runtime.time.Instant
910
import aws.smithy.kotlin.runtime.time.TimestampFormat
11+
import aws.smithy.kotlin.runtime.time.epochMilliseconds
1012
import aws.smithy.kotlin.runtime.util.encodeToHex
1113

1214
/**
@@ -64,7 +66,13 @@ internal class DefaultSignatureCalculator(private val sha256Provider: HashSuppli
6466
appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED))
6567
appendLine(config.credentialScope)
6668
appendLine(prevSignature.decodeToString()) // Should already be a byte array of ASCII hex chars
67-
appendLine(HashSpecification.EmptyBody.hash)
69+
70+
val nonSignatureHeadersHash = when (config.signatureType) {
71+
AwsSignatureType.HTTP_REQUEST_EVENT -> eventStreamNonSignatureHeaders(config.signingDate)
72+
else -> HashSpecification.EmptyBody.hash
73+
}
74+
75+
appendLine(nonSignatureHeadersHash)
6876
append(chunkBody.hash(sha256Provider).encodeToHex())
6977
}
7078

@@ -86,3 +94,39 @@ internal class DefaultSignatureCalculator(private val sha256Provider: HashSuppli
8694
append(canonicalRequest.encodeToByteArray().hash(sha256Provider).encodeToHex())
8795
}
8896
}
97+
98+
private const val HEADER_TIMESTAMP_TYPE: Byte = 8
99+
100+
/**
101+
* Return the sha256 hex representation of the encoded event stream date header
102+
*
103+
* ```
104+
* sha256Hex( Header(":date", HeaderValue::Timestamp(date)).encodeToByteArray() )
105+
* ```
106+
*
107+
* NOTE: This duplicates parts of the event stream encoding implementation here to avoid a direct dependency.
108+
* Should this become more involved than encoding a single date header we should reconsider this choice.
109+
*
110+
* see [Event Stream implementation](https://github.com/awslabs/aws-sdk-kotlin/blob/v0.16.4-beta/aws-runtime/protocols/aws-event-stream/common/src/aws/sdk/kotlin/runtime/protocol/eventstream/Header.kt#L51)
111+
*/
112+
private fun eventStreamNonSignatureHeaders(date: Instant): String {
113+
val bytes = ByteArray(15)
114+
// encode header name
115+
val name = ":date".encodeToByteArray()
116+
var offset = 0
117+
bytes[offset++] = name.size.toByte()
118+
name.copyInto(bytes, destinationOffset = offset)
119+
offset += name.size
120+
121+
// encode header value
122+
bytes[offset++] = HEADER_TIMESTAMP_TYPE
123+
writeLongBE(bytes, offset, date.epochMilliseconds)
124+
return bytes.sha256().encodeToHex()
125+
}
126+
127+
private fun writeLongBE(dest: ByteArray, offset: Int, x: Long) {
128+
var idx = offset
129+
for (i in 7 downTo 0) {
130+
dest[idx++] = ((x ushr (i * 8)) and 0xff).toByte()
131+
}
132+
}

runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSignatureCalculatorTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.auth.awssigning
66

77
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
88
import aws.smithy.kotlin.runtime.auth.awssigning.tests.testCredentialsProvider
9+
import aws.smithy.kotlin.runtime.hashing.sha256
910
import aws.smithy.kotlin.runtime.time.Instant
1011
import aws.smithy.kotlin.runtime.util.decodeHexBytes
1112
import aws.smithy.kotlin.runtime.util.encodeToHex
@@ -80,4 +81,44 @@ class DefaultSignatureCalculatorTest {
8081
val actual = SignatureCalculator.Default.stringToSign(canonicalRequest, config)
8182
assertEquals(expected, actual)
8283
}
84+
85+
private data class ChunkStringToSignTest(val signatureType: AwsSignatureType, val expectedNonSignatureHeaderHash: String)
86+
@Test
87+
fun testChunkStringToSign() {
88+
// Test event stream signing
89+
// https://docs.aws.amazon.com/transcribe/latest/dg/streaming-http2.html
90+
// Adapted from: https://github.com/awslabs/smithy-rs/blob/v0.38.0/aws/rust-runtime/aws-sigv4/src/event_stream.rs#L166
91+
val tests = listOf(
92+
ChunkStringToSignTest(AwsSignatureType.HTTP_REQUEST_CHUNK, HashSpecification.EmptyBody.hash),
93+
ChunkStringToSignTest(
94+
AwsSignatureType.HTTP_REQUEST_EVENT,
95+
"0c0e3b3bf66b59b976181bd7d401927bbd624107303c713fd1e5f3d3c8dd1b1e"
96+
)
97+
)
98+
99+
val epoch = Instant.fromEpochSeconds(123_456_789L, 1234)
100+
val prevSignature = "last message sts".encodeToByteArray().sha256().encodeToHex().encodeToByteArray()
101+
val chunkBody = "test payload".encodeToByteArray()
102+
103+
for (test in tests) {
104+
val config = AwsSigningConfig {
105+
credentialsProvider = testCredentialsProvider
106+
signingDate = epoch
107+
region = "us-east-1"
108+
service = "testservice"
109+
signatureType = test.signatureType
110+
}
111+
val expected = """
112+
AWS4-HMAC-SHA256-PAYLOAD
113+
19731129T213309Z
114+
19731129/us-east-1/testservice/aws4_request
115+
be1f8c7d79ef8e1abc5254a2c70e4da3bfaf4f07328f527444e1fc6ea67273e2
116+
${test.expectedNonSignatureHeaderHash}
117+
813ca5285c28ccee5cab8b10ebda9c908fd6d78ed9dc94cc65ea6cb67a7f13ae
118+
""".trimIndent()
119+
120+
val actual = SignatureCalculator.Default.chunkStringToSign(chunkBody, prevSignature, config)
121+
assertEquals(expected, actual)
122+
}
123+
}
83124
}

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ object RuntimeTypes {
199199
val AwsSigner = runtimeSymbol("AwsSigner", KotlinDependency.AWS_SIGNING_COMMON)
200200
val AwsSigningAttributes = runtimeSymbol("AwsSigningAttributes", KotlinDependency.AWS_SIGNING_COMMON)
201201
val AwsSigningMiddleware = runtimeSymbol("AwsSigningMiddleware", KotlinDependency.AWS_SIGNING_COMMON, "middleware")
202+
val HashSpecification = runtimeSymbol("HashSpecification", KotlinDependency.AWS_SIGNING_COMMON)
202203
val createPresignedRequest = runtimeSymbol("createPresignedRequest", KotlinDependency.AWS_SIGNING_COMMON)
203204
val PresignedRequestConfig = runtimeSymbol("PresignedRequestConfig", KotlinDependency.AWS_SIGNING_COMMON)
204205
val PresigningLocation = runtimeSymbol("PresigningLocation", KotlinDependency.AWS_SIGNING_COMMON)

0 commit comments

Comments
 (0)