Skip to content

Commit fa7f73e

Browse files
committed
Commit latest changes
1 parent 59c73d5 commit fa7f73e

File tree

7 files changed

+278
-18
lines changed

7 files changed

+278
-18
lines changed

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

Lines changed: 12 additions & 9 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.SigV4,
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() }

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

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*/
55
package aws.smithy.kotlin.runtime.auth.awssigning
66

7+
import aws.smithy.kotlin.runtime.content.BigInteger
78
import aws.smithy.kotlin.runtime.hashing.*
9+
import aws.smithy.kotlin.runtime.text.encoding.decodeHexBytes
810
import aws.smithy.kotlin.runtime.text.encoding.encodeToHex
911
import aws.smithy.kotlin.runtime.time.Instant
1012
import aws.smithy.kotlin.runtime.time.TimestampFormat
@@ -16,9 +18,14 @@ import aws.smithy.kotlin.runtime.time.epochMilliseconds
1618
internal interface SignatureCalculator {
1719
companion object {
1820
/**
19-
* The default implementation of [SignatureCalculator].
21+
* The SigV4 implementation of [SignatureCalculator].
2022
*/
21-
val Default = DefaultSignatureCalculator()
23+
val SigV4 = SigV4SignatureCalculator()
24+
25+
/**
26+
* The SigV4a implementation of [SignatureCalculator].
27+
*/
28+
val SigV4a = SigV4aSignatureCalculator()
2229
}
2330

2431
/**
@@ -63,7 +70,7 @@ internal interface SignatureCalculator {
6370
fun chunkTrailerStringToSign(trailingHeaders: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String
6471
}
6572

66-
internal class DefaultSignatureCalculator(private val sha256Provider: HashSupplier = ::Sha256) : SignatureCalculator {
73+
internal class SigV4SignatureCalculator(private val sha256Provider: HashSupplier = ::Sha256) : SignatureCalculator {
6774
override fun calculate(signingKey: ByteArray, stringToSign: String): String =
6875
hmac(signingKey, stringToSign.encodeToByteArray(), sha256Provider).encodeToHex()
6976

@@ -111,13 +118,12 @@ internal class DefaultSignatureCalculator(private val sha256Provider: HashSuppli
111118
}
112119
}
113120

114-
// FIXME Copied a lot of functions from SigV4SignatureCalculator, refactor to share
121+
// FIXME Copied a few functions from SigV4SignatureCalculator, refactor to share
115122
internal class SigV4aSignatureCalculator(
116123
val sha256Provider: HashSupplier = ::Sha256
117124
): SignatureCalculator {
118-
override fun calculate(signingKey: ByteArray, stringToSign: String): String {
119-
ecdsasecp256r1(signingKey, stringToSign.encodeToByteArray(), sha256Provider).encodeToHex()
120-
}
125+
override fun calculate(signingKey: ByteArray, stringToSign: String): String =
126+
ecdsasecp256r1(signingKey, stringToSign.encodeToByteArray()).encodeToHex()
121127

122128
override fun stringToSign(
123129
canonicalRequest: String,
@@ -129,9 +135,37 @@ internal class SigV4aSignatureCalculator(
129135
append(canonicalRequest.encodeToByteArray().hash(sha256Provider).encodeToHex())
130136
}
131137

132-
// FIXME SigV4a doesn't need a signingKey
133138
override fun signingKey(config: AwsSigningConfig): ByteArray {
134-
return byteArrayOf()
139+
// 1.1: Compute fixed input string
140+
val label = "AWS4-ECDSA-P256-SHA256".encodeToByteArray()
141+
var counter: Byte = 0x01
142+
var privateKey: ByteArray
143+
144+
do {
145+
val context = config.credentials.accessKeyId.encodeToByteArray() + byteArrayOf(counter)
146+
val length = byteArrayOf(0x01, 0x00) // "256"
147+
val fixedInputString: ByteArray = label + byteArrayOf(0x00) + context + length
148+
149+
// 1.2: Compute K0
150+
val inputKey = ("AWS4A" + config.credentials.secretAccessKey).encodeToByteArray()
151+
val k0 = hmac(inputKey, byteArrayOf(0x01) + fixedInputString, sha256Provider)
152+
153+
// 2: Compute the ECC key pair
154+
val c = BigInteger(k0)
155+
156+
val nBytes = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551".decodeHexBytes()
157+
val n = BigInteger(nBytes)
158+
159+
privateKey = (n + BigInteger("1")).toByteArray()
160+
161+
if (counter == Byte.MAX_VALUE && c <= n - BigInteger("2")) {
162+
throw IllegalStateException("Counter exceeded maximum length")
163+
}
164+
165+
counter = (counter + 0x01).toByte()
166+
} while (c <= n - BigInteger("2"))
167+
168+
return privateKey
135169
}
136170

137171
override fun chunkStringToSign(

runtime/runtime-core/api/runtime-core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,10 @@ public final class aws/smithy/kotlin/runtime/hashing/Crc32cKt {
693693
public static final fun crc32c ([B)I
694694
}
695695

696+
public final class aws/smithy/kotlin/runtime/hashing/EcdsaJVMKt {
697+
public static final fun ecdsasecp256r1 ([B[B)[B
698+
}
699+
696700
public abstract interface class aws/smithy/kotlin/runtime/hashing/HashFunction {
697701
public abstract fun digest ()[B
698702
public abstract fun getBlockSizeBytes ()I
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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.hashing
6+
7+
/**
8+
* ECDSA on the SECP256R1 curve.
9+
*/
10+
public expect fun ecdsasecp256r1(key: ByteArray, message: ByteArray): ByteArray
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.hashing
6+
7+
import aws.smithy.kotlin.runtime.content.BigInteger
8+
import java.security.*
9+
import java.security.spec.*
10+
import java.security.interfaces.*
11+
12+
/**
13+
* ECDSA on the SECP256R1 curve.
14+
*/
15+
public actual fun ecdsasecp256r1(key: ByteArray, message: ByteArray): ByteArray {
16+
// Convert byte array to BigInteger for private key
17+
val d = BigInteger(key)
18+
19+
// Get EC parameters for SECP256R1
20+
val spec = ECGenParameterSpec("secp256r1")
21+
22+
// Create key pair generator to get curve parameters
23+
val keyGen = KeyPairGenerator.getInstance("EC")
24+
keyGen.initialize(spec)
25+
val params = (keyGen.generateKeyPair().private as ECPrivateKey).params
26+
27+
// Create private key directly from the provided key bytes
28+
val privateKeySpec = ECPrivateKeySpec(d.toJvm(), params)
29+
val keyFactory = KeyFactory.getInstance("EC")
30+
val privateKey = keyFactory.generatePrivate(privateKeySpec)
31+
32+
// Sign the message
33+
return Signature.getInstance("SHA256withECDSA").apply {
34+
initSign(privateKey)
35+
update(message)
36+
}.sign()
37+
}
38+
39+
//public actual fun ecdsasecp256r1(key: ByteArray, message: ByteArray): ByteArray {
40+
// val d = BigInteger(key)
41+
//
42+
// // Get EC curve parameters for SECP256R1
43+
// val spec = ECGenParameterSpec("secp256r1")
44+
// val keyFactory = KeyFactory.getInstance("EC")
45+
//
46+
// // Generate ECPrivateKeySpec
47+
// val keyPairGenerator = KeyPairGenerator.getInstance("EC")
48+
// keyPairGenerator.initialize(spec)
49+
// val keyPair = keyPairGenerator.generateKeyPair()
50+
// val ecParams = (keyPair.private as ECPrivateKey).params
51+
//
52+
// // Compute the public key point P = d * G
53+
// val G = ecParams.generator
54+
// val publicPoint = ecParams.curve.multiply(G, d)
55+
//
56+
// // Create EC public key
57+
// val pubKeySpec = ECPublicKeySpec(publicPoint, ecParams)
58+
// val publicKey = keyFactory.generatePublic(pubKeySpec) as ECPublicKey
59+
//
60+
// // Create EC private key
61+
// val privateKeySpec = ECPrivateKeySpec(d, ecParams)
62+
// val privateKey = keyFactory.generatePrivate(privateKeySpec) as ECPrivateKey
63+
//
64+
// // Sign the message
65+
// return Signature.getInstance("SHA256withECDSA").apply {
66+
// initSign(privateKey)
67+
// update(message)
68+
// }.sign()
69+
//}
70+
71+
///**
72+
// * ECDSA on the SECP256R1 curve.
73+
// */
74+
//public actual fun ecdsasecp256r1(key: ByteArray, message: ByteArray): ByteArray {
75+
// val d = BigInteger(key)
76+
//
77+
// // Get SECP256R1 parameters from Java's built-in provider
78+
// val params = AlgorithmParameters.getInstance("EC").apply {
79+
// init(ECGenParameterSpec("secp256r1"))
80+
// }
81+
// val ecSpec = params.getParameterSpec(ECParameterSpec::class.java)
82+
//
83+
// // Create private key spec and generate key
84+
// val keySpec = ECPrivateKeySpec(d.toJvm(), ecSpec)
85+
// val kf = KeyFactory.getInstance("EC")
86+
// val privateKey = kf.generatePrivate(keySpec)
87+
//
88+
// // Sign the message
89+
// return Signature.getInstance("SHA256withECDSA").apply {
90+
// initSign(privateKey)
91+
// update(message)
92+
// }.sign()
93+
//}
94+
95+
private fun BigInteger.toJvm(): java.math.BigInteger = java.math.BigInteger(1, toByteArray())
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.hashing
6+
7+
import org.junit.jupiter.api.Assertions.assertFalse
8+
import org.junit.jupiter.api.Assertions.assertTrue
9+
import java.security.*
10+
import java.security.spec.*
11+
import java.security.interfaces.*
12+
import kotlin.test.Test
13+
import kotlin.test.assertNotNull
14+
15+
class EcdsaJVMTest {
16+
// Helper function to generate valid test key
17+
private fun generateValidPrivateKey(): ByteArray {
18+
val keyGen = KeyPairGenerator.getInstance("EC")
19+
keyGen.initialize(ECGenParameterSpec("secp256r1"))
20+
val keyPair = keyGen.generateKeyPair()
21+
val privateKey = keyPair.private as ECPrivateKey
22+
return privateKey.s.toByteArray()
23+
}
24+
25+
@Test
26+
fun testValidSignature() {
27+
val privateKey = generateValidPrivateKey()
28+
val message = "Hello, World!".toByteArray()
29+
30+
val signature = ecdsasecp256r1(privateKey, message)
31+
32+
assertNotNull(signature)
33+
assertTrue(signature.size >= 64) // ECDSA signatures are typically 70-72 bytes in DER format
34+
}
35+
36+
@Test
37+
fun testSignatureDeterminism() {
38+
val privateKey = generateValidPrivateKey()
39+
val message = "Hello, World!".toByteArray()
40+
41+
val signature1 = ecdsasecp256r1(privateKey, message)
42+
val signature2 = ecdsasecp256r1(privateKey, message)
43+
44+
// Note: ECDSA is not deterministic by default, so signatures will be different
45+
assertNotNull(signature1)
46+
assertNotNull(signature2)
47+
}
48+
49+
@Test
50+
fun testDifferentMessages() {
51+
val privateKey = generateValidPrivateKey()
52+
val message1 = "Hello, World!".toByteArray()
53+
val message2 = "Different message".toByteArray()
54+
55+
val signature1 = ecdsasecp256r1(privateKey, message1)
56+
val signature2 = ecdsasecp256r1(privateKey, message2)
57+
58+
assertNotNull(signature1)
59+
assertNotNull(signature2)
60+
assertFalse(signature1.contentEquals(signature2))
61+
}
62+
63+
@Test
64+
fun testEmptyMessage() {
65+
val privateKey = generateValidPrivateKey()
66+
val message = ByteArray(0)
67+
68+
val signature = ecdsasecp256r1(privateKey, message)
69+
assertNotNull(signature)
70+
}
71+
72+
@Test
73+
fun testLargeMessage() {
74+
val privateKey = generateValidPrivateKey()
75+
val largeMessage = ByteArray(1000000) { it.toByte() }
76+
77+
val signature = ecdsasecp256r1(privateKey, largeMessage)
78+
assertNotNull(signature)
79+
}
80+
81+
@Test
82+
fun testVerifySignature() {
83+
val keyGen = KeyPairGenerator.getInstance("EC")
84+
keyGen.initialize(ECGenParameterSpec("secp256r1"))
85+
val keyPair = keyGen.generateKeyPair()
86+
val privateKey = (keyPair.private as ECPrivateKey).s.toByteArray()
87+
val publicKey = keyPair.public
88+
89+
val message = "Hello, World!".toByteArray()
90+
val signature = ecdsasecp256r1(privateKey, message)
91+
92+
val verifier = Signature.getInstance("SHA256withECDSA")
93+
verifier.initVerify(publicKey)
94+
verifier.update(message)
95+
96+
assertTrue(verifier.verify(signature))
97+
}
98+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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.hashing
6+
7+
/**
8+
* ECDSA on the SECP256R1 curve.
9+
*/
10+
// FIXME Implement using aws-c-cal: https://github.com/awslabs/aws-c-cal/blob/main/include/aws/cal/ecc.h
11+
/**
12+
* Will need to be implemented and exposed in aws-crt-kotlin.
13+
* Or maybe we can _only_ offer the CRT signer on Native?
14+
* Will require updating DefaultAwsSigner to be expect/actual and set to CrtSigner on Native.
15+
*/
16+
public actual fun ecdsasecp256r1(key: ByteArray, message: ByteArray): ByteArray = TODO("Not yet implemented")

0 commit comments

Comments
 (0)