Merge pull request #19219 from ktoso/wip-sslconfig-ktoso
Apply ssl-config and include hostname validation
This commit is contained in:
commit
59a079d1f2
12 changed files with 189 additions and 47 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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 ⇒
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue