Skip to content
ecdsa.com

Self-signed certificate · Kotlin

How to create a self-signed EC P-256 certificate with SANs in Kotlin

Kotlin uses the same Bouncy Castle builder as Java: JcaX509v3CertificateBuilder with one name as issuer and subject, a random serial and a 365-day validity, the extensions added inside apply, and SHA256withECDSA from JcaContentSignerBuilder; the key comes from the JDK. The run compiles with kotlinc, decodes cert.pem with OpenSSL and verifies it for localhost and 127.0.0.1.

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

SelfSigned.kt — P-256 key and self-signed server certificate with Bouncy Castle
import java.io.FileWriter
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec
import java.time.Duration
import java.time.Instant
import java.util.Date
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.BasicConstraints
import org.bouncycastle.asn1.x509.ExtendedKeyUsage
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralNames
import org.bouncycastle.asn1.x509.KeyPurposeId
import org.bouncycastle.asn1.x509.KeyUsage
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
fun main() {
val keys = KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair()
val name = X500Name("CN=localhost")
val notBefore = Instant.now()
val cert = JcaX509v3CertificateBuilder(
name, BigInteger(127, SecureRandom()),
Date.from(notBefore), Date.from(notBefore.plus(Duration.ofDays(365))),
name, keys.public,
).apply {
addExtension(
Extension.subjectAlternativeName, false,
GeneralNames(arrayOf(GeneralName(GeneralName.dNSName, "localhost"), GeneralName(GeneralName.iPAddress, "127.0.0.1"))),
)
addExtension(Extension.basicConstraints, true, BasicConstraints(false))
addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.digitalSignature))
addExtension(Extension.extendedKeyUsage, false, ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
}.build(JcaContentSignerBuilder("SHA256withECDSA").build(keys.private))
JcaPEMWriter(FileWriter("cert.pem")).use { it.writeObject(cert) }
JcaPEMWriter(FileWriter("key.pem")).use { it.writeObject(JcaPKCS8Generator(keys.private, null)) }
}

How it works

  1. KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) } creates the P-256 key pair.
  2. BigInteger(127, SecureRandom()) is the serial, and Date.from(notBefore.plus(Duration.ofDays(365))) the end of the validity.
  3. Inside apply, addExtension(oid, critical, value) adds the SANs, basicConstraints, keyUsage and extendedKeyUsage; build(signer) returns an X509CertificateHolder.
  4. JcaPEMWriter(FileWriter(…)).use { … } writes and closes each file: the certificate, and the key as PKCS#8 through JcaPKCS8Generator.

Gotchas

  • use matters: JcaPEMWriter buffers, and a writer that is never closed leaves an empty file.
  • GeneralName(GeneralName.iPAddress, …) accepts only a literal address; a host name fails with IllegalArgumentException: IP Address is invalid.
  • kotlinc -cp 'lib/*' does not expand the wildcard the way java -cp does, so the run lists the three jars by name.
  • writeObject(keys.private) would write the legacy BEGIN EC PRIVATE KEY block; JcaPKCS8Generator gives the standard PKCS#8 BEGIN PRIVATE KEY.

Related recipes