How to make an HTTP Request with SSL certificate in Kotlin using a PEM file
With CURL you can send a request using the PEM file, but in Java/Kotlin, it's not that easy. You need some magic.
To send a request using CURL you just need to use this option:
CURLOPT_SSLCERT => "path/to/pem/file"
Well, if you want to send a CURL request from Java/Kotlin you can do something like this.
But I want to use HTTP Client to send request. So I need something else.
To send an HTTP request with SSL Certificate in Kotlin, we need to use a Keystore and we need certificates.
To export certificates from your PEM file, you have to run these commands:
# Extract key
openssl pkey -in foo.pem -out foo-key.pem
# Extract all the certs
openssl crl2pkcs7 -nocrl -certfile file.pem |
openssl pkcs7 -print_certs -out file-certs.pem
Then follow this tutorial to generate a keystore.ks
file.
When you have keystore.ks
file(and password for this file), you can use it to create an SSL Context and use it in the request.
Here you can find a function that creates an HTTP Client. You can use this Client to send an HTTP request with SSL Certificate.
fun buildClient(): HttpClient {
//locate keystore file with password to that file
val keystore = "path/to/keystore.ks"
val keystorePass = "password"
val ks = KeyStore.getInstance("pkcs12")
val kmf = KeyManagerFactory.getInstance("SunX509")
//create new ssl context
val sslContext = SSLContext.getInstance("SSL")
//load your file to keystore
ks.load(FileInputStream(keystore), keystorePass.toCharArray())
kmf.init(ks, keystorePass.toCharArray())
//set global system properties
System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol")
System.setProperty("javax.net.ssl.keyStoreType", "pkcs12")
System.setProperty("javax.net.ssl.keyStore", keystore)
System.setProperty("javax.net.ssl.keyStorePassword", keystorePass)
//initialize sslContext
sslContext.init(kmf.keyManagers, getTrustAllCert(), null)
val client = java.net.http.HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.sslContext(sslContext)
.build()
//you can send request with your ssl certificate
return client
}
private fun getTrustAllCert(): Array<TrustManager> {
return arrayOf(object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate>? = null
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
})
}
However, if you don't want to use all these tricks, you can parse the PEM file directly in your code.
See the example below:
fun buildClient(): HttpClient {
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate>? = null
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
})
val context = SSLContext.getInstance("TLS")
val certAndKey = Files.readAllBytes(Paths.get("/path/to/file.pem"))
//get cert and key
val certBytes = parseDERFromPEM(certAndKey, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----")
val keyBytes = parseDERFromPEM(certAndKey, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----")
val cert = generateCertificateFromDER(certBytes)
val key = generatePrivateKeyFromDER(keyBytes)
val keystore = KeyStore.getInstance("JKS")
keystore.load(null)
keystore.setCertificateEntry("alias", cert)
keystore.setKeyEntry("alias", key, "password".toCharArray(), arrayOf<Certificate>(cert))
//save certificate to keyManager
val kmf = KeyManagerFactory.getInstance("SunX509")
kmf.init(keystore, "password".toCharArray())
context.init(kmf.keyManagers, trustAllCerts, null)
val client = java.net.http.HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.sslContext(context)
.build()
return client
}
private fun parseDERFromPEM(pem: ByteArray, beginDelimiter: String, endDelimiter: String): ByteArray {
val data = String(pem)
var tokens = data.split(beginDelimiter).toTypedArray()
tokens = tokens[1].split(endDelimiter).toTypedArray()
return DatatypeConverter.parseBase64Binary(tokens[0])
}
@Throws(InvalidKeySpecException::class, NoSuchAlgorithmException::class)
private fun generatePrivateKeyFromDER(keyBytes: ByteArray): RSAPrivateKey {
val spec = PKCS8EncodedKeySpec(keyBytes)
val factory = KeyFactory.getInstance("RSA")
return factory.generatePrivate(spec) as RSAPrivateKey
}
@Throws(CertificateException::class)
private fun generateCertificateFromDER(certBytes: ByteArray): X509Certificate {
val factory = CertificateFactory.getInstance("X.509")
return factory.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate
}
I was a bit shocked that I need to write so many lines of code to achieve this in Kotlin/Java. Although using CURL it is so easy to do.