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:
parent
783d961142
commit
5d03902c5e
3 changed files with 101 additions and 122 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue