Skip to content

Commit 17f5a08

Browse files
sakewaltkb
andauthored
Add secp256k1 & brainpool EC curves (#78)
Co-authored-by: waltkb <[email protected]>
1 parent d01b8db commit 17f5a08

File tree

7 files changed

+123
-23
lines changed

7 files changed

+123
-23
lines changed

cryptography-core/api/cryptography-core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,13 @@ public final class dev/whyoleg/cryptography/algorithms/EC$Curve {
264264
}
265265

266266
public final class dev/whyoleg/cryptography/algorithms/EC$Curve$Companion {
267+
public final fun getBrainpoolP256r1-pVITJAk ()Ljava/lang/String;
268+
public final fun getBrainpoolP384r1-pVITJAk ()Ljava/lang/String;
269+
public final fun getBrainpoolP512r1-pVITJAk ()Ljava/lang/String;
267270
public final fun getP256-pVITJAk ()Ljava/lang/String;
268271
public final fun getP384-pVITJAk ()Ljava/lang/String;
269272
public final fun getP521-pVITJAk ()Ljava/lang/String;
273+
public final fun getSecp256k1-pVITJAk ()Ljava/lang/String;
270274
}
271275

272276
public abstract interface class dev/whyoleg/cryptography/algorithms/EC$KeyPair : dev/whyoleg/cryptography/materials/key/Key {

cryptography-core/api/cryptography-core.klib.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ abstract interface <#A: dev.whyoleg.cryptography.algorithms/EC.PublicKey, #B: de
277277
final fun <get-P384>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.P384.<get-P384>|<get-P384>(){}[0]
278278
final val P521 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.P521|{}P521[0]
279279
final fun <get-P521>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.P521.<get-P521>|<get-P521>(){}[0]
280+
final val brainpoolP256r1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP256r1|{}brainpoolP256r1[0]
281+
final fun <get-brainpoolP256r1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP256r1.<get-brainpoolP256r1>|<get-brainpoolP256r1>(){}[0]
282+
final val brainpoolP384r1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP384r1|{}brainpoolP384r1[0]
283+
final fun <get-brainpoolP384r1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP384r1.<get-brainpoolP384r1>|<get-brainpoolP384r1>(){}[0]
284+
final val brainpoolP512r1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP512r1|{}brainpoolP512r1[0]
285+
final fun <get-brainpoolP512r1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.brainpoolP512r1.<get-brainpoolP512r1>|<get-brainpoolP512r1>(){}[0]
286+
final val secp256k1 // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.secp256k1|{}secp256k1[0]
287+
final fun <get-secp256k1>(): dev.whyoleg.cryptography.algorithms/EC.Curve // dev.whyoleg.cryptography.algorithms/EC.Curve.Companion.secp256k1.<get-secp256k1>|<get-secp256k1>(){}[0]
280288
}
281289
}
282290
}

cryptography-core/src/commonMain/kotlin/algorithms/EC.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ public interface EC<PublicK : EC.PublicKey, PrivateK : EC.PrivateKey, KP : EC.Ke
2020
public val P256: Curve get() = Curve("P-256")
2121
public val P384: Curve get() = Curve("P-384")
2222
public val P521: Curve get() = Curve("P-521")
23+
24+
public val secp256k1: Curve get() = Curve("secp256k1")
25+
26+
// Brainpool curves (used in European standards and some government applications)
27+
public val brainpoolP256r1: Curve get() = Curve("brainpoolP256r1")
28+
public val brainpoolP384r1: Curve get() = Curve("brainpoolP384r1")
29+
public val brainpoolP512r1: Curve get() = Curve("brainpoolP512r1")
2330
}
2431
}
2532

