Skip to content
ecdsa.com

Certificate expiry · Kotlin

How to check SSL certificate expiration date in Kotlin

To check SSL certificate expiration date in Kotlin, read notAfter from an X509Certificate — parsed from a file with CertificateFactory, or taken from an SSLSocket session after the handshake. This recipe uses only the JDK from Kotlin, trusts a private test root, verifies the host name, and shows the failure a verifying client gets from an expired certificate.

Tested with Kotlin 2.4.21 (Kotlin/JVM, OpenJDK 25.0.4.1 (Temurin), Gradle 9.8.1) and OpenSSL 3.0.13 (openssl s_server as the local TLS servers).

Main.kt — notAfter from files and from a TLS server, with a custom trust root
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.Instant
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.TrustManagerFactory
import kotlin.io.path.Path
import kotlin.io.path.inputStream
fun readCertificate(path: String): X509Certificate =
Path(path).inputStream().use { CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate }
/** Days until notAfter; negative once the certificate has expired. */
fun X509Certificate.daysLeft(now: Instant = Instant.now()): Long = Duration.between(now, notAfter.toInstant()).toDays()
/** A TLS context that trusts only the given root (instead of the JDK's cacerts). */
fun trustingOnly(root: X509Certificate): SSLContext {
val store = KeyStore.getInstance("PKCS12").apply {
load(null, null)
setCertificateEntry("root", root)
}
val trust = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(store) }
return SSLContext.getInstance("TLS").apply { init(null, trust.trustManagers, null) }
}
/** The leaf certificate a server presents, after full verification including the host name. */
fun serverCertificate(host: String, port: Int, tls: SSLContext): X509Certificate =
(tls.socketFactory.createSocket(host, port) as SSLSocket).use { socket ->
socket.sslParameters = socket.sslParameters.apply { endpointIdentificationAlgorithm = "HTTPS" }
socket.startHandshake()
socket.session.peerCertificates[0] as X509Certificate
}
fun main() {
for (path in listOf("leaf.pem", "expired.pem")) {
val cert = readCertificate(path)
println("$path: notAfter ${cert.notAfter.toInstant()}, ${cert.daysLeft()} days left")
}
val tls = trustingOnly(readCertificate("root.pem"))
println("api.example.com: notAfter ${serverCertificate("api.example.com", 8443, tls).notAfter.toInstant()}")
runCatching { serverCertificate("expired.example.com", 9443, tls) }
.onFailure { println("expired.example.com: ${it.message}") }
}

How it works

  1. readCertificate parses PEM or DER with CertificateFactory; the daysLeft extension compares notAfter.toInstant() with now and goes negative after expiry.
  2. trustingOnly(root) builds an SSLContext whose trust managers know only the given root — the usual way to check servers of a private CA without touching the JDK's cacerts.
  3. endpointIdentificationAlgorithm = "HTTPS" makes the raw SSLSocket check the host name, which plain sockets skip; startHandshake() then verifies everything before you read peerCertificates[0].
  4. The run prints 2035-12-31T23:59:59Z for the test leaf from the file and from the local server.

Gotchas

  • The handshake with an expired certificate fails with (certificate_expired) PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed, so serverCertificate throws instead of returning a date. Report that failure as "expired".
  • notAfter is a java.util.Date that prints in the JVM's time zone; use toInstant() for UTC output and comparisons, as the code does.
  • Without endpointIdentificationAlgorithm, an SSLSocket accepts a trusted certificate issued for another host name: the run connects to localhost and gets the api.example.com certificate with a successful handshake. With it, the same connection fails with No subject alternative DNS name matching localhost found.

Related recipes