Skip to content

Commit

Permalink
=pro merge ssl-config-akka from ssl-config project into akka-stream
Browse files Browse the repository at this point in the history
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
lightbend/ssl-config@470fae7

See lightbend/ssl-config#47
  • Loading branch information
jrudolph committed Oct 28, 2016
1 parent ff78b84 commit 55f301d
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 3 deletions.
6 changes: 6 additions & 0 deletions akka-stream/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* 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

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())
}

}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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))

}
5 changes: 3 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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-RC1"
Expand Down Expand Up @@ -62,7 +63,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
Expand Down Expand Up @@ -173,8 +174,8 @@ object Dependencies {
// akka stream

lazy val stream = l ++= Seq[sbt.ModuleID](
sslConfigAkka,
reactiveStreams,
sslConfigCore,
Test.junitIntf,
Test.scalatest.value)

Expand Down
8 changes: 7 additions & 1 deletion project/OSGi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._

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

Expand Down

0 comments on commit 55f301d

Please sign in to comment.