Skip to content
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

Add support for encoding/decoding compressed EC keys #57

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 10 additions & 2 deletions cryptography-core/api/cryptography-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,21 @@ public final class dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$PEM :
public fun hashCode ()I
}

public final class dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW : dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format {
public static final field INSTANCE Ldev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW;
public abstract class dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW : dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format {
public static final field Uncompressed Ldev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW$Uncompressed;
}

public final class dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW$Compressed : dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW {
public static final field INSTANCE Ldev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW$Compressed;
public fun equals (Ljava/lang/Object;)Z
public fun getName ()Ljava/lang/String;
public fun hashCode ()I
}

public final class dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW$Uncompressed : dev/whyoleg/cryptography/algorithms/EC$PublicKey$Format$RAW {
public fun getName ()Ljava/lang/String;
}

public abstract interface class dev/whyoleg/cryptography/algorithms/ECDH : dev/whyoleg/cryptography/algorithms/EC {
public static final field Companion Ldev/whyoleg/cryptography/algorithms/ECDH$Companion;
public fun getId ()Ldev/whyoleg/cryptography/CryptographyAlgorithmId;
Expand Down
23 changes: 15 additions & 8 deletions cryptography-core/api/cryptography-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ abstract interface <#A: dev.whyoleg.cryptography.algorithms/EC.PublicKey, #B: de
sealed class Format : dev.whyoleg.cryptography.materials.key/KeyFormat { // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format|null[0]
final fun toString(): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.toString|toString(){}[0]

sealed class RAW : dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format { // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW|null[0]
final object Compressed : dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW { // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Compressed|null[0]
final val name // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Compressed.name|{}name[0]
final fun <get-name>(): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Compressed.name.<get-name>|<get-name>(){}[0]

final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Compressed.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Compressed.hashCode|hashCode(){}[0]
}

final object Uncompressed : dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW { // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Uncompressed|null[0]
final val name // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Uncompressed.name|{}name[0]
final fun <get-name>(): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Uncompressed.name.<get-name>|<get-name>(){}[0]
}
}

final object DER : dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format { // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.DER|null[0]
final val name // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.DER.name|{}name[0]
final fun <get-name>(): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.DER.name.<get-name>|<get-name>(){}[0]
Expand All @@ -237,14 +252,6 @@ abstract interface <#A: dev.whyoleg.cryptography.algorithms/EC.PublicKey, #B: de
final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.PEM.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.PEM.hashCode|hashCode(){}[0]
}

final object RAW : dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format { // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW|null[0]
final val name // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.name|{}name[0]
final fun <get-name>(): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.name.<get-name>|<get-name>(){}[0]

final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.hashCode|hashCode(){}[0]
}
}
}

Expand Down
16 changes: 12 additions & 4 deletions cryptography-core/src/commonMain/kotlin/algorithms/EC.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,18 @@ public interface EC<PublicK : EC.PublicKey, PrivateK : EC.PrivateKey, KP : EC.Ke
override val name: String get() = "JWK"
}

