From 8cf86e693c568babebf05e08b37f426ce8c5c901 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 17 Dec 2015 14:57:19 +0100 Subject: [PATCH 1/2] =htp #19184 better TLS defaults and hostname verification + depends on com.typesafe.ssl-config which we'll need to release + also makes tests compile on JDK6, to enable testing this on JDK6 --- .../akka/http/impl/util/Java6Compat.scala | 9 +- .../main/scala/akka/http/scaladsl/Http.scala | 89 ++++++++++++++----- .../test/resources/keys/{README => README.md} | 18 ++++ .../client/TlsEndpointVerificationSpec.scala | 47 +++++++++- .../http/impl/util/ExampleHttpContexts.scala | 2 +- .../directives/FileUploadDirectivesSpec.scala | 3 +- .../scala/akka/stream/testkit/AkkaSpec.scala | 4 + akka-stream/src/main/resources/reference.conf | 7 ++ .../stream/impl/io/SslTlsCipherActor.scala | 24 +++-- .../main/scala/akka/stream/io/SslTls.scala | 3 +- 10 files changed, 171 insertions(+), 35 deletions(-) rename akka-http-core/src/test/resources/keys/{README => README.md} (87%) diff --git a/akka-http-core/src/main/scala/akka/http/impl/util/Java6Compat.scala b/akka-http-core/src/main/scala/akka/http/impl/util/Java6Compat.scala index a1329c5e49..2d3fb3eb19 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/util/Java6Compat.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/util/Java6Compat.scala @@ -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 = { diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/Http.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/Http.scala index c5d709ce27..f4eccfd6c8 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/Http.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/Http.scala @@ -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)) + } + +} \ No newline at end of file diff --git a/akka-http-core/src/test/resources/keys/README b/akka-http-core/src/test/resources/keys/README.md similarity index 87% rename from akka-http-core/src/test/resources/keys/README rename to akka-http-core/src/test/resources/keys/README.md index e09f34715d..1353642d4e 100644 --- a/akka-http-core/src/test/resources/keys/README +++ b/akka-http-core/src/test/resources/keys/README.md @@ -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 +``` diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/client/TlsEndpointVerificationSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/client/TlsEndpointVerificationSpec.scala index dd875a3ca4..450d82713a 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/client/TlsEndpointVerificationSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/client/TlsEndpointVerificationSpec.scala @@ -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 EventFilter[SSLException](occurrences = 1).intercept { @@ -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 ⇒ diff --git a/akka-http-core/src/test/scala/akka/http/impl/util/ExampleHttpContexts.scala b/akka-http-core/src/test/scala/akka/http/impl/util/ExampleHttpContexts.scala index 09b9816caa..82750ce9f1 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/util/ExampleHttpContexts.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/util/ExampleHttpContexts.scala @@ -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)) } diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala index 396bb3f177..22f41b0b0d 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala @@ -5,7 +5,6 @@ package akka.http.scaladsl.server.directives import java.io.{ FileInputStream, File } -import java.nio.charset.StandardCharsets import akka.http.scaladsl.model._ import akka.http.scaladsl.server.{ MissingFormFieldRejection, RoutingSpec } import akka.util.ByteString @@ -145,7 +144,7 @@ class FileUploadDirectivesSpec extends RoutingSpec { try { val buffer = new Array[Byte](1024) in.read(buffer) - new String(buffer, StandardCharsets.UTF_8) + new String(buffer, "UTF-8") } finally { in.close() } diff --git a/akka-stream-testkit/src/test/scala/akka/stream/testkit/AkkaSpec.scala b/akka-stream-testkit/src/test/scala/akka/stream/testkit/AkkaSpec.scala index 8e4150413f..636b40e2ac 100644 --- a/akka-stream-testkit/src/test/scala/akka/stream/testkit/AkkaSpec.scala +++ b/akka-stream-testkit/src/test/scala/akka/stream/testkit/AkkaSpec.scala @@ -1,3 +1,7 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + package akka.stream.testkit import akka.testkit.TestKit diff --git a/akka-stream/src/main/resources/reference.conf b/akka-stream/src/main/resources/reference.conf index c1f4918269..2c53344c75 100644 --- a/akka-stream/src/main/resources/reference.conf +++ b/akka-stream/src/main/resources/reference.conf @@ -75,4 +75,11 @@ akka { } } } + + # configure overrides to ssl-configuration here (to be used by akka-streams, and akka-http – i.e. when serving https connections) + ssl-config { + # due to still supporting JDK6 in this release + # TODO once JDK 8 is required switch this to TLSv1.2 (or remove entirely, leave up to ssl-config to pick) + protocol = "TLSv1" + } } diff --git a/akka-stream/src/main/scala/akka/stream/impl/io/SslTlsCipherActor.scala b/akka-stream/src/main/scala/akka/stream/impl/io/SslTlsCipherActor.scala index dd305cebb7..323e08c853 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/io/SslTlsCipherActor.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/io/SslTlsCipherActor.scala @@ -9,11 +9,12 @@ import javax.net.ssl.SSLEngineResult.HandshakeStatus._ import javax.net.ssl.SSLEngineResult.Status._ import javax.net.ssl._ import akka.actor._ -import akka.stream.ActorMaterializerSettings +import akka.stream.{ ConnectionException, ActorMaterializerSettings } import akka.stream.impl.FanIn.InputBunch import akka.stream.impl.FanOut.OutputBunch import akka.stream.impl._ import akka.util.ByteString +import com.typesafe.sslconfig.akka.{ AkkaSSLConfig, SSLEngineConfigurator } import scala.annotation.tailrec import akka.stream.io._ @@ -41,7 +42,8 @@ private[akka] object SslTlsCipherActor { /** * INTERNAL API. */ -private[akka] class SslTlsCipherActor(settings: ActorMaterializerSettings, sslContext: SSLContext, +private[akka] class SslTlsCipherActor(settings: ActorMaterializerSettings, + sslContext: SSLContext, firstSession: NegotiateNewSession, role: Role, closing: Closing, hostInfo: Option[(String, Int)], tracing: Boolean) extends Actor with ActorLogging with Pump { @@ -139,12 +141,16 @@ private[akka] class SslTlsCipherActor(settings: ActorMaterializerSettings, sslCo val transportInChoppingBlock = new ChoppingBlock(TransportIn, "TransportIn") transportInChoppingBlock.prepare(transportInBuffer) + // ssl-config + val sslConfig = AkkaSSLConfig(context.system) + val hostnameVerifier = sslConfig.hostnameVerifier + val engine: SSLEngine = { val e = hostInfo match { case Some((hostname, port)) ⇒ sslContext.createSSLEngine(hostname, port) case None ⇒ sslContext.createSSLEngine() } - + sslConfig.sslEngineConfigurator.configure(e, sslContext) e.setUseClientMode(role == Client) e } @@ -162,6 +168,7 @@ private[akka] class SslTlsCipherActor(settings: ActorMaterializerSettings, sslCo case None ⇒ // do nothing } sslParameters foreach (p ⇒ engine.setSSLParameters(p)) + engine.beginHandshake() lastHandshakeStatus = engine.getHandshakeStatus } @@ -417,8 +424,15 @@ private[akka] class SslTlsCipherActor(settings: ActorMaterializerSettings, sslCo private def handshakeFinished(): Unit = { if (tracing) log.debug("handshake finished") - currentSession = engine.getSession - corkUser = false + val session = engine.getSession + + hostInfo.map(_._1) match { + case Some(hostname) if !hostnameVerifier.verify(hostname, session) ⇒ + fail(new ConnectionException(s"Hostname verification failed! Expected session to be for $hostname"), closeTransport = true) + case _ ⇒ + currentSession = session + corkUser = false + } } override def receive = inputBunch.subreceive.orElse[Any, Unit](outputBunch.subreceive) diff --git a/akka-stream/src/main/scala/akka/stream/io/SslTls.scala b/akka-stream/src/main/scala/akka/stream/io/SslTls.scala index 8433417969..35dbfd483f 100644 --- a/akka-stream/src/main/scala/akka/stream/io/SslTls.scala +++ b/akka-stream/src/main/scala/akka/stream/io/SslTls.scala @@ -114,7 +114,8 @@ object SslTls { private[akka] case class TlsModule(plainIn: Inlet[SslTlsOutbound], plainOut: Outlet[SslTlsInbound], cipherIn: Inlet[ByteString], cipherOut: Outlet[ByteString], shape: Shape, attributes: Attributes, - sslContext: SSLContext, firstSession: NegotiateNewSession, + sslContext: SSLContext, + firstSession: NegotiateNewSession, role: Role, closing: Closing, hostInfo: Option[(String, Int)]) extends Module { override def subModules: Set[Module] = Set.empty From d71c55174c3e9ec75e174fc48428f635e2549b44 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 21 Dec 2015 13:39:15 +0100 Subject: [PATCH 2/2] =doc explain hostname verification, recommend upgrading --- .../rst/java/http/client-side/https-support.rst | 15 +++++++++------ .../rst/scala/http/client-side/https-support.rst | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/akka-docs-dev/rst/java/http/client-side/https-support.rst b/akka-docs-dev/rst/java/http/client-side/https-support.rst index 68f392810b..6ccc818815 100644 --- a/akka-docs-dev/rst/java/http/client-side/https-support.rst +++ b/akka-docs-dev/rst/java/http/client-side/https-support.rst @@ -45,18 +45,21 @@ to rely on the configured default client-side ``HttpsContext``. If no custom ``HttpsContext`` is defined the default context uses Java's default TLS settings. Customizing the ``HttpsContext`` can make the Https client less secure. Understand what you are doing! -Hostname verification on Java 6 -------------------------------- +Hostname verification +--------------------- Hostname verification proves that the Akka HTTP client is actually communicating with the server it intended to communicate with. Without this check a man-in-the-middle attack is possible. In the attack scenario, an alternative certificate would be presented which was issued for another host name. Checking the host name in the certificate against the host name the connection was opened against is therefore vital. -The default ``HttpsContext`` enables hostname verification. Akka HTTP relies on a Java 7 feature to implement -the verification. To prevent an unintended security downgrade, accessing the default ``HttpsContext`` on Java 6 -will fail with an exception. Specifying a custom ``HttpsContext`` or customizing the default one is also possible -on Java 6. +The default ``HttpsContext`` enables hostname verification. Akka HTTP relies on the `Typesafe SSL-Config`_ library +to implement this and security options for SSL/TLS. Hostname verification is provided by the JDK +and used by Akka HTTP since Java 7, and on Java 6 the verification is implemented by ssl-config manually. +.. note:: + We highly recommend updating your Java runtime to the latest available release, + preferably JDK 8, as it includes this and many more security features related to TLS. +.. _Typesafe SSL-Config: https://github.com/typesafehub/ssl-config .. _akka.http.javadsl.Http: @github@/akka-http-core/src/main/scala/akka/http/javadsl/Http.scala diff --git a/akka-docs-dev/rst/scala/http/client-side/https-support.rst b/akka-docs-dev/rst/scala/http/client-side/https-support.rst index 5e155f7c87..be5620c65b 100644 --- a/akka-docs-dev/rst/scala/http/client-side/https-support.rst +++ b/akka-docs-dev/rst/scala/http/client-side/https-support.rst @@ -44,18 +44,21 @@ to rely on the configured default client-side ``HttpsContext``. If no custom ``HttpsContext`` is defined the default context uses Java's default TLS settings. Customizing the ``HttpsContext`` can make the Https client less secure. Understand what you are doing! -Hostname verification on Java 6 -------------------------------- +Hostname verification +--------------------- Hostname verification proves that the Akka HTTP client is actually communicating with the server it intended to communicate with. Without this check a man-in-the-middle attack is possible. In the attack scenario, an alternative certificate would be presented which was issued for another host name. Checking the host name in the certificate against the host name the connection was opened against is therefore vital. -The default ``HttpsContext`` enables hostname verification. Akka HTTP relies on a Java 7 feature to implement -the verification. To prevent an unintended security downgrade, accessing the default ``HttpsContext`` on Java 6 -will fail with an exception. Specifying a custom ``HttpsContext`` or customizing the default one is also possible -on Java 6. +The default ``HttpsContext`` enables hostname verification. Akka HTTP relies on the `Typesafe SSL-Config`_ library +to implement this and security options for SSL/TLS. Hostname verification is provided by the JDK +and used by Akka HTTP since Java 7, and on Java 6 the verification is implemented by ssl-config manually. +.. note:: + We highly recommend updating your Java runtime to the latest available release, + preferably JDK 8, as it includes this and many more security features related to TLS. +.. _Typesafe SSL-Config: https://github.com/typesafehub/ssl-config .. _akka.http.scaladsl.Http: @github@/akka-http-core/src/main/scala/akka/http/scaladsl/Http.scala