From c9854e43503c20a2c5dfa9f8c73acfe27e07a3ee Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Fri, 28 Oct 2016 16:41:26 +0200 Subject: [PATCH] =pro merge ssl-config-akka from ssl-config project into akka-stream (#21551) This will fix the cyclic dependency issue between the ssl-config repo and akka. It would have been better if akka-stream would not require these changes at all but com.typesafe.sslconfig.akka.AkkaSSLConfig is part of the public interface of akka-stream's TLS stage. So, this can only be fixed in the next major version. Source code was copied over from the tree at commit https://github.com/typesafehub/ssl-config/commit/470fae76f38dbbd4a42a4000675f66fa67e5f1da See https://github.com/typesafehub/ssl-config/issues/47 --- akka-stream/src/main/resources/reference.conf | 6 + .../main/scala/akka/stream/scaladsl/TLS.scala | 2 +- .../sslconfig/akka/AkkaSSLConfig.scala | 212 ++++++++++++++++++ .../akka/SSLEngineConfigurator.scala | 27 +++ .../akka/util/AkkaLoggerBridge.scala | 32 +++ project/Dependencies.scala | 5 +- project/OSGi.scala | 8 +- 7 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala create mode 100644 akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala create mode 100644 akka-stream/src/main/scala/com/typesafe/sslconfig/akka/util/AkkaLoggerBridge.scala diff --git a/akka-stream/src/main/resources/reference.conf b/akka-stream/src/main/resources/reference.conf index 845ade285e..4de4cbb94e 100644 --- a/akka-stream/src/main/resources/reference.conf +++ b/akka-stream/src/main/resources/reference.conf @@ -96,3 +96,9 @@ akka { protocol = "TLSv1.2" } } + +# ssl configuration +# folded in from former ssl-config-akka module +ssl-config { + logger = "com.typesafe.sslconfig.akka.util.AkkaLoggerBridge" +} \ No newline at end of file diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala index ca54ea50c1..fa77dbb87a 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala @@ -64,7 +64,7 @@ object TLS { * configured using [[javax.net.ssl.SSLParameters.setEndpointIdentificationAlgorithm]]. */ def apply( - sslContext: SSLContext, + sslContext: SSLContext, // TODO: in 2.5.x replace sslContext and sslConfig by generic SSLEngine constructor function, see https://github.com/akka/akka/issues/21753 sslConfig: Option[AkkaSSLConfig], firstSession: NegotiateNewSession, role: TLSRole, closing: TLSClosing = IgnoreComplete, hostInfo: Option[(String, Int)] = None): scaladsl.BidiFlow[SslTlsOutbound, ByteString, ByteString, SslTlsInbound, NotUsed] = diff --git a/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala new file mode 100644 index 0000000000..ce51abad73 --- /dev/null +++ b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2015-2016 Lightbend Inc. + */ + +package com.typesafe.sslconfig.akka + +import java.security.KeyStore +import java.security.cert.CertPathValidatorException +import java.util.Collections +import javax.net.ssl._ + +import akka.actor._ +import akka.event.Logging +import com.typesafe.sslconfig.akka.util.AkkaLoggerFactory +import com.typesafe.sslconfig.ssl._ +import com.typesafe.sslconfig.util.LoggerFactory + +// TODO: remove again in 2.5.x, see https://github.com/akka/akka/issues/21753 +object AkkaSSLConfig extends ExtensionId[AkkaSSLConfig] with ExtensionIdProvider { + + //////////////////// EXTENSION SETUP /////////////////// + + override def get(system: ActorSystem): AkkaSSLConfig = super.get(system) + def apply()(implicit system: ActorSystem): AkkaSSLConfig = super.apply(system) + + override def lookup() = AkkaSSLConfig + + override def createExtension(system: ExtendedActorSystem): AkkaSSLConfig = + new AkkaSSLConfig(system, defaultSSLConfigSettings(system)) + + def defaultSSLConfigSettings(system: ActorSystem): SSLConfigSettings = { + val akkaOverrides = system.settings.config.getConfig("akka.ssl-config") + val defaults = system.settings.config.getConfig("ssl-config") + SSLConfigFactory.parse(akkaOverrides withFallback defaults) + } + +} + +final class AkkaSSLConfig(system: ExtendedActorSystem, val config: SSLConfigSettings) extends Extension { + + private val mkLogger = new AkkaLoggerFactory(system) + + private val log = Logging(system, getClass) + log.debug("Initializing AkkaSSLConfig extension...") + + /** Can be used to modify the underlying config, most typically used to change a few values in the default config */ + def withSettings(c: SSLConfigSettings): AkkaSSLConfig = + new AkkaSSLConfig(system, c) + + /** + * Returns a new [[AkkaSSLConfig]] instance with the settings changed by the given function. + * Please note that the ActorSystem-wide extension always remains configured via typesafe config, + * custom ones can be created for special-handling specific connections + */ + def mapSettings(f: SSLConfigSettings ⇒ SSLConfigSettings): AkkaSSLConfig = + new AkkaSSLConfig(system, f(config)) + + /** + * Returns a new [[AkkaSSLConfig]] instance with the settings changed by the given function. + * Please note that the ActorSystem-wide extension always remains configured via typesafe config, + * custom ones can be created for special-handling specific connections + * + * Java API + */ + // Not same signature as mapSettings to allow latter deprecation of this once we hit Scala 2.12 + def convertSettings(f: java.util.function.Function[SSLConfigSettings, SSLConfigSettings]): AkkaSSLConfig = + new AkkaSSLConfig(system, f.apply(config)) + + val hostnameVerifier = buildHostnameVerifier(config) + + val sslEngineConfigurator = { + val sslContext = if (config.default) { + log.info("ssl-config.default is true, using the JDK's default SSLContext") + validateDefaultTrustManager(config) + SSLContext.getDefault + } else { + // break out the static methods as much as we can... + val keyManagerFactory = buildKeyManagerFactory(config) + val trustManagerFactory = buildTrustManagerFactory(config) + new ConfigSSLContextBuilder(mkLogger, config, keyManagerFactory, trustManagerFactory).build() + } + + // protocols! + val defaultParams = sslContext.getDefaultSSLParameters + val defaultProtocols = defaultParams.getProtocols + val protocols = configureProtocols(defaultProtocols, config) + + // ciphers! + val defaultCiphers = defaultParams.getCipherSuites + val cipherSuites = configureCipherSuites(defaultCiphers, config) + + // apply "loose" settings + // !! SNI! + looseDisableSNI(defaultParams) + + new DefaultSSLEngineConfigurator(config, protocols, cipherSuites) + } + + ////////////////// CONFIGURING ////////////////////// + + def buildKeyManagerFactory(ssl: SSLConfigSettings): KeyManagerFactoryWrapper = { + val keyManagerAlgorithm = ssl.keyManagerConfig.algorithm + new DefaultKeyManagerFactoryWrapper(keyManagerAlgorithm) + } + + def buildTrustManagerFactory(ssl: SSLConfigSettings): TrustManagerFactoryWrapper = { + val trustManagerAlgorithm = ssl.trustManagerConfig.algorithm + new DefaultTrustManagerFactoryWrapper(trustManagerAlgorithm) + } + + def buildHostnameVerifier(conf: SSLConfigSettings): HostnameVerifier = { + val clazz: Class[HostnameVerifier] = + if (config.loose.disableHostnameVerification) classOf[DisabledComplainingHostnameVerifier].asInstanceOf[Class[HostnameVerifier]] + else config.hostnameVerifierClass.asInstanceOf[Class[HostnameVerifier]] + + val v = system.dynamicAccess.createInstanceFor[HostnameVerifier](clazz, Nil) + .orElse(system.dynamicAccess.createInstanceFor[HostnameVerifier](clazz, List(classOf[LoggerFactory] → mkLogger))) + .getOrElse(throw new Exception("Unable to obtain hostname verifier for class: " + clazz)) + + log.debug("buildHostnameVerifier: created hostname verifier: {}", v) + v + } + + def validateDefaultTrustManager(sslConfig: SSLConfigSettings) { + // If we are using a default SSL context, we can't filter out certificates with weak algorithms + // We ALSO don't have access to the trust manager from the SSLContext without doing horrible things + // with reflection. + // + // However, given that the default SSLContextImpl will call out to the TrustManagerFactory and any + // configuration with system properties will also apply with the factory, we can use the factory + // method to recreate the trust manager and validate the trust certificates that way. + // + // This is really a last ditch attempt to satisfy https://wiki.mozilla.org/CA:MD5and1024 on root certificates. + // + // http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7-b147/sun/security/ssl/SSLContextImpl.java#79 + + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + tmf.init(null.asInstanceOf[KeyStore]) + val trustManager: X509TrustManager = tmf.getTrustManagers()(0).asInstanceOf[X509TrustManager] + + // val disabledKeyAlgorithms = sslConfig.disabledKeyAlgorithms.getOrElse(Algorithms.disabledKeyAlgorithms) // was Option + val disabledKeyAlgorithms = sslConfig.disabledKeyAlgorithms.mkString(",") // TODO Sub optimal, we got a Seq... + val constraints = AlgorithmConstraintsParser.parseAll(AlgorithmConstraintsParser.line, disabledKeyAlgorithms).get.toSet + val algorithmChecker = new AlgorithmChecker(mkLogger, keyConstraints = constraints, signatureConstraints = Set()) + for (cert ← trustManager.getAcceptedIssuers) { + try { + algorithmChecker.checkKeyAlgorithms(cert) + } catch { + case e: CertPathValidatorException ⇒ + log.warning("You are using ssl-config.default=true and have a weak certificate in your default trust store! (You can modify akka.ssl-config.disabledKeyAlgorithms to remove this message.)", e) + } + } + } + + def configureProtocols(existingProtocols: Array[String], sslConfig: SSLConfigSettings): Array[String] = { + val definedProtocols = sslConfig.enabledProtocols match { + case Some(configuredProtocols) ⇒ + // If we are given a specific list of protocols, then return it in exactly that order, + // assuming that it's actually possible in the SSL context. + configuredProtocols.filter(existingProtocols.contains).toArray + + case None ⇒ + // Otherwise, we return the default protocols in the given list. + Protocols.recommendedProtocols.filter(existingProtocols.contains) + } + + val allowWeakProtocols = sslConfig.loose.allowWeakProtocols + if (!allowWeakProtocols) { + val deprecatedProtocols = Protocols.deprecatedProtocols + for (deprecatedProtocol ← deprecatedProtocols) { + if (definedProtocols.contains(deprecatedProtocol)) { + throw new IllegalStateException(s"Weak protocol $deprecatedProtocol found in ssl-config.protocols!") + } + } + } + definedProtocols + } + + def configureCipherSuites(existingCiphers: Array[String], sslConfig: SSLConfigSettings): Array[String] = { + val definedCiphers = sslConfig.enabledCipherSuites match { + case Some(configuredCiphers) ⇒ + // If we are given a specific list of ciphers, return it in that order. + configuredCiphers.filter(existingCiphers.contains(_)).toArray + + case None ⇒ + Ciphers.recommendedCiphers.filter(existingCiphers.contains(_)).toArray + } + + val allowWeakCiphers = sslConfig.loose.allowWeakCiphers + if (!allowWeakCiphers) { + val deprecatedCiphers = Ciphers.deprecatedCiphers + for (deprecatedCipher ← deprecatedCiphers) { + if (definedCiphers.contains(deprecatedCipher)) { + throw new IllegalStateException(s"Weak cipher $deprecatedCipher found in ssl-config.ciphers!") + } + } + } + definedCiphers + } + + // LOOSE SETTINGS // + + private def looseDisableSNI(defaultParams: SSLParameters): Unit = if (config.loose.disableSNI) { + // this will be logged once for each AkkaSSLConfig + log.warning("You are using ssl-config.loose.disableSNI=true! " + + "It is strongly discouraged to disable Server Name Indication, as it is crucial to preventing man-in-the-middle attacks.") + + defaultParams.setServerNames(Collections.emptyList()) + defaultParams.setSNIMatchers(Collections.emptyList()) + } + +} \ No newline at end of file diff --git a/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala new file mode 100644 index 0000000000..5eaf793150 --- /dev/null +++ b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015-2016 Lightbend Inc. + */ + +package com.typesafe.sslconfig.akka + +import javax.net.ssl.{ SSLContext, SSLEngine } + +import com.typesafe.sslconfig.ssl.SSLConfigSettings + +/** + * Gives the chance to configure the SSLContext before it is going to be used. + * The passed in context will be already set in client mode and provided with hostInfo during initialization. + */ +trait SSLEngineConfigurator { + def configure(engine: SSLEngine, sslContext: SSLContext): SSLEngine +} + +final class DefaultSSLEngineConfigurator(config: SSLConfigSettings, enabledProtocols: Array[String], enabledCipherSuites: Array[String]) + extends SSLEngineConfigurator { + def configure(engine: SSLEngine, sslContext: SSLContext): SSLEngine = { + engine.setSSLParameters(sslContext.getDefaultSSLParameters) + engine.setEnabledProtocols(enabledProtocols) + engine.setEnabledCipherSuites(enabledCipherSuites) + engine + } +} diff --git a/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/util/AkkaLoggerBridge.scala b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/util/AkkaLoggerBridge.scala new file mode 100644 index 0000000000..b1e16cd73d --- /dev/null +++ b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/util/AkkaLoggerBridge.scala @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015-2016 Lightbend Inc. + */ + +package com.typesafe.sslconfig.akka.util + +import akka.actor.ActorSystem +import akka.event.{ DummyClassForStringSources, EventStream } +import akka.event.Logging._ +import com.typesafe.sslconfig.util.{ LoggerFactory, NoDepsLogger } + +final class AkkaLoggerFactory(system: ActorSystem) extends LoggerFactory { + override def apply(clazz: Class[_]): NoDepsLogger = new AkkaLoggerBridge(system.eventStream, clazz) + + override def apply(name: String): NoDepsLogger = new AkkaLoggerBridge(system.eventStream, name, classOf[DummyClassForStringSources]) +} + +class AkkaLoggerBridge(bus: EventStream, logSource: String, logClass: Class[_]) extends NoDepsLogger { + def this(bus: EventStream, clazz: Class[_]) { this(bus, clazz.getCanonicalName, clazz) } + + override def isDebugEnabled: Boolean = true + + override def debug(msg: String): Unit = bus.publish(Debug(logSource, logClass, msg)) + + override def info(msg: String): Unit = bus.publish(Info(logSource, logClass, msg)) + + override def warn(msg: String): Unit = bus.publish(Warning(logSource, logClass, msg)) + + override def error(msg: String): Unit = bus.publish(Error(logSource, logClass, msg)) + override def error(msg: String, throwable: Throwable): Unit = bus.publish(Error(logSource, logClass, msg)) + +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index a9c910e031..bdafe911c2 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -14,6 +14,7 @@ object Dependencies { lazy val scalaCheckVersion = settingKey[String]("The version of ScalaCheck to use.") lazy val java8CompatVersion = settingKey[String]("The version of scala-java8-compat to use.") val junitVersion = "4.12" + val sslConfigVersion = "0.2.1" val Versions = Seq( crossScalaVersions := Seq("2.11.8"), // "2.12.0-RC2" @@ -61,7 +62,7 @@ object Dependencies { val reactiveStreams = "org.reactivestreams" % "reactive-streams" % "1.0.0" // CC0 // ssl-config - val sslConfigAkka = "com.typesafe" %% "ssl-config-akka" % "0.2.1" // ApacheV2 + val sslConfigCore = "com.typesafe" %% "ssl-config-core" % sslConfigVersion // ApacheV2 // For akka-http-testkit-java val junit = "junit" % "junit" % junitVersion // Common Public License 1.0 @@ -172,8 +173,8 @@ object Dependencies { // akka stream lazy val stream = l ++= Seq[sbt.ModuleID]( - sslConfigAkka, reactiveStreams, + sslConfigCore, Test.junitIntf, Test.scalatest.value) diff --git a/project/OSGi.scala b/project/OSGi.scala index 869bffd371..3018d7c284 100644 --- a/project/OSGi.scala +++ b/project/OSGi.scala @@ -5,6 +5,7 @@ package akka import com.typesafe.sbt.osgi.OsgiKeys import com.typesafe.sbt.osgi.SbtOsgi._ +import com.typesafe.sbt.osgi.SbtOsgi.autoImport._ import sbt._ import sbt.Keys._ @@ -81,7 +82,12 @@ object OSGi { val httpJackson = exports(Seq("akka.http.javadsl.marshallers.jackson")) - val stream = exports(Seq("akka.stream.*"), imports = Seq(scalaJava8CompatImport())) + val stream = + exports( + packages = Seq("akka.stream.*", + "com.typesafe.sslconfig.akka.*"), + imports = Seq(scalaJava8CompatImport())) ++ + Seq(OsgiKeys.requireBundle := Seq(s"""com.typesafe.sslconfig;bundle-version="${Dependencies.sslConfigVersion}"""")) val streamTestkit = exports(Seq("akka.stream.testkit.*"))