// only uncompressed format is supported
// format defined in X963: 04 | X | Y
public data object RAW : Format() {
override val name: String get() = "RAW"
public sealed class RAW : Format() {

// uncompressed format: 0x04 | X | Y
public companion object Uncompressed : RAW() {
override val name: String get() = "RAW"
}

// compressed format: 0x02 | X (odd Y)
// 0x03 | X (even Y)
public data object Compressed : RAW() {
override val name: String get() = "RAW/COMPRESSED"
}
}

// SPKI = SubjectPublicKeyInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ fun AlgorithmTestScope<*>.supportsDigest(digest: CryptographyAlgorithmId<Digest>
fun AlgorithmTestScope<*>.supportsKeyFormat(format: KeyFormat): Boolean = supports {
when {
// only WebCrypto supports JWK for now
format.name == "JWK" &&
!provider.isWebCrypto -> "JWK key format"
else -> null
format.name == "JWK" && !provider.isWebCrypto
-> "JWK key format"
format == EC.PublicKey.Format.RAW.Compressed && provider.isApple
-> "compressed key format"
else -> null
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlin.test.*
private val publicKeyFormats = listOf(
EC.PublicKey.Format.JWK,
EC.PublicKey.Format.RAW,
EC.PublicKey.Format.RAW.Compressed,
EC.PublicKey.Format.DER,
EC.PublicKey.Format.PEM,
).associateBy { it.name }
Expand Down Expand Up @@ -84,6 +85,7 @@ abstract class EcCompatibilityTest<PublicK : EC.PublicKey, PrivateK : EC.Private
when (format) {
EC.PublicKey.Format.JWK -> {}
EC.PublicKey.Format.RAW,
EC.PublicKey.Format.RAW.Compressed,
EC.PublicKey.Format.DER,
EC.PublicKey.Format.PEM,
-> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ private class EcdsaPublicKeyDecoder(
) : KeyDecoder<EC.PublicKey.Format, ECDSA.PublicKey> {
override fun decodeFromByteArrayBlocking(format: EC.PublicKey.Format, bytes: ByteArray): ECDSA.PublicKey {
val rawKey = when (format) {
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> bytes
EC.PublicKey.Format.DER -> decodeDer(bytes)
EC.PublicKey.Format.PEM -> decodeDer(unwrapPem(PemLabel.PublicKey, bytes))
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> bytes
EC.PublicKey.Format.RAW.Compressed -> error("$format is not supported")
EC.PublicKey.Format.DER -> decodeDer(bytes)
EC.PublicKey.Format.PEM -> decodeDer(unwrapPem(PemLabel.PublicKey, bytes))
}
check(rawKey.size == curve.orderSize * 2 + 1) {
"Invalid raw key size: ${rawKey.size}, expected: ${curve.orderSize * 2 + 1}"
Expand Down Expand Up @@ -89,7 +90,7 @@ private class EcdsaPrivateKeyDecoder(
override fun decodeFromByteArrayBlocking(format: EC.PrivateKey.Format, bytes: ByteArray): ECDSA.PrivateKey {
val rawKey = when (format) {
EC.PrivateKey.Format.JWK -> error("$format is not supported")
EC.PrivateKey.Format.RAW -> error("$format is not supported")
EC.PrivateKey.Format.RAW -> error("$format is not supported")
EC.PrivateKey.Format.DER -> decodeDerPkcs8(bytes)
EC.PrivateKey.Format.PEM -> decodeDerPkcs8(unwrapPem(PemLabel.PrivateKey, bytes))
EC.PrivateKey.Format.DER.SEC1 -> decodeDerSec1(bytes)
Expand Down Expand Up @@ -176,10 +177,11 @@ private class EcdsaPublicKey(
val rawKey = exportSecKey(publicKey)

return when (format) {
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> rawKey
EC.PublicKey.Format.DER -> encodeDer(rawKey)
EC.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, encodeDer(rawKey))
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> rawKey
EC.PublicKey.Format.RAW.Compressed -> error("$format is not supported")
EC.PublicKey.Format.DER -> encodeDer(rawKey)
EC.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, encodeDer(rawKey))
}
}

Expand Down Expand Up @@ -212,7 +214,7 @@ private class EcdsaPrivateKey(
val rawKey = exportSecKey(privateKey)
return when (format) {
EC.PrivateKey.Format.JWK -> error("$format is not supported")
EC.PrivateKey.Format.RAW -> rawKey.copyOfRange(curve.orderSize * 2 + 1, curve.orderSize * 3 + 1)
EC.PrivateKey.Format.RAW -> rawKey.copyOfRange(curve.orderSize * 2 + 1, curve.orderSize * 3 + 1)
EC.PrivateKey.Format.DER -> encodeDerPkcs8(rawKey)
EC.PrivateKey.Format.PEM -> wrapPem(PemLabel.PrivateKey, encodeDerPkcs8(rawKey))
EC.PrivateKey.Format.DER.SEC1 -> encodeDerEcPrivateKey(rawKey)
Expand Down
104 changes: 74 additions & 30 deletions cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkEc.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,24 @@ internal sealed class JdkEc<PublicK : EC.PublicKey, PrivateK : EC.PrivateKey, KP
}

override fun decodeFromByteArrayBlocking(format: EC.PublicKey.Format, bytes: ByteArray): PublicK = when (format) {
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> {
check(bytes.isNotEmpty() && bytes[0].toInt() == 4) { "Encoded key should be in uncompressed format" }
val parameters = algorithmParameters(ECGenParameterSpec(curveName)).getParameterSpec(ECParameterSpec::class.java)
val fieldSize = parameters.curveOrderSize()
check(bytes.size == fieldSize * 2 + 1) { "Wrong encoded key size" }
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> decodeFromRaw(bytes)
EC.PublicKey.Format.RAW.Compressed -> decodeFromRaw(bytes)
EC.PublicKey.Format.DER -> decodeFromDer(bytes)
EC.PublicKey.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.PublicKey, bytes))
}

val x = bytes.copyOfRange(1, fieldSize + 1)
val y = bytes.copyOfRange(fieldSize + 1, fieldSize + 1 + fieldSize)
val point = ECPoint(BigInteger(1, x), BigInteger(1, y))
// use RawEncodedKeySpec
private fun decodeFromRaw(bytes: ByteArray): PublicK = run {
check(bytes.isNotEmpty()) { "Encoded key is empty!" }
val parameters = algorithmParameters(ECGenParameterSpec(curveName)).getParameterSpec(ECParameterSpec::class.java)
val point = parameters.decodePoint(bytes)

keyFactory.use {
it.generatePublic(ECPublicKeySpec(point, parameters))
}.convert()
}
EC.PublicKey.Format.DER -> decodeFromDer(bytes)
EC.PublicKey.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.PublicKey, bytes))
keyFactory.use {
it.generatePublic(ECPublicKeySpec(point, parameters))
}.convert()
}

}

private inner class EcPrivateKeyDecoder(
Expand All @@ -103,14 +103,14 @@ internal sealed class JdkEc<PublicK : EC.PublicKey, PrivateK : EC.PrivateKey, KP
}

override fun decodeFromByteArrayBlocking(format: EC.PrivateKey.Format, bytes: ByteArray): PrivateK = when (format) {
EC.PrivateKey.Format.JWK -> error("$format is not supported")
EC.PrivateKey.Format.RAW -> {
EC.PrivateKey.Format.JWK -> error("$format is not supported")
EC.PrivateKey.Format.RAW -> {
val parameters = algorithmParameters(ECGenParameterSpec(curveName)).getParameterSpec(ECParameterSpec::class.java)
// decode as positive value
decode(ECPrivateKeySpec(BigInteger(1, bytes), parameters))
}
EC.PrivateKey.Format.DER -> decodeFromDer(bytes)
EC.PrivateKey.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.PrivateKey, bytes))
EC.PrivateKey.Format.DER -> decodeFromDer(bytes)
EC.PrivateKey.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.PrivateKey, bytes))
EC.PrivateKey.Format.DER.SEC1 -> decodeFromDer(convertSec1ToPkcs8(bytes))
EC.PrivateKey.Format.PEM.SEC1 -> decodeFromDer(convertSec1ToPkcs8(unwrapPem(PemLabel.EcPrivateKey, bytes)))
}
Expand All @@ -133,24 +133,35 @@ internal sealed class JdkEc<PublicK : EC.PublicKey, PrivateK : EC.PrivateKey, KP
private val key: JPublicKey,
) : EC.PublicKey, JdkEncodableKey<EC.PublicKey.Format>(key) {
final override fun encodeToByteArrayBlocking(format: EC.PublicKey.Format): ByteArray = when (format) {
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> {
key as ECPublicKey
EC.PublicKey.Format.JWK -> error("$format is not supported")
EC.PublicKey.Format.RAW -> encodeToRaw(false)
EC.PublicKey.Format.RAW.Compressed -> encodeToRaw(true)
EC.PublicKey.Format.DER -> encodeToDer()
EC.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, encodeToDer())
}

val fieldSize = key.params.curveOrderSize()
val x = key.w.affineX.toByteArray().trimLeadingZeros()
val y = key.w.affineY.toByteArray().trimLeadingZeros()
check(x.size <= fieldSize && y.size <= fieldSize)
private fun encodeToRaw(compressed: Boolean): ByteArray = run {
key as ECPublicKey

val fieldSize = key.params.curveOrderSize()
val x = key.w.affineX.toByteArray().trimLeadingZeros()
val y = key.w.affineY.toByteArray().trimLeadingZeros()
check(x.size <= fieldSize && y.size <= fieldSize)

if (compressed) {
val output = ByteArray(fieldSize + 1)
output[0] = if (!BigInteger(1, y).testBit(0)) 0x02 else 0x03
x.copyInto(output, fieldSize - x.size + 1)
output
} else {
val output = ByteArray(fieldSize * 2 + 1)
output[0] = 4 // uncompressed
output[0] = 0x04
x.copyInto(output, fieldSize - x.size + 1)
y.copyInto(output, fieldSize * 2 - y.size + 1)
output
}
EC.PublicKey.Format.DER -> encodeToDer()
EC.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, encodeToDer())
}

}

