Skip to content
ecdsa.com

Generate a key pair · Kotlin

How to generate an ECDSA key pair in Kotlin

To generate an ECDSA key pair in Kotlin on the JVM, use the JDK's KeyPairGenerator and an ECGenParameterSpec — no library for P-256, P-384 or P-521. The recipe writes the keys as standard PEM files (PKCS#8 private, SPKI public) using only the Kotlin standard library, and adds Bouncy Castle for secp256k1, the curve the JDK dropped.

Tested with Kotlin 2.4.21 (Kotlin/JVM, OpenJDK 25.0.4.1 (Temurin), Gradle 9.8.1), Bouncy Castle (bcprov-jdk18on) 1.86 (secp256k1 key generation), and OpenSSL 3.0.13 (openssl pkey cross-check).

Main.kt — a P-256 key pair written as PEM files
import java.nio.file.attribute.PosixFilePermissions
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.spec.ECGenParameterSpec
import kotlin.io.encoding.Base64
import kotlin.io.path.Path
import kotlin.io.path.createFile
import kotlin.io.path.writeText
fun pem(label: String, der: ByteArray): String =
"-----BEGIN $label-----\n${Base64.encode(der).chunked(64).joinToString("\n")}\n-----END $label-----\n"
fun generateP256(): KeyPair =
KeyPairGenerator.getInstance("EC").run {
initialize(ECGenParameterSpec("secp256r1")) // P-256; "P-256" itself is not accepted here
generateKeyPair()
}
fun main() {
val pair = generateP256()
// encoded forms: PKCS#8 for the private key, SPKI for the public key
Path("key.pem")
.createFile(PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")))
.writeText(pem("PRIVATE KEY", pair.private.encoded))
Path("pub.pem").writeText(pem("PUBLIC KEY", pair.public.encoded))
print(pem("PUBLIC KEY", pair.public.encoded))
}
build.gradle.kts — Bouncy Castle, for secp256k1 only
// build.gradle.kts — only needed for curves the JDK no longer ships, such as secp256k1
dependencies {
implementation("org.bouncycastle:bcprov-jdk18on:1.86")
}
a secp256k1 key pair through the Bouncy Castle provider
import org.bouncycastle.jce.provider.BouncyCastleProvider
// The JDK removed secp256k1 in JDK 16; Bouncy Castle still generates it
val bitcoinCurveKey: KeyPair = KeyPairGenerator.getInstance("EC", BouncyCastleProvider()).run {
initialize(ECGenParameterSpec("secp256k1"))
generateKeyPair()
}

How it works

  1. KeyPairGenerator.getInstance("EC") initialized with ECGenParameterSpec("secp256r1") creates a P-256 pair from the JDK's own SunEC provider.
  2. pair.private.encoded is PKCS#8 DER and pair.public.encoded is SPKI DER — the bodies of BEGIN PRIVATE KEY and BEGIN PUBLIC KEY. The pem helper base64-encodes them and wraps the text at 64 columns with chunked(64).
  3. Path.createFile with a POSIX permission attribute creates key.pem as owner-only (mode 600) before any key material is written, and fails rather than overwrite an existing key.
  4. For secp256k1, pass a BouncyCastleProvider instance to getInstance: only that generator uses Bouncy Castle, and the encoded keys are the same standard PKCS#8 and SPKI structures OpenSSL reads.

Gotchas

  • Base64.Pem from the Kotlin standard library wraps at 64 columns but ends the lines with CRLF. Combined with LF header lines it produces mixed line endings — OpenSSL still parses the file, but byte comparisons and strict parsers do not. Base64.encode(der).chunked(64) keeps everything LF.
  • The JDK only knows the SEC name and its aliases: ECGenParameterSpec("P-256") and "prime256v1" throw Unknown curve name; "secp256r1" works.
  • ECGenParameterSpec("secp256k1") on the default provider throws Curve not supported: secp256k1 (1.3.132.0.10) since JDK 16 — the run above shows it. Registering Bouncy Castle globally with Security.addProvider also works, but changes provider selection for every library in the process.
  • On Android, keys meant to stay on the device belong in the Android Keystore (KeyPairGenerator.getInstance("EC", "AndroidKeyStore") with a KeyGenParameterSpec); such private keys are not exportable, so the PEM step does not apply to them.

Related recipes