Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ kotlin {
val jvmTest by getting {
dependencies {
val junitVersion: String by project
api("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
api("org.jetbrains.kotlin:kotlin-test-junit5:$kotlinVersion")
implementation("org.junit.jupiter:junit-jupiter:$junitVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:$coroutinesVersion")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ kotestVersion=4.6.2
kotlinxCliVersion=0.3.2

# JVM
crtJavaVersion=0.14.2
crtJavaVersion=0.14.7
2 changes: 2 additions & 0 deletions src/common/src/aws/sdk/kotlin/crt/auth/signing/AwsSigner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ import aws.sdk.kotlin.crt.http.HttpRequest

public expect object AwsSigner {
public suspend fun signRequest(request: HttpRequest, config: AwsSigningConfig): HttpRequest

public suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import aws.sdk.kotlin.crt.auth.credentials.Credentials
import aws.sdk.kotlin.crt.auth.credentials.CredentialsProvider

public enum class AwsSigningAlgorithm(public val value: Int) {
SIGV4(0);
SIGV4(0),
SIGV4_ASYMMETRIC(1);
}

public enum class AwsSignatureType(public val value: Int) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package aws.sdk.kotlin.crt.auth.signing

import aws.sdk.kotlin.crt.http.HttpRequest

/**
* Wrapper that holds signing-related output. Depending on the signing configuration, not all members may be
* assigned and some members, like signature, may have a variable format.
*/
public data class AwsSigningResult(
/**
* The signed HTTP request from the result, may be null if an http request was not signed
*/
val signedRequest: HttpRequest?,

/**
* Gets the signature value from the result. Depending on the requested signature type and algorithm, this value
* will be in one of the following formats:
*
* (1) HTTP_REQUEST_VIA_HEADERS - hex encoding of the binary signature value
* (2) HTTP_REQUEST_VIA_QUERY_PARAMS - hex encoding of the binary signature value
* (3) HTTP_REQUEST_CHUNK/SIGV4 - hex encoding of the binary signature value
* (4) HTTP_REQUEST_CHUNK/SIGV4_ASYMMETRIC - '*'-padded hex encoding of the binary signature value
* (5) HTTP_REQUEST_EVENT - binary signature value (NYI)
*
* @return the signature value from the signing process
*/
val signature: ByteArray
) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as AwsSigningResult

if (signedRequest != other.signedRequest) return false
if (!signature.contentEquals(other.signature)) return false

return true
}

override fun hashCode(): Int {
var result = signedRequest?.hashCode() ?: 0
result = 31 * result + signature.contentHashCode()
return result
}
Comment on lines +30 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why are these custom equals/hashCode implementations necessary? They seem identical to what I'd expect from the default implementations in a data class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ yup that's why

}
30 changes: 29 additions & 1 deletion src/common/test/aws/sdk/kotlin/crt/auth/signing/SigningTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ class SigningTest : CrtTest() {
signedRequest.headers.contains(
"Authorization",
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11"
)
),
"sigv4 authorization not equal: " + signedRequest.headers["Authorization"]
)
}
}
Expand Down Expand Up @@ -165,4 +166,31 @@ class SigningTest : CrtTest() {
assertEquals("AWS_AUTH_SIGNING_ILLEGAL_REQUEST_HEADER", ex.errorName)
}
}

@Test
fun testSigningSigV4Asymmetric() = runSuspendTest {
StaticCredentialsProvider.build {
accessKeyId = TEST_ACCESS_KEY_ID
secretAccessKey = TEST_SECRET_ACCESS_KEY
}.use { provider ->
val request = createSigV4TestSuiteRequest()
val signingConfig = AwsSigningConfig.build {
algorithm = AwsSigningAlgorithm.SIGV4_ASYMMETRIC
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_HEADERS
region = "us-east-1"
service = "service"
date = TEST_DATE_EPOCH_MILLI
credentialsProvider = provider
useDoubleUriEncode = true
normalizeUriPath = true
signedBodyValue = AwsSignedBodyValue.EMPTY_SHA256
expirationInSeconds = 60
}

val signedRequest = AwsSigner.signRequest(request, signingConfig)
assertTrue(signedRequest.headers.contains("X-Amz-Date", "20150830T123600Z"), "${signedRequest.headers}")
val prefix = "AWS4-ECDSA-P256-SHA256 Credential=AKIDEXAMPLE/20150830/service/aws4_request, SignedHeaders=host;x-amz-date;x-amz-region-set, Signature="
assertTrue(signedRequest.headers["Authorization"]!!.contains(prefix), signedRequest.headers["Authorization"])
}
}
}
28 changes: 24 additions & 4 deletions src/jvm/src/aws/sdk/kotlin/crt/auth/signing/AwsSignerJVM.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,35 @@ import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignatureType
import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignedBodyHeaderType as AwsSignedBodyHeaderTypeJni
import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSigningAlgorithm as AwsSigningAlgorithmJni

/**
* Static class for a variety of AWS signing APIs.
*/
public actual object AwsSigner {
public actual suspend fun signRequest(request: HttpRequest, config: AwsSigningConfig): HttpRequest {

/**
* Signs an http request according to the supplied signing configuration
* @param request http request to sign
* @param config signing configuration
* @return signed request
*/
public actual suspend fun signRequest(request: HttpRequest, config: AwsSigningConfig): HttpRequest =
checkNotNull(sign(request, config).signedRequest) { "AwsSigningResult request must not be null" }
Comment on lines +33 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: What circumstances could lead to the signing result's request being null? Looking through the code I can't see how that would occur.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the comments from crt-java:
https://github.com/awslabs/aws-crt-java/blob/main/src/main/java/software/amazon/awssdk/crt/auth/signing/AwsSigningResult.java#L15-L19

I think it's due to the flexibility in the signing implementation but in practice it shouldn't be null the way we use it. Or at least that's my understanding.


/**
* Signs an http request according to the supplied signing configuration
* @param request http request to sign
* @param config signing configuration
* @return signing result, which provides access to all signing-related result properties
*/
public actual suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult {
// FIXME - this would be a good area where talking directly to JNI would be convenient so we don't have to
// do [KotlinHttpReq -> CrtJava -> Native] and back
val jniReq = request.into()
return asyncCrtJniCall {
val reqFuture = AwsSignerJni.signRequest(jniReq, config.into())
val signedJniReq = reqFuture.await()
HttpRequest.from(signedJniReq)
val reqFuture = AwsSignerJni.sign(jniReq, config.into())
val jniResult = reqFuture.await()
val signedRequest = HttpRequest.from(jniResult.signedRequest)
AwsSigningResult(signedRequest, jniResult.signature)
}
}
}
Expand Down