From 57ce1b151a38cbba3449f543ec08bb2dee2a27cd Mon Sep 17 00:00:00 2001 From: huskcasaca <100605532+huskcasaca@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:56:00 +0800 Subject: [PATCH 1/6] Add support for compressed EC public keys on JDK(BC) --- .../src/commonMain/kotlin/algorithms/EC.kt | 16 ++- .../compatibility/EcCompatibilityTest.kt | 2 + .../commonMain/kotlin/algorithms/SecEcdsa.kt | 22 ++-- .../src/jvmMain/kotlin/algorithms/JdkEc.kt | 104 +++++++++++++----- 4 files changed, 100 insertions(+), 44 deletions(-) diff --git a/cryptography-core/src/commonMain/kotlin/algorithms/EC.kt b/cryptography-core/src/commonMain/kotlin/algorithms/EC.kt index aa914f25..3e128527 100644 --- a/cryptography-core/src/commonMain/kotlin/algorithms/EC.kt +++ b/cryptography-core/src/commonMain/kotlin/algorithms/EC.kt @@ -38,10 +38,18 @@ public interface EC {} EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, EC.PublicKey.Format.DER, EC.PublicKey.Format.PEM, -> { diff --git a/cryptography-providers/apple/src/commonMain/kotlin/algorithms/SecEcdsa.kt b/cryptography-providers/apple/src/commonMain/kotlin/algorithms/SecEcdsa.kt index 716a4cbc..665f7fe8 100644 --- a/cryptography-providers/apple/src/commonMain/kotlin/algorithms/SecEcdsa.kt +++ b/cryptography-providers/apple/src/commonMain/kotlin/algorithms/SecEcdsa.kt @@ -58,10 +58,11 @@ private class EcdsaPublicKeyDecoder( ) : KeyDecoder { 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}" @@ -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) @@ -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)) } } @@ -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) diff --git a/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkEc.kt b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkEc.kt index 9be55c19..28b1b345 100644 --- a/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkEc.kt +++ b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkEc.kt @@ -70,24 +70,24 @@ internal sealed class JdkEc 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( @@ -103,14 +103,14 @@ internal sealed class JdkEc 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))) } @@ -133,24 +133,35 @@ internal sealed class JdkEc(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( @@ -159,7 +170,7 @@ internal sealed class JdkEc 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() @@ -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 +} + private fun JAlgorithmParameters.curveName(): String { return getParameterSpec(ECGenParameterSpec::class.java).name } From 5bd40b5e75dc3048397791e8da9fd01acd0ed6fd Mon Sep 17 00:00:00 2001 From: huskcasaca <100605532+huskcasaca@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:47:56 +0800 Subject: [PATCH 2/6] Add support for compressed EC public keys on Openssl3 --- .../src/commonMain/kotlin/algorithms/Openssl3Ec.kt | 11 +++++++++++ .../src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt | 5 +++-- .../src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt | 5 +++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ec.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ec.kt index 27700862..d7019797 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ec.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ec.kt @@ -99,6 +99,17 @@ internal fun encodePublicRawKey(key: CPointer): ByteArray = memScoped output.ensureSizeExactly(outVar.value.convert()) } +@OptIn(UnsafeNumber::class) +internal fun encodePublicRawCompressedKey(key: CPointer): 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): ByteArray = memScoped { val orderSize = EC_order_size(key) val privVar = alloc>() diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt index 33a4ebf6..5905a458 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt @@ -53,7 +53,7 @@ internal object Openssl3Ecdh : ECDH { override fun inputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW -> error("should not be called: handled explicitly in decodeFromBlocking") + EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in decodeFromBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } @@ -122,12 +122,13 @@ internal object Openssl3Ecdh : ECDH { override fun outputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in encodeToBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } override fun encodeToByteArrayBlocking(format: EC.PublicKey.Format): ByteArray = when (format) { EC.PublicKey.Format.RAW -> encodePublicRawKey(key) + EC.PublicKey.Format.RAW.Compressed -> encodePublicRawCompressedKey(key) else -> super.encodeToByteArrayBlocking(format) } diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt index 7ff52302..b6652971 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt @@ -55,7 +55,7 @@ internal object Openssl3Ecdsa : ECDSA { override fun inputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW -> error("should not be called: handled explicitly in decodeFromBlocking") + EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in decodeFromBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } @@ -124,12 +124,13 @@ internal object Openssl3Ecdsa : ECDSA { override fun outputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in encodeToBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } override fun encodeToByteArrayBlocking(format: EC.PublicKey.Format): ByteArray = when (format) { EC.PublicKey.Format.RAW -> encodePublicRawKey(key) + EC.PublicKey.Format.RAW.Compressed -> encodePublicRawCompressedKey(key) else -> super.encodeToByteArrayBlocking(format) } From 7f3d07016ad224ab99815e54e8437330d03651c5 Mon Sep 17 00:00:00 2001 From: huskcasaca <100605532+huskcasaca@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:16:24 +0800 Subject: [PATCH 3/6] Fix OpenSSL ECDH compressed key --- .../kotlin/algorithms/Openssl3Ecdh.kt | 26 ++++++++++++------- .../kotlin/algorithms/Openssl3Ecdsa.kt | 26 ++++++++++++------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt index 5905a458..7fe507b2 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdh.kt @@ -26,13 +26,13 @@ internal object Openssl3Ecdh : ECDH { override fun inputType(format: EC.PrivateKey.Format): String = when (format) { EC.PrivateKey.Format.DER, EC.PrivateKey.Format.DER.SEC1 -> "DER" EC.PrivateKey.Format.PEM, EC.PrivateKey.Format.PEM.SEC1 -> "PEM" - EC.PrivateKey.Format.RAW -> "DER" // with custom processing + EC.PrivateKey.Format.RAW -> "DER" // with custom processing EC.PrivateKey.Format.JWK -> error("JWK format is not supported") } override fun inputStruct(format: EC.PrivateKey.Format): String = when (format) { EC.PrivateKey.Format.DER.SEC1, EC.PrivateKey.Format.PEM.SEC1 -> "EC" - EC.PrivateKey.Format.RAW -> "EC" // with custom processing + EC.PrivateKey.Format.RAW -> "EC" // with custom processing else -> super.inputStruct(format) } @@ -53,13 +53,17 @@ internal object Openssl3Ecdh : ECDH { override fun inputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in decodeFromBlocking") + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> error("should not be called: handled explicitly in decodeFromBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } override fun decodeFromByteArrayBlocking(format: EC.PublicKey.Format, bytes: ByteArray): ECDH.PublicKey = when (format) { - EC.PublicKey.Format.RAW -> wrapKey(decodePublicRawKey(curve, bytes)) - else -> super.decodeFromByteArrayBlocking(format, bytes) + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> wrapKey(decodePublicRawKey(curve, bytes)) + else -> super.decodeFromByteArrayBlocking(format, bytes) } override fun wrapKey(key: CPointer): ECDH.PublicKey { @@ -93,7 +97,7 @@ internal object Openssl3Ecdh : ECDH { override fun outputType(format: EC.PrivateKey.Format): String = when (format) { EC.PrivateKey.Format.DER, EC.PrivateKey.Format.DER.SEC1 -> "DER" EC.PrivateKey.Format.PEM, EC.PrivateKey.Format.PEM.SEC1 -> "PEM" - EC.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + EC.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") EC.PrivateKey.Format.JWK -> error("JWK format is not supported") } @@ -104,7 +108,7 @@ internal object Openssl3Ecdh : ECDH { override fun encodeToByteArrayBlocking(format: EC.PrivateKey.Format): ByteArray = when (format) { EC.PrivateKey.Format.RAW -> encodePrivateRawKey(key) - else -> super.encodeToByteArrayBlocking(format) + else -> super.encodeToByteArrayBlocking(format) } override fun sharedSecretGenerator(): SharedSecretGenerator = this @@ -122,14 +126,16 @@ internal object Openssl3Ecdh : ECDH { override fun outputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in encodeToBlocking") + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> error("should not be called: handled explicitly in encodeToBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } override fun encodeToByteArrayBlocking(format: EC.PublicKey.Format): ByteArray = when (format) { - EC.PublicKey.Format.RAW -> encodePublicRawKey(key) + EC.PublicKey.Format.RAW -> encodePublicRawKey(key) EC.PublicKey.Format.RAW.Compressed -> encodePublicRawCompressedKey(key) - else -> super.encodeToByteArrayBlocking(format) + else -> super.encodeToByteArrayBlocking(format) } override fun sharedSecretGenerator(): SharedSecretGenerator = this diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt index b6652971..a8527387 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Ecdsa.kt @@ -28,13 +28,13 @@ internal object Openssl3Ecdsa : ECDSA { override fun inputType(format: EC.PrivateKey.Format): String = when (format) { EC.PrivateKey.Format.DER, EC.PrivateKey.Format.DER.SEC1 -> "DER" EC.PrivateKey.Format.PEM, EC.PrivateKey.Format.PEM.SEC1 -> "PEM" - EC.PrivateKey.Format.RAW -> "DER" // with custom processing + EC.PrivateKey.Format.RAW -> "DER" // with custom processing EC.PrivateKey.Format.JWK -> error("JWK format is not supported") } override fun inputStruct(format: EC.PrivateKey.Format): String = when (format) { EC.PrivateKey.Format.DER.SEC1, EC.PrivateKey.Format.PEM.SEC1 -> "EC" - EC.PrivateKey.Format.RAW -> "EC" // with custom processing + EC.PrivateKey.Format.RAW -> "EC" // with custom processing else -> super.inputStruct(format) } @@ -55,13 +55,17 @@ internal object Openssl3Ecdsa : ECDSA { override fun inputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in decodeFromBlocking") + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> error("should not be called: handled explicitly in decodeFromBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } override fun decodeFromByteArrayBlocking(format: EC.PublicKey.Format, bytes: ByteArray): ECDSA.PublicKey = when (format) { - EC.PublicKey.Format.RAW -> wrapKey(decodePublicRawKey(curve, bytes)) - else -> super.decodeFromByteArrayBlocking(format, bytes) + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> wrapKey(decodePublicRawKey(curve, bytes)) + else -> super.decodeFromByteArrayBlocking(format, bytes) } override fun wrapKey(key: CPointer): ECDSA.PublicKey { @@ -95,7 +99,7 @@ internal object Openssl3Ecdsa : ECDSA { override fun outputType(format: EC.PrivateKey.Format): String = when (format) { EC.PrivateKey.Format.DER, EC.PrivateKey.Format.DER.SEC1 -> "DER" EC.PrivateKey.Format.PEM, EC.PrivateKey.Format.PEM.SEC1 -> "PEM" - EC.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + EC.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") EC.PrivateKey.Format.JWK -> error("JWK format is not supported") } @@ -106,7 +110,7 @@ internal object Openssl3Ecdsa : ECDSA { override fun encodeToByteArrayBlocking(format: EC.PrivateKey.Format): ByteArray = when (format) { EC.PrivateKey.Format.RAW -> encodePrivateRawKey(key) - else -> super.encodeToByteArrayBlocking(format) + else -> super.encodeToByteArrayBlocking(format) } override fun signatureGenerator(digest: CryptographyAlgorithmId, format: ECDSA.SignatureFormat): SignatureGenerator { @@ -124,14 +128,16 @@ internal object Openssl3Ecdsa : ECDSA { override fun outputType(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.DER -> "DER" EC.PublicKey.Format.PEM -> "PEM" - EC.PublicKey.Format.RAW, EC.PublicKey.Format.RAW.Compressed -> error("should not be called: handled explicitly in encodeToBlocking") + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> error("should not be called: handled explicitly in encodeToBlocking") EC.PublicKey.Format.JWK -> error("JWK format is not supported") } override fun encodeToByteArrayBlocking(format: EC.PublicKey.Format): ByteArray = when (format) { - EC.PublicKey.Format.RAW -> encodePublicRawKey(key) + EC.PublicKey.Format.RAW -> encodePublicRawKey(key) EC.PublicKey.Format.RAW.Compressed -> encodePublicRawCompressedKey(key) - else -> super.encodeToByteArrayBlocking(format) + else -> super.encodeToByteArrayBlocking(format) } override fun signatureVerifier(digest: CryptographyAlgorithmId, format: ECDSA.SignatureFormat): SignatureVerifier { From 77266e716daeaffaf16b59c8733823360b4b87e6 Mon Sep 17 00:00:00 2001 From: huskcasaca <100605532+huskcasaca@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:42:11 +0800 Subject: [PATCH 4/6] Add support for compressed EC public keys on WebCrypto --- .../kotlin/algorithms/WebCryptoEc.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEc.kt b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEc.kt index 178985b1..1789dbf4 100644 --- a/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEc.kt +++ b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEc.kt @@ -55,7 +55,9 @@ internal sealed class WebCryptoEc() { override fun stringFormat(format: EC.PublicKey.Format): String = when (format) { EC.PublicKey.Format.JWK -> "jwk" - EC.PublicKey.Format.RAW -> "raw" + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> "raw" EC.PublicKey.Format.DER, EC.PublicKey.Format.PEM, -> "spki" @@ -63,7 +65,9 @@ private object EcPublicKeyProcessor : WebCryptoKeyProcessor override fun beforeDecoding(algorithm: Algorithm, format: EC.PublicKey.Format, key: ByteArray): ByteArray = when (format) { EC.PublicKey.Format.JWK -> key - EC.PublicKey.Format.RAW -> key + EC.PublicKey.Format.RAW, + EC.PublicKey.Format.RAW.Compressed, + -> key EC.PublicKey.Format.DER -> key EC.PublicKey.Format.PEM -> unwrapPem(PemLabel.PublicKey, key) } @@ -71,9 +75,18 @@ private object EcPublicKeyProcessor : WebCryptoKeyProcessor override fun afterEncoding(format: EC.PublicKey.Format, key: ByteArray): ByteArray = when (format) { EC.PublicKey.Format.JWK -> key EC.PublicKey.Format.RAW -> key + EC.PublicKey.Format.RAW.Compressed + -> compressPublicKey(key) EC.PublicKey.Format.DER -> key EC.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, key) } + + private fun compressPublicKey(key: ByteArray): ByteArray { + require(key[0] == 0x04.toByte() && key.size % 2 != 0) { "Invalid key format" } + return key.copyOfRange(0, (key.size - 1) / 2 + 1).also { + it[0] = if (key.last() % 2 == 0) 0x02 else 0x03 + } + } } private object EcPrivateKeyProcessor : WebCryptoKeyProcessor() { @@ -89,7 +102,7 @@ private object EcPrivateKeyProcessor : WebCryptoKeyProcessor key - EC.PrivateKey.Format.RAW -> { + EC.PrivateKey.Format.RAW -> { val curveId = when (val namedCurve = algorithm.ecKeyAlgorithmNamedCurve) { EC.Curve.P256.name -> ObjectIdentifier.secp256r1 EC.Curve.P384.name -> ObjectIdentifier.secp384r1 @@ -110,7 +123,7 @@ private object EcPrivateKeyProcessor : WebCryptoKeyProcessor key - EC.PrivateKey.Format.RAW -> { + EC.PrivateKey.Format.RAW -> { Der.decodeFromByteArray( EcPrivateKey.serializer(), unwrapPrivateKey(ObjectIdentifier.EC, key) From 75a39c45a83c29c30d59864824b07dbec5a90eb4 Mon Sep 17 00:00:00 2001 From: huskcasaca <100605532+huskcasaca@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:05:28 +0800 Subject: [PATCH 5/6] Add API for compressed ec public key format --- cryptography-core/api/cryptography-core.api | 12 ++++++++-- .../api/cryptography-core.klib.api | 23 ++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cryptography-core/api/cryptography-core.api b/cryptography-core/api/cryptography-core.api index 28cc80fa..ea1c7774 100644 --- a/cryptography-core/api/cryptography-core.api +++ b/cryptography-core/api/cryptography-core.api @@ -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; diff --git a/cryptography-core/api/cryptography-core.klib.api b/cryptography-core/api/cryptography-core.klib.api index 5fc5301e..66ef8334 100644 --- a/cryptography-core/api/cryptography-core.klib.api +++ b/cryptography-core/api/cryptography-core.klib.api @@ -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 (): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Compressed.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 (): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.Uncompressed.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 (): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.DER.name.|(){}[0] @@ -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 (): kotlin/String // dev.whyoleg.cryptography.algorithms/EC.PublicKey.Format.RAW.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] - } } } From 938d50c61019df2b0260d681ac039e9a6cc13bc1 Mon Sep 17 00:00:00 2001 From: huskcasaca <100605532+huskcasaca@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:44:08 +0800 Subject: [PATCH 6/6] Exclude compressed ec public key format from Apple --- .../src/commonMain/kotlin/support.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt b/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt index 8aee96c6..ae3713dc 100644 --- a/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt +++ b/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt @@ -36,9 +36,11 @@ fun AlgorithmTestScope<*>.supportsDigest(digest: CryptographyAlgorithmId 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 } }