Merge pull request #19219 from ktoso/wip-sslconfig-ktoso

Apply ssl-config and include hostname validation
This commit is contained in:
Konrad Malawski 2015-12-21 14:41:40 +01:00
commit 59a079d1f2
12 changed files with 189 additions and 47 deletions

View file

@ -15,10 +15,17 @@ import scala.util.control.NonFatal
* Enables accessing SslParameters even if compiled against Java 6.
*/
private[http] object Java6Compat {
def isJava6: Boolean =
System.getProperty("java.version").take(4) match {
case "1.6." true
case _ false
}
/**
* Returns true if setting the algorithm was successful.
*/
def setEndpointIdentificationAlgorithm(parameters: SSLParameters, algorithm: String): Boolean =
def trySetEndpointIdentificationAlgorithm(parameters: SSLParameters, algorithm: String): Boolean =
setEndpointIdentificationAlgorithmFunction(parameters, algorithm)
private[this] val setEndpointIdentificationAlgorithmFunction: (SSLParameters, String) Boolean = {

View file

@ -7,10 +7,10 @@ package akka.http.scaladsl
import java.net.InetSocketAddress
import java.util.concurrent.ConcurrentHashMap
import java.util.{ Collection JCollection }
import javax.net.ssl.{ SSLContext, SSLParameters }
import javax.net.ssl._
import akka.actor._
import akka.event.LoggingAdapter
import akka.event.{ Logging, LoggingAdapter }
import akka.http._
import akka.http.impl.engine.HttpConnectionTimeoutException
import akka.http.impl.engine.client._
@ -26,16 +26,22 @@ import akka.stream.Materializer
import akka.stream.io._
import akka.stream.scaladsl._
import com.typesafe.config.Config
import com.typesafe.sslconfig.akka.util.AkkaLoggerFactory
import com.typesafe.sslconfig.akka._
import com.typesafe.sslconfig.ssl._
import scala.collection.immutable
import scala.concurrent.{ ExecutionContext, Future, Promise, TimeoutException }
import scala.util.Try
import scala.util.control.NonFatal
class HttpExt(config: Config)(implicit system: ActorSystem) extends akka.actor.Extension {
class HttpExt(private val config: Config)(implicit val system: ActorSystem) extends akka.actor.Extension
with DefaultSSLContextCreation {
import Http._
override val sslConfig = AkkaSSLConfig(system)
// configured default HttpsContext for the client-side
// SYNCHRONIZED ACCESS ONLY!
private[this] var _defaultClientHttpsContext: HttpsContext = _
@ -491,25 +497,13 @@ class HttpExt(config: Config)(implicit system: ActorSystem) extends akka.actor.E
synchronized {
_defaultClientHttpsContext match {
case null
val ctx = createDefaultClientHttpsContext
val ctx = createDefaultClientHttpsContext()
_defaultClientHttpsContext = ctx
ctx
case ctx ctx
}
}
private def createDefaultClientHttpsContext: HttpsContext = {
val defaultCtx = SSLContext.getDefault
val params = new SSLParameters
if (!Java6Compat.setEndpointIdentificationAlgorithm(params, "https"))
throw new ReadTheDocumentationException(
"Cannot enable HTTPS hostname verification on Java 6. See the " +
"\"Client-Side HTTPS Support\" section in the documentation")
HttpsContext(defaultCtx, sslParameters = Some(params))
}
/**
* Sets the default client-side [[HttpsContext]].
*/
@ -584,8 +578,10 @@ class HttpExt(config: Config)(implicit system: ActorSystem) extends akka.actor.E
private[http] def sslTlsStage(httpsContext: Option[HttpsContext], role: Role, hostInfo: Option[(String, Int)] = None) =
httpsContext match {
case Some(hctx) SslTls(hctx.sslContext, hctx.firstSession, role, hostInfo = hostInfo)
case None SslTlsPlacebo.forScala
case Some(hctx)
SslTls(hctx.sslContext, hctx.firstSession, role, hostInfo = hostInfo)
case None
SslTlsPlacebo.forScala
}
}
@ -724,17 +720,64 @@ final case class HttpsContext(sslContext: SSLContext,
def firstSession = NegotiateNewSession(enabledCipherSuites, enabledProtocols, clientAuth, sslParameters)
/** Java API */
def getSslContext: SSLContext = sslContext
override def getSslContext: SSLContext = sslContext
/** Java API */
def getEnabledCipherSuites: japi.Option[JCollection[String]] = enabledCipherSuites.map(_.asJavaCollection)
override def getEnabledCipherSuites: japi.Option[JCollection[String]] = enabledCipherSuites.map(_.asJavaCollection)
/** Java API */
def getEnabledProtocols: japi.Option[JCollection[String]] = enabledProtocols.map(_.asJavaCollection)
override def getEnabledProtocols: japi.Option[JCollection[String]] = enabledProtocols.map(_.asJavaCollection)
/** Java API */
def getClientAuth: japi.Option[ClientAuth] = clientAuth
override def getClientAuth: japi.Option[ClientAuth] = clientAuth
/** Java API */
def getSslParameters: japi.Option[SSLParameters] = sslParameters
override def getSslParameters: japi.Option[SSLParameters] = sslParameters
}
trait DefaultSSLContextCreation {
protected def system: ActorSystem
protected def sslConfig: AkkaSSLConfig
protected def createDefaultClientHttpsContext(): HttpsContext = {
val config = sslConfig.config
val log = Logging(system, getClass)
val mkLogger = new AkkaLoggerFactory(system)
// initial ssl context!
val sslContext = if (sslConfig.config.default) {
log.debug("buildSSLContext: ssl-config.default is true, using default SSLContext")
sslConfig.validateDefaultTrustManager(config)
SSLContext.getDefault
} else {
// break out the static methods as much as we can...
val keyManagerFactory = sslConfig.buildKeyManagerFactory(config)
val trustManagerFactory = sslConfig.buildTrustManagerFactory(config)
new ConfigSSLContextBuilder(mkLogger, config, keyManagerFactory, trustManagerFactory).build()
}
// protocols!
val defaultParams = sslContext.getDefaultSSLParameters
val defaultProtocols = defaultParams.getProtocols
val protocols = sslConfig.configureProtocols(defaultProtocols, config)
defaultParams.setProtocols(protocols)
// ciphers!
val defaultCiphers = defaultParams.getCipherSuites
val cipherSuites = sslConfig.configureCipherSuites(defaultCiphers, config)
defaultParams.setCipherSuites(cipherSuites)
// hostname!
if (!Java6Compat.trySetEndpointIdentificationAlgorithm(defaultParams, "https")) {
log.info("Unable to use JDK built-in hostname verification, please consider upgrading your Java runtime to " +
"a more up to date version (JDK7+). Using Typesafe ssl-config hostname verification.")
// enabling the JDK7+ solution did not work, however this is fine since we do handle hostname
// verification directly in SslTlsCipherActor manually by applying an ssl-config provider verifier
}
HttpsContext(sslContext, sslParameters = Some(defaultParams))
}
}

View file

@ -9,31 +9,49 @@ Instructions adapted from
# Create a rootCA key:
```
openssl genrsa -out rootCA.key 2048
```
# Self-sign CA:
```
openssl req -x509 -new -nodes -key rootCA.key -days 3560 -out rootCA.crt
```
# Create server key:
```
openssl genrsa -out server.key 2048
```
# Create server CSR (you need to set the common name CN to "akka.example.org"):
```
openssl req -new -key server.key -out server.csr
```
# Create server certificate:
```
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 3560
```
# Create certificate chain:
```
cat server.crt rootCA.crt > chain.pem
```
# Convert certificate and key to pkcs12 (you need to provide a password manually, `ExampleHttpContexts`
# expects the password to be "abcdef"):
```
openssl pkcs12 -export -name servercrt -in chain.pem -inkey server.key -out server.p12
```
# For investigating remote certs use:
```
openssl s_client -showcerts -connect 54.173.126.144:443
```

View file

@ -20,10 +20,19 @@ import akka.testkit.EventFilter
import javax.net.ssl.SSLException
class TlsEndpointVerificationSpec extends AkkaSpec("""
akka.loglevel = DEBUG
akka.io.tcp.trace-logging = off""") with ScalaFutures {
akka.loglevel = INFO
akka.io.tcp.trace-logging = off
""") with ScalaFutures {
implicit val materializer = ActorMaterializer()
/*
* Useful when debugging against "what if we hit a real website"
*/
val includeTestsHittingActualWebsites = false
val timeout = Timeout(Span(3, Seconds))
implicit val patience = PatienceConfig(Span(10, Seconds))
"The client implementation" should {
"not accept certificates signed by unknown CA" in {
@ -47,6 +56,40 @@ class TlsEndpointVerificationSpec extends AkkaSpec("""
e shouldBe an[Exception]
}
}
if (includeTestsHittingActualWebsites) {
/*
* Requires the following DNS spoof to be running:
* sudo /usr/local/bin/python ./dnschef.py --fakedomains www.howsmyssl.com --fakeip 54.173.126.144
*
* Read up about it on: https://tersesystems.com/2014/03/31/testing-hostname-verification/
*/
"fail hostname verification on spoofed https://www.howsmyssl.com/" in {
val req = HttpRequest(uri = "https://www.howsmyssl.com/")
val ex = intercept[Exception] {
Http().singleRequest(req).futureValue
}
if (Java6Compat.isJava6) {
// our manual verification
ex.getMessage should include("Hostname verification failed")
} else {
// JDK built-in verification
val expectedMsg = "No subject alternative DNS name matching www.howsmyssl.com found"
var e: Throwable = ex
while (e.getCause != null) e = e.getCause
info("TLS failure cause: " + e.getMessage)
e.getMessage should include(expectedMsg)
}
}
"pass hostname verification on https://www.playframework.com/" in {
val req = HttpRequest(uri = "https://www.playframework.com/")
val res = Http().singleRequest(req).futureValue
res.status should ===(StatusCodes.OK)
}
}
}
def pipeline(clientContext: HttpsContext, hostname: String): HttpRequest Future[HttpResponse] = req

View file

@ -43,7 +43,7 @@ object ExampleHttpContexts {
context.init(null, certManagerFactory.getTrustManagers, new SecureRandom)
val params = new SSLParameters()
Java6Compat.setEndpointIdentificationAlgorithm(params, "https")
Java6Compat.trySetEndpointIdentificationAlgorithm(params, "https")
HttpsContext(context, sslParameters = Some(params))
}