1import java.security.KeyStore
2import java.security.cert.CertificateFactory
3import java.security.cert.X509Certificate
4import java.time.Duration
5import java.time.Instant
6import javax.net.ssl.SSLContext
7import javax.net.ssl.SSLSocket
8import javax.net.ssl.TrustManagerFactory
9import kotlin.io.path.Path
10import kotlin.io.path.inputStream
11
12fun readCertificate(path: String): X509Certificate =
13 Path(path).inputStream().use { CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate }
14
15/** Days until notAfter; negative once the certificate has expired. */
16fun X509Certificate.daysLeft(now: Instant = Instant.now()): Long = Duration.between(now, notAfter.toInstant()).toDays()
17
18/** A TLS context that trusts only the given root (instead of the JDK's cacerts). */
19fun trustingOnly(root: X509Certificate): SSLContext {
20 val store = KeyStore.getInstance("PKCS12").apply {
21 load(null, null)
22 setCertificateEntry("root", root)
23 }
24 val trust = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { init(store) }
25 return SSLContext.getInstance("TLS").apply { init(null, trust.trustManagers, null) }
26}
27
28/** The leaf certificate a server presents, after full verification including the host name. */
29fun serverCertificate(host: String, port: Int, tls: SSLContext): X509Certificate =
30 (tls.socketFactory.createSocket(host, port) as SSLSocket).use { socket ->
31 socket.sslParameters = socket.sslParameters.apply { endpointIdentificationAlgorithm = "HTTPS" }
32 socket.startHandshake()
33 socket.session.peerCertificates[0] as X509Certificate
34 }
35
36fun main() {
37 for (path in listOf("leaf.pem", "expired.pem")) {
38 val cert = readCertificate(path)
39 println("$path: notAfter ${cert.notAfter.toInstant()}, ${cert.daysLeft()} days left")
40 }
41 val tls = trustingOnly(readCertificate("root.pem"))
42 println("api.example.com: notAfter ${serverCertificate("api.example.com", 8443, tls).notAfter.toInstant()}")
43 runCatching { serverCertificate("expired.example.com", 9443, tls) }
44 .onFailure { println("expired.example.com: ${it.message}") }
45}
46