import java.security.MessageDigestimport java.security.cert.CertificateFactoryimport java.security.cert.X509Certificateimport java.security.interfaces.ECPublicKeyimport javax.naming.ldap.LdapNameimport kotlin.io.path.Pathimport 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 = ":" })}")} // keyUsage is a BooleanArray indexed by the RFC 5280 bit numbers, null when the extension is absentval 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 OIDsval 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
Path.inputStream().use { … }closes the file aftergenerateCertificate, which accepts PEM text and binary DER alike — the run feeds it both.LdapNameparses the RFC 2253 subject string into attributes, so the CN comes out without fragilesubstringAfter("CN=")string cutting.subjectAlternativeNamesreturns[type, value]lists; type2is a DNS name,7an IP address.orEmpty()covers certificates without the extension.keyUsageis aBooleanArraywhose index is the RFC 5280 bit number, andextendedKeyUsageis a list of OID strings — both are mapped to names here.basicConstraintsreturns -1 for a non-CA certificate.- The fingerprint is SHA-256 over
cert.encoded, the DER of the whole certificate;HexFormatprints it in the colon-separated uppercase style browsers and OpenSSL use.
Gotchas
subjectAlternativeNames,keyUsageandextendedKeyUsageare platform types that returnnullwhen the extension is missing. Kotlin cannot see that from the Java signature, so a bare.filtercompiles and then throws aNullPointerExceptionon certificates without SANs — useorEmpty()or?..subjectX500Principal.nameprints RFC 2253 order (C=US,O=…,CN=…), the reverse of OpenSSL's display; compareX500Principalobjects rather than strings.sigAlgNameis the issuer's signature algorithm, not the certificate key's type — checkpublicKeyfor 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.