cryptography-providers-tests-api/src/commonMain/kotlin/support.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ fun AlgorithmTestScope<RSA.PKCS1>.supportsEncryption(): Boolean = supports {
9393

9494
fun AlgorithmTestScope<out EC<*, *, *>>.supportsCurve(curve: EC.Curve): Boolean = supports {
9595
when {
96-
// JDK default, WebCrypto and Apple doesn't support secp256k1
97-
curve.name == "secp256k1" && (
96+
// JDK default, WebCrypto and Apple don't support secp256k1 or brainpool
97+
curve in listOf(EC.Curve.secp256k1, EC.Curve.brainpoolP256r1, EC.Curve.brainpoolP384r1, EC.Curve.brainpoolP512r1) && (
9898
provider.isJdkDefault || provider.isWebCrypto || provider.isApple || provider.isCryptoKit
9999
) -> "ECDSA ${curve.name}"
100+
100101
else -> null
101102
}
102103
}

cryptography-providers-tests/src/commonMain/kotlin/compatibility/EcCompatibilityTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ abstract class EcCompatibilityTest<PublicK : EC.PublicKey, PrivateK : EC.Private
4141
}
4242

4343
protected inline fun generateCurves(block: (curve: EC.Curve) -> Unit) {
44-
generate(block, EC.Curve.P256, EC.Curve.P384, EC.Curve.P521, EC.Curve("secp256k1"))
44+
generate(block,
45+
EC.Curve.P256, EC.Curve.P384, EC.Curve.P521,
46+
EC.Curve.secp256k1,
47+
EC.Curve.brainpoolP256r1, EC.Curve.brainpoolP384r1, EC.Curve.brainpoolP512r1,
48+
)
4549
}
4650

4751
protected suspend fun CompatibilityTestScope<A>.generateKeys(

cryptography-providers-tests/src/commonMain/kotlin/default/EcdsaTest.kt

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,40 +24,94 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(
2424
data class EcdsaSize(
2525
val curve: EC.Curve,
2626
val rawSignatureSize: Int,
27-
val derSignatureSizes: List<Int>,
27+
val derSignatureSizes: IntRange,
2828
val publicKeySize: Int,
2929
val privateKeySizes: List<Int>,
3030
)
3131

3232
@Test
3333
fun testSizes() = testWithAlgorithm {
3434
listOf(
35-
EcdsaSize(EC.Curve.P256, 64, listOf(68, 69, 70, 71, 72), 91, listOf(67, 138, 150)),
36-
EcdsaSize(EC.Curve.P384, 96, listOf(100, 101, 102, 103, 104), 120, listOf(80, 185, 194)),
37-
EcdsaSize(EC.Curve.P521, 132, listOf(136, 137, 138, 139), 158, listOf(98, 241, 250)),
38-
EcdsaSize(EC.Curve("secp256k1"), 64, listOf(68, 69, 70, 71, 72), 88, listOf(135, 144)),
35+
// NIST curves
36+
EcdsaSize(EC.Curve.P256, 64, 68.rangeTo(72), 91, listOf(67, 138, 150)),
37+
EcdsaSize(EC.Curve.P384, 96, 100.rangeTo(104), 120, listOf(80, 185, 194)),
38+
EcdsaSize(EC.Curve.P521, 132, 136.rangeTo(139), 158, listOf(98, 241, 250)),
39+
40+
// Note "private key sizes": smaller = openssl, larger = BouncyCastle
41+
42+
// SECP256k1
43+
EcdsaSize(EC.Curve.secp256k1, 64, 68.rangeTo(72), 88, listOf(135, 144)),
44+
45+
// Brainpool curves
46+
EcdsaSize(EC.Curve.brainpoolP256r1, 64, 68.rangeTo(72), 92, listOf(139, 152)),
47+
EcdsaSize(EC.Curve.brainpoolP384r1, 96, 100.rangeTo(104), 124, listOf(189, 202)),
48+
EcdsaSize(
49+
EC.Curve.brainpoolP512r1,
50+
128,
51+
132.rangeTo(139),
52+
158,
53+
listOf(239, 252)
54+
) // Raw 128, DER sig slightly larger; PubKey ~154; PrivKey ~P521
55+
56+
3957
).forEach { (curve, rawSignatureSize, derSignatureSizes, publicKeySize, privateKeySizes) ->
40-
if (!supportsCurve(curve)) return@forEach
58+
if (!supportsCurve(curve)) {
59+
logger.log { "Skipping size test for unsupported curve: ${curve.name}" }
60+
return@forEach
61+
}
4162

63+
logger.log { "\nRunning size test for curve: ${curve.name}" }
4264
val keyPair = algorithm.keyPairGenerator(curve).generateKey()
4365

44-
assertEquals(publicKeySize, keyPair.publicKey.encodeToByteString(EC.PublicKey.Format.DER).size)
45-
assertContains(privateKeySizes, keyPair.privateKey.encodeToByteString(EC.PrivateKey.Format.DER).size)
66+
val actualPublicKeySize = keyPair.publicKey.encodeToByteString(EC.PublicKey.Format.DER).size
67+
logger.log { "Got ${curve.name} public key size: $actualPublicKeySize (expected $publicKeySize)" }
68+
assertEquals(
69+
publicKeySize,
70+
actualPublicKeySize,
71+
"Public key size mismatch for ${curve.name}, expected: $publicKeySize, but got $actualPublicKeySize"
72+
)
73+
val actualPrivateKeySize = keyPair.privateKey.encodeToByteString(EC.PrivateKey.Format.DER).size
74+
logger.log { "Got ${curve.name} private key size: $actualPrivateKeySize (allowed $privateKeySizes)" }
75+
assertContains(
76+
privateKeySizes,
77+
actualPrivateKeySize,
78+
"Private key size mismatch for ${curve.name}, expected one of $privateKeySizes, but got $actualPrivateKeySize"
79+
)
4680

4781
generateDigests { digest, _ ->
48-
if (!supportsDigest(digest)) return@generateDigests
82+
if (!supportsDigest(digest)) {
83+
logger.log { "Skipping digest $digest for curve ${curve.name}" }
84+
return@generateDigests
85+
}
4986

5087
// RAW signature
5188
run {
5289
val verifier = keyPair.publicKey.signatureVerifier(digest, ECDSA.SignatureFormat.RAW)
5390
keyPair.privateKey.signatureGenerator(digest, ECDSA.SignatureFormat.RAW).run {
54-
assertEquals(rawSignatureSize, generateSignature(ByteArray(0)).size)
91+
val sigEmpty = generateSignature(ByteArray(0))
92+
assertEquals(
93+
rawSignatureSize,
94+
sigEmpty.size,
95+
"RAW signature size mismatch for empty data on ${curve.name} / ${digest.name}"
96+
)
97+
assertTrue(
98+
verifier.tryVerifySignature(ByteArray(0), sigEmpty),
99+
"RAW signature verification failed for empty data on ${curve.name} / ${digest.name}"
100+
)
101+
55102
repeat(8) { n ->
56103
val size = 10.0.pow(n).toInt()
57104
val data = CryptographyRandom.nextBytes(size)
58105
val signature = generateSignature(data)
59-
assertEquals(rawSignatureSize, signature.size)
60-
verifier.assertVerifySignature(data, signature)
106+
assertEquals(
107+
rawSignatureSize,
108+
signature.size,
109+
"RAW signature size mismatch for data size $size on ${curve.name} / ${digest.name}"
110+
)
111+
assertTrue(
112+
verifier.tryVerifySignature(data, signature),
113+
"RAW signature verification failed for data size $size on ${curve.name} / ${digest.name}"
114+
)
61115
}
62116
}
63117
}
@@ -68,7 +122,12 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(
68122
fun assertSignatureSize(signature: ByteArray) {
69123
if (signature.size in derSignatureSizes) return
70124
// enhance a message with Base64 encoded signature
71-
assertContains(derSignatureSizes, signature.size, "DER: ${Base64.encode(signature)}")
125+
126+
assertContains(
127+
derSignatureSizes, signature.size, "DER signature size mismatch on ${curve.name} / ${digest.name}. " +
128+
"Expected one of $derSignatureSizes, got ${signature.size}. " +
129+
"Signature (Base64): ${Base64.encode(signature)}"
130+
)
72131
}
73132

74133
assertSignatureSize(generateSignature(ByteArray(0)))
@@ -77,7 +136,10 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(
77136
val data = CryptographyRandom.nextBytes(size)
78137
val signature = generateSignature(data)
79138
assertSignatureSize(signature)
80-
verifier.assertVerifySignature(data, signature)
139+
assertTrue(
140+
verifier.tryVerifySignature(data, signature),
141+
"DER signature verification failed for data size $size on ${curve.name} / ${digest.name}"
142+
)
81143
}
82144
}
83145
}
@@ -87,27 +149,41 @@ abstract class EcdsaTest(provider: CryptographyProvider) : AlgorithmTest<ECDSA>(
87149

88150
@Test
89151
fun testFunctions() = testWithAlgorithm {
90-
if (!supportsFunctions()) return@testWithAlgorithm
152+
if (!supportsFunctions()) {
153+
logger.log { "Skipping function test because functions are not supported by provider" }
154+
return@testWithAlgorithm
155+
}
91156

92157
listOf(
93158
EC.Curve.P256,
94159
EC.Curve.P384,
95160
EC.Curve.P521,
96-
EC.Curve("secp256k1"),
161+
EC.Curve.secp256k1,
162+
EC.Curve.brainpoolP256r1,
163+
EC.Curve.brainpoolP384r1,
164+
EC.Curve.brainpoolP512r1,
97165
).forEach { curve ->
98-
if (!supportsCurve(curve)) return@forEach
166+
if (!supportsCurve(curve)) {
167+
logger.log { "Skipping function test for unsupported curve: ${curve.name}" }
168+
return@forEach
169+
}
170+
logger.log { "Running function test for curve: ${curve.name}" }
99171

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

102174
generateDigests { digest, _ ->
103-
if (!supportsDigest(digest)) return@generateDigests
175+
if (!supportsDigest(digest)) {
176+
logger.log { "Skipping digest $digest for curve ${curve.name}" }
177+
return@generateDigests
178+
}
104179

105180
ECDSA.SignatureFormat.entries.forEach { format ->
181+
logger.log { "Testing format $format for ${curve.name} / ${digest.name}" }
106182
val signatureGenerator = keyPair.privateKey.signatureGenerator(digest, format)
107183
val signatureVerifier = keyPair.publicKey.signatureVerifier(digest, format)
108184

109185
repeat(10) {
110-
val size = CryptographyRandom.nextInt(20000)
186+
val size = CryptographyRandom.nextInt(1024, 20000) // Ensure non-trivial size
111187
val data = ByteString(CryptographyRandom.nextBytes(size))
112188
assertSignaturesViaFunction(signatureGenerator, signatureVerifier, data)
113189
}

cryptography-providers/jdk/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ For supported targets and algorithms, please consult [Supported primitives secti
1111
## Custom Java providers
1212

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

0 commit comments

Comments
 (0)