=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
470fae76f3

See https://github.com/typesafehub/ssl-config/issues/47
This commit is contained in:
Johannes Rudolph 2016-10-28 16:41:26 +02:00 committed by Konrad Malawski
parent 0127d4f424
commit c9854e4350
7 changed files with 288 additions and 4 deletions

View file

@ -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"
}

View file

@ -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] =

View file

@ -0,0 +1,212 @@
/*
* Copyright (C) 2015-2016 Lightbend Inc. <http://www.lightbend.com>
*/
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())
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (C) 2015-2016 Lightbend Inc. <http://www.lightbend.com>
*/
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
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (C) 2015-2016 Lightbend Inc. <http://www.lightbend.com>
*/
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))
}

View file

@ -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)

View file

@ -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.*"))