From f25e962f7eb2d547773786be5c8d64631a92019f Mon Sep 17 00:00:00 2001 From: Peter Badenhorst Date: Fri, 25 May 2012 00:59:17 +0200 Subject: [PATCH 1/5] Added changes to Netty pipelines to support SSL/TLS. Fixes #1978 1) Netty server and client pipelines updated to conditionally load keystore/truststore if SSL is enabled in the config 2) Supports any available encryption protocol via 'ssl-protocol' 3) Supported encryption algorithms are specified via 'ssl-encryption-protocol' config key --- akka-remote/src/main/resources/reference.conf | 27 +++++++ .../main/scala/akka/remote/netty/Client.scala | 60 +++++++++++++++- .../main/scala/akka/remote/netty/Server.scala | 70 +++++++++++++++++-- .../scala/akka/remote/netty/Settings.scala | 41 +++++++++++ .../akka/remote/Ticket1978ConfigSpec.scala | 46 ++++++++++++ 5 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index 4512ea3a98..c96ec951d7 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -151,6 +151,33 @@ akka { # (O) Maximum time window that a client should try to reconnect for reconnection-time-window = 600s + + # (I&O) Enable SSL/TLS encryption. + # This must be enabled on both the client and server to work. + enable-ssl = off + + # (I) This is the Java Key Store used by the server connection + ssl-key-store = "keystore" + + # This password is used for decrypting the key store + ssl-key-store-password = "changeme" + + # (O) This is the Java Key Store used by the client connection + ssl-trust-store = "truststore" + + # This password is used for decrypting the trust store + ssl-trust-store-password = "changeme" + + # (I&O) Protocol to use for SSL encryption, choose from: + # Java 6 & 7: + # SSLv3, TLSv1, + # Java 7: + # TLSv1.1, TLSv1.2 + ssl-protocol = "TLSv1" + + # You need to install the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256 + # More info here: http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#SunJCEProvider + ssl-supported-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] } } } diff --git a/akka-remote/src/main/scala/akka/remote/netty/Client.scala b/akka-remote/src/main/scala/akka/remote/netty/Client.scala index 7baf3011ee..36df8b4d1f 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Client.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Client.scala @@ -20,10 +20,15 @@ import akka.actor.ActorRef import org.jboss.netty.channel.ChannelFutureListener import akka.remote.RemoteClientWriteFailed import java.net.InetAddress +import java.security.{ SecureRandom, KeyStore, GeneralSecurityException } import org.jboss.netty.util.TimerTask import org.jboss.netty.util.Timeout import java.util.concurrent.TimeUnit import org.jboss.netty.handler.timeout.{ IdleState, IdleStateEvent, IdleStateAwareChannelHandler, IdleStateHandler } +import java.security.cert.X509Certificate +import javax.net.ssl.{ SSLContext, X509TrustManager, TrustManagerFactory, TrustManager } +import org.jboss.netty.handler.ssl.SslHandler +import java.io.FileInputStream class RemoteClientMessageBufferException(message: String, cause: Throwable) extends AkkaException(message, cause) { def this(msg: String) = this(msg, null) @@ -329,7 +334,53 @@ class ActiveRemoteClientPipelineFactory( import client.netty.settings + def initTLS(trustStorePath: String, trustStorePassword: String): Option[SSLContext] = { + if (trustStorePath != null && trustStorePassword != null) + try { + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) + val stream = new FileInputStream(trustStorePath) + trustStore.load(stream, trustStorePassword.toCharArray) + trustManagerFactory.init(trustStore); + val trustManagers: Array[TrustManager] = trustManagerFactory.getTrustManagers + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustManagers, new SecureRandom()) + Some(sslContext) + } catch { + case e: GeneralSecurityException ⇒ { + client.log.error(e, "TLS connection could not be established. TLS is not used!"); + None + } + } + else { + client.log.error("TLS connection could not be established because trust store details are missing") + None + } + } + + def getSSLHandler_? : Option[SslHandler] = { + val sslContext: Option[SSLContext] = { + if (settings.EnableSSL) { + client.log.debug("Client SSL is enabled, initialising ...") + initTLS(settings.SSLTrustStore.get, settings.SSLTrustStorePassword.get) + } else { + None + } + } + if (sslContext.isDefined) { + client.log.debug("Client Using SSL context to create SSLEngine ...") + val sslEngine = sslContext.get.createSSLEngine + sslEngine.setUseClientMode(true) + sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) + Some(new SslHandler(sslEngine)) + } else { + None + } + } + def getPipeline: ChannelPipeline = { + val sslHandler = getSSLHandler_? val timeout = new IdleStateHandler(client.netty.timer, settings.ReadTimeout.toSeconds.toInt, settings.WriteTimeout.toSeconds.toInt, @@ -340,7 +391,14 @@ class ActiveRemoteClientPipelineFactory( val messageEnc = new RemoteMessageEncoder(client.netty) val remoteClient = new ActiveRemoteClientHandler(name, bootstrap, remoteAddress, localAddress, client.netty.timer, client) - new StaticChannelPipeline(timeout, lenDec, messageDec, lenPrep, messageEnc, executionHandler, remoteClient) + val stages: List[ChannelHandler] = timeout :: lenDec :: messageDec :: lenPrep :: messageEnc :: executionHandler :: remoteClient :: Nil + if (sslHandler.isDefined) { + client.log.debug("Client creating pipeline with SSL handler...") + new StaticChannelPipeline(sslHandler.get :: stages: _*) + } else { + client.log.debug("Client creating pipeline without SSL handler...") + new StaticChannelPipeline(stages: _*) + } } } diff --git a/akka-remote/src/main/scala/akka/remote/netty/Server.scala b/akka-remote/src/main/scala/akka/remote/netty/Server.scala index 7e4d1eaaa9..2f572ba1d7 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Server.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Server.scala @@ -5,6 +5,7 @@ package akka.remote.netty import java.net.InetSocketAddress import java.util.concurrent.Executors +import java.io.FileNotFoundException import scala.Option.option2Iterable import org.jboss.netty.bootstrap.ServerBootstrap import org.jboss.netty.channel.ChannelHandler.Sharable @@ -12,13 +13,17 @@ import org.jboss.netty.channel.group.ChannelGroup import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory import org.jboss.netty.handler.codec.frame.{ LengthFieldPrepender, LengthFieldBasedFrameDecoder } import org.jboss.netty.handler.execution.ExecutionHandler -import akka.event.Logging import akka.remote.RemoteProtocol.{ RemoteControlProtocol, CommandType, AkkaRemoteProtocol } import akka.remote.{ RemoteServerShutdown, RemoteServerError, RemoteServerClientDisconnected, RemoteServerClientConnected, RemoteServerClientClosed, RemoteProtocol, RemoteMessage } import akka.actor.Address import java.net.InetAddress import akka.actor.ActorSystemImpl import org.jboss.netty.channel._ +import org.jboss.netty.handler.ssl.SslHandler +import java.security.{ SecureRandom, KeyStore, GeneralSecurityException } +import javax.net.ssl.{ KeyManagerFactory, SSLContext } +import java.io.FileInputStream +import akka.event.{ LoggingAdapter, Logging } class NettyRemoteServer(val netty: NettyRemoteTransport) { @@ -26,6 +31,8 @@ class NettyRemoteServer(val netty: NettyRemoteTransport) { val ip = InetAddress.getByName(settings.Hostname) + lazy val log = Logging(netty.system, "NettyRemoteServer(" + ip + ")") + private val factory = settings.UseDispatcherForIO match { case Some(id) ⇒ @@ -42,7 +49,7 @@ class NettyRemoteServer(val netty: NettyRemoteTransport) { private val bootstrap = { val b = new ServerBootstrap(factory) - b.setPipelineFactory(new RemoteServerPipelineFactory(openChannels, executionHandler, netty)) + b.setPipelineFactory(new RemoteServerPipelineFactory(openChannels, executionHandler, netty, log)) b.setOption("backlog", settings.Backlog) b.setOption("tcpNoDelay", true) b.setOption("child.keepAlive", true) @@ -85,11 +92,60 @@ class NettyRemoteServer(val netty: NettyRemoteTransport) { class RemoteServerPipelineFactory( val openChannels: ChannelGroup, val executionHandler: ExecutionHandler, - val netty: NettyRemoteTransport) extends ChannelPipelineFactory { + val netty: NettyRemoteTransport, + val log: LoggingAdapter) extends ChannelPipelineFactory { import netty.settings + def initTLS(keyStorePath: String, keyStorePassword: String): Option[SSLContext] = { + if (keyStorePath != null && keyStorePassword != null) { + try { + val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + val stream = new FileInputStream(keyStorePath) + keyStore.load(stream, keyStorePassword.toCharArray) + factory.init(keyStore, keyStorePassword.toCharArray) + val sslContext = SSLContext.getInstance(settings.SSLProtocol.get) + sslContext.init(factory.getKeyManagers, null, new SecureRandom()) + Some(sslContext) + } catch { + case e: FileNotFoundException ⇒ { + log.error(e, "TLS connection could not be established because keystore could not be loaded") + None + } + case e: GeneralSecurityException ⇒ { + log.error(e, "TLS connection could not be established") + None + } + } + } else { + log.error("TLS connection could not be established because key store details are missing") + None + } + } + + def getSSLHandler_? : Option[SslHandler] = { + val sslContext: Option[SSLContext] = { + if (settings.EnableSSL) { + log.debug("SSL is enabled, initialising...") + initTLS(settings.SSLKeyStore.get, settings.SSLKeyStorePassword.get) + } else { + None + } + } + if (sslContext.isDefined) { + log.debug("Using SSL context to create SSLEngine...") + val sslEngine = sslContext.get.createSSLEngine + sslEngine.setUseClientMode(false) + sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) + Some(new SslHandler(sslEngine)) + } else { + None + } + } + def getPipeline: ChannelPipeline = { + val sslHandler = getSSLHandler_? val lenDec = new LengthFieldBasedFrameDecoder(settings.MessageFrameSize, 0, 4, 0, 4) val lenPrep = new LengthFieldPrepender(4) val messageDec = new RemoteMessageDecoder @@ -98,7 +154,13 @@ class RemoteServerPipelineFactory( val authenticator = if (settings.RequireCookie) new RemoteServerAuthenticationHandler(settings.SecureCookie) :: Nil else Nil val remoteServer = new RemoteServerHandler(openChannels, netty) val stages: List[ChannelHandler] = lenDec :: messageDec :: lenPrep :: messageEnc :: executionHandler :: authenticator ::: remoteServer :: Nil - new StaticChannelPipeline(stages: _*) + if (sslHandler.isDefined) { + log.debug("Creating pipeline with SSL handler...") + new StaticChannelPipeline(sslHandler.get :: stages: _*) + } else { + log.debug("Creating pipeline without SSL handler...") + new StaticChannelPipeline(stages: _*) + } } } diff --git a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala index e2f69d77b5..2105620c18 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala @@ -73,4 +73,45 @@ class NettySettings(config: Config, val systemName: String) { case sz ⇒ sz } + val SSLKeyStore = getString("ssl-key-store") match { + case "" ⇒ None + case keyStore ⇒ Some(keyStore) + } + + val SSLTrustStore = getString("ssl-trust-store") match { + case "" ⇒ None + case trustStore ⇒ Some(trustStore) + } + + val SSLKeyStorePassword = getString("ssl-key-store-password") match { + case "" ⇒ None + case password ⇒ Some(password) + } + + val SSLTrustStorePassword = getString("ssl-trust-store-password") match { + case "" ⇒ None + case password ⇒ Some(password) + } + + val SSLSupportedAlgorithms = getStringList("ssl-supported-algorithms") + + val SSLProtocol = getString("ssl-protocol") match { + case "" ⇒ None + case protocol ⇒ Some(protocol) + } + + val EnableSSL = { + val enableSSL = getBoolean("enable-ssl") + if (enableSSL) { + if (SSLProtocol.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.enable-ssl is turned on but no protocol is defined in 'akka.remote.netty.ssl-protocol'.") + if (SSLKeyStore.isEmpty && SSLTrustStore.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.enable-ssl is turned on but no key/trust store is defined in 'akka.remote.netty.ssl-key-store' / 'akka.remote.netty.ssl-trust-store'.") + if (SSLKeyStore.isDefined && SSLKeyStorePassword.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.ssl-key-store' is defined but no key-store password is defined in 'akka.remote.netty.ssl-key-store-password'.") + if (SSLTrustStore.isDefined && SSLTrustStorePassword.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.ssl-trust-store' is defined but no trust-store password is defined in 'akka.remote.netty.ssl-trust-store-password'.") + } + enableSSL + } } \ No newline at end of file diff --git a/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala new file mode 100644 index 0000000000..0d429043c2 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala @@ -0,0 +1,46 @@ +package akka.remote + +import akka.testkit._ +import akka.actor._ +import com.typesafe.config._ +import akka.actor.ExtendedActorSystem +import akka.util.duration._ +import akka.util.Duration +import akka.remote.netty.NettyRemoteTransport +import java.util.ArrayList + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class Ticket1978ConfigSpec extends AkkaSpec(""" +akka { + actor.provider = "akka.remote.RemoteActorRefProvider" + remote.netty { + hostname = localhost + port = 12345 + } + actor.deployment { + /blub.remote = "akka://remote-sys@localhost:12346" + /looker/child.remote = "akka://remote-sys@localhost:12346" + /looker/child/grandchild.remote = "akka://RemoteCommunicationSpec@localhost:12345" + } +} +""") with ImplicitSender with DefaultTimeout { + + "SSL Remoting" must { + "be able to parse these extra Netty config elements" in { + val settings = + system.asInstanceOf[ExtendedActorSystem] + .provider.asInstanceOf[RemoteActorRefProvider] + .transport.asInstanceOf[NettyRemoteTransport] + .settings + import settings._ + + EnableSSL must be(false) + SSLKeyStore must be(Some("keystore")) + SSLKeyStorePassword must be(Some("changeme")) + SSLTrustStore must be(Some("truststore")) + SSLTrustStorePassword must be(Some("changeme")) + SSLProtocol must be(Some("TLSv1")) + SSLSupportedAlgorithms must be(java.util.Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA")) + } + } +} From dbc3d91395fa79a06bb74f659118fb63cbc1ddba Mon Sep 17 00:00:00 2001 From: Peter Badenhorst Date: Fri, 25 May 2012 00:59:17 +0200 Subject: [PATCH 2/5] Added changes to Netty pipelines to support SSL/TLS. Fixes #1978 1) Netty server and client pipelines updated to conditionally load keystore/truststore if SSL is enabled in the config 2) Supports any available encryption protocol via 'ssl-protocol' 3) Supported encryption algorithms are specified via 'ssl-encryption-protocol' config key Conflicts: akka-remote/src/main/scala/akka/remote/netty/Client.scala akka-remote/src/main/scala/akka/remote/netty/Server.scala akka-remote/src/main/scala/akka/remote/netty/Settings.scala --- akka-remote/src/main/resources/reference.conf | 27 ++++++ .../main/scala/akka/remote/netty/Client.scala | 88 +++++++++++++++++++ .../main/scala/akka/remote/netty/Server.scala | 84 +++++++++++++++++- .../scala/akka/remote/netty/Settings.scala | 43 ++++++++- .../akka/remote/Ticket1978ConfigSpec.scala | 46 ++++++++++ 5 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index 97b85895ed..5c7b802f1c 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -155,6 +155,33 @@ akka { # (O) Maximum time window that a client should try to reconnect for reconnection-time-window = 600s + + # (I&O) Enable SSL/TLS encryption. + # This must be enabled on both the client and server to work. + enable-ssl = off + + # (I) This is the Java Key Store used by the server connection + ssl-key-store = "keystore" + + # This password is used for decrypting the key store + ssl-key-store-password = "changeme" + + # (O) This is the Java Key Store used by the client connection + ssl-trust-store = "truststore" + + # This password is used for decrypting the trust store + ssl-trust-store-password = "changeme" + + # (I&O) Protocol to use for SSL encryption, choose from: + # Java 6 & 7: + # SSLv3, TLSv1, + # Java 7: + # TLSv1.1, TLSv1.2 + ssl-protocol = "TLSv1" + + # You need to install the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256 + # More info here: http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#SunJCEProvider + ssl-supported-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] } } } diff --git a/akka-remote/src/main/scala/akka/remote/netty/Client.scala b/akka-remote/src/main/scala/akka/remote/netty/Client.scala index c1737831da..b1b37a08f8 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Client.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Client.scala @@ -18,6 +18,19 @@ import akka.actor.{ Address, ActorRef } import akka.AkkaException import akka.event.Logging import akka.util.Switch +import akka.actor.ActorRef +import org.jboss.netty.channel.ChannelFutureListener +import akka.remote.RemoteClientWriteFailed +import java.net.InetAddress +import java.security.{ SecureRandom, KeyStore, GeneralSecurityException } +import org.jboss.netty.util.TimerTask +import org.jboss.netty.util.Timeout +import java.util.concurrent.TimeUnit +import org.jboss.netty.handler.timeout.{ IdleState, IdleStateEvent, IdleStateAwareChannelHandler, IdleStateHandler } +import java.security.cert.X509Certificate +import javax.net.ssl.{ SSLContext, X509TrustManager, TrustManagerFactory, TrustManager } +import org.jboss.netty.handler.ssl.SslHandler +import java.io.FileInputStream /** * This is the abstract baseclass for netty remote clients, currently there's only an @@ -310,6 +323,81 @@ private[akka] class PassiveRemoteClient(val currentChannel: Channel, netty: NettyRemoteTransport, remoteAddress: Address) extends RemoteClient(netty, remoteAddress) { + import client.netty.settings + + def initTLS(trustStorePath: String, trustStorePassword: String): Option[SSLContext] = { + if (trustStorePath != null && trustStorePassword != null) + try { + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) + val stream = new FileInputStream(trustStorePath) + trustStore.load(stream, trustStorePassword.toCharArray) + trustManagerFactory.init(trustStore); + val trustManagers: Array[TrustManager] = trustManagerFactory.getTrustManagers + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustManagers, new SecureRandom()) + Some(sslContext) + } catch { + case e: GeneralSecurityException ⇒ { + client.log.error(e, "TLS connection could not be established. TLS is not used!"); + None + } + } + else { + client.log.error("TLS connection could not be established because trust store details are missing") + None + } + } + + def getSSLHandler_? : Option[SslHandler] = { + val sslContext: Option[SSLContext] = { + if (settings.EnableSSL) { + client.log.debug("Client SSL is enabled, initialising ...") + initTLS(settings.SSLTrustStore.get, settings.SSLTrustStorePassword.get) + } else { + None + } + } + if (sslContext.isDefined) { + client.log.debug("Client Using SSL context to create SSLEngine ...") + val sslEngine = sslContext.get.createSSLEngine + sslEngine.setUseClientMode(true) + sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) + Some(new SslHandler(sslEngine)) + } else { + None + } + } + + def getPipeline: ChannelPipeline = { + val sslHandler = getSSLHandler_? + val timeout = new IdleStateHandler(client.netty.timer, + settings.ReadTimeout.toSeconds.toInt, + settings.WriteTimeout.toSeconds.toInt, + settings.AllTimeout.toSeconds.toInt) + val lenDec = new LengthFieldBasedFrameDecoder(settings.MessageFrameSize, 0, 4, 0, 4) + val lenPrep = new LengthFieldPrepender(4) + val messageDec = new RemoteMessageDecoder + val messageEnc = new RemoteMessageEncoder(client.netty) + val remoteClient = new ActiveRemoteClientHandler(name, bootstrap, remoteAddress, localAddress, client.netty.timer, client) + + val stages: List[ChannelHandler] = timeout :: lenDec :: messageDec :: lenPrep :: messageEnc :: executionHandler :: remoteClient :: Nil + if (sslHandler.isDefined) { + client.log.debug("Client creating pipeline with SSL handler...") + new StaticChannelPipeline(sslHandler.get :: stages: _*) + } else { + client.log.debug("Client creating pipeline without SSL handler...") + new StaticChannelPipeline(stages: _*) + } + } +} + +class PassiveRemoteClient(val currentChannel: Channel, + netty: NettyRemoteTransport, + remoteAddress: Address) + extends RemoteClient(netty, remoteAddress) { + def connect(reconnectIfAlreadyConnected: Boolean = false): Boolean = runSwitch switchOn { netty.notifyListeners(RemoteClientStarted(netty, remoteAddress)) log.debug("Starting remote client connection to [{}]", remoteAddress) diff --git a/akka-remote/src/main/scala/akka/remote/netty/Server.scala b/akka-remote/src/main/scala/akka/remote/netty/Server.scala index cc3310fada..ace45677f1 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Server.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Server.scala @@ -5,6 +5,7 @@ package akka.remote.netty import java.net.InetSocketAddress import java.util.concurrent.Executors +import java.io.FileNotFoundException import scala.Option.option2Iterable import org.jboss.netty.bootstrap.ServerBootstrap import org.jboss.netty.channel.ChannelHandler.Sharable @@ -12,13 +13,17 @@ import org.jboss.netty.channel.group.ChannelGroup import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory import org.jboss.netty.handler.codec.frame.{ LengthFieldPrepender, LengthFieldBasedFrameDecoder } import org.jboss.netty.handler.execution.ExecutionHandler -import akka.event.Logging import akka.remote.RemoteProtocol.{ RemoteControlProtocol, CommandType, AkkaRemoteProtocol } import akka.remote.{ RemoteServerShutdown, RemoteServerError, RemoteServerClientDisconnected, RemoteServerClientConnected, RemoteServerClientClosed, RemoteProtocol, RemoteMessage } import akka.actor.Address import java.net.InetAddress import akka.actor.ActorSystemImpl import org.jboss.netty.channel._ +import org.jboss.netty.handler.ssl.SslHandler +import java.security.{ SecureRandom, KeyStore, GeneralSecurityException } +import javax.net.ssl.{ KeyManagerFactory, SSLContext } +import java.io.FileInputStream +import akka.event.{ LoggingAdapter, Logging } private[akka] class NettyRemoteServer(val netty: NettyRemoteTransport) { @@ -26,6 +31,8 @@ private[akka] class NettyRemoteServer(val netty: NettyRemoteTransport) { val ip = InetAddress.getByName(settings.Hostname) + lazy val log = Logging(netty.system, "NettyRemoteServer(" + ip + ")") + private val factory = settings.UseDispatcherForIO match { case Some(id) ⇒ @@ -80,6 +87,81 @@ private[akka] class NettyRemoteServer(val netty: NettyRemoteTransport) { } } +class RemoteServerPipelineFactory( + val openChannels: ChannelGroup, + val executionHandler: ExecutionHandler, + val netty: NettyRemoteTransport, + val log: LoggingAdapter) extends ChannelPipelineFactory { + + import netty.settings + + def initTLS(keyStorePath: String, keyStorePassword: String): Option[SSLContext] = { + if (keyStorePath != null && keyStorePassword != null) { + try { + val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + val stream = new FileInputStream(keyStorePath) + keyStore.load(stream, keyStorePassword.toCharArray) + factory.init(keyStore, keyStorePassword.toCharArray) + val sslContext = SSLContext.getInstance(settings.SSLProtocol.get) + sslContext.init(factory.getKeyManagers, null, new SecureRandom()) + Some(sslContext) + } catch { + case e: FileNotFoundException ⇒ { + log.error(e, "TLS connection could not be established because keystore could not be loaded") + None + } + case e: GeneralSecurityException ⇒ { + log.error(e, "TLS connection could not be established") + None + } + } + } else { + log.error("TLS connection could not be established because key store details are missing") + None + } + } + + def getSSLHandler_? : Option[SslHandler] = { + val sslContext: Option[SSLContext] = { + if (settings.EnableSSL) { + log.debug("SSL is enabled, initialising...") + initTLS(settings.SSLKeyStore.get, settings.SSLKeyStorePassword.get) + } else { + None + } + } + if (sslContext.isDefined) { + log.debug("Using SSL context to create SSLEngine...") + val sslEngine = sslContext.get.createSSLEngine + sslEngine.setUseClientMode(false) + sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) + Some(new SslHandler(sslEngine)) + } else { + None + } + } + + def getPipeline: ChannelPipeline = { + val sslHandler = getSSLHandler_? + val lenDec = new LengthFieldBasedFrameDecoder(settings.MessageFrameSize, 0, 4, 0, 4) + val lenPrep = new LengthFieldPrepender(4) + val messageDec = new RemoteMessageDecoder + val messageEnc = new RemoteMessageEncoder(netty) + + val authenticator = if (settings.RequireCookie) new RemoteServerAuthenticationHandler(settings.SecureCookie) :: Nil else Nil + val remoteServer = new RemoteServerHandler(openChannels, netty) + val stages: List[ChannelHandler] = lenDec :: messageDec :: lenPrep :: messageEnc :: executionHandler :: authenticator ::: remoteServer :: Nil + if (sslHandler.isDefined) { + log.debug("Creating pipeline with SSL handler...") + new StaticChannelPipeline(sslHandler.get :: stages: _*) + } else { + log.debug("Creating pipeline without SSL handler...") + new StaticChannelPipeline(stages: _*) + } + } +} + @ChannelHandler.Sharable private[akka] class RemoteServerAuthenticationHandler(secureCookie: Option[String]) extends SimpleChannelUpstreamHandler { val authenticated = new AnyRef diff --git a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala index 64bc184408..d753a743b6 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala @@ -73,4 +73,45 @@ private[akka] class NettySettings(config: Config, val systemName: String) { case sz ⇒ sz } -} + val SSLKeyStore = getString("ssl-key-store") match { + case "" ⇒ None + case keyStore ⇒ Some(keyStore) + } + + val SSLTrustStore = getString("ssl-trust-store") match { + case "" ⇒ None + case trustStore ⇒ Some(trustStore) + } + + val SSLKeyStorePassword = getString("ssl-key-store-password") match { + case "" ⇒ None + case password ⇒ Some(password) + } + + val SSLTrustStorePassword = getString("ssl-trust-store-password") match { + case "" ⇒ None + case password ⇒ Some(password) + } + + val SSLSupportedAlgorithms = getStringList("ssl-supported-algorithms") + + val SSLProtocol = getString("ssl-protocol") match { + case "" ⇒ None + case protocol ⇒ Some(protocol) + } + + val EnableSSL = { + val enableSSL = getBoolean("enable-ssl") + if (enableSSL) { + if (SSLProtocol.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.enable-ssl is turned on but no protocol is defined in 'akka.remote.netty.ssl-protocol'.") + if (SSLKeyStore.isEmpty && SSLTrustStore.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.enable-ssl is turned on but no key/trust store is defined in 'akka.remote.netty.ssl-key-store' / 'akka.remote.netty.ssl-trust-store'.") + if (SSLKeyStore.isDefined && SSLKeyStorePassword.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.ssl-key-store' is defined but no key-store password is defined in 'akka.remote.netty.ssl-key-store-password'.") + if (SSLTrustStore.isDefined && SSLTrustStorePassword.isEmpty) throw new ConfigurationException( + "Configuration option 'akka.remote.netty.ssl-trust-store' is defined but no trust-store password is defined in 'akka.remote.netty.ssl-trust-store-password'.") + } + enableSSL + } +} \ No newline at end of file diff --git a/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala new file mode 100644 index 0000000000..0d429043c2 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala @@ -0,0 +1,46 @@ +package akka.remote + +import akka.testkit._ +import akka.actor._ +import com.typesafe.config._ +import akka.actor.ExtendedActorSystem +import akka.util.duration._ +import akka.util.Duration +import akka.remote.netty.NettyRemoteTransport +import java.util.ArrayList + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class Ticket1978ConfigSpec extends AkkaSpec(""" +akka { + actor.provider = "akka.remote.RemoteActorRefProvider" + remote.netty { + hostname = localhost + port = 12345 + } + actor.deployment { + /blub.remote = "akka://remote-sys@localhost:12346" + /looker/child.remote = "akka://remote-sys@localhost:12346" + /looker/child/grandchild.remote = "akka://RemoteCommunicationSpec@localhost:12345" + } +} +""") with ImplicitSender with DefaultTimeout { + + "SSL Remoting" must { + "be able to parse these extra Netty config elements" in { + val settings = + system.asInstanceOf[ExtendedActorSystem] + .provider.asInstanceOf[RemoteActorRefProvider] + .transport.asInstanceOf[NettyRemoteTransport] + .settings + import settings._ + + EnableSSL must be(false) + SSLKeyStore must be(Some("keystore")) + SSLKeyStorePassword must be(Some("changeme")) + SSLTrustStore must be(Some("truststore")) + SSLTrustStorePassword must be(Some("changeme")) + SSLProtocol must be(Some("TLSv1")) + SSLSupportedAlgorithms must be(java.util.Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA")) + } + } +} From 56cd9692edab6a308855e6af7c5c1cc0670a04b2 Mon Sep 17 00:00:00 2001 From: Peter Badenhorst Date: Mon, 28 May 2012 23:51:47 +0200 Subject: [PATCH 3/5] Reverted changes to client and server files and moved the code to NettySSLSupport.scala Updated configuration file to reflect new netty.ssl hierarchy. --- .../TestConductorTransport.scala | 4 +- akka-remote/src/main/resources/reference.conf | 42 +++--- .../main/scala/akka/remote/netty/Client.scala | 90 +------------ .../remote/netty/NettyRemoteSupport.scala | 13 +- .../akka/remote/netty/NettySSLSupport.scala | 122 ++++++++++++++++++ .../main/scala/akka/remote/netty/Server.scala | 85 +----------- .../scala/akka/remote/netty/Settings.scala | 22 ++-- 7 files changed, 166 insertions(+), 212 deletions(-) create mode 100644 akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala diff --git a/akka-remote-tests/src/main/scala/akka/remote/testconductor/TestConductorTransport.scala b/akka-remote-tests/src/main/scala/akka/remote/testconductor/TestConductorTransport.scala index dbf17fa5a7..f7b7943275 100644 --- a/akka-remote-tests/src/main/scala/akka/remote/testconductor/TestConductorTransport.scala +++ b/akka-remote-tests/src/main/scala/akka/remote/testconductor/TestConductorTransport.scala @@ -16,9 +16,9 @@ import org.jboss.netty.channel.ChannelPipelineFactory private[akka] class TestConductorTransport(_system: ExtendedActorSystem, _provider: RemoteActorRefProvider) extends NettyRemoteTransport(_system, _provider) { - override def createPipeline(endpoint: ⇒ ChannelHandler, withTimeout: Boolean): ChannelPipelineFactory = + override def createPipeline(endpoint: ⇒ ChannelHandler, withTimeout: Boolean, isClient: Boolean): ChannelPipelineFactory = new ChannelPipelineFactory { - def getPipeline = PipelineFactory(new NetworkFailureInjector(system) +: PipelineFactory.defaultStack(withTimeout) :+ endpoint) + def getPipeline = PipelineFactory(new NetworkFailureInjector(system) +: PipelineFactory.defaultStack(withTimeout, isClient) :+ endpoint) } } \ No newline at end of file diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index 5c7b802f1c..d20a57d1a5 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -156,32 +156,34 @@ akka { # (O) Maximum time window that a client should try to reconnect for reconnection-time-window = 600s - # (I&O) Enable SSL/TLS encryption. - # This must be enabled on both the client and server to work. - enable-ssl = off + ssl { + # (I&O) Enable SSL/TLS encryption. + # This must be enabled on both the client and server to work. + enable = off - # (I) This is the Java Key Store used by the server connection - ssl-key-store = "keystore" + # (I) This is the Java Key Store used by the server connection + key-store = "keystore" - # This password is used for decrypting the key store - ssl-key-store-password = "changeme" + # This password is used for decrypting the key store + key-store-password = "changeme" - # (O) This is the Java Key Store used by the client connection - ssl-trust-store = "truststore" + # (O) This is the Java Key Store used by the client connection + trust-store = "truststore" - # This password is used for decrypting the trust store - ssl-trust-store-password = "changeme" + # This password is used for decrypting the trust store + trust-store-password = "changeme" - # (I&O) Protocol to use for SSL encryption, choose from: - # Java 6 & 7: - # SSLv3, TLSv1, - # Java 7: - # TLSv1.1, TLSv1.2 - ssl-protocol = "TLSv1" + # (I&O) Protocol to use for SSL encryption, choose from: + # Java 6 & 7: + # SSLv3, TLSv1, + # Java 7: + # TLSv1.1, TLSv1.2 + protocol = "TLSv1" - # You need to install the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256 - # More info here: http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#SunJCEProvider - ssl-supported-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] + # You need to install the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256 + # More info here: http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#SunJCEProvider + supported-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] + } } } } diff --git a/akka-remote/src/main/scala/akka/remote/netty/Client.scala b/akka-remote/src/main/scala/akka/remote/netty/Client.scala index b1b37a08f8..e3037b71ad 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Client.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Client.scala @@ -18,19 +18,6 @@ import akka.actor.{ Address, ActorRef } import akka.AkkaException import akka.event.Logging import akka.util.Switch -import akka.actor.ActorRef -import org.jboss.netty.channel.ChannelFutureListener -import akka.remote.RemoteClientWriteFailed -import java.net.InetAddress -import java.security.{ SecureRandom, KeyStore, GeneralSecurityException } -import org.jboss.netty.util.TimerTask -import org.jboss.netty.util.Timeout -import java.util.concurrent.TimeUnit -import org.jboss.netty.handler.timeout.{ IdleState, IdleStateEvent, IdleStateAwareChannelHandler, IdleStateHandler } -import java.security.cert.X509Certificate -import javax.net.ssl.{ SSLContext, X509TrustManager, TrustManagerFactory, TrustManager } -import org.jboss.netty.handler.ssl.SslHandler -import java.io.FileInputStream /** * This is the abstract baseclass for netty remote clients, currently there's only an @@ -156,7 +143,7 @@ private[akka] class ActiveRemoteClient private[akka] ( openChannels = new DefaultDisposableChannelGroup(classOf[RemoteClient].getName) val b = new ClientBootstrap(netty.clientChannelFactory) - b.setPipelineFactory(netty.createPipeline(new ActiveRemoteClientHandler(name, b, remoteAddress, localAddress, netty.timer, this), true)) + b.setPipelineFactory(netty.createPipeline(new ActiveRemoteClientHandler(name, b, remoteAddress, localAddress, netty.timer, this), withTimeout = true, isClient = true)) b.setOption("tcpNoDelay", true) b.setOption("keepAlive", true) b.setOption("connectTimeoutMillis", settings.ConnectionTimeout.toMillis) @@ -323,81 +310,6 @@ private[akka] class PassiveRemoteClient(val currentChannel: Channel, netty: NettyRemoteTransport, remoteAddress: Address) extends RemoteClient(netty, remoteAddress) { - import client.netty.settings - - def initTLS(trustStorePath: String, trustStorePassword: String): Option[SSLContext] = { - if (trustStorePath != null && trustStorePassword != null) - try { - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) - val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val stream = new FileInputStream(trustStorePath) - trustStore.load(stream, trustStorePassword.toCharArray) - trustManagerFactory.init(trustStore); - val trustManagers: Array[TrustManager] = trustManagerFactory.getTrustManagers - - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustManagers, new SecureRandom()) - Some(sslContext) - } catch { - case e: GeneralSecurityException ⇒ { - client.log.error(e, "TLS connection could not be established. TLS is not used!"); - None - } - } - else { - client.log.error("TLS connection could not be established because trust store details are missing") - None - } - } - - def getSSLHandler_? : Option[SslHandler] = { - val sslContext: Option[SSLContext] = { - if (settings.EnableSSL) { - client.log.debug("Client SSL is enabled, initialising ...") - initTLS(settings.SSLTrustStore.get, settings.SSLTrustStorePassword.get) - } else { - None - } - } - if (sslContext.isDefined) { - client.log.debug("Client Using SSL context to create SSLEngine ...") - val sslEngine = sslContext.get.createSSLEngine - sslEngine.setUseClientMode(true) - sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) - Some(new SslHandler(sslEngine)) - } else { - None - } - } - - def getPipeline: ChannelPipeline = { - val sslHandler = getSSLHandler_? - val timeout = new IdleStateHandler(client.netty.timer, - settings.ReadTimeout.toSeconds.toInt, - settings.WriteTimeout.toSeconds.toInt, - settings.AllTimeout.toSeconds.toInt) - val lenDec = new LengthFieldBasedFrameDecoder(settings.MessageFrameSize, 0, 4, 0, 4) - val lenPrep = new LengthFieldPrepender(4) - val messageDec = new RemoteMessageDecoder - val messageEnc = new RemoteMessageEncoder(client.netty) - val remoteClient = new ActiveRemoteClientHandler(name, bootstrap, remoteAddress, localAddress, client.netty.timer, client) - - val stages: List[ChannelHandler] = timeout :: lenDec :: messageDec :: lenPrep :: messageEnc :: executionHandler :: remoteClient :: Nil - if (sslHandler.isDefined) { - client.log.debug("Client creating pipeline with SSL handler...") - new StaticChannelPipeline(sslHandler.get :: stages: _*) - } else { - client.log.debug("Client creating pipeline without SSL handler...") - new StaticChannelPipeline(stages: _*) - } - } -} - -class PassiveRemoteClient(val currentChannel: Channel, - netty: NettyRemoteTransport, - remoteAddress: Address) - extends RemoteClient(netty, remoteAddress) { - def connect(reconnectIfAlreadyConnected: Boolean = false): Boolean = runSwitch switchOn { netty.notifyListeners(RemoteClientStarted(netty, remoteAddress)) log.debug("Starting remote client connection to [{}]", remoteAddress) diff --git a/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala b/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala index b42239f470..32aba84893 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala @@ -61,17 +61,18 @@ private[akka] class NettyRemoteTransport(_system: ExtendedActorSystem, _provider * * @param withTimeout determines whether an IdleStateHandler shall be included */ - def apply(endpoint: ⇒ Seq[ChannelHandler], withTimeout: Boolean): ChannelPipelineFactory = + def apply(endpoint: ⇒ Seq[ChannelHandler], withTimeout: Boolean, isClient: Boolean): ChannelPipelineFactory = new ChannelPipelineFactory { - def getPipeline = apply(defaultStack(withTimeout) ++ endpoint) + def getPipeline = apply(defaultStack(withTimeout, isClient) ++ endpoint) } /** * Construct a default protocol stack, excluding the “head” handler (i.e. the one which * actually dispatches the received messages to the local target actors). */ - def defaultStack(withTimeout: Boolean): Seq[ChannelHandler] = - (if (withTimeout) timeout :: Nil else Nil) ::: + def defaultStack(withTimeout: Boolean, isClient: Boolean): Seq[ChannelHandler] = + (if (settings.EnableSSL) NettySSLSupport(settings, NettyRemoteTransport.this, isClient) :: Nil else Nil) ::: + (if (withTimeout) timeout :: Nil else Nil) ::: msgFormat ::: authenticator ::: executionHandler :: @@ -119,8 +120,8 @@ private[akka] class NettyRemoteTransport(_system: ExtendedActorSystem, _provider * This method is factored out to provide an extension point in case the * pipeline shall be changed. It is recommended to use */ - def createPipeline(endpoint: ⇒ ChannelHandler, withTimeout: Boolean): ChannelPipelineFactory = - PipelineFactory(Seq(endpoint), withTimeout) + def createPipeline(endpoint: ⇒ ChannelHandler, withTimeout: Boolean, isClient: Boolean): ChannelPipelineFactory = + PipelineFactory(Seq(endpoint), withTimeout, isClient) private val remoteClients = new HashMap[Address, RemoteClient] private val clientsLock = new ReentrantReadWriteLock diff --git a/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala b/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala new file mode 100644 index 0000000000..d830c87a07 --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.remote.netty + +import org.jboss.netty.handler.ssl.SslHandler +import com.sun.xml.internal.bind.v2.model.core.NonElement +import com.sun.xml.internal.ws.resources.SoapMessages +import javax.net.ssl.{ KeyManagerFactory, TrustManager, TrustManagerFactory, SSLContext } +import akka.remote.{ RemoteClientError, RemoteTransportException, RemoteServerError } +import java.security.{ GeneralSecurityException, SecureRandom, KeyStore } +import java.io.{ IOException, FileNotFoundException, FileInputStream } + +object NettySSLSupport { + /** + * Construct a SSLHandler which can be inserted into a Netty server/client pipeline + */ + def apply(settings: NettySettings, netty: NettyRemoteTransport, isClient: Boolean): SslHandler = { + if (isClient) initialiseClientSSL(settings, netty) + else initialiseServerSSL(settings, netty) + } + + private def initialiseClientSSL(settings: NettySettings, netty: NettyRemoteTransport): SslHandler = { + netty.log.debug("Client SSL is enabled, initialising ...") + val sslContext: Option[SSLContext] = { + (settings.SSLTrustStore, settings.SSLTrustStorePassword, settings.SSLProtocol) match { + case (Some(trustStore), Some(password), Some(protocol)) ⇒ constructClientContext(settings, netty, trustStore, password, protocol) + case _ ⇒ throw new GeneralSecurityException("Could not find all SSL trust store settings") + } + } + sslContext match { + case Some(context) ⇒ { + netty.log.debug("Using client SSL context to create SSLEngine ...") + val sslEngine = context.createSSLEngine + sslEngine.setUseClientMode(true) + sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) + new SslHandler(sslEngine) + } + case None ⇒ throw new GeneralSecurityException("Failed to initialise client SSL") + } + } + + private def constructClientContext(settings: NettySettings, netty: NettyRemoteTransport, trustStorePath: String, trustStorePassword: String, protocol: String): Option[SSLContext] = { + try { + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) + val stream = new FileInputStream(trustStorePath) + trustStore.load(stream, trustStorePassword.toCharArray) + trustManagerFactory.init(trustStore) + val trustManagers: Array[TrustManager] = trustManagerFactory.getTrustManagers + val sslContext = SSLContext.getInstance(protocol) + sslContext.init(null, trustManagers, new SecureRandom()) + Some(sslContext) + } catch { + case e: FileNotFoundException ⇒ { + val exception = new RemoteTransportException("Client SSL connection could not be established because trust store could not be loaded", e) + netty.notifyListeners(RemoteClientError(exception, netty, netty.address)) + throw exception + } + case e: IOException ⇒ { + val exception = new RemoteTransportException("Client SSL connection could not be established because: " + e.getMessage, e) + netty.notifyListeners(RemoteClientError(exception, netty, netty.address)) + throw exception + } + case e: GeneralSecurityException ⇒ { + val exception = new RemoteTransportException("Client SSL connection could not be established because SSL context could not be constructed", e) + netty.notifyListeners(RemoteClientError(exception, netty, netty.address)) + throw exception + } + } + } + + private def initialiseServerSSL(settings: NettySettings, netty: NettyRemoteTransport): SslHandler = { + netty.log.debug("Server SSL is enabled, initialising ...") + val sslContext: Option[SSLContext] = { + (settings.SSLKeyStore, settings.SSLKeyStorePassword, settings.SSLProtocol) match { + case (Some(keyStore), Some(password), Some(protocol)) ⇒ constructServerContext(settings, netty, keyStore, password, protocol) + case _ ⇒ throw new GeneralSecurityException("Could not find all SSL key store settings") + } + } + sslContext match { + case Some(context) ⇒ { + netty.log.debug("Using server SSL context to create SSLEngine ...") + val sslEngine = context.createSSLEngine + sslEngine.setUseClientMode(false) + sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) + new SslHandler(sslEngine) + } + case None ⇒ throw new GeneralSecurityException("Failed to initialise server SSL") + } + } + + private def constructServerContext(settings: NettySettings, netty: NettyRemoteTransport, keyStorePath: String, keyStorePassword: String, protocol: String): Option[SSLContext] = { + try { + val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + val stream = new FileInputStream(keyStorePath) + keyStore.load(stream, keyStorePassword.toCharArray) + factory.init(keyStore, keyStorePassword.toCharArray) + val sslContext = SSLContext.getInstance(protocol) + sslContext.init(factory.getKeyManagers, null, new SecureRandom()) + Some(sslContext) + } catch { + case e: FileNotFoundException ⇒ { + val exception = new RemoteTransportException("Server SSL connection could not be established because key store could not be loaded", e) + netty.notifyListeners(RemoteServerError(exception, netty)) + throw exception + } + case e: IOException ⇒ { + val exception = new RemoteTransportException("Server SSL connection could not be established because: " + e.getMessage, e) + netty.notifyListeners(RemoteServerError(exception, netty)) + throw exception + } + case e: GeneralSecurityException ⇒ { + val exception = new RemoteTransportException("Server SSL connection could not be established because SSL context could not be constructed", e) + netty.notifyListeners(RemoteServerError(exception, netty)) + throw exception + } + } + } +} diff --git a/akka-remote/src/main/scala/akka/remote/netty/Server.scala b/akka-remote/src/main/scala/akka/remote/netty/Server.scala index ace45677f1..789cc71b6c 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Server.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Server.scala @@ -5,7 +5,6 @@ package akka.remote.netty import java.net.InetSocketAddress import java.util.concurrent.Executors -import java.io.FileNotFoundException import scala.Option.option2Iterable import org.jboss.netty.bootstrap.ServerBootstrap import org.jboss.netty.channel.ChannelHandler.Sharable @@ -19,11 +18,6 @@ import akka.actor.Address import java.net.InetAddress import akka.actor.ActorSystemImpl import org.jboss.netty.channel._ -import org.jboss.netty.handler.ssl.SslHandler -import java.security.{ SecureRandom, KeyStore, GeneralSecurityException } -import javax.net.ssl.{ KeyManagerFactory, SSLContext } -import java.io.FileInputStream -import akka.event.{ LoggingAdapter, Logging } private[akka] class NettyRemoteServer(val netty: NettyRemoteTransport) { @@ -31,8 +25,6 @@ private[akka] class NettyRemoteServer(val netty: NettyRemoteTransport) { val ip = InetAddress.getByName(settings.Hostname) - lazy val log = Logging(netty.system, "NettyRemoteServer(" + ip + ")") - private val factory = settings.UseDispatcherForIO match { case Some(id) ⇒ @@ -47,7 +39,7 @@ private[akka] class NettyRemoteServer(val netty: NettyRemoteTransport) { private val bootstrap = { val b = new ServerBootstrap(factory) - b.setPipelineFactory(netty.createPipeline(new RemoteServerHandler(openChannels, netty), false)) + b.setPipelineFactory(netty.createPipeline(new RemoteServerHandler(openChannels, netty), withTimeout = false, isClient = false)) b.setOption("backlog", settings.Backlog) b.setOption("tcpNoDelay", true) b.setOption("child.keepAlive", true) @@ -87,81 +79,6 @@ private[akka] class NettyRemoteServer(val netty: NettyRemoteTransport) { } } -class RemoteServerPipelineFactory( - val openChannels: ChannelGroup, - val executionHandler: ExecutionHandler, - val netty: NettyRemoteTransport, - val log: LoggingAdapter) extends ChannelPipelineFactory { - - import netty.settings - - def initTLS(keyStorePath: String, keyStorePassword: String): Option[SSLContext] = { - if (keyStorePath != null && keyStorePassword != null) { - try { - val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) - val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) - val stream = new FileInputStream(keyStorePath) - keyStore.load(stream, keyStorePassword.toCharArray) - factory.init(keyStore, keyStorePassword.toCharArray) - val sslContext = SSLContext.getInstance(settings.SSLProtocol.get) - sslContext.init(factory.getKeyManagers, null, new SecureRandom()) - Some(sslContext) - } catch { - case e: FileNotFoundException ⇒ { - log.error(e, "TLS connection could not be established because keystore could not be loaded") - None - } - case e: GeneralSecurityException ⇒ { - log.error(e, "TLS connection could not be established") - None - } - } - } else { - log.error("TLS connection could not be established because key store details are missing") - None - } - } - - def getSSLHandler_? : Option[SslHandler] = { - val sslContext: Option[SSLContext] = { - if (settings.EnableSSL) { - log.debug("SSL is enabled, initialising...") - initTLS(settings.SSLKeyStore.get, settings.SSLKeyStorePassword.get) - } else { - None - } - } - if (sslContext.isDefined) { - log.debug("Using SSL context to create SSLEngine...") - val sslEngine = sslContext.get.createSSLEngine - sslEngine.setUseClientMode(false) - sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) - Some(new SslHandler(sslEngine)) - } else { - None - } - } - - def getPipeline: ChannelPipeline = { - val sslHandler = getSSLHandler_? - val lenDec = new LengthFieldBasedFrameDecoder(settings.MessageFrameSize, 0, 4, 0, 4) - val lenPrep = new LengthFieldPrepender(4) - val messageDec = new RemoteMessageDecoder - val messageEnc = new RemoteMessageEncoder(netty) - - val authenticator = if (settings.RequireCookie) new RemoteServerAuthenticationHandler(settings.SecureCookie) :: Nil else Nil - val remoteServer = new RemoteServerHandler(openChannels, netty) - val stages: List[ChannelHandler] = lenDec :: messageDec :: lenPrep :: messageEnc :: executionHandler :: authenticator ::: remoteServer :: Nil - if (sslHandler.isDefined) { - log.debug("Creating pipeline with SSL handler...") - new StaticChannelPipeline(sslHandler.get :: stages: _*) - } else { - log.debug("Creating pipeline without SSL handler...") - new StaticChannelPipeline(stages: _*) - } - } -} - @ChannelHandler.Sharable private[akka] class RemoteServerAuthenticationHandler(secureCookie: Option[String]) extends SimpleChannelUpstreamHandler { val authenticated = new AnyRef diff --git a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala index d753a743b6..5d829127f8 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala @@ -73,44 +73,44 @@ private[akka] class NettySettings(config: Config, val systemName: String) { case sz ⇒ sz } - val SSLKeyStore = getString("ssl-key-store") match { + val SSLKeyStore = getString("ssl.key-store") match { case "" ⇒ None case keyStore ⇒ Some(keyStore) } - val SSLTrustStore = getString("ssl-trust-store") match { + val SSLTrustStore = getString("ssl.trust-store") match { case "" ⇒ None case trustStore ⇒ Some(trustStore) } - val SSLKeyStorePassword = getString("ssl-key-store-password") match { + val SSLKeyStorePassword = getString("ssl.key-store-password") match { case "" ⇒ None case password ⇒ Some(password) } - val SSLTrustStorePassword = getString("ssl-trust-store-password") match { + val SSLTrustStorePassword = getString("ssl.trust-store-password") match { case "" ⇒ None case password ⇒ Some(password) } - val SSLSupportedAlgorithms = getStringList("ssl-supported-algorithms") + val SSLSupportedAlgorithms = getStringList("ssl.supported-algorithms") - val SSLProtocol = getString("ssl-protocol") match { + val SSLProtocol = getString("ssl.protocol") match { case "" ⇒ None case protocol ⇒ Some(protocol) } val EnableSSL = { - val enableSSL = getBoolean("enable-ssl") + val enableSSL = getBoolean("ssl.enable") if (enableSSL) { if (SSLProtocol.isEmpty) throw new ConfigurationException( - "Configuration option 'akka.remote.netty.enable-ssl is turned on but no protocol is defined in 'akka.remote.netty.ssl-protocol'.") + "Configuration option 'akka.remote.netty.ssl.enable is turned on but no protocol is defined in 'akka.remote.netty.ssl.protocol'.") if (SSLKeyStore.isEmpty && SSLTrustStore.isEmpty) throw new ConfigurationException( - "Configuration option 'akka.remote.netty.enable-ssl is turned on but no key/trust store is defined in 'akka.remote.netty.ssl-key-store' / 'akka.remote.netty.ssl-trust-store'.") + "Configuration option 'akka.remote.netty.ssl.enable is turned on but no key/trust store is defined in 'akka.remote.netty.ssl.key-store' / 'akka.remote.netty.ssl.trust-store'.") if (SSLKeyStore.isDefined && SSLKeyStorePassword.isEmpty) throw new ConfigurationException( - "Configuration option 'akka.remote.netty.ssl-key-store' is defined but no key-store password is defined in 'akka.remote.netty.ssl-key-store-password'.") + "Configuration option 'akka.remote.netty.ssl.key-store' is defined but no key-store password is defined in 'akka.remote.netty.ssl.key-store-password'.") if (SSLTrustStore.isDefined && SSLTrustStorePassword.isEmpty) throw new ConfigurationException( - "Configuration option 'akka.remote.netty.ssl-trust-store' is defined but no trust-store password is defined in 'akka.remote.netty.ssl-trust-store-password'.") + "Configuration option 'akka.remote.netty.ssl.trust-store' is defined but no trust-store password is defined in 'akka.remote.netty.ssl.trust-store-password'.") } enableSSL } From c64775857900ffa168e6d68a7314ff82f3ad8f10 Mon Sep 17 00:00:00 2001 From: Peter Badenhorst Date: Tue, 5 Jun 2012 13:44:05 +0200 Subject: [PATCH 4/5] Updated to support 3 different random number generators: 1) SecureRandom supported by Java (default) 2) SHA1PRNG (causes problems on Linux) 3) Various versions of the AES Counter RNG (faster than default at generating random data) --- .../provider/AES128CounterRNGFast.java | 51 ++++++ .../provider/AES128CounterRNGSecure.java | 49 ++++++ .../provider/AES256CounterRNGSecure.java | 49 ++++++ .../akka/security/provider/AkkaProvider.java | 37 ++++ akka-remote/src/main/resources/reference.conf | 19 ++- .../remote/netty/NettyRemoteSupport.scala | 2 +- .../akka/remote/netty/NettySSLSupport.scala | 159 ++++++++++++------ .../scala/akka/remote/netty/Settings.scala | 10 ++ .../akka/remote/Ticket1978ConfigSpec.scala | 2 + project/AkkaBuild.scala | 4 +- 10 files changed, 324 insertions(+), 58 deletions(-) create mode 100644 akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java create mode 100644 akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java create mode 100644 akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java create mode 100644 akka-remote/src/main/java/akka/security/provider/AkkaProvider.java diff --git a/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java b/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java new file mode 100644 index 0000000000..a982a6f705 --- /dev/null +++ b/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider; + +import org.uncommons.maths.random.SecureRandomSeedGenerator; +import org.uncommons.maths.random.SeedException; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; + +/** + * Internal API + */ +public class AES128CounterRNGFast extends java.security.SecureRandomSpi { + private org.uncommons.maths.random.AESCounterRNG rng; + + public AES128CounterRNGFast() throws SeedException, GeneralSecurityException { + rng = new org.uncommons.maths.random.AESCounterRNG(new SecureRandomSeedGenerator()); + } + + /** + * This is managed internally only + */ + @Override + protected void engineSetSeed(byte[] seed) { + + } + + /** + * Generates a user-specified number of random bytes. + * + * @param bytes the array to be filled in with random bytes. + */ + @Override + protected void engineNextBytes(byte[] bytes) { + rng.nextBytes(bytes); + } + + /** + * Returns the given number of seed bytes. This call may be used to + * seed other random number generators. + * + * @param numBytes the number of seed bytes to generate. + * @return the seed bytes. + */ + @Override + protected byte[] engineGenerateSeed(int numBytes) { + return (new SecureRandom()).generateSeed(numBytes); + } +} diff --git a/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java b/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java new file mode 100644 index 0000000000..178a6c392b --- /dev/null +++ b/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider; + +import org.uncommons.maths.random.DefaultSeedGenerator; + +import java.security.GeneralSecurityException; + +/** + * Internal API + */ +public class AES128CounterRNGSecure extends java.security.SecureRandomSpi { + private org.uncommons.maths.random.AESCounterRNG rng; + + public AES128CounterRNGSecure() throws GeneralSecurityException { + rng = new org.uncommons.maths.random.AESCounterRNG(); + } + + /** + * This is managed internally only + */ + @Override + protected void engineSetSeed(byte[] seed) { + + } + + /** + * Generates a user-specified number of random bytes. + * + * @param bytes the array to be filled in with random bytes. + */ + @Override + protected void engineNextBytes(byte[] bytes) { + rng.nextBytes(bytes); + } + + /** + * Returns the given number of seed bytes. This call may be used to + * seed other random number generators. + * + * @param numBytes the number of seed bytes to generate. + * @return the seed bytes. + */ + @Override + protected byte[] engineGenerateSeed(int numBytes) { + return DefaultSeedGenerator.getInstance().generateSeed(numBytes); + } +} diff --git a/akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java b/akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java new file mode 100644 index 0000000000..48d651b86b --- /dev/null +++ b/akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider; + +import org.uncommons.maths.random.DefaultSeedGenerator; + +import java.security.GeneralSecurityException; + +/** + * Internal API + */ +public class AES256CounterRNGSecure extends java.security.SecureRandomSpi { + private org.uncommons.maths.random.AESCounterRNG rng; + + public AES256CounterRNGSecure() throws GeneralSecurityException { + rng = new org.uncommons.maths.random.AESCounterRNG(32); + } + + /** + * This is managed internally only + */ + @Override + protected void engineSetSeed(byte[] seed) { + + } + + /** + * Generates a user-specified number of random bytes. + * + * @param bytes the array to be filled in with random bytes. + */ + @Override + protected void engineNextBytes(byte[] bytes) { + rng.nextBytes(bytes); + } + + /** + * Returns the given number of seed bytes. This call may be used to + * seed other random number generators. + * + * @param numBytes the number of seed bytes to generate. + * @return the seed bytes. + */ + @Override + protected byte[] engineGenerateSeed(int numBytes) { + return DefaultSeedGenerator.getInstance().generateSeed(numBytes); + } +} diff --git a/akka-remote/src/main/java/akka/security/provider/AkkaProvider.java b/akka-remote/src/main/java/akka/security/provider/AkkaProvider.java new file mode 100644 index 0000000000..9c4a0c2181 --- /dev/null +++ b/akka-remote/src/main/java/akka/security/provider/AkkaProvider.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider; + +import java.security.AccessController; +import java.security.Provider; + +/** + * A provider that for AES128CounterRNGFast, a cryptographically secure random number generator through SecureRandom + */ +public final class AkkaProvider extends Provider { + public AkkaProvider() { + super("Akka", 1.0, "Akka provider 1.0 that implements a secure AES random number generator"); + + AccessController.doPrivileged(new java.security.PrivilegedAction() { + public Object run() { + + /** + * SecureRandom + */ + put("SecureRandom.AES128CounterRNGFast", "akka.security.provider.AES128CounterRNGFast"); + put("SecureRandom.AES128CounterRNGSecure", "akka.security.provider.AES128CounterRNGSecure"); + put("SecureRandom.AES256CounterRNGSecure", "akka.security.provider.AES256CounterRNGSecure"); + + /** + * Implementation type: software or hardware + */ + put("SecureRandom.AES128CounterRNGFast ImplementedIn", "Software"); + put("SecureRandom.AES128CounterRNGSecure ImplementedIn", "Software"); + put("SecureRandom.AES256CounterRNGSecure ImplementedIn", "Software"); + + return null; + } + }); + } +} diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index d20a57d1a5..80719decf4 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -175,14 +175,29 @@ akka { # (I&O) Protocol to use for SSL encryption, choose from: # Java 6 & 7: - # SSLv3, TLSv1, + # 'SSLv3', 'TLSv1' # Java 7: - # TLSv1.1, TLSv1.2 + # 'TLSv1.1', 'TLSv1.2' protocol = "TLSv1" # You need to install the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256 # More info here: http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#SunJCEProvider supported-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] + + # Using /dev/./urandom is only necessary when using SHA1PRNG on Linux to prevent blocking + # It is NOT as secure because it reuses the seed + # '' => defaults to /dev/random or whatever is set in java.security for example: securerandom.source=file:/dev/random + # '/dev/./urandom' => NOT '/dev/urandom' as that doesn't work according to: http://bugs.sun.com/view_bug.do;jsessionid=ff625daf459fdffffffffcd54f1c775299e0?bug_id=6202721 + sha1prng-random-source = "" + + # There are three options, in increasing order of security: + # "" or SecureRandom => (default) + # "SHA1PRNG" => Can be slow because of blocking issues on Linux + # "AES128CounterRNGFast" => fastest startup and based on AES encryption algorithm + # The following use one of 3 possible seed sources, depending on availability: /dev/random, random.org and SecureRandom (provided by Java) + # "AES128CounterRNGSecure" + # "AES256CounterRNGSecure" (Install JCE Unlimited Strength Jurisdiction Policy Files first) + random-number-generator = "" } } } diff --git a/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala b/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala index 32aba84893..84a46f05cd 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala @@ -71,7 +71,7 @@ private[akka] class NettyRemoteTransport(_system: ExtendedActorSystem, _provider * actually dispatches the received messages to the local target actors). */ def defaultStack(withTimeout: Boolean, isClient: Boolean): Seq[ChannelHandler] = - (if (settings.EnableSSL) NettySSLSupport(settings, NettyRemoteTransport.this, isClient) :: Nil else Nil) ::: + (if (settings.EnableSSL) NettySSLSupport(settings, NettyRemoteTransport.this.log, isClient) :: Nil else Nil) ::: (if (withTimeout) timeout :: Nil else Nil) ::: msgFormat ::: authenticator ::: diff --git a/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala b/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala index d830c87a07..011aa92233 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala @@ -4,44 +4,112 @@ package akka.remote.netty +import _root_.java.security.Provider +import _root_.java.security.SecureRandom +import _root_.java.security.Security import org.jboss.netty.handler.ssl.SslHandler -import com.sun.xml.internal.bind.v2.model.core.NonElement -import com.sun.xml.internal.ws.resources.SoapMessages import javax.net.ssl.{ KeyManagerFactory, TrustManager, TrustManagerFactory, SSLContext } -import akka.remote.{ RemoteClientError, RemoteTransportException, RemoteServerError } -import java.security.{ GeneralSecurityException, SecureRandom, KeyStore } +import akka.remote.{ RemoteTransportException } +import akka.event.LoggingAdapter import java.io.{ IOException, FileNotFoundException, FileInputStream } +import java.security.{ SecureRandom, GeneralSecurityException, KeyStore } +import akka.security.provider.AkkaProvider +import com.sun.xml.internal.bind.v2.model.core.NonElement -object NettySSLSupport { +/** + * Used for adding SSL support to Netty pipeline + * Internal use only + */ +private object NettySSLSupport { /** * Construct a SSLHandler which can be inserted into a Netty server/client pipeline */ - def apply(settings: NettySettings, netty: NettyRemoteTransport, isClient: Boolean): SslHandler = { - if (isClient) initialiseClientSSL(settings, netty) - else initialiseServerSSL(settings, netty) + def apply(settings: NettySettings, log: LoggingAdapter, isClient: Boolean): SslHandler = { + if (isClient) initialiseClientSSL(settings, log) + else initialiseServerSSL(settings, log) } - private def initialiseClientSSL(settings: NettySettings, netty: NettyRemoteTransport): SslHandler = { - netty.log.debug("Client SSL is enabled, initialising ...") + private def initialiseCustomSecureRandom(settings: NettySettings, log: LoggingAdapter): SecureRandom = { + /** + * According to this bug report: http://bugs.sun.com/view_bug.do;jsessionid=ff625daf459fdffffffffcd54f1c775299e0?bug_id=6202721 + * Using /dev/./urandom is only necessary when using SHA1PRNG on Linux + * Use 'new SecureRandom()' instead of 'SecureRandom.getInstance("SHA1PRNG")' to avoid having problems + */ + settings.SSLRandomSource match { + case Some(path) ⇒ System.setProperty("java.security.egd", path) + case None ⇒ + } + + val rng = settings.SSLRandomNumberGenerator match { + case Some(generator) ⇒ generator match { + case "AES128CounterRNGFast" ⇒ { + log.debug("SSL random number generator set to: AES128CounterRNGFast") + val akka = new AkkaProvider + Security.addProvider(akka) + SecureRandom.getInstance("AES128CounterRNGFast", akka) + } + case "AES128CounterRNGSecure" ⇒ { + log.debug("SSL random number generator set to: AES128CounterRNGSecure") + val akka = new AkkaProvider + Security.addProvider(akka) + SecureRandom.getInstance("AES128CounterRNGSecure", akka) + } + case "AES256CounterRNGSecure" ⇒ { + log.debug("SSL random number generator set to: AES256CounterRNGSecure") + val akka = new AkkaProvider + Security.addProvider(akka) + SecureRandom.getInstance("AES256CounterRNGSecure", akka) + } + case "SHA1PRNG" ⇒ { + log.debug("SSL random number generator set to: SHA1PRNG") + // This needs /dev/urandom to be the source on Linux to prevent problems with /dev/random blocking + // However, this also makes the seed source insecure as the seed is reused to avoid blocking (not a problem on FreeBSD). + SecureRandom.getInstance("SHA1PRNG") + } + case _ ⇒ { + log.debug("SSL random number generator set to default: SecureRandom") + new SecureRandom + } + } + case None ⇒ { + log.debug("SSL random number generator not set. Setting to default: SecureRandom") + new SecureRandom + } + } + // prevent stall on first access + rng.nextInt() + rng + } + + private def initialiseClientSSL(settings: NettySettings, log: LoggingAdapter): SslHandler = { + log.debug("Client SSL is enabled, initialising ...") val sslContext: Option[SSLContext] = { (settings.SSLTrustStore, settings.SSLTrustStorePassword, settings.SSLProtocol) match { - case (Some(trustStore), Some(password), Some(protocol)) ⇒ constructClientContext(settings, netty, trustStore, password, protocol) - case _ ⇒ throw new GeneralSecurityException("Could not find all SSL trust store settings") + case (Some(trustStore), Some(password), Some(protocol)) ⇒ constructClientContext(settings, log, trustStore, password, protocol) + case (trustStore, password, protocol) ⇒ + val msg = "SSL trust store settings went missing. [trust-store: %s] [trust-store-password: %s] [protocol: %s]" + .format(trustStore, password, protocol) + throw new GeneralSecurityException(msg) } } sslContext match { case Some(context) ⇒ { - netty.log.debug("Using client SSL context to create SSLEngine ...") + log.debug("Using client SSL context to create SSLEngine ...") val sslEngine = context.createSSLEngine sslEngine.setUseClientMode(true) sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) new SslHandler(sslEngine) } - case None ⇒ throw new GeneralSecurityException("Failed to initialise client SSL") + case None ⇒ { + val msg = "Failed to initialise client SSL because SSL context could not be found. " + + "Make sure your settings are correct: [trust-store: %s] [trust-store-password: %s] [protocol: %s]" + .format(settings.SSLTrustStore, settings.SSLTrustStorePassword, settings.SSLProtocol) + throw new GeneralSecurityException(msg) + } } } - private def constructClientContext(settings: NettySettings, netty: NettyRemoteTransport, trustStorePath: String, trustStorePassword: String, protocol: String): Option[SSLContext] = { + private def constructClientContext(settings: NettySettings, log: LoggingAdapter, trustStorePath: String, trustStorePassword: String, protocol: String): Option[SSLContext] = { try { val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) @@ -50,48 +118,43 @@ object NettySSLSupport { trustManagerFactory.init(trustStore) val trustManagers: Array[TrustManager] = trustManagerFactory.getTrustManagers val sslContext = SSLContext.getInstance(protocol) - sslContext.init(null, trustManagers, new SecureRandom()) + sslContext.init(null, trustManagers, initialiseCustomSecureRandom(settings, log)) Some(sslContext) } catch { - case e: FileNotFoundException ⇒ { - val exception = new RemoteTransportException("Client SSL connection could not be established because trust store could not be loaded", e) - netty.notifyListeners(RemoteClientError(exception, netty, netty.address)) - throw exception - } - case e: IOException ⇒ { - val exception = new RemoteTransportException("Client SSL connection could not be established because: " + e.getMessage, e) - netty.notifyListeners(RemoteClientError(exception, netty, netty.address)) - throw exception - } - case e: GeneralSecurityException ⇒ { - val exception = new RemoteTransportException("Client SSL connection could not be established because SSL context could not be constructed", e) - netty.notifyListeners(RemoteClientError(exception, netty, netty.address)) - throw exception - } + 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) } } - private def initialiseServerSSL(settings: NettySettings, netty: NettyRemoteTransport): SslHandler = { - netty.log.debug("Server SSL is enabled, initialising ...") + private def initialiseServerSSL(settings: NettySettings, log: LoggingAdapter): SslHandler = { + log.debug("Server SSL is enabled, initialising ...") val sslContext: Option[SSLContext] = { (settings.SSLKeyStore, settings.SSLKeyStorePassword, settings.SSLProtocol) match { - case (Some(keyStore), Some(password), Some(protocol)) ⇒ constructServerContext(settings, netty, keyStore, password, protocol) - case _ ⇒ throw new GeneralSecurityException("Could not find all SSL key store settings") + case (Some(keyStore), Some(password), Some(protocol)) ⇒ constructServerContext(settings, log, keyStore, password, protocol) + case (keyStore, password, protocol) ⇒ + val msg = "SSL key store settings went missing. [key-store: %s] [key-store-password: %s] [protocol: %s]".format(keyStore, password, protocol) + throw new GeneralSecurityException(msg) } } sslContext match { case Some(context) ⇒ { - netty.log.debug("Using server SSL context to create SSLEngine ...") + log.debug("Using server SSL context to create SSLEngine ...") val sslEngine = context.createSSLEngine sslEngine.setUseClientMode(false) sslEngine.setEnabledCipherSuites(settings.SSLSupportedAlgorithms.toArray.map(_.toString)) new SslHandler(sslEngine) } - case None ⇒ throw new GeneralSecurityException("Failed to initialise server SSL") + case None ⇒ { + val msg = "Failed to initialise 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) + throw new GeneralSecurityException(msg) + } } } - private def constructServerContext(settings: NettySettings, netty: NettyRemoteTransport, keyStorePath: String, keyStorePassword: String, protocol: String): Option[SSLContext] = { + private def constructServerContext(settings: NettySettings, log: LoggingAdapter, keyStorePath: String, keyStorePassword: String, protocol: String): Option[SSLContext] = { try { val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) @@ -99,24 +162,12 @@ object NettySSLSupport { keyStore.load(stream, keyStorePassword.toCharArray) factory.init(keyStore, keyStorePassword.toCharArray) val sslContext = SSLContext.getInstance(protocol) - sslContext.init(factory.getKeyManagers, null, new SecureRandom()) + sslContext.init(factory.getKeyManagers, null, initialiseCustomSecureRandom(settings, log)) Some(sslContext) } catch { - case e: FileNotFoundException ⇒ { - val exception = new RemoteTransportException("Server SSL connection could not be established because key store could not be loaded", e) - netty.notifyListeners(RemoteServerError(exception, netty)) - throw exception - } - case e: IOException ⇒ { - val exception = new RemoteTransportException("Server SSL connection could not be established because: " + e.getMessage, e) - netty.notifyListeners(RemoteServerError(exception, netty)) - throw exception - } - case e: GeneralSecurityException ⇒ { - val exception = new RemoteTransportException("Server SSL connection could not be established because SSL context could not be constructed", e) - netty.notifyListeners(RemoteServerError(exception, netty)) - throw exception - } + 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) } } } diff --git a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala index 5d829127f8..22a659958c 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/Settings.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/Settings.scala @@ -100,6 +100,16 @@ private[akka] class NettySettings(config: Config, val systemName: String) { case protocol ⇒ Some(protocol) } + val SSLRandomSource = getString("ssl.sha1prng-random-source") match { + case "" ⇒ None + case path ⇒ Some(path) + } + + val SSLRandomNumberGenerator = getString("ssl.random-number-generator") match { + case "" ⇒ None + case rng ⇒ Some(rng) + } + val EnableSSL = { val enableSSL = getBoolean("ssl.enable") if (enableSSL) { diff --git a/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala index 0d429043c2..c6556f0160 100644 --- a/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala @@ -41,6 +41,8 @@ akka { SSLTrustStorePassword must be(Some("changeme")) SSLProtocol must be(Some("TLSv1")) SSLSupportedAlgorithms must be(java.util.Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA")) + SSLRandomSource must be(None) + SSLRandomNumberGenerator must be(None) } } } diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 4b8f72e424..3416993f89 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -413,7 +413,7 @@ object Dependencies { ) val remote = Seq( - netty, protobuf, Test.junit, Test.scalatest + netty, protobuf, uncommonsMath, Test.junit, Test.scalatest ) val cluster = Seq(Test.junit, Test.scalatest) @@ -451,6 +451,7 @@ object Dependency { val ScalaStm = "0.5" val Scalatest = "1.6.1" val Slf4j = "1.6.4" + val UncommonsMath = "1.2.2a" } // Compile @@ -460,6 +461,7 @@ object Dependency { val protobuf = "com.google.protobuf" % "protobuf-java" % V.Protobuf // New BSD val scalaStm = "org.scala-tools" % "scala-stm_2.9.1" % V.ScalaStm // Modified BSD (Scala) val slf4jApi = "org.slf4j" % "slf4j-api" % V.Slf4j // MIT + val uncommonsMath = "org.uncommons.maths" % "uncommons-maths" % V.UncommonsMath // ApacheV2 val zeroMQ = "org.zeromq" % "zeromq-scala-binding_2.9.1" % "0.0.6" // ApacheV2 // Runtime From 399a08b8b37990827e03f41d4fd68f7a5d82a3f0 Mon Sep 17 00:00:00 2001 From: Peter Badenhorst Date: Mon, 11 Jun 2012 18:33:05 +0200 Subject: [PATCH 5/5] Used RemoteCommunicationSpec as a template to implement a functional spec to test SSL communication. 1) Converted provider and related RNG's from Java to Scala 2) Added trust/key stores for testing purposes 3) As stated in the test comments, Internet access is required for the 2 'Secure' RNG variants to function within the time limit. 4) Fixed unnecessary imports --- .../provider/AES128CounterRNGFast.java | 51 ------ .../provider/AES128CounterRNGSecure.java | 49 ------ .../provider/AES256CounterRNGSecure.java | 49 ------ .../akka/security/provider/AkkaProvider.java | 37 ---- akka-remote/src/main/resources/reference.conf | 2 +- .../akka/remote/netty/NettySSLSupport.scala | 63 +++---- .../provider/AES128CounterRNGFast.scala | 41 +++++ .../provider/AES128CounterRNGSecure.scala | 40 +++++ .../provider/AES256CounterRNGSecure.scala | 40 +++++ .../akka/security/provider/AkkaProvider.scala | 31 ++++ akka-remote/src/test/resources/keystore | Bin 0 -> 1342 bytes akka-remote/src/test/resources/truststore | Bin 0 -> 637 bytes .../remote/Ticket1978CommunicationSpec.scala | 161 ++++++++++++++++++ 13 files changed, 333 insertions(+), 231 deletions(-) delete mode 100644 akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java delete mode 100644 akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java delete mode 100644 akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java delete mode 100644 akka-remote/src/main/java/akka/security/provider/AkkaProvider.java create mode 100644 akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGFast.scala create mode 100644 akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGSecure.scala create mode 100644 akka-remote/src/main/scala/akka/security/provider/AES256CounterRNGSecure.scala create mode 100644 akka-remote/src/main/scala/akka/security/provider/AkkaProvider.scala create mode 100644 akka-remote/src/test/resources/keystore create mode 100644 akka-remote/src/test/resources/truststore create mode 100644 akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala diff --git a/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java b/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java deleted file mode 100644 index a982a6f705..0000000000 --- a/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGFast.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (C) 2009-2012 Typesafe Inc. - */ -package akka.security.provider; - -import org.uncommons.maths.random.SecureRandomSeedGenerator; -import org.uncommons.maths.random.SeedException; - -import java.security.GeneralSecurityException; -import java.security.SecureRandom; - -/** - * Internal API - */ -public class AES128CounterRNGFast extends java.security.SecureRandomSpi { - private org.uncommons.maths.random.AESCounterRNG rng; - - public AES128CounterRNGFast() throws SeedException, GeneralSecurityException { - rng = new org.uncommons.maths.random.AESCounterRNG(new SecureRandomSeedGenerator()); - } - - /** - * This is managed internally only - */ - @Override - protected void engineSetSeed(byte[] seed) { - - } - - /** - * Generates a user-specified number of random bytes. - * - * @param bytes the array to be filled in with random bytes. - */ - @Override - protected void engineNextBytes(byte[] bytes) { - rng.nextBytes(bytes); - } - - /** - * Returns the given number of seed bytes. This call may be used to - * seed other random number generators. - * - * @param numBytes the number of seed bytes to generate. - * @return the seed bytes. - */ - @Override - protected byte[] engineGenerateSeed(int numBytes) { - return (new SecureRandom()).generateSeed(numBytes); - } -} diff --git a/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java b/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java deleted file mode 100644 index 178a6c392b..0000000000 --- a/akka-remote/src/main/java/akka/security/provider/AES128CounterRNGSecure.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (C) 2009-2012 Typesafe Inc. - */ -package akka.security.provider; - -import org.uncommons.maths.random.DefaultSeedGenerator; - -import java.security.GeneralSecurityException; - -/** - * Internal API - */ -public class AES128CounterRNGSecure extends java.security.SecureRandomSpi { - private org.uncommons.maths.random.AESCounterRNG rng; - - public AES128CounterRNGSecure() throws GeneralSecurityException { - rng = new org.uncommons.maths.random.AESCounterRNG(); - } - - /** - * This is managed internally only - */ - @Override - protected void engineSetSeed(byte[] seed) { - - } - - /** - * Generates a user-specified number of random bytes. - * - * @param bytes the array to be filled in with random bytes. - */ - @Override - protected void engineNextBytes(byte[] bytes) { - rng.nextBytes(bytes); - } - - /** - * Returns the given number of seed bytes. This call may be used to - * seed other random number generators. - * - * @param numBytes the number of seed bytes to generate. - * @return the seed bytes. - */ - @Override - protected byte[] engineGenerateSeed(int numBytes) { - return DefaultSeedGenerator.getInstance().generateSeed(numBytes); - } -} diff --git a/akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java b/akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java deleted file mode 100644 index 48d651b86b..0000000000 --- a/akka-remote/src/main/java/akka/security/provider/AES256CounterRNGSecure.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (C) 2009-2012 Typesafe Inc. - */ -package akka.security.provider; - -import org.uncommons.maths.random.DefaultSeedGenerator; - -import java.security.GeneralSecurityException; - -/** - * Internal API - */ -public class AES256CounterRNGSecure extends java.security.SecureRandomSpi { - private org.uncommons.maths.random.AESCounterRNG rng; - - public AES256CounterRNGSecure() throws GeneralSecurityException { - rng = new org.uncommons.maths.random.AESCounterRNG(32); - } - - /** - * This is managed internally only - */ - @Override - protected void engineSetSeed(byte[] seed) { - - } - - /** - * Generates a user-specified number of random bytes. - * - * @param bytes the array to be filled in with random bytes. - */ - @Override - protected void engineNextBytes(byte[] bytes) { - rng.nextBytes(bytes); - } - - /** - * Returns the given number of seed bytes. This call may be used to - * seed other random number generators. - * - * @param numBytes the number of seed bytes to generate. - * @return the seed bytes. - */ - @Override - protected byte[] engineGenerateSeed(int numBytes) { - return DefaultSeedGenerator.getInstance().generateSeed(numBytes); - } -} diff --git a/akka-remote/src/main/java/akka/security/provider/AkkaProvider.java b/akka-remote/src/main/java/akka/security/provider/AkkaProvider.java deleted file mode 100644 index 9c4a0c2181..0000000000 --- a/akka-remote/src/main/java/akka/security/provider/AkkaProvider.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (C) 2009-2012 Typesafe Inc. - */ -package akka.security.provider; - -import java.security.AccessController; -import java.security.Provider; - -/** - * A provider that for AES128CounterRNGFast, a cryptographically secure random number generator through SecureRandom - */ -public final class AkkaProvider extends Provider { - public AkkaProvider() { - super("Akka", 1.0, "Akka provider 1.0 that implements a secure AES random number generator"); - - AccessController.doPrivileged(new java.security.PrivilegedAction() { - public Object run() { - - /** - * SecureRandom - */ - put("SecureRandom.AES128CounterRNGFast", "akka.security.provider.AES128CounterRNGFast"); - put("SecureRandom.AES128CounterRNGSecure", "akka.security.provider.AES128CounterRNGSecure"); - put("SecureRandom.AES256CounterRNGSecure", "akka.security.provider.AES256CounterRNGSecure"); - - /** - * Implementation type: software or hardware - */ - put("SecureRandom.AES128CounterRNGFast ImplementedIn", "Software"); - put("SecureRandom.AES128CounterRNGSecure ImplementedIn", "Software"); - put("SecureRandom.AES256CounterRNGSecure ImplementedIn", "Software"); - - return null; - } - }); - } -} diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index 80719decf4..0172b14e38 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -187,7 +187,7 @@ akka { # Using /dev/./urandom is only necessary when using SHA1PRNG on Linux to prevent blocking # It is NOT as secure because it reuses the seed # '' => defaults to /dev/random or whatever is set in java.security for example: securerandom.source=file:/dev/random - # '/dev/./urandom' => NOT '/dev/urandom' as that doesn't work according to: http://bugs.sun.com/view_bug.do;jsessionid=ff625daf459fdffffffffcd54f1c775299e0?bug_id=6202721 + # '/dev/./urandom' => NOT '/dev/urandom' as that doesn't work according to: http://bugs.sun.com/view_bug.do?bug_id=6202721 sha1prng-random-source = "" # There are three options, in increasing order of security: diff --git a/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala b/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala index 011aa92233..99f56bf301 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/NettySSLSupport.scala @@ -4,17 +4,13 @@ package akka.remote.netty -import _root_.java.security.Provider -import _root_.java.security.SecureRandom -import _root_.java.security.Security import org.jboss.netty.handler.ssl.SslHandler import javax.net.ssl.{ KeyManagerFactory, TrustManager, TrustManagerFactory, SSLContext } -import akka.remote.{ RemoteTransportException } +import akka.remote.RemoteTransportException import akka.event.LoggingAdapter import java.io.{ IOException, FileNotFoundException, FileInputStream } -import java.security.{ SecureRandom, GeneralSecurityException, KeyStore } +import java.security.{ SecureRandom, GeneralSecurityException, KeyStore, Security } import akka.security.provider.AkkaProvider -import com.sun.xml.internal.bind.v2.model.core.NonElement /** * Used for adding SSL support to Netty pipeline @@ -31,50 +27,29 @@ private object NettySSLSupport { private def initialiseCustomSecureRandom(settings: NettySettings, log: LoggingAdapter): SecureRandom = { /** - * According to this bug report: http://bugs.sun.com/view_bug.do;jsessionid=ff625daf459fdffffffffcd54f1c775299e0?bug_id=6202721 + * According to this bug report: http://bugs.sun.com/view_bug.do?bug_id=6202721 * Using /dev/./urandom is only necessary when using SHA1PRNG on Linux * Use 'new SecureRandom()' instead of 'SecureRandom.getInstance("SHA1PRNG")' to avoid having problems */ - settings.SSLRandomSource match { - case Some(path) ⇒ System.setProperty("java.security.egd", path) - case None ⇒ - } + settings.SSLRandomSource foreach { path ⇒ System.setProperty("java.security.egd", path) } val rng = settings.SSLRandomNumberGenerator match { - case Some(generator) ⇒ generator match { - case "AES128CounterRNGFast" ⇒ { - log.debug("SSL random number generator set to: AES128CounterRNGFast") - val akka = new AkkaProvider - Security.addProvider(akka) - SecureRandom.getInstance("AES128CounterRNGFast", akka) - } - case "AES128CounterRNGSecure" ⇒ { - log.debug("SSL random number generator set to: AES128CounterRNGSecure") - val akka = new AkkaProvider - Security.addProvider(akka) - SecureRandom.getInstance("AES128CounterRNGSecure", akka) - } - case "AES256CounterRNGSecure" ⇒ { - log.debug("SSL random number generator set to: AES256CounterRNGSecure") - val akka = new AkkaProvider - Security.addProvider(akka) - SecureRandom.getInstance("AES256CounterRNGSecure", akka) - } - case "SHA1PRNG" ⇒ { - log.debug("SSL random number generator set to: SHA1PRNG") - // This needs /dev/urandom to be the source on Linux to prevent problems with /dev/random blocking - // However, this also makes the seed source insecure as the seed is reused to avoid blocking (not a problem on FreeBSD). - SecureRandom.getInstance("SHA1PRNG") - } - case _ ⇒ { - log.debug("SSL random number generator set to default: SecureRandom") - new SecureRandom - } - } - case None ⇒ { - log.debug("SSL random number generator not set. Setting to default: SecureRandom") + case Some(r @ ("AES128CounterRNGFast" | "AES128CounterRNGSecure" | "AES256CounterRNGSecure")) ⇒ + log.debug("SSL random number generator set to: {}", r) + val akka = new AkkaProvider + Security.addProvider(akka) + SecureRandom.getInstance(r, akka) + case Some("SHA1PRNG") ⇒ + log.debug("SSL random number generator set to: SHA1PRNG") + // This needs /dev/urandom to be the source on Linux to prevent problems with /dev/random blocking + // However, this also makes the seed source insecure as the seed is reused to avoid blocking (not a problem on FreeBSD). + SecureRandom.getInstance("SHA1PRNG") + case Some(unknown) ⇒ + log.debug("Unknown SSLRandomNumberGenerator [{}] falling back to SecureRandom", unknown) + new SecureRandom + case None ⇒ + log.debug("SSLRandomNumberGenerator not specified, falling back to SecureRandom") new SecureRandom - } } // prevent stall on first access rng.nextInt() diff --git a/akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGFast.scala b/akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGFast.scala new file mode 100644 index 0000000000..12f0d2a83e --- /dev/null +++ b/akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGFast.scala @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider + +import org.uncommons.maths.random.{ AESCounterRNG, SecureRandomSeedGenerator } +import java.security.SecureRandom + +/** + * Internal API + */ +class AES128CounterRNGFast extends java.security.SecureRandomSpi { + private val rng = new AESCounterRNG(new SecureRandomSeedGenerator()) + + /** + * This is managed internally only + */ + protected def engineSetSeed(seed: Array[Byte]) { + } + + /** + * Generates a user-specified number of random bytes. + * + * @param bytes the array to be filled in with random bytes. + */ + protected def engineNextBytes(bytes: Array[Byte]) { + rng.nextBytes(bytes) + } + + /** + * Returns the given number of seed bytes. This call may be used to + * seed other random number generators. + * + * @param numBytes the number of seed bytes to generate. + * @return the seed bytes. + */ + protected def engineGenerateSeed(numBytes: Int): Array[Byte] = { + (new SecureRandom).generateSeed(numBytes) + } +} + diff --git a/akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGSecure.scala b/akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGSecure.scala new file mode 100644 index 0000000000..4859a8ea4b --- /dev/null +++ b/akka-remote/src/main/scala/akka/security/provider/AES128CounterRNGSecure.scala @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider + +import org.uncommons.maths.random.{ AESCounterRNG, DefaultSeedGenerator } + +/** + * Internal API + */ +class AES128CounterRNGSecure extends java.security.SecureRandomSpi { + private val rng = new AESCounterRNG() + + /** + * This is managed internally only + */ + protected def engineSetSeed(seed: Array[Byte]) { + } + + /** + * Generates a user-specified number of random bytes. + * + * @param bytes the array to be filled in with random bytes. + */ + protected def engineNextBytes(bytes: Array[Byte]) { + rng.nextBytes(bytes) + } + + /** + * Returns the given number of seed bytes. This call may be used to + * seed other random number generators. + * + * @param numBytes the number of seed bytes to generate. + * @return the seed bytes. + */ + protected def engineGenerateSeed(numBytes: Int): Array[Byte] = { + DefaultSeedGenerator.getInstance.generateSeed(numBytes) + } +} + diff --git a/akka-remote/src/main/scala/akka/security/provider/AES256CounterRNGSecure.scala b/akka-remote/src/main/scala/akka/security/provider/AES256CounterRNGSecure.scala new file mode 100644 index 0000000000..3aeda2b1a1 --- /dev/null +++ b/akka-remote/src/main/scala/akka/security/provider/AES256CounterRNGSecure.scala @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider + +import org.uncommons.maths.random.{ AESCounterRNG, DefaultSeedGenerator } + +/** + * Internal API + */ +class AES256CounterRNGSecure extends java.security.SecureRandomSpi { + private val rng = new AESCounterRNG(32) + + /** + * This is managed internally only + */ + protected def engineSetSeed(seed: Array[Byte]) { + } + + /** + * Generates a user-specified number of random bytes. + * + * @param bytes the array to be filled in with random bytes. + */ + protected def engineNextBytes(bytes: Array[Byte]) { + rng.nextBytes(bytes) + } + + /** + * Returns the given number of seed bytes. This call may be used to + * seed other random number generators. + * + * @param numBytes the number of seed bytes to generate. + * @return the seed bytes. + */ + protected def engineGenerateSeed(numBytes: Int): Array[Byte] = { + DefaultSeedGenerator.getInstance.generateSeed(numBytes) + } +} + diff --git a/akka-remote/src/main/scala/akka/security/provider/AkkaProvider.scala b/akka-remote/src/main/scala/akka/security/provider/AkkaProvider.scala new file mode 100644 index 0000000000..705afa37ba --- /dev/null +++ b/akka-remote/src/main/scala/akka/security/provider/AkkaProvider.scala @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.security.provider + +import java.security.{ PrivilegedAction, AccessController, Provider } + +/** + * A provider that for AES128CounterRNGFast, a cryptographically secure random number generator through SecureRandom + */ +final class AkkaProvider extends Provider("Akka", 1.0, "Akka provider 1.0 that implements a secure AES random number generator") { + AccessController.doPrivileged(new PrivilegedAction[AkkaProvider] { + def run = { + /** + * SecureRandom + */ + put("SecureRandom.AES128CounterRNGFast", "akka.security.provider.AES128CounterRNGFast") + put("SecureRandom.AES128CounterRNGSecure", "akka.security.provider.AES128CounterRNGSecure") + put("SecureRandom.AES256CounterRNGSecure", "akka.security.provider.AES256CounterRNGSecure") + + /** + * Implementation type: software or hardware + */ + put("SecureRandom.AES128CounterRNGFast ImplementedIn", "Software") + put("SecureRandom.AES128CounterRNGSecure ImplementedIn", "Software") + put("SecureRandom.AES256CounterRNGSecure ImplementedIn", "Software") + null + } + }) +} + diff --git a/akka-remote/src/test/resources/keystore b/akka-remote/src/test/resources/keystore new file mode 100644 index 0000000000000000000000000000000000000000..ee5581d930a1cb38981f2a547aab3acf24861e71 GIT binary patch literal 1342 zcmezO_TO6u1_mYu1_nkjX3ee4POW5MU^GA7;L^;%z_in#iD|0=9~+l88zT#&7Ly<& zBP#<-6Vt*Lr#iO3d!phiO(!{)GWxCBqR4KxSN-)mxxc<^>*`br4K@BHt~ki3Iko1i zFn8Nbu2)`EZ_Af-S6!S9{%lD6ax4vqIuM+Etxqil0#b86~A#=eO1S5e{%P` zhYnuWFE;T9el?upf9KpL2M6scKEBl)M^>G$c$Iv(L^i>I!QWzb!&R<^cdxnpEHmO( z8K}k0SiDi;JC}m__6Fv zLMoe`r*C1^2g5)1?{|Du3sS23oqUC_hIh`US(9us=SS{HdR0C@@tH=BqR!UWPX(7B zc(K)gj>j3T1Dh}Os_XUsR>{b^%=Y@v!@M(BcO70}bu#5n%8SYCluveUTh=L8%Co}! ziN?j`{(_9t4(j}lldH}KYrgy6xz4Ts!i&f6l%-A8epId97}3*F%Bq{?*?z~a?Uee| z>=?rnTQ!?fvab9)lz02}_wuuP$6Bu3YU7$zcjVX3{m-Jq=DX`WySH-R8&3IH@nbxb z!yl?F4PSZT-p`n*S+*5LO^akEIn`c2Z@kTLRm{)xt2V`I&+7gar>VgG)K>i8$(yCG zYpW)|Uia<8N70(OcT;D&UkVZV&3%DOI#oYIXiZPJQweYQUItB7d8acv+a~mEI`{98=_Og#`sGaDy|wHLA{Iza z>N)g8dddg$GixUvnOn#F=Y3Cdo#t-7gp2G(I{#xNPF?Bsn-F+P6_TDK^h^yb85o%C z4Vsv&4VoC2EMR70WMX3Rzj*3`0WTY;R+~rLcV0$DR#pasL_=-^PB!LH7B*p~C`Usc z11=DULzuZdH3`OJhwuaq_(5`9!WG&C}Y3edw5jq{Ox&dAEZ+}O)t z(Ade;*vQbe%rjTX@(7Re$BD;t4m_~q`sUtt=l0fllUD55_+Zv%*=G|Zd;gv3ICyPh z$PTVI^Rt*QIf$K+oB6h{YVS?~4+ZwyU0L!~i!XU<>#G$+{5`r|Eze=8p45+HOG3MD z1q;|e++`MRP-bc;e<>nd*K6Ut+1^vWHbk%aQtlt)ee=|(4ZTdvj0}v(&SM2RuaO}n z{@bs4%#V`Vwz&V>%<+7Jc9XZ=O@ZUCPcEk=o?m+74v(IM-nyGQtKAkoOGN8U;EPUNAwEWKh{Z;W<0QMEHk{lvQ`-Y DEz%@Z literal 0 HcmV?d00001 diff --git a/akka-remote/src/test/resources/truststore b/akka-remote/src/test/resources/truststore new file mode 100644 index 0000000000000000000000000000000000000000..cc07616dad6cd4bb2833468ee5b4e6bf79b62b97 GIT binary patch literal 637 zcmezO_TO6u1_mYu1_nkj&6-=8om$Djz-WHDxleOoi}O4j*SmyZI*pDL9+MXnT~_kCWh?bdNV(Z`I3X! z8M&En`>OWt6!1`BzulE3U$yv>r?$RYLB!vq+tusGLU{li^m(FSFv zcJh}Z!gakC&YSH$Dq51hHg}QgTH^`-7oL^i+^@&VxzKTvSwMBYx2U(gzo&W#< literal 0 HcmV?d00001 diff --git a/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala b/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala new file mode 100644 index 0000000000..ff41e369ff --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala @@ -0,0 +1,161 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.remote + +import akka.testkit._ +import akka.actor._ +import com.typesafe.config._ +import akka.dispatch.{ Await, Future } +import akka.pattern.ask +import java.io.File +import java.security.{ PrivilegedAction, AccessController } + +object Configuration { + // set this in your JAVA_OPTS to see all ssl debug info: "-Djavax.net.debug=ssl,keymanager" + // The certificate will expire in 2109 + private val trustStore = getPath("truststore") + private val keyStore = getPath("keystore") + private def getPath(name: String): String = (new File("akka-remote/src/test/resources/" + name)).getAbsolutePath.replace("\\", "\\\\") + private val conf = """ + akka { + actor.provider = "akka.remote.RemoteActorRefProvider" + remote.netty { + hostname = localhost + port = 12345 + ssl { + enable = on + trust-store = "%s" + key-store = "%s" + random-number-generator = "%s" + } + } + actor.deployment { + /blub.remote = "akka://remote-sys@localhost:12346" + /looker/child.remote = "akka://remote-sys@localhost:12346" + /looker/child/grandchild.remote = "akka://Ticket1978CommunicationSpec@localhost:12345" + } + } + """ + + def getConfig(rng: String): String = { + conf.format(trustStore, keyStore, rng) + } +} + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class Ticket1978SHA1PRNG extends Ticket1978CommunicationSpec(Configuration.getConfig("SHA1PRNG")) + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class Ticket1978AES128CounterRNGFast extends Ticket1978CommunicationSpec(Configuration.getConfig("AES128CounterRNGFast")) + +/** + * Both of the Secure variants require access to the Internet to access random.org. + */ +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class Ticket1978AES128CounterRNGSecure extends Ticket1978CommunicationSpec(Configuration.getConfig("AES128CounterRNGSecure")) + +/** + * Both of the Secure variants require access to the Internet to access random.org. + */ +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class Ticket1978AES256CounterRNGSecure extends Ticket1978CommunicationSpec(Configuration.getConfig("AES256CounterRNGSecure")) + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class Ticket1978CommunicationSpec(val configuration: String) + extends AkkaSpec(configuration) with ImplicitSender with DefaultTimeout { + + import RemoteCommunicationSpec._ + + // default SecureRandom RNG + def this() = this(Configuration.getConfig("")) + + val conf = ConfigFactory.parseString("akka.remote.netty.port=12346").withFallback(system.settings.config) + val other = ActorSystem("remote-sys", conf) + + val remote = other.actorOf(Props(new Actor { + def receive = { + case "ping" ⇒ sender ! (("pong", sender)) + } + }), "echo") + + val here = system.actorFor("akka://remote-sys@localhost:12346/user/echo") + + override def atTermination() { + other.shutdown() + } + + "SSL Remoting" must { + + "support remote look-ups" in { + here ! "ping" + expectMsgPF() { + case ("pong", s: AnyRef) if s eq testActor ⇒ true + } + } + + "send error message for wrong address" in { + EventFilter.error(start = "dropping", occurrences = 1).intercept { + system.actorFor("akka://remotesys@localhost:12346/user/echo") ! "ping" + }(other) + } + + "support ask" in { + Await.result(here ? "ping", timeout.duration) match { + case ("pong", s: akka.pattern.PromiseActorRef) ⇒ // good + case m ⇒ fail(m + " was not (pong, AskActorRef)") + } + } + + "send dead letters on remote if actor does not exist" in { + EventFilter.warning(pattern = "dead.*buh", occurrences = 1).intercept { + system.actorFor("akka://remote-sys@localhost:12346/does/not/exist") ! "buh" + }(other) + } + + "create and supervise children on remote node" in { + val r = system.actorOf(Props[Echo], "blub") + r.path.toString must be === "akka://remote-sys@localhost:12346/remote/Ticket1978CommunicationSpec@localhost:12345/user/blub" + r ! 42 + expectMsg(42) + EventFilter[Exception]("crash", occurrences = 1).intercept { + r ! new Exception("crash") + }(other) + expectMsg("preRestart") + r ! 42 + expectMsg(42) + system.stop(r) + expectMsg("postStop") + } + + "look-up actors across node boundaries" in { + val l = system.actorOf(Props(new Actor { + def receive = { + case (p: Props, n: String) ⇒ sender ! context.actorOf(p, n) + case s: String ⇒ sender ! context.actorFor(s) + } + }), "looker") + l ! (Props[Echo], "child") + val r = expectMsgType[ActorRef] + r ! (Props[Echo], "grandchild") + val remref = expectMsgType[ActorRef] + remref.isInstanceOf[LocalActorRef] must be(true) + val myref = system.actorFor(system / "looker" / "child" / "grandchild") + myref.isInstanceOf[RemoteActorRef] must be(true) + myref ! 43 + expectMsg(43) + lastSender must be theSameInstanceAs remref + r.asInstanceOf[RemoteActorRef].getParent must be(l) + system.actorFor("/user/looker/child") must be theSameInstanceAs r + Await.result(l ? "child/..", timeout.duration).asInstanceOf[AnyRef] must be theSameInstanceAs l + Await.result(system.actorFor(system / "looker" / "child") ? "..", timeout.duration).asInstanceOf[AnyRef] must be theSameInstanceAs l + } + + "not fail ask across node boundaries" in { + val f = for (_ ← 1 to 1000) yield here ? "ping" mapTo manifest[(String, ActorRef)] + Await.result(Future.sequence(f), remaining).map(_._1).toSet must be(Set("pong")) + } + + } + +}