protected abstract class BaseEcPrivateKey(
Expand All @@ -159,7 +170,7 @@ internal sealed class JdkEc<PublicK : EC.PublicKey, PrivateK : EC.PrivateKey, KP
final override fun encodeToByteArrayBlocking(format: EC.PrivateKey.Format): ByteArray = when (format) {
EC.PrivateKey.Format.JWK -> error("$format is not supported")
EC.PrivateKey.Format.DER -> encodeToDer()
EC.PrivateKey.Format.RAW -> {
EC.PrivateKey.Format.RAW -> {
key as ECPrivateKey
val fieldSize = key.params.curveOrderSize()
val secret = key.s.toByteArray().trimLeadingZeros()
Expand Down Expand Up @@ -197,6 +208,39 @@ internal fun ECParameterSpec.curveOrderSize(): Int {
return (curve.field.fieldSize + 7) / 8
}

internal fun ECParameterSpec.decodePoint(bytes: ByteArray): ECPoint {
val fieldSize = curveOrderSize()
return when (bytes[0].toInt()) {
0x02, // compressed evenY
0x03, // compressed oddY
-> {
check(bytes.size == fieldSize + 1) { "Wrong compressed key size ${bytes.size}" }
val p = (curve.field as ECFieldFp).p
val a = curve.a
val b = curve.b
val x = BigInteger(1, bytes.copyOfRange(1, bytes.size))
var y = x.multiply(x).add(a).multiply(x).add(b).mod(p).modSqrt(p)
if (y.testBit(0) != (bytes[0].toInt() == 0x03)) {
y = p.subtract(y)
}
ECPoint(x, y)
}
0x04, // uncompressed
-> {
check(bytes.size == fieldSize * 2 + 1) { "Wrong uncompressed key size ${bytes.size}" }
val x = bytes.copyOfRange(1, fieldSize + 1)
val y = bytes.copyOfRange(fieldSize + 1, fieldSize + 1 + fieldSize)
ECPoint(BigInteger(1, x), BigInteger(1, y))
}
else -> error("Unsupported key type ${bytes[0].toInt()}")
}
}

internal fun BigInteger.modSqrt(p: BigInteger): BigInteger {
check(p.testBit(0) && p.testBit(1)) { "Unsupported curve modulus" } // p ≡ 3 (mod 4)
return modPow(p.add(BigInteger.ONE).shiftRight(2), p) // Tonelli-Shanks
}

Comment on lines +211 to +243
Copy link
Contributor Author

Choose a reason for hiding this comment

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

BC is added as a provider without some API access so it requires manually calculating y.

private fun JAlgorithmParameters.curveName(): String {
return getParameterSpec(ECGenParameterSpec::class.java).name
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ internal fun encodePublicRawKey(key: CPointer<EVP_PKEY>): ByteArray = memScoped
output.ensureSizeExactly(outVar.value.convert())
}

@OptIn(UnsafeNumber::class)
internal fun encodePublicRawCompressedKey(key: CPointer<EVP_PKEY>): ByteArray = memScoped {
val ecKey = checkError(EVP_PKEY_get1_EC_KEY(key))
val ecGroup = checkError(EC_KEY_get0_group(ecKey))
val ecPoint = checkError(EC_KEY_get0_public_key(ecKey))
val size = checkError(EC_POINT_point2oct(ecGroup, ecPoint, POINT_CONVERSION_COMPRESSED, null, 0.convert(), null))
val output = ByteArray(size.convert())
checkError(EC_POINT_point2oct(ecGroup, ecPoint, POINT_CONVERSION_COMPRESSED, output.safeRefToU(0), size, null))
output
}

internal fun encodePrivateRawKey(key: CPointer<EVP_PKEY>): ByteArray = memScoped {
val orderSize = EC_order_size(key)
val privVar = alloc<CPointerVar<BIGNUM>>()
Expand Down
Loading
Loading