Skip to content
ecdsa.com

Decode a certificate · Kotlin

How to decode an X.509 certificate in Kotlin

To decode an X.509 certificate in Kotlin on the JVM, use the JDK's CertificateFactory — PEM or DER in, a typed X509Certificate out. The recipe reads the fields people search for (common name, SANs, validity, fingerprint) and turns the JDK's raw arrays for key usage and extended key usage into readable Kotlin lists.

Tested with Kotlin 2.4.21 (Kotlin/JVM, OpenJDK 25.0.4.1 (Temurin), Gradle 9.8.1) and OpenSSL 3.0.13 (DER copy of the certificate).

Main.kt — subject, SANs, validity, key and fingerprint
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.interfaces.ECPublicKey
import javax.naming.ldap.LdapName
import kotlin.io.path.Path
import kotlin.io.path.inputStream
fun readCertificate(path: String): X509Certificate =
Path(path).inputStream().use { // PEM or DER
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate
}
/** The CN attribute of a distinguished name, parsed instead of cut out of the string. */
fun X509Certificate.commonName(): String? =
LdapName(subjectX500Principal.name).rdns.lastOrNull { it.type.equals("CN", ignoreCase = true) }?.value as String?
fun main() {
val cert = readCertificate("cert.pem")
println("common name: ${cert.commonName()}")
println("issuer: ${cert.issuerX500Principal.name}")
println("serial: ${cert.serialNumber.toString(16)}")
println("valid: ${cert.notBefore.toInstant()} -> ${cert.notAfter.toInstant()}")
println("signed with: ${cert.sigAlgName}") // the issuer's algorithm
// subjectAlternativeNames: null when absent, otherwise [type, value] pairs; 2 = dNSName
val dnsNames = cert.subjectAlternativeNames.orEmpty().filter { it[0] == 2 }.map { it[1] as String }
println("DNS names: $dnsNames")
val key = cert.publicKey
if (key is ECPublicKey) println("key: EC, ${key.params.curve.field.fieldSize}-bit curve")
val fingerprint = MessageDigest.getInstance("SHA-256").digest(cert.encoded) // the whole DER certificate
println("SHA-256 fingerprint: ${fingerprint.toHexString(HexFormat { upperCase = true; bytes.byteSeparator = ":" })}")
}
key usage, extended key usage and the CA flag
// keyUsage is a BooleanArray indexed by the RFC 5280 bit numbers, null when the extension is absent
val keyUsageBits = listOf(
"digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
"keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly",
)
val usages = cert.keyUsage?.withIndex()?.filter { it.value }?.map { keyUsageBits[it.index] }
println("key usage: $usages")
// extendedKeyUsage is a list of OIDs
val ekuNames = mapOf("1.3.6.1.5.5.7.3.1" to "serverAuth", "1.3.6.1.5.5.7.3.2" to "clientAuth")
println("extended key usage: ${cert.extendedKeyUsage?.map { ekuNames[it] ?: it }}")
println("CA certificate: ${cert.basicConstraints >= 0}") // -1 means "not a CA"

How it works

  1. Path.inputStream().use { … } closes the file after generateCertificate, which accepts PEM text and binary DER alike — the run feeds it both.
  2. LdapName parses the RFC 2253 subject string into attributes, so the CN comes out without fragile substringAfter("CN=") string cutting.
  3. subjectAlternativeNames returns [type, value] lists; type 2 is a DNS name, 7 an IP address. orEmpty() covers certificates without the extension.
  4. keyUsage is a BooleanArray whose index is the RFC 5280 bit number, and extendedKeyUsage is a list of OID strings — both are mapped to names here. basicConstraints returns -1 for a non-CA certificate.
  5. The fingerprint is SHA-256 over cert.encoded, the DER of the whole certificate; HexFormat prints it in the colon-separated uppercase style browsers and OpenSSL use.

Gotchas

  • subjectAlternativeNames, keyUsage and extendedKeyUsage are platform types that return null when the extension is missing. Kotlin cannot see that from the Java signature, so a bare .filter compiles and then throws a NullPointerException on certificates without SANs — use orEmpty() or ?..
  • subjectX500Principal.name prints RFC 2253 order (C=US,O=…,CN=…), the reverse of OpenSSL's display; compare X500Principal objects rather than strings.
  • sigAlgName is the issuer's signature algorithm, not the certificate key's type — check publicKey for that.
  • Decoding proves nothing about trust. Expired and self-signed certificates parse fine; use CertPathValidator (or your TLS stack) with trust anchors to validate a chain.

Related recipes