Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 4 additions & 0 deletions runtime/runtime-core/api/runtime-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,10 @@ public final class aws/smithy/kotlin/runtime/hashing/EcdsaJVMKt {
public static final fun ecdsaSecp256r1 ([B[B)[B
Copy link
Contributor Author

@xinsong-cui xinsong-cui Nov 4, 2025

Choose a reason for hiding this comment

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

Existing ecdsaSecp256r1(key, message) calls work unchanged, should not be a breaking change

Copy link
Contributor

Choose a reason for hiding this comment

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

This is definitely a breaking change because you've altered the method signature. Older consumers of runtime-core will encounter a NoSuchMethodError exception at runtime. The only way to do this safely without breaking binary compatibility is to maintain the old method signature and add a new one.

I recommend one of the following approaches:

  • Expose a new method ecdsaSecp256r1Rs which invokes ecdsaSecp256r1Rs and extracts the r and s values. This could be implemented in common since it's just byte manipulation
  • Expose a new method asn1DerToRs which converts any ByteArray from ASN.1 DER format to r||s. Callers would then invoke ecdsaSecp256r1Rs first and then asn1DerToRs. This could also be implemented in common.

}

public final class aws/smithy/kotlin/runtime/hashing/EcdsaKt {
public static final fun ecdsaSecp256r1Rs ([B[B)[B
}

public abstract interface class aws/smithy/kotlin/runtime/hashing/HashFunction {
public abstract fun digest ()[B
public abstract fun getBlockSizeBytes ()I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@
package aws.smithy.kotlin.runtime.hashing

/**
* ECDSA on the SECP256R1 curve.
* ECDSA on the SECP256R1 curve returning ASN.1 DER format.
*/
public expect fun ecdsaSecp256r1(key: ByteArray, message: ByteArray): ByteArray

/**
* ECDSA on the SECP256R1 curve returning raw r||s format.
*/
public fun ecdsaSecp256r1Rs(key: ByteArray, message: ByteArray): ByteArray {
val derSignature = ecdsaSecp256r1(key, message)
return parseDerSignature(derSignature)
}
Comment on lines +12 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we create some unit tests for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, tests added.
Note: I have to disable signature verification test on JVM8 since SHA256withECDSAinP1363Format is not supported


/**
* Parses an ASN.1 DER encoded ECDSA signature and converts it to raw r||s format.
*/
private fun parseDerSignature(derSignature: ByteArray): ByteArray {
var index = 2 // Skip SEQUENCE tag and length

// Read r
index++ // Skip INTEGER tag
val rLength = derSignature[index++].toInt() and 0xFF
val r = derSignature.sliceArray(index until index + rLength)
index += rLength

// Read s
index++ // Skip INTEGER tag
val sLength = derSignature[index++].toInt() and 0xFF
val s = derSignature.sliceArray(index until index + sLength)

// Remove leading zero bytes and pad to 32 bytes
val rFixed = r.dropWhile { it == 0.toByte() }.toByteArray()
val sFixed = s.dropWhile { it == 0.toByte() }.toByteArray()

val rPadded = if (rFixed.size < 32) ByteArray(32 - rFixed.size) + rFixed else rFixed
val sPadded = if (sFixed.size < 32) ByteArray(32 - sFixed.size) + sFixed else sFixed

return rPadded + sPadded
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package aws.smithy.kotlin.runtime.hashing

import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.condition.EnabledForJreRange
import org.junit.jupiter.api.condition.JRE
import java.security.*
import java.security.interfaces.*
import java.security.spec.*
Expand All @@ -30,6 +32,10 @@ class EcdsaJVMTest {

assertTrue(signature.isNotEmpty())
assertTrue(signature.size >= 64) // ECDSA signatures are typically 70-72 bytes in DER format

val rawSignature = ecdsaSecp256r1Rs(privateKey, message)
assertTrue(signature.isNotEmpty())
assertTrue(rawSignature.size == 64)
}

@Test
Expand All @@ -44,6 +50,13 @@ class EcdsaJVMTest {
assertTrue(signature1.isNotEmpty())
assertTrue(signature2.isNotEmpty())
assertFalse(signature1.contentEquals(signature2))

val rawSignature1 = ecdsaSecp256r1Rs(privateKey, message1)
val rawSignature2 = ecdsaSecp256r1Rs(privateKey, message2)

assertTrue(rawSignature1.isNotEmpty())
assertTrue(rawSignature2.isNotEmpty())
assertFalse(rawSignature1.contentEquals(rawSignature2))
}

@Test
Expand All @@ -53,6 +66,9 @@ class EcdsaJVMTest {

val signature = ecdsaSecp256r1(privateKey, message)
assertTrue(signature.isNotEmpty())

val rawSignature = ecdsaSecp256r1Rs(privateKey, message)
assertTrue(rawSignature.isNotEmpty())
}

@Test
Expand All @@ -62,6 +78,9 @@ class EcdsaJVMTest {

val signature = ecdsaSecp256r1(privateKey, largeMessage)
assertTrue(signature.isNotEmpty())

val rawSignature = ecdsaSecp256r1Rs(privateKey, largeMessage)
assertTrue(rawSignature.isNotEmpty())
}

@Test
Expand All @@ -81,4 +100,23 @@ class EcdsaJVMTest {

assertTrue(verifier.verify(signature))
}

@Test
@EnabledForJreRange(min = JRE.JAVA_11)
fun testVerifyRawSignature() {
val keyGen = KeyPairGenerator.getInstance("EC")
keyGen.initialize(ECGenParameterSpec("secp256r1"))
val keyPair = keyGen.generateKeyPair()
val privateKey = (keyPair.private as ECPrivateKey).s.toByteArray()
val publicKey = keyPair.public

val message = "Hello, World!".toByteArray()
val signature = ecdsaSecp256r1Rs(privateKey, message)

val verifier = Signature.getInstance("SHA256withECDSAinP1363Format")
verifier.initVerify(publicKey)
verifier.update(message)

assertTrue(verifier.verify(signature))
}
}
Loading