Skip to content

Add secp256k1 & brainpool EC curves #60

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

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
4 changes: 4 additions & 0 deletions cryptography-core/api/cryptography-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,13 @@ public final class dev/whyoleg/cryptography/algorithms/EC$Curve {
}

public final class dev/whyoleg/cryptography/algorithms/EC$Curve$Companion {
public final fun getBrainpoolP256r1-pVITJAk ()Ljava/lang/String;
public final fun getBrainpoolP384r1-pVITJAk ()Ljava/lang/String;
public final fun getBrainpoolP512r1-pVITJAk ()Ljava/lang/String;
public final fun getP256-pVITJAk ()Ljava/lang/String;
public final fun getP384-pVITJAk ()Ljava/lang/String;
public final fun getP521-pVITJAk ()Ljava/lang/String;
public final fun getSecp256k1-pVITJAk ()Ljava/lang/String;
}

public abstract interface class dev/whyoleg/cryptography/algorithms/EC$KeyPair : dev/whyoleg/cryptography/materials/key/Key {
Expand Down
8 changes: 8 additions & 0 deletions cryptography-core/api/cryptography-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,14 @@ abstract interface <#A: dev.whyoleg.cryptography.algorithms/EC.PublicKey, #B: de
final fun <get-P384>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.P384.<get-P384>|<get-P384>(){}[0]
final val P521 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.P521|{}P521[0]
final fun <get-P521>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.P521.<get-P521>|<get-P521>(){}[0]
final val brainpoolP256r1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP256r1|{}brainpoolP256r1[0]
final fun <get-brainpoolP256r1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP256r1.<get-brainpoolP256r1>|<get-brainpoolP256r1>(){}[0]
final val brainpoolP384r1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP384r1|{}brainpoolP384r1[0]
final fun <get-brainpoolP384r1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP384r1.<get-brainpoolP384r1>|<get-brainpoolP384r1>(){}[0]
final val brainpoolP512r1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP512r1|{}brainpoolP512r1[0]
final fun <get-brainpoolP512r1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP512r1.<get-brainpoolP512r1>|<get-brainpoolP512r1>(){}[0]
final val secp256k1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.secp256k1|{}secp256k1[0]
final fun <get-secp256k1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.secp256k1.<get-secp256k1>|<get-secp256k1>(){}[0]
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions cryptography-core/src/commonMain/kotlin/algorithms/EC.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ public interface EC<PublicK : EC.PublicKey, PrivateK : EC.PrivateKey, KP : EC.Ke
public val P256: Curve get() = Curve("P-256")
public val P384: Curve get() = Curve("P-384")
public val P521: Curve get() = Curve("P-521")

public val secp256k1: Curve get() = Curve("secp256k1")

// Brainpool curves (used in European standards and some government applications)
public val brainpoolP256r1: Curve get() = Curve("brainpoolP256r1")
public val brainpoolP384r1: Curve get() = Curve("brainpoolP384r1")
public val brainpoolP512r1: Curve get() = Curve("brainpoolP512r1")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ fun AlgorithmTestScope<RSA.PKCS1>.supportsEncryption(): Boolean = supports {

fun AlgorithmTestScope<out EC<*, *, *>>.supportsCurve(curve: EC.Curve): Boolean = supports {
when {
// JDK default, WebCrypto and Apple doesn't support secp256k1
curve.name == "secp256k1" && (
// JDK default/WebCrypto/Apple don't support secp256k1 or brainpool
curve in listOf(EC.Curve.secp256k1, EC.Curve.brainpoolP256r1, EC.Curve.brainpoolP384r1, EC.Curve.brainpoolP512r1) && (
provider.isJdkDefault || provider.isWebCrypto || provider.isApple
) -> "ECDSA ${curve.name}"

else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ abstract class EcCompatibilityTest<PublicK : EC.PublicKey, PrivateK : EC.Private
}

protected inline fun generateCurves(block: (curve: EC.Curve) -> Unit) {
generate(block, EC.Curve.P256, EC.Curve.P384, EC.Curve.P521, EC.Curve("secp256k1"))
generate(block,
EC.Curve.P256, EC.Curve.P384, EC.Curve.P521,
EC.Curve.secp256k1,
EC.Curve.brainpoolP256r1, EC.Curve.brainpoolP384r1, EC.Curve.brainpoolP512r1,
)
}

protected suspend fun CompatibilityTestScope<A>.generateKeys(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,94 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(
data class EcdsaSize(
val curve: EC.Curve,
val rawSignatureSize: Int,
val derSignatureSizes: List<Int>,
val derSignatureSizes: IntRange,
Copy link
Owner

Choose a reason for hiding this comment

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

nice idea!

val publicKeySize: Int,
val privateKeySizes: List<Int>,
)

@Test
fun testSizes() = testWithAlgorithm {
listOf(
EcdsaSize(EC.Curve.P256, 64, listOf(68, 69, 70, 71, 72), 91, listOf(67, 138, 150)),
EcdsaSize(EC.Curve.P384, 96, listOf(100, 101, 102, 103, 104), 120, listOf(80, 185, 194)),
EcdsaSize(EC.Curve.P521, 132, listOf(136, 137, 138, 139), 158, listOf(98, 241, 250)),
EcdsaSize(EC.Curve("secp256k1"), 64, listOf(68, 69, 70, 71, 72), 88, listOf(135, 144)),
// NIST curves
EcdsaSize(EC.Curve.P256, 64, 68.rangeTo(72), 91, listOf(67, 138, 150)),
EcdsaSize(EC.Curve.P384, 96, 100.rangeTo(104), 120, listOf(80, 185, 194)),
EcdsaSize(EC.Curve.P521, 132, 136.rangeTo(139), 158, listOf(98, 241, 250)),

// Note "private key sizes": smaller = openssl, larger = BouncyCastle

// SECP256k1
EcdsaSize(EC.Curve.secp256k1, 64, 68.rangeTo(72), 88, listOf(135, 144)),

// Brainpool curves
EcdsaSize(EC.Curve.brainpoolP256r1, 64, 68.rangeTo(72), 92, listOf(139, 152)),
EcdsaSize(EC.Curve.brainpoolP384r1, 96, 100.rangeTo(104), 124, listOf(189, 202)),
EcdsaSize(
EC.Curve.brainpoolP512r1,
128,
132.rangeTo(139),
158,
listOf(239, 252)
) // Raw 128, DER sig slightly larger; PubKey ~154; PrivKey ~P521


).forEach { (curve, rawSignatureSize, derSignatureSizes, publicKeySize, privateKeySizes) ->
if (!supportsCurve(curve)) return@forEach
if (!supportsCurve(curve)) {
println("Skipping size test for unsupported curve: ${curve.name}")
return@forEach
}

println("\nRunning size test for curve: ${curve.name}")
val keyPair = algorithm.keyPairGenerator(curve).generateKey()

assertEquals(publicKeySize, keyPair.publicKey.encodeToByteString(EC.PublicKey.Format.DER).size)
assertContains(privateKeySizes, keyPair.privateKey.encodeToByteString(EC.PrivateKey.Format.DER).size)
val actualPublicKeySize = keyPair.publicKey.encodeToByteString(EC.PublicKey.Format.DER).size
println("Got ${curve.name} public key size: $actualPublicKeySize (expected $publicKeySize)")
assertEquals(
publicKeySize,
actualPublicKeySize,
"Public key size mismatch for ${curve.name}, expected: $publicKeySize, but got $actualPublicKeySize"
)
val actualPrivateKeySize = keyPair.privateKey.encodeToByteString(EC.PrivateKey.Format.DER).size
println("Got ${curve.name} private key size: $actualPrivateKeySize (allowed $privateKeySizes)")
assertContains(
privateKeySizes,
actualPrivateKeySize,
"Private key size mismatch for ${curve.name}, expected one of $privateKeySizes, but got $actualPrivateKeySize"
)

generateDigests { digest, _ ->
if (!supportsDigest(digest)) return@generateDigests
if (!supportsDigest(digest)) {
println("Skipping digest $digest for curve ${curve.name}")
Copy link
Owner

Choose a reason for hiding this comment

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

Could you please remove all such println?
It's possible to enable more logging for supports* and executes algorithms via flipping enabled flag here

In case you think that the logging is necessary, please use logger.log, as there could be problems with excessive logging on CI if enabled :(

return@generateDigests
}

// RAW signature
run {
val verifier = keyPair.publicKey.signatureVerifier(digest, ECDSA.SignatureFormat.RAW)
keyPair.privateKey.signatureGenerator(digest, ECDSA.SignatureFormat.RAW).run {
assertEquals(rawSignatureSize, generateSignature(ByteArray(0)).size)
val sigEmpty = generateSignature(ByteArray(0))
assertEquals(
rawSignatureSize,
sigEmpty.size,
"RAW signature size mismatch for empty data on ${curve.name} / ${digest.name}"
Copy link
Owner

Choose a reason for hiding this comment

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

Nice! Thanks for adding those!

)
assertTrue(
verifier.tryVerifySignature(ByteArray(0), sigEmpty),
"RAW signature verification failed for empty data on ${curve.name} / ${digest.name}"
)

repeat(8) { n ->
val size = 10.0.pow(n).toInt()
val data = CryptographyRandom.nextBytes(size)
val signature = generateSignature(data)
assertEquals(rawSignatureSize, signature.size)
assertTrue(verifier.tryVerifySignature(data, signature))
assertEquals(
rawSignatureSize,
signature.size,
"RAW signature size mismatch for data size $size on ${curve.name} / ${digest.name}"
)
assertTrue(
verifier.tryVerifySignature(data, signature),
"RAW signature verification failed for data size $size on ${curve.name} / ${digest.name}"
)
}
}
}
Expand All @@ -68,7 +122,12 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(
fun assertSignatureSize(signature: ByteArray) {
if (signature.size in derSignatureSizes) return
// enhance a message with Base64 encoded signature
assertContains(derSignatureSizes, signature.size, "DER: ${Base64.encode(signature)}")

assertContains(
derSignatureSizes, signature.size, "DER signature size mismatch on ${curve.name} / ${digest.name}. " +
"Expected one of $derSignatureSizes, got ${signature.size}. " +
"Signature (Base64): ${Base64.encode(signature)}"
)
}

assertSignatureSize(generateSignature(ByteArray(0)))
Expand All @@ -77,7 +136,10 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(
val data = CryptographyRandom.nextBytes(size)
val signature = generateSignature(data)
assertSignatureSize(signature)
assertTrue(verifier.tryVerifySignature(data, signature))
assertTrue(
verifier.tryVerifySignature(data, signature),
"DER signature verification failed for data size $size on ${curve.name} / ${digest.name}"
)
}
}
}
Expand All @@ -87,27 +149,41 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(

@Test
fun testFunctions() = testWithAlgorithm {
if (!supportsFunctions()) return@testWithAlgorithm
if (!supportsFunctions()) {
println("Skipping function test because functions are not supported by provider")
Copy link
Owner

Choose a reason for hiding this comment

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

save here about println (in whole file)

return@testWithAlgorithm
}

listOf(
EC.Curve.P256,
EC.Curve.P384,
EC.Curve.P521,
EC.Curve("secp256k1"),
EC.Curve.secp256k1,
EC.Curve.brainpoolP256r1,
EC.Curve.brainpoolP384r1,
EC.Curve.brainpoolP512r1,
).forEach { curve ->
if (!supportsCurve(curve)) return@forEach
if (!supportsCurve(curve)) {
println("Skipping function test for unsupported curve: ${curve.name}")
return@forEach
}
println("Running function test for curve: ${curve.name}")

val keyPair = algorithm.keyPairGenerator(curve).generateKey()

generateDigests { digest, _ ->
if (!supportsDigest(digest)) return@generateDigests
if (!supportsDigest(digest)) {
println("Skipping digest $digest for curve ${curve.name}")
return@generateDigests
}

ECDSA.SignatureFormat.entries.forEach { format ->
println("Testing format $format for ${curve.name} / ${digest.name}")
val signatureGenerator = keyPair.privateKey.signatureGenerator(digest, format)
val signatureVerifier = keyPair.publicKey.signatureVerifier(digest, format)

repeat(10) {
val size = CryptographyRandom.nextInt(20000)
val size = CryptographyRandom.nextInt(1024, 20000) // Ensure non-trivial size
val data = ByteString(CryptographyRandom.nextBytes(size))
assertSignaturesViaFunction(signatureGenerator, signatureVerifier, data)
}
Expand Down
2 changes: 1 addition & 1 deletion cryptography-providers/jdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ For supported targets and algorithms, please consult [Supported primitives secti
## Custom Java providers

Some specific algorithms (SHA3 family of digests on JDK 8) or parameters (`secp256k1` curve for ECDSA) could be not supported by default JDK
provider, but it doesn't mean, that you can not use them with `cryptography-kotlin`.
provider, but it doesn't mean, that you cannot use them with `cryptography-kotlin`.
There is a possibility to create [CryptographyProvider][CryptographyProvider]
from [java.util.Provider](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Provider.html), f.e.
using [BouncyCastle](https://www.bouncycastle.org):
Expand Down