-
Notifications
You must be signed in to change notification settings - Fork 30
feat: Kotlin implementation of SigV4a signing #1246
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
Changes from 22 commits
59c73d5
fa7f73e
071defd
f2bd711
0623841
5f9786c
52df61d
00b62ee
a6a2a78
dc51c46
0c49144
580925b
f2ae996
75b84af
7303ac7
23364f7
7b81166
a34b9b5
3e897b1
5d7de0c
e5d6d3c
b2afdf5
e14154c
4761322
53cb567
9c69efe
c1c4e7c
d61e010
e054ee1
c931684
29b34aa
08a7ef8
fb52307
41cf492
9e09404
0aa93f7
3023dc6
ec832b1
12a2c0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "id": "4b6debe1-7706-484a-8599-ef8c14cecde2", | ||
| "type": "feature", | ||
| "description": "Add SigV4a support to the default AWS signer" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package aws.smithy.kotlin.runtime.auth.awssigning | ||
|
|
||
| import aws.smithy.kotlin.runtime.hashing.HashSupplier | ||
| import aws.smithy.kotlin.runtime.hashing.Sha256 | ||
| import aws.smithy.kotlin.runtime.hashing.hmac | ||
| import aws.smithy.kotlin.runtime.text.encoding.encodeToHex | ||
| import aws.smithy.kotlin.runtime.time.TimestampFormat | ||
|
|
||
| /** | ||
| * [SignatureCalculator] for the SigV4 ("AWS4-HMAC-SHA256") algorithm | ||
| * @param sha256Provider the [HashSupplier] to use for computing SHA-256 hashes | ||
| */ | ||
| internal class SigV4SignatureCalculator(override val sha256Provider: HashSupplier = ::Sha256) : SigV4xSignatureCalculator(AwsSigningAlgorithm.SIGV4, sha256Provider) { | ||
| override fun calculate(signingKey: ByteArray, stringToSign: String): String = | ||
| hmac(signingKey, stringToSign.encodeToByteArray(), sha256Provider).encodeToHex() | ||
|
|
||
| override fun signingKey(config: AwsSigningConfig): ByteArray { | ||
| fun hmac(key: ByteArray, message: String) = hmac(key, message.encodeToByteArray(), sha256Provider) | ||
|
|
||
| val initialKey = ("AWS4" + config.credentials.secretAccessKey).encodeToByteArray() | ||
| val kDate = hmac(initialKey, config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE)) | ||
| val kRegion = hmac(kDate, config.region) | ||
| val kService = hmac(kRegion, config.service) | ||
| return hmac(kService, "aws4_request") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package aws.smithy.kotlin.runtime.auth.awssigning | ||
|
|
||
| import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials | ||
| import aws.smithy.kotlin.runtime.collections.ReadThroughCache | ||
| import aws.smithy.kotlin.runtime.content.BigInteger | ||
| import aws.smithy.kotlin.runtime.hashing.HashSupplier | ||
| import aws.smithy.kotlin.runtime.hashing.Sha256 | ||
| import aws.smithy.kotlin.runtime.hashing.ecdsasecp256r1 | ||
| import aws.smithy.kotlin.runtime.hashing.hmac | ||
| import aws.smithy.kotlin.runtime.text.encoding.decodeHexBytes | ||
| import aws.smithy.kotlin.runtime.text.encoding.encodeToHex | ||
| import aws.smithy.kotlin.runtime.time.Instant | ||
| import aws.smithy.kotlin.runtime.util.ExpiringValue | ||
| import kotlinx.coroutines.runBlocking | ||
| import kotlin.time.Duration.Companion.hours | ||
|
|
||
| /** | ||
| * The maximum number of iterations to attempt private key derivation using KDF in counter mode | ||
| * Taken from CRT: https://github.com/awslabs/aws-c-auth/blob/e8360a65e0f3337d4ac827945e00c3b55a641a5f/source/key_derivation.c#L22 | ||
| */ | ||
| internal const val MAX_KDF_COUNTER_ITERATIONS = 254.toByte() | ||
|
|
||
| // N value from NIST P-256 curve, minus two. | ||
| internal val N_MINUS_TWO = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254F".decodeHexBytes().toPositiveBigInteger() | ||
|
|
||
| /** | ||
| * A [SignatureCalculator] for the SigV4a ("AWS4-ECDSA-P256-SHA256") algorithm. | ||
| * @param sha256Provider the [HashSupplier] to use for computing SHA-256 hashes | ||
| */ | ||
| internal class SigV4aSignatureCalculator(override val sha256Provider: HashSupplier = ::Sha256) : SigV4xSignatureCalculator(AwsSigningAlgorithm.SIGV4_ASYMMETRIC, sha256Provider) { | ||
| private val privateKeyCache = ReadThroughCache<Credentials, ByteArray>( | ||
| minimumSweepPeriod = 1.hours, // note: Sweeps are effectively a no-op because expiration is [Instant.MAX_VALUE] | ||
| ) | ||
|
|
||
| override fun calculate(signingKey: ByteArray, stringToSign: String): String = ecdsasecp256r1(signingKey, stringToSign.encodeToByteArray()).encodeToHex() | ||
|
|
||
| /** | ||
| * Retrieve a signing key based on the signing credentials. If not cached, the key will be derived using a counter-based key derivation function (KDF) | ||
| * as specified in NIST SP 800-108. | ||
| * | ||
| * See https://github.com/awslabs/aws-c-auth/blob/e8360a65e0f3337d4ac827945e00c3b55a641a5f/source/key_derivation.c#L70 and | ||
| * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#derive-signing-key-sigv4a for | ||
| * more information on the derivation process. | ||
| */ | ||
| override fun signingKey(config: AwsSigningConfig): ByteArray = runBlocking { | ||
| privateKeyCache.get(config.credentials) { | ||
| var counter: Byte = 1 | ||
| var privateKey: ByteArray | ||
|
|
||
| val inputKey = ("AWS4A" + config.credentials.secretAccessKey).encodeToByteArray() | ||
|
|
||
| do { | ||
| val k0 = hmac(inputKey, fixedInputString(config.credentials.accessKeyId, counter), sha256Provider) | ||
|
|
||
| val c = k0.toPositiveBigInteger() | ||
| privateKey = (c + BigInteger("1")).toByteArray() | ||
|
|
||
| if (counter == MAX_KDF_COUNTER_ITERATIONS && c > N_MINUS_TWO) { | ||
| throw IllegalStateException("Counter exceeded maximum length") | ||
| } else { | ||
| counter++ | ||
| } | ||
| } while (c > N_MINUS_TWO) | ||
|
|
||
| ExpiringValue<ByteArray>(privateKey, Instant.MAX_VALUE) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Forms the fixed input string used for ECDSA private key derivation | ||
| * The final output looks like: | ||
| * 0x00000001 || "AWS4-ECDSA-P256-SHA256" || 0x00 || AccessKeyId || counter || 0x00000100 | ||
| */ | ||
| private fun fixedInputString(accessKeyId: String, counter: Byte): ByteArray = | ||
| byteArrayOf(0x00, 0x00, 0x00, 0x01) + | ||
| "AWS4-ECDSA-P256-SHA256".encodeToByteArray() + | ||
| byteArrayOf(0x00) + | ||
| accessKeyId.encodeToByteArray() + | ||
| counter + | ||
| byteArrayOf(0x00, 0x00, 0x01, 0x00) | ||
| } | ||
|
|
||
| // Convert [this] [ByteArray] to a positive [BigInteger] | ||
| private fun ByteArray.toPositiveBigInteger(): BigInteger = if (isNotEmpty() && (get(0).toInt() and 0x80) != 0) { | ||
| BigInteger(byteArrayOf(0x00) + this) // Prepend 0x00 to ensure positive value | ||
| } else { | ||
| BigInteger(this) | ||
| } | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package aws.smithy.kotlin.runtime.auth.awssigning | ||
|
|
||
| import aws.smithy.kotlin.runtime.hashing.HashSupplier | ||
| import aws.smithy.kotlin.runtime.hashing.Sha256 | ||
| import aws.smithy.kotlin.runtime.hashing.hash | ||
| import aws.smithy.kotlin.runtime.hashing.sha256 | ||
| import aws.smithy.kotlin.runtime.text.encoding.encodeToHex | ||
| import aws.smithy.kotlin.runtime.time.Instant | ||
| import aws.smithy.kotlin.runtime.time.TimestampFormat | ||
| import aws.smithy.kotlin.runtime.time.epochMilliseconds | ||
|
|
||
| /** | ||
| * Common signature implementation used for SigV4 and SigV4a, primarily for forming the strings-to-sign which don't differ | ||
| * between the two signing algorithms (besides their names). | ||
| */ | ||
| internal abstract class SigV4xSignatureCalculator( | ||
|
||
| val algorithm: AwsSigningAlgorithm, | ||
| open val sha256Provider: HashSupplier = ::Sha256, | ||
| ) : SignatureCalculator { | ||
| init { | ||
| check(algorithm == AwsSigningAlgorithm.SIGV4 || algorithm == AwsSigningAlgorithm.SIGV4_ASYMMETRIC) { | ||
| "This class should only be used for the ${AwsSigningAlgorithm.SIGV4} or ${AwsSigningAlgorithm.SIGV4_ASYMMETRIC} algorithms, got $algorithm" | ||
| } | ||
|
||
| } | ||
|
|
||
| override fun stringToSign(canonicalRequest: String, config: AwsSigningConfig): String = buildString { | ||
| appendLine(algorithm.signingName) | ||
| appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) | ||
| appendLine(config.credentialScope) | ||
| append(canonicalRequest.encodeToByteArray().hash(sha256Provider).encodeToHex()) | ||
| } | ||
|
|
||
| override fun chunkStringToSign(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String = buildString { | ||
| appendLine("${algorithm.signingName}-PAYLOAD") | ||
| appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) | ||
| appendLine(config.credentialScope) | ||
| appendLine(prevSignature.decodeToString()) // Should already be a byte array of ASCII hex chars | ||
|
|
||
| val nonSignatureHeadersHash = when (config.signatureType) { | ||
| AwsSignatureType.HTTP_REQUEST_EVENT -> eventStreamNonSignatureHeaders(config.signingDate) | ||
| else -> HashSpecification.EmptyBody.hash | ||
| } | ||
|
|
||
| appendLine(nonSignatureHeadersHash) | ||
| append(chunkBody.hash(sha256Provider).encodeToHex()) | ||
| } | ||
|
|
||
| override fun chunkTrailerStringToSign(trailingHeaders: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String = | ||
| buildString { | ||
| appendLine("${algorithm.signingName}-TRAILER") | ||
| appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) | ||
| appendLine(config.credentialScope) | ||
| appendLine(prevSignature.decodeToString()) | ||
| append(trailingHeaders.hash(sha256Provider).encodeToHex()) | ||
| } | ||
| } | ||
|
|
||
| internal val AwsSigningAlgorithm.signingName: String | ||
| get() = when (this) { | ||
| AwsSigningAlgorithm.SIGV4 -> "AWS4-HMAC-SHA256" | ||
| AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> "AWS4-ECDSA-P256-SHA256" | ||
| } | ||
|
||
|
|
||
| private const val HEADER_TIMESTAMP_TYPE: Byte = 8 | ||
|
|
||
| /** | ||
| * Return the sha256 hex representation of the encoded event stream date header | ||
| * | ||
| * ``` | ||
| * sha256Hex( Header(":date", HeaderValue::Timestamp(date)).encodeToByteArray() ) | ||
| * ``` | ||
| * | ||
| * NOTE: This duplicates parts of the event stream encoding implementation here to avoid a direct dependency. | ||
| * Should this become more involved than encoding a single date header we should reconsider this choice. | ||
| * | ||
| * 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) | ||
|
||
| */ | ||
| private fun eventStreamNonSignatureHeaders(date: Instant): String { | ||
| val bytes = ByteArray(15) | ||
| // encode header name | ||
| val name = ":date".encodeToByteArray() | ||
| var offset = 0 | ||
| bytes[offset++] = name.size.toByte() | ||
| name.copyInto(bytes, destinationOffset = offset) | ||
| offset += name.size | ||
|
|
||
| // encode header value | ||
| bytes[offset++] = HEADER_TIMESTAMP_TYPE | ||
| writeLongBE(bytes, offset, date.epochMilliseconds) | ||
| return bytes.sha256().encodeToHex() | ||
| } | ||
|
|
||
| private fun writeLongBE(dest: ByteArray, offset: Int, x: Long) { | ||
| var idx = offset | ||
| for (i in 7 downTo 0) { | ||
| dest[idx++] = ((x ushr (i * 8)) and 0xff).toByte() | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Could reuse
AwsSigningAlgorithm.SIGV4_ASYMMETRIC.signingName