Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
59c73d5
Commit latest changes
lauzadis Feb 13, 2025
fa7f73e
Commit latest changes
lauzadis Feb 14, 2025
071defd
SigV4a request working!
lauzadis Feb 17, 2025
f2bd711
SigV4a
lauzadis Feb 17, 2025
0623841
Merge branch 'main' of github.com:smithy-lang/smithy-kotlin into sigv4a
lauzadis Feb 18, 2025
5f9786c
Expand on comment
lauzadis Feb 18, 2025
52df61d
Commonize into `Sigv4xSignatureCalculator`
lauzadis Feb 18, 2025
00b62ee
optimize with nMinusTwo
lauzadis Feb 18, 2025
a6a2a78
ktlint
lauzadis Feb 18, 2025
dc51c46
Definitely working now.
lauzadis Feb 18, 2025
0c49144
(working) Refactor to separate files
lauzadis Feb 18, 2025
580925b
Ensure BigIntegers N and C are positive
lauzadis Feb 19, 2025
f2ae996
ktlint
lauzadis Feb 19, 2025
75b84af
Enable more SigV4a tests
lauzadis Feb 20, 2025
7303ac7
kt
lauzadis Feb 20, 2025
23364f7
Remove FIXME, signing region set config is already routed correctly
lauzadis Feb 20, 2025
7b81166
Add some docs
lauzadis Feb 20, 2025
a34b9b5
Refactor tests
lauzadis Feb 20, 2025
3e897b1
Refactor nMinusTwo to a const
lauzadis Feb 20, 2025
5d7de0c
changelog
lauzadis Feb 20, 2025
e5d6d3c
Deprecate UnsupportedSigningAlgorithmException
lauzadis Feb 20, 2025
b2afdf5
Fix a typo
lauzadis Feb 20, 2025
e14154c
Rename ecdsaSecp256r1
lauzadis Feb 20, 2025
4761322
Always prepend 0x00
lauzadis Feb 20, 2025
53cb567
Rename `BaseSigV4SignatureCalculator`
lauzadis Feb 20, 2025
9c69efe
Refactor to a single `supportedAlgorithms` list
lauzadis Feb 20, 2025
c1c4e7c
Add `signingName` to `AwsSigningAlgorithm` class
lauzadis Feb 20, 2025
d61e010
Refresh stale link
lauzadis Feb 20, 2025
e054ee1
Using signingName in fixedInputString
lauzadis Feb 20, 2025
c931684
apiDump
lauzadis Feb 20, 2025
29b34aa
ktlint
lauzadis Feb 20, 2025
08a7ef8
Add SigV4aSignatureCalculatorTest
lauzadis Feb 21, 2025
fb52307
Add a TODO
lauzadis Feb 21, 2025
41cf492
Remove duplicated test suite
lauzadis Feb 21, 2025
9e09404
Normalize CRLF
lauzadis Feb 24, 2025
0aa93f7
ktlint
lauzadis Feb 24, 2025
3023dc6
Unescaped CRLF
lauzadis Feb 24, 2025
ec832b1
Fix mixing of KDocs and TODOs/FIXMEs
lauzadis Feb 24, 2025
12a2c0d
Embed signingName as a constructor parameter
lauzadis Feb 24, 2025
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/4b6debe1-7706-484a-8599-ef8c14cecde2.json
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
Expand Up @@ -55,6 +55,7 @@ public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm
public static final field SIGV4 Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
public static final field SIGV4_ASYMMETRIC Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public final fun getSigningName ()Ljava/lang/String;
public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
public static fun values ()[Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ public typealias ShouldSignHeaderPredicate = (String) -> Boolean

/**
* Defines the AWS signature version to use
* @param signingName The name of this algorithm to use when signing requests.
*/
public enum class AwsSigningAlgorithm {
public enum class AwsSigningAlgorithm(public val signingName: String) {
/**
* AWS Signature Version 4
* see: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
*/
SIGV4,
SIGV4("AWS4-HMAC-SHA256"),

/**
* AWS Signature Version 4 Asymmetric
*/
SIGV4_ASYMMETRIC,
SIGV4_ASYMMETRIC("AWS4-ECDSA-P256-SHA256"),
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import aws.smithy.kotlin.runtime.InternalApi
* @param cause The cause of the exception
*/
@InternalApi
@Deprecated("This exception is no longer thrown. It will be removed in the next minor version, v1.5.x.")
public class UnsupportedSigningAlgorithmException(
message: String,
public val signingAlgorithm: AwsSigningAlgorithm,
Expand Down
1 change: 1 addition & 0 deletions runtime/auth/aws-signing-default/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ kotlin {
dependencies {
implementation(project(":runtime:auth:aws-signing-tests"))
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotlinx.serialization.json)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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 BaseSigV4SignatureCalculator(
val algorithm: AwsSigningAlgorithm,
open val sha256Provider: HashSupplier = ::Sha256,
) : SignatureCalculator {
private val supportedAlgorithms = setOf(AwsSigningAlgorithm.SIGV4, AwsSigningAlgorithm.SIGV4_ASYMMETRIC)

init {
check(algorithm in supportedAlgorithms) {
"This class should only be used for ${supportedAlgorithms.joinToString()}, 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())
}
}

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/smithy-lang/smithy-kotlin/blob/612c39ba446e6403ea2bd9a51c4d1db111b6e26f/runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/Header.kt#L52)
*/
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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,13 @@ internal class DefaultCanonicalizer(private val sha256Supplier: HashSupplier = :
}

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

val headers = builder
.headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,29 @@ public class DefaultAwsSignerBuilder {
)
}

private val AwsSigningAlgorithm.signatureCalculator
get() = when (this) {
AwsSigningAlgorithm.SIGV4 -> SignatureCalculator.SigV4
AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> SignatureCalculator.SigV4a
}

@OptIn(ExperimentalApi::class)
internal class DefaultAwsSignerImpl(
private val canonicalizer: Canonicalizer = Canonicalizer.Default,
private val signatureCalculator: SignatureCalculator = SignatureCalculator.Default,
private val requestMutator: RequestMutator = RequestMutator.Default,
private val telemetryProvider: TelemetryProvider? = null,
) : AwsSigner {
override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult<HttpRequest> {
val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner")
?: coroutineContext.logger<DefaultAwsSignerImpl>()

// TODO: implement SigV4a
if (config.algorithm != AwsSigningAlgorithm.SIGV4) {
throw UnsupportedSigningAlgorithmException(
"${config.algorithm} support is not yet implemented for the default signer.",
config.algorithm,
)
}

val canonical = canonicalizer.canonicalRequest(request, config)
if (config.logRequest) {
logger.trace { "Canonical request:\n${canonical.requestString}" }
}

val signatureCalculator = config.algorithm.signatureCalculator

val stringToSign = signatureCalculator.stringToSign(canonical.requestString, config)
logger.trace { "String to sign:\n$stringToSign" }

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

val signatureCalculator = config.algorithm.signatureCalculator

val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config)
logger.trace { "Chunk string to sign:\n$stringToSign" }

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

val signatureCalculator = config.algorithm.signatureCalculator

// FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient.
// canonicalize the headers
val trailingHeadersBytes = trailingHeaders.entries().sortedBy { e -> e.key.lowercase() }
Expand All @@ -117,16 +120,16 @@ internal class DefaultAwsSignerImpl(
}
}

/** The name of the SigV4 algorithm. */
internal const val ALGORITHM_NAME = "AWS4-HMAC-SHA256"

/**
* Formats a credential scope consisting of a signing date, region, service, and a signature type
* Formats a credential scope consisting of a signing date, region (SigV4 only), service, and a signature type
*/
internal val AwsSigningConfig.credentialScope: String
get() {
get() = run {
val signingDate = signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE)
return "$signingDate/$region/$service/aws4_request"
return when (algorithm) {
AwsSigningAlgorithm.SIGV4 -> "$signingDate/$region/$service/aws4_request"
AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> "$signingDate/$service/aws4_request"
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal class DefaultRequestMutator : RequestMutator {
val credential = "Credential=${credentialValue(config)}"
val signedHeaders = "SignedHeaders=${canonical.signedHeaders}"
val signature = "Signature=$signatureHex"
canonical.request.headers["Authorization"] = "$ALGORITHM_NAME $credential, $signedHeaders, $signature"
canonical.request.headers["Authorization"] = "${config.algorithm.signingName} $credential, $signedHeaders, $signature"
}

AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS ->
Expand Down
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) : BaseSigV4SignatureCalculator(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,88 @@
/*
* 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) : BaseSigV4SignatureCalculator(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) +
AwsSigningAlgorithm.SIGV4_ASYMMETRIC.signingName.encodeToByteArray() +
byteArrayOf(0x00) +
accessKeyId.encodeToByteArray() +
counter +
byteArrayOf(0x00, 0x00, 0x01, 0x00)
}

// Convert [this] [ByteArray] to a positive [BigInteger] by prepending 0x00.
private fun ByteArray.toPositiveBigInteger() = BigInteger(byteArrayOf(0x00) + this)
Loading
Loading