yet_another_programmer
yet_another_programmer

Reputation: 327

Akka. How to set a pem certificate in a https request

I'm using Akka (version 2.5.18) to send JSON strings to a specific server via https. I have used a poolRouter (balancing-pool with 10 instances) in order to create a pool of actors that are going to send JSONs (generated from different customers) to a single server:

  val router: ActorRef = system.actorOf(
    FromConfig.props(Props(new SenderActor(configuration.getString("https://server.com"), this.self))),
    "poolRouter"
  )

The project specification says that the requests can also be sent using curl:

curl -X PUT --cert certificate.pem --key private.key -H 'Content-Type: application / json' -H 'cache-control: no-cache' -d '[{"id" : "test"}] 'https://server.com'

Where "certificate.pem" is the tls certificate of the customer and "private.key" is the private key used to generate the CSR of the customer.

I'm using a balancing-pool because I will have a very big set of certificates (one for each customer) and I need to send the requests concurrently.

My approach is to have a "SenderActor" class that will be created by the balancing pool. Each actor, upon the reception of a message with a "customerId" and the JSON data generated by this customer, will send a https request:

  override def receive: Receive = {
    case Data(customerId, jsonData) =>
      send(customerId(cid, jsonData))

Each SenderActor will read the certificate (and the private key) based on a path using the customerId. For instance, the customerId: "cust1" will have their certificate and key stored in "/home/test/cust1". This way, the same actor class can be used for all the customers.

According to the documentation, I need to create a HttpsConnectionContext in order to send the different requests:

def send(customerId: String, dataToSend): Future[HttpResponse] = {

    // Create the request
    val req = HttpRequest(
      PUT,
      uri = "https://server.com",
      entity = HttpEntity(`application/x-www-form-urlencoded` withCharset `UTF-8`, dataToSend),
      protocol = `HTTP/1.0`)

    val ctx: SSLContext = SSLContext.getInstance("TLS")

    val permissiveTrustManager: TrustManager = new X509TrustManager() {
      override def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = {}
      override def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = {}
      override def getAcceptedIssuers(): Array[X509Certificate] = Array.empty
    }


    ctx.init(Array.empty, Array(permissiveTrustManager), new SecureRandom())
    val httpsConnContext: HttpsConnectionContext = ConnectionContext.https(ctx)

    // Send the request
    Http(system).singleRequest(req, httpsConnContext)
}

The problem I have is that I don't have any clue about how to "set the certificate and the key" in the request, so that the server accepts them.

For instance, I can read the certificate using the following code:

import java.util.Base64

val certificate: String => String = (customer: String) => IO {
Source.fromInputStream(getClass.getClassLoader
  .getResourceAsStream("/home/test/".concat(customer).concat("_cert.pem")))
  .getLines().mkString
}.unsafeRunSync()

val decodedCertificate = Base64.getDecoder.decode(certificate(customerId)
  .replaceAll(X509Factory.BEGIN_CERT, "").replaceAll(X509Factory.END_CERT, ""))
val cert: Certificate = CertificateFactory.getInstance("X.509")
  .generateCertificate(new ByteArrayInputStream(decodedCertificate))

But I don't know how to "set" this certificate and the private key in the request (which is protected by a passphrase), so that the server accepts it.

Any hint or help would be greatly appreciated.

Upvotes: 1

Views: 1925

Answers (1)

Ivan Stanislavciuc
Ivan Stanislavciuc

Reputation: 7275

The following allows making a https request and identifying yourself with a private key from a x.509 certificate.

The following libraries are used to manage ssl configuration and to make https calls:

Convert your pem certificate to pks12 format as defined here

openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt

Define key-store in your application.conf. It supports only pkcs12 and because of this step 1 is required.

ssl-config {
  keyManager {
    stores = [
      {
        type = "pkcs12"
        path = "/path/to/pkcs12/cetificate"
        password = changeme //the password is set when using openssl
      }
    ]
  }
}

Load ssl config using special akka trait DefaultSSLContextCreation

import akka.actor.ActorSystem
import akka.actor.ExtendedActorSystem
import akka.http.scaladsl.DefaultSSLContextCreation
import com.typesafe.sslconfig.akka.AkkaSSLConfig
import com.typesafe.sslconfig.ssl.SSLConfigFactory

class TlsProvider(val actorSystem: ActorSystem) extends DefaultSSLContextCreation {

  override protected def sslConfig: AkkaSSLConfig =
    throw new RuntimeException("Unsupported behaviour when creating new sslConfig")

  def httpsConnectionContext() = {
    val akkaSslConfig =
      new AkkaSSLConfig(system.asInstanceOf[ExtendedActorSystem], SSLConfigFactory.parse(system.settings.config))
    createClientHttpsContext(akkaSslConfig)
  }
}

Create a https context and use in http connection pool.

 Http(actorSystem).cachedHostConnectionPoolHttps[RequestContext](
            host = host,
            port = portValue,
            connectionContext = new TlsProvider(actorSystem).httpsConnectionContext()
          )

Or set connection context to Http(actorSystem).singleRequest method.

In summary, I used ssl-config library to manage certificates instead of doing it programmatically yourself. By defining a keyManager in a ssl-config, any http request done with help of custom httpsConnectionContext will use the certificate to identify the caller/client.

I focused on describing how to establish a https connection using client certificate. Any dynamic behavior for managing multiple certificates is omitted. But I hope this code should be able give you understanding how to proceed.

Upvotes: 2

Related Questions