Skip to content
ecdsa.com

Generate a CSR · Kotlin

How to generate a CSR for an EC P-256 key with SANs in Kotlin

Kotlin on the JVM generates a CSR the way Java does: the JDK creates the P-256 key, and Bouncy Castle's JcaPKCS10CertificationRequestBuilder builds the PKCS#10 request with an extensionRequest attribute for the SANs, signed with SHA256withECDSA. The run decodes the CSR with OpenSSL.

Tested with org.bouncycastle:bcpkix-jdk18on 1.86 (Kotlin 2.4.21, OpenJDK 25.0.4.1 (Temurin)).

Csr.kt — P-256 key from the JDK, CSR with Bouncy Castle (bcpkix 1.86)
import java.io.FileWriter
import java.security.KeyPairGenerator
import java.security.spec.ECGenParameterSpec
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.ExtensionsGenerator
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralNames
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
fun main() {
val keys = KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair()
val extensions = ExtensionsGenerator().apply {
addExtension(
Extension.subjectAlternativeName, false,
GeneralNames(arrayOf(
GeneralName(GeneralName.dNSName, "api.example.com"),
GeneralName(GeneralName.dNSName, "www.example.com"),
)),
)
}
val csr = JcaPKCS10CertificationRequestBuilder(X500Name("C=US, O=Example Corp, CN=api.example.com"), keys.public)
.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensions.generate())
.build(JcaContentSignerBuilder("SHA256withECDSA").build(keys.private))
JcaPEMWriter(FileWriter("csr.pem")).use { it.writeObject(csr) }
JcaPEMWriter(FileWriter("key.pem")).use { it.writeObject(JcaPKCS8Generator(keys.private, null)) }
}

How it works

  1. KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) } creates the key.
  2. ExtensionsGenerator().apply { addExtension(…) } collects the subjectAltName with two dNSName entries.
  3. The builder takes the subject as an X500Name and the public key; addAttribute(pkcs_9_at_extensionRequest, …) attaches the extensions.
  4. JcaPEMWriter(…).use { … } writes the PEM files and closes them, also when an exception is thrown.

Gotchas

  • X500Name, GeneralName and PKCSObjectIdentifiers live in bcprov, not bcpkix: compiling against bcpkix alone fails on unresolved references. Put bcpkix, bcprov and bcutil (same version) on the compile class path.
  • writeObject(keys.private) writes a legacy SEC 1 block (BEGIN EC PRIVATE KEY); JcaPKCS8Generator writes the standard BEGIN PRIVATE KEY that the JDK's PKCS8EncodedKeySpec reads.
  • The JDK's own sun.security.pkcs10.PKCS10 cannot be used: package sun.security.pkcs10 is not visible (… declared in module java.base, which does not export it).
  • The key comes from the JDK's SunEC provider and Bouncy Castle only encodes and signs the request — registering BC as a JCA provider is not needed for this.

Related recipes