Skip to content
ecdsa.com

Create a PKCS#12 bundle · Kotlin

How to create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Kotlin

To create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Kotlin, use the JDK's KeyStore of type PKCS12: setKeyEntry with the chain, then store() with a password. This recipe wraps writing and reading in small functions, bundles a P-256 key with its certificate and issuing CA, and checks the file with OpenSSL.

Tested with Kotlin 2.4.21 (Kotlin/JVM, OpenJDK 25.0.4.1 (Temurin), Gradle 9.8.1) and OpenSSL 3.0.13 (reads the bundle back (inspect.sh)).

Main.kt — write the bundle, read it back, refuse a wrong password
import java.security.KeyFactory
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import kotlin.io.path.Path
import kotlin.io.path.inputStream
import kotlin.io.path.outputStream
import kotlin.io.path.readText
fun readCertificate(path: String): Certificate =
Path(path).inputStream().use { CertificateFactory.getInstance("X.509").generateCertificate(it) }
fun readPkcs8Key(path: String): PrivateKey {
val der = Base64.getMimeDecoder().decode(Path(path).readText().replace(Regex("-----[^-]+-----"), ""))
return KeyFactory.getInstance("EC").generatePrivate(PKCS8EncodedKeySpec(der))
}
fun writeBundle(path: String, alias: String, key: PrivateKey, chain: List<Certificate>, password: CharArray) {
val store = KeyStore.getInstance("PKCS12").apply { load(null, null) }
store.setKeyEntry(alias, key, password, chain.toTypedArray()) // leaf first; alias = friendly name
Path(path).outputStream().use { store.store(it, password) }
}
fun readBundle(path: String, password: CharArray): KeyStore =
KeyStore.getInstance("PKCS12").apply { Path(path).inputStream().use { load(it, password) } }
fun main() {
val password = System.getenv("P12_PASSWORD").toCharArray()
writeBundle(
"bundle.p12", "api.example.com", readPkcs8Key("key.pem"),
listOf(readCertificate("leaf.pem"), readCertificate("intermediate.pem")), password,
)
val store = readBundle("bundle.p12", password)
for (alias in store.aliases().toList()) {
val key = store.getKey(alias, password)
val chain = store.getCertificateChain(alias).map { it as X509Certificate }
println("alias $alias: ${key.algorithm} key, chain ${chain.map { it.subjectX500Principal.name }}")
}
println("wrong password: ${runCatching { readBundle("bundle.p12", "wrong-password".toCharArray()) }.exceptionOrNull()?.message}")
}

How it works

  1. readPkcs8Key decodes a BEGIN PRIVATE KEY PEM with the MIME base64 decoder and PKCS8EncodedKeySpec; readCertificate uses CertificateFactory.
  2. writeBundle creates an empty KeyStore.getInstance("PKCS12"), adds the key with its chain (leaf first) under an alias and stores it with the password.
  3. On JDK 25 the result uses AES-256-CBC with PBKDF2-HMAC-SHA256 and an HMAC-SHA-256 MAC, 10 000 iterations — the run reads that from openssl pkcs12 -info.
  4. readBundle loads the file; getKey and getCertificateChain return the entry, and the alias is the friendly name api.example.com.

Gotchas

  • A wrong password fails in load() with keystore password was incorrect; runCatching turns that into a value instead of an uncaught exception.
  • The alias you pass to setKeyEntry is the only name the entry gets: OpenSSL reports it as the key's friendlyName (api.example.com in the run), and it is what keytool -list shows.

Related recipes