2

I am trying to make spray-client connect using https to restricted rest api. The problem is that the certificate of the remote server is not registered as trusted, the simple Get() connection is then refused with SSLHandshakeException and I struggle to find any information about how to make this work. This somehow does work from my local machine without a need to change something.

I have found tutorials about how to put the certificate into jvm truststore, however since I am using dokku/docker, AFAIK the jvm instance is container specific (or?). Even though, I may in the future redeploy application on different machines, I'd like to have it defined in the application rather than setting jvm up everytime.

This is the first time I am facing SSL programmatically, so I may make wrong assumptions about how it works. Can you help?

Matej Briškár
  • 599
  • 4
  • 17

2 Answers2

2

I am not an expert in scala and I have never used spray-client but I will try to help you based on my Java experience.

You have two options, initialize a SSLContext with a TrustManagerFactory from a keystore with the server certificate (SECURE)

File keyStoreFile = new File("./myKeyStore");
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(new FileInputStream(keyStoreFile), "keyStorePassword".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(ks);
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());

or create a Dummy TrustManagerFactory which accepts any certificate (INSECURE)

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

public class DummyTrustManager implements X509TrustManager {

    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }

    /* (non-Javadoc)
    * @see javax.net.ssl.X509TrustManager#checkClientTrusted(X509Certificate[], java.lang.String)
    */
    public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
    }
    /* (non-Javadoc)
    * @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], java.lang.String)
    */
    public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
    }
}

initialize SSLContext by this way (it is very similar in spray-client)

SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[] { new DummyTrustManager() }, new java.security.SecureRandom());

I don't know the Scala syntax but should not be difficult to translate it to you.

Hope this helps.


EDIT (suggested by Matej Briškár): The above is the correct approach, however for spray-client it is not that easy. To make sendReceive work with SSL, you need to first establish connection and then pass this connection to sendReceive.

First create implicit trust manager as described above. For example:

implicit def sslContext: SSLContext = { 
    val context = SSLContext.getInstance("TLS") 
    context.init(null, Array[TrustManager](new DummyTrustManager), new SecureRandom())
    context
}

Note that this connection will time out after a while so you may want to change this default behaviour.

Then you need to establish the connection that will use this implicit like:

val connection = { 
    Await.result((IO(Http) ? HostConnectorSetup(host, port = 443, sslEncryption = true)).map { case HostConnectorInfo(hostConnector, _) => hostConnector }, timeout.duration) 
}

Note: host means the URL you are trying to reach. Also timeout is coming from outside of this code snippet.

And finally you can use sendReceive(connection) to access the SSL encrypted host.


Note: The original edit had a reference:

According to discussion online the issue is going to be fixed though.

However, the discussion is from 2013 and now it's 2016. The problem of needing a connection be made to get SSL working seems still be there. Not sure if the discussion is relevant, any more.

akauppi
  • 17,018
  • 15
  • 95
  • 120
vzamanillo
  • 9,905
  • 1
  • 36
  • 56
  • Thank you very much, it looks like you answer the question, I will try this in the evening and let you know. It looks very similar to what I have seen for the server side and tried to apply for my client-side though (I thought there is bigger difference), but without any luck. I know now I was going the right way. – Matej Briškár Mar 12 '15 at 10:22
  • Take a look at this post, maybe helps you too https://stackoverflow.com/questions/20706026/bing-azure-search-api/20789379#20789379 – vzamanillo Mar 12 '15 at 14:07
  • 1
    I'have just made it work. The thing is spray-client is not spray-can and it is too high-level approach communication on Request/Response basis. I had to establish the connection and put that inside. – Matej Briškár Mar 12 '15 at 14:26
  • I have edited your answer so I can mark it as correct. Hope you don't mind. – Matej Briškár Mar 12 '15 at 14:34
  • That's weird :-) Thanks – Matej Briškár Mar 12 '15 at 15:25
  • Anyway, keep in mind that using the dummy TrustManager is the insecure way, the initialization of the TrustManager with a keystore who keeps the server certificate would be the right thing. – vzamanillo Mar 12 '15 at 16:28
  • Thanks, I already did it that way. Dummy was good to make it work firstly and to know the problem is not in the keystore but in the code. – Matej Briškár Mar 13 '15 at 10:17
  • @akauppi, what link? http://spray.io/documentation/1.2.2/spray-client/ works and GitHub too, https://github.com/spray/spray/wiki/spray-client. – vzamanillo May 12 '16 at 07:31
  • @vzamanillo The "it is very similar in spray-client" gives to me: "The Page You Are Looking For Cannot Be Found". Likely should point to http://spray.io/documentation/1.2.2/spray-can/http-client/#ssl-support – akauppi May 12 '16 at 08:19
  • @vzamanillo Could you explain ultra-shortly what the "keyStorePassword" is actually about? I don't expect it matters - would hate to place anything looking like a pw in source code. – akauppi May 12 '16 at 12:28
  • + I think the `isClientTrusted` and `isServerTrusted` methods are unnecessary in the dummy, since they don't overload any of `X509TrustManager` methods. – akauppi May 12 '16 at 12:33
1

Here is my 2 cents if you just want to do it in INSECURE way, I just create my sendReceive method to send (HttpRequest, HostConnectorSetup) instead of HttpRequest

import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.{SSLContext, TrustManager, X509TrustManager}

import akka.actor.ActorRefFactory
import akka.io.IO
import akka.pattern.ask
import akka.util.Timeout
import spray.can.Http
import spray.can.Http.HostConnectorSetup
import spray.client.pipelining._
import spray.http.{HttpResponse, HttpResponsePart}
import spray.io.ClientSSLEngineProvider
import spray.util._

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._


object Test {
  // prepare your sslContext and engine Provider
  implicit lazy val engineProvider = ClientSSLEngineProvider(engine => engine)

  implicit lazy val sslContext: SSLContext = {
    val context = SSLContext.getInstance("TLS")
    context.init(null, Array[TrustManager](new DummyTrustManager), new SecureRandom)
    context
  }

  private class DummyTrustManager extends X509TrustManager {

    def isClientTrusted(cert: Array[X509Certificate]): Boolean = true

    def isServerTrusted(cert: Array[X509Certificate]): Boolean = true

    override def getAcceptedIssuers: Array[X509Certificate] = Array.empty

    override def checkClientTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = {}

    override def checkServerTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = {}
  }

  // rewrite sendReceiveMethod fron spray.client.pipelining
  def mySendReceive(implicit refFactory: ActorRefFactory, executionContext: ExecutionContext,
                    futureTimeout: Timeout = 60.seconds): SendReceive = {
    val transport =  IO(Http)(actorSystem)
    // HttpManager actually also accepts Msg (HttpRequest, HostConnectorSetup)
    request =>
      val uri = request.uri
      val setup = HostConnectorSetup(uri.authority.host.toString, uri.effectivePort, uri.scheme == "https")
      transport ? (request, setup) map {
        case x: HttpResponse          => x
        case x: HttpResponsePart      => sys.error("sendReceive doesn't support chunked responses, try sendTo instead")
        case x: Http.ConnectionClosed => sys.error("Connection closed before reception of response: " + x)
        case x                        => sys.error("Unexpected response from HTTP transport: " + x)
      }
  }

  // use mySendReceive instead spray.client.pipelining.sendReceive
}
keshin
  • 534
  • 4
  • 8