Add flag to enable mutual certificate authentication for old Akka Remote SSL transport (#21748)

* =rem #13874 further cleanup of SSLSettings / NettySSLSupport

* +rem #13874 allow requiring mutual authentication for old akka remote ssl transport
This commit is contained in:
Johannes Rudolph 2016-10-28 17:03:07 +02:00 committed by Konrad Malawski
parent 783d961142
commit 5d03902c5e
3 changed files with 101 additions and 122 deletions

View file

@ -625,6 +625,28 @@ akka {
# Setting a value here may require you to supply the appropriate cipher # Setting a value here may require you to supply the appropriate cipher
# suite (see enabled-algorithms section above) # suite (see enabled-algorithms section above)
random-number-generator = "" random-number-generator = ""
# Require mutual authentication between TLS peers
#
# Without mutual authentication only the peer that actively establishes a connection (TLS client side)
# checks if the passive side (TLS server side) sends over a trusted certificate. With the flag turned on,
# the passive side will also request and verify a certificate from the connecting peer.
#
# To prevent man-in-the-middle attacks you should enable this setting. For compatibility reasons it is
# still set to 'off' per default.
#
# Note: Nodes that are configured with this setting to 'on' might not be able to receive messages from nodes that
# run on older versions of akka-remote. This is because in older versions of Akka the active side of the remoting
# connection will not send over certificates.
#
# However, starting from the version this setting was added, even with this setting "off", the active side
# (TLS client side) will use the given key-store to send over a certificate if asked. A rolling upgrades from
# older versions of Akka can therefore work like this:
# - upgrade all nodes to an Akka version supporting this flag, keeping it off
# - then switch the flag on and do again a rolling upgrade of all nodes
# The first step ensures that all nodes will send over a certificate when asked to. The second
# step will ensure that all nodes finally enforce the secure checking of client certificates.
require-mutual-authentication = off
} }
} }

View file

@ -4,25 +4,26 @@
package akka.remote.transport.netty package akka.remote.transport.netty
import akka.ConfigurationException import java.io.{ FileInputStream, FileNotFoundException, IOException }
import akka.event.{ LogMarker, LoggingAdapter, MarkerLoggingAdapter } import java.security._
import java.util.concurrent.atomic.AtomicReference
import javax.net.ssl.{ KeyManagerFactory, SSLContext, TrustManagerFactory }
import akka.event.{ LogMarker, MarkerLoggingAdapter }
import akka.japi.Util._ import akka.japi.Util._
import akka.remote.RemoteTransportException import akka.remote.RemoteTransportException
import akka.remote.security.provider.AkkaProvider import akka.remote.security.provider.AkkaProvider
import com.typesafe.config.Config import com.typesafe.config.Config
import java.io.{ FileInputStream, FileNotFoundException, IOException }
import java.security._
import javax.net.ssl.{ KeyManagerFactory, SSLContext, TrustManager, TrustManagerFactory }
import org.jboss.netty.handler.ssl.SslHandler import org.jboss.netty.handler.ssl.SslHandler
import scala.annotation.tailrec
import scala.util.Try import scala.util.Try
/** /**
* INTERNAL API * INTERNAL API
*/ */
private[akka] class SSLSettings(config: Config) { private[akka] class SSLSettings(config: Config) {
import config.{ getString, getStringList } import config.{ getBoolean, getString, getStringList }
val SSLKeyStore = getString("key-store") val SSLKeyStore = getString("key-store")
val SSLTrustStore = getString("trust-store") val SSLTrustStore = getString("trust-store")
@ -36,25 +37,51 @@ private[akka] class SSLSettings(config: Config) {
val SSLProtocol = getString("protocol") val SSLProtocol = getString("protocol")
val SSLRandomNumberGenerator = getString("random-number-generator") val SSLRandomNumberGenerator = getString("random-number-generator")
}
/** val SSLRequireMutualAuthentication = getBoolean("require-mutual-authentication")
* INTERNAL API
*
* Used for adding SSL support to Netty pipeline
*/
private[akka] object NettySSLSupport {
Security addProvider AkkaProvider private val sslContext = new AtomicReference[SSLContext]()
@tailrec final def getOrCreateContext(log: MarkerLoggingAdapter): SSLContext =
sslContext.get() match {
case null
val newCtx = constructContext(log)
if (sslContext.compareAndSet(null, newCtx)) newCtx
else getOrCreateContext(log)
case ctx ctx
}
/** private def constructContext(log: MarkerLoggingAdapter): SSLContext =
* Construct a SSLHandler which can be inserted into a Netty server/client pipeline try {
*/ def loadKeystore(filename: String, password: String): KeyStore = {
def apply(settings: SSLSettings, log: MarkerLoggingAdapter, isClient: Boolean): SslHandler = val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
if (isClient) initializeClientSSL(settings, log) else initializeServerSSL(settings, log) val fin = new FileInputStream(filename)
try keyStore.load(fin, password.toCharArray) finally Try(fin.close())
keyStore
}
def initializeCustomSecureRandom(rngName: String, log: MarkerLoggingAdapter): SecureRandom = { val keyManagers = {
val rng = rngName match { val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
factory.init(loadKeystore(SSLKeyStore, SSLKeyStorePassword), SSLKeyPassword.toCharArray)
factory.getKeyManagers
}
val trustManagers = {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
trustManagerFactory.init(loadKeystore(SSLTrustStore, SSLTrustStorePassword))
trustManagerFactory.getTrustManagers
}
val rng = createSecureRandom(log)
val ctx = SSLContext.getInstance(SSLProtocol)
ctx.init(keyManagers, trustManagers, rng)
ctx
} catch {
case e: FileNotFoundException throw new RemoteTransportException("Server SSL connection could not be established because key store could not be loaded", e)
case e: IOException throw new RemoteTransportException("Server SSL connection could not be established because: " + e.getMessage, e)
case e: GeneralSecurityException throw new RemoteTransportException("Server SSL connection could not be established because SSL context could not be constructed", e)
}
def createSecureRandom(log: MarkerLoggingAdapter): SecureRandom = {
val rng = SSLRandomNumberGenerator match {
case r @ ("AES128CounterSecureRNG" | "AES256CounterSecureRNG") case r @ ("AES128CounterSecureRNG" | "AES256CounterSecureRNG")
log.debug("SSL random number generator set to: {}", r) log.debug("SSL random number generator set to: {}", r)
SecureRandom.getInstance(r, AkkaProvider) SecureRandom.getInstance(r, AkkaProvider)
@ -79,94 +106,27 @@ private[akka] object NettySSLSupport {
rng.nextInt() // prevent stall on first access rng.nextInt() // prevent stall on first access
rng rng
} }
}
def initializeClientSSL(settings: SSLSettings, log: MarkerLoggingAdapter): SslHandler = { /**
log.debug("Client SSL is enabled, initialising ...") * INTERNAL API
*
* Used for adding SSL support to Netty pipeline
*/
private[akka] object NettySSLSupport {
def constructClientContext(settings: SSLSettings, log: MarkerLoggingAdapter, trustStorePath: String, trustStorePassword: String, protocol: String): Option[SSLContext] = Security addProvider AkkaProvider
try {
val rng = initializeCustomSecureRandom(settings.SSLRandomNumberGenerator, log)
val trustManagers: Array[TrustManager] = {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
trustManagerFactory.init({
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType)
val fin = new FileInputStream(trustStorePath)
try trustStore.load(fin, trustStorePassword.toCharArray) finally Try(fin.close())
trustStore
})
trustManagerFactory.getTrustManagers
}
Option(SSLContext.getInstance(protocol)) map { ctx ctx.init(null, trustManagers, rng); ctx }
} catch {
case e: FileNotFoundException throw new RemoteTransportException("Client SSL connection could not be established because trust store could not be loaded", e)
case e: IOException throw new RemoteTransportException("Client SSL connection could not be established because: " + e.getMessage, e)
case e: GeneralSecurityException throw new RemoteTransportException("Client SSL connection could not be established because SSL context could not be constructed", e)
}
constructClientContext(settings, log, settings.SSLTrustStore, settings.SSLTrustStorePassword, settings.SSLProtocol) match { /**
case Some(context) * Construct a SSLHandler which can be inserted into a Netty server/client pipeline
log.debug("Using client SSL context to create SSLEngine ...") */
new SslHandler({ def apply(settings: SSLSettings, log: MarkerLoggingAdapter, isClient: Boolean): SslHandler = {
val sslEngine = context.createSSLEngine val sslEngine = settings.getOrCreateContext(log).createSSLEngine // TODO: pass host information to enable host verification
sslEngine.setUseClientMode(true) sslEngine.setUseClientMode(isClient)
sslEngine.setEnabledCipherSuites(settings.SSLEnabledAlgorithms.toArray) sslEngine.setEnabledCipherSuites(settings.SSLEnabledAlgorithms.toArray)
sslEngine.setEnabledProtocols(Array(settings.SSLProtocol)) sslEngine.setEnabledProtocols(Array(settings.SSLProtocol))
sslEngine
})
case None
throw new GeneralSecurityException(
"""Failed to initialize client SSL because SSL context could not be found." +
"Make sure your settings are correct: [trust-store: %s] [protocol: %s]""".format(
settings.SSLTrustStore,
settings.SSLProtocol))
}
}
def initializeServerSSL(settings: SSLSettings, log: MarkerLoggingAdapter): SslHandler = { if (!isClient && settings.SSLRequireMutualAuthentication) sslEngine.setNeedClientAuth(true)
log.debug("Server SSL is enabled, initialising ...") new SslHandler(sslEngine)
def constructServerContext(settings: SSLSettings, log: MarkerLoggingAdapter, keyStorePath: String, keyStorePassword: String, keyPassword: String, protocol: String): Option[SSLContext] =
try {
val rng = initializeCustomSecureRandom(settings.SSLRandomNumberGenerator, log)
val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
factory.init({
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
val fin = new FileInputStream(keyStorePath)
try keyStore.load(fin, keyStorePassword.toCharArray) finally Try(fin.close())
keyStore
}, keyPassword.toCharArray)
val trustManagers: Array[TrustManager] = {
val pwd = settings.SSLTrustStorePassword.toCharArray
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
trustManagerFactory.init({
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType)
val fin = new FileInputStream(settings.SSLTrustStore)
try trustStore.load(fin, pwd) finally Try(fin.close())
trustStore
})
trustManagerFactory.getTrustManagers
}
Option(SSLContext.getInstance(protocol)) map { ctx ctx.init(factory.getKeyManagers, trustManagers, rng); ctx }
} catch {
case e: FileNotFoundException throw new RemoteTransportException("Server SSL connection could not be established because key store could not be loaded", e)
case e: IOException throw new RemoteTransportException("Server SSL connection could not be established because: " + e.getMessage, e)
case e: GeneralSecurityException throw new RemoteTransportException("Server SSL connection could not be established because SSL context could not be constructed", e)
}
constructServerContext(settings, log, settings.SSLKeyStore, settings.SSLKeyStorePassword, settings.SSLKeyPassword, settings.SSLProtocol) match {
case Some(context)
log.debug("Using server SSL context to create SSLEngine ...")
val sslEngine = context.createSSLEngine
sslEngine.setUseClientMode(false)
sslEngine.setEnabledCipherSuites(settings.SSLEnabledAlgorithms.toArray)
new SslHandler(sslEngine)
case None throw new GeneralSecurityException(
"""Failed to initialize server SSL because SSL context could not be found.
Make sure your settings are correct: [key-store: %s] [key-store-password: %s] [protocol: %s]""".format(
settings.SSLKeyStore,
settings.SSLKeyStorePassword,
settings.SSLProtocol))
}
} }
} }

View file

@ -3,24 +3,21 @@
*/ */
package akka.remote package akka.remote
import akka.testkit._
import akka.actor._
import com.typesafe.config._
import scala.concurrent.Future
import scala.reflect.classTag
import akka.pattern.ask
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import akka.util.Timeout import akka.actor._
import akka.event.NoMarkerLogging
import scala.concurrent.Await import akka.pattern.ask
import scala.concurrent.duration._ import akka.remote.Configuration.{ CipherConfig, getCipherConfig }
import akka.event.{ NoLogging, NoMarkerLogging }
import akka.remote.transport.netty.{ NettySSLSupport, SSLSettings } import akka.remote.transport.netty.{ NettySSLSupport, SSLSettings }
import Configuration.{ CipherConfig, getCipherConfig } import akka.testkit._
import akka.util.Timeout
import com.typesafe.config._
import org.uncommons.maths.random.RandomDotOrgSeedGenerator import org.uncommons.maths.random.RandomDotOrgSeedGenerator
import scala.concurrent.{ Await, Future }
import scala.concurrent.duration._
import scala.reflect.classTag
import scala.util.control.NonFatal import scala.util.control.NonFatal
object Configuration { object Configuration {
@ -67,13 +64,13 @@ object Configuration {
val fullConfig = config.withFallback(AkkaSpec.testConf).withFallback(ConfigFactory.load).getConfig("akka.remote.netty.ssl.security") val fullConfig = config.withFallback(AkkaSpec.testConf).withFallback(ConfigFactory.load).getConfig("akka.remote.netty.ssl.security")
val settings = new SSLSettings(fullConfig) val settings = new SSLSettings(fullConfig)
val rng = NettySSLSupport.initializeCustomSecureRandom(settings.SSLRandomNumberGenerator, NoMarkerLogging) val rng = settings.createSecureRandom(NoMarkerLogging)
rng.nextInt() // Has to work rng.nextInt() // Has to work
val sRng = settings.SSLRandomNumberGenerator val sRng = settings.SSLRandomNumberGenerator
rng.getAlgorithm == sRng || (throw new NoSuchAlgorithmException(sRng)) rng.getAlgorithm == sRng || (throw new NoSuchAlgorithmException(sRng))
val engine = NettySSLSupport.initializeClientSSL(settings, NoMarkerLogging).getEngine val engine = NettySSLSupport(settings, NoMarkerLogging, isClient = true).getEngine
val gotAllSupported = enabled.toSet diff engine.getSupportedCipherSuites.toSet val gotAllSupported = enabled.toSet diff engine.getSupportedCipherSuites.toSet
val gotAllEnabled = enabled.toSet diff engine.getEnabledCipherSuites.toSet val gotAllEnabled = enabled.toSet diff engine.getEnabledCipherSuites.toSet
gotAllSupported.isEmpty || (throw new IllegalArgumentException("Cipher Suite not supported: " + gotAllSupported)) gotAllSupported.isEmpty || (throw new IllegalArgumentException("Cipher Suite not supported: " + gotAllSupported))