diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index 8e74e151c8..06a0248730 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -1098,7 +1098,6 @@ akka { outbound-client-hostname = "" } - } # SSL configuration that is used when transport=tls-tcp. @@ -1176,6 +1175,63 @@ akka { hostname-verification = off } + # Config of akka.remote.artery.tcp.ssl.RotatingKeysSSLEngineProvider + # This engine provider reads PEM files from a mount point shared with the secret + # manager. The constructed SSLContext is cached some time (configurable) so when + # the credentials rotate the new credentials are eventually picked up. + # By default mTLS is enabled. + # This provider also includes a verification fase that runs after the TLS handshake + # phase. In this verification, both peers run an authorization and verify they are + # part of the same akka cluster. The verification happens via comparing the subject + # names in the peer's certificate with the name on the own certificate so if you + # use this SSLEngineProvider you should make sure all nodes on the cluster include + # at least one common subject name (CN or SAN). + # The Key setup this implementation supports has some limitations: + # 1. the private key must be provided on a PKCS#1 or a non-encrypted PKCS#8 PEM-formatted file + # 2. the private key must be be of an algorythm supported by `akka-pki` tools (e.g. "RSA", not "EC") + # 3. the node certificate must be issued by a root CA (not an intermediate CA) + # 4. both the node and the CA certificates must be provided in PEM-formatted files + rotating-keys-engine { + + # This is a convention that people may follow if they wish to save themselves some configuration + secret-mount-point = /var/run/secrets/akka-tls/rotating-keys-engine + + # The absolute path the PEM file with the private key. + key-file = ${akka.remote.artery.ssl.rotating-keys-engine.secret-mount-point}/tls.key + # The absolute path to the PEM file of the certificate for the private key above. + cert-file = ${akka.remote.artery.ssl.rotating-keys-engine.secret-mount-point}/tls.crt + # The absolute path to the PEM file of the certificate of the CA that emited + # the node certificate above. + ca-cert-file = ${akka.remote.artery.ssl.rotating-keys-engine.secret-mount-point}/ca.crt + + # There are two options, and the default SecureRandom is recommended: + # "" or "SecureRandom" => (default) + # "SHA1PRNG" => Can be slow because of blocking issues on Linux + # + # Setting a value here may require you to supply the appropriate cipher + # suite (see enabled-algorithms section) + random-number-generator = "" + + # Example: ["TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + # "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + # "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + # "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"] + # If you use a JDK 8 prior to 8u161 you need to install + # the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256. + # More info here: + # https://www.oracle.com/java/technologies/javase-jce-all-downloads.html + enabled-algorithms = ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"] + + # Protocol to use for SSL encryption, choose from: + # TLS 1.2 is available since JDK7, and default since JDK8: + # https://blogs.oracle.com/java-platform-group/entry/java_8_will_use_tls + protocol = "TLSv1.2" + + # How long should an SSLContext instance be cached. When rotating keys and certificates, + # there must a time overlap between the old certificate/key and the new ones. The + # value of this setting should be lower than duration of that overlap. + ssl-context-cache-ttl = 5m + } } } } diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/ConfigSSLEngineProvider.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/ConfigSSLEngineProvider.scala new file mode 100644 index 0000000000..cf79ba5b3b --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/ConfigSSLEngineProvider.scala @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp + +import java.io.FileNotFoundException +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths +import java.security.GeneralSecurityException +import java.security.KeyStore +import java.security.SecureRandom + +import akka.actor.ActorSystem +import akka.event.LogMarker +import akka.event.Logging +import akka.event.MarkerLoggingAdapter +import akka.remote.artery.tcp.ssl.SSLEngineConfig +import akka.stream.TLSRole +import com.typesafe.config.Config +import javax.net.ssl.KeyManager +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory + +import scala.util.Try + +/** + * Config in akka.remote.artery.ssl.config-ssl-engine + * + * Subclass may override protected methods to replace certain parts, such as key and trust manager. + */ +class ConfigSSLEngineProvider(protected val config: Config, protected val log: MarkerLoggingAdapter) + extends SSLEngineProvider { + + def this(system: ActorSystem) = + this( + system.settings.config.getConfig("akka.remote.artery.ssl.config-ssl-engine"), + Logging.withMarker(system, classOf[ConfigSSLEngineProvider].getName)) + + private val sslEngineConfig = new SSLEngineConfig(config) + + val SSLKeyStore: String = config.getString("key-store") + val SSLTrustStore: String = config.getString("trust-store") + val SSLKeyStorePassword: String = config.getString("key-store-password") + val SSLKeyPassword: String = config.getString("key-password") + val SSLTrustStorePassword: String = config.getString("trust-store-password") + val SSLEnabledAlgorithms: Set[String] = sslEngineConfig.SSLEnabledAlgorithms + val SSLProtocol: String = sslEngineConfig.SSLProtocol + val SSLRandomNumberGenerator: String = sslEngineConfig.SSLRandomNumberGenerator + val SSLRequireMutualAuthentication: Boolean = sslEngineConfig.SSLRequireMutualAuthentication + val HostnameVerification: Boolean = sslEngineConfig.HostnameVerification + + private lazy val sslContext: SSLContext = { + // log hostname verification warning once + if (HostnameVerification) + log.debug("TLS/SSL hostname verification is enabled.") + else + log.info( + LogMarker.Security, + "TLS/SSL hostname verification is disabled. See Akka reference documentation for more information.") + + constructContext() + } + + private def constructContext(): SSLContext = { + try { + val rng = createSecureRandom() + val ctx = SSLContext.getInstance(SSLProtocol) + ctx.init(keyManagers, trustManagers, rng) + ctx + } catch { + case e: FileNotFoundException => + throw new SslTransportException( + "Server SSL connection could not be established because key store could not be loaded", + e) + case e: IOException => + throw new SslTransportException("Server SSL connection could not be established because: " + e.getMessage, e) + case e: GeneralSecurityException => + throw new SslTransportException( + "Server SSL connection could not be established because SSL context could not be constructed", + e) + } + } + + /** + * Subclass may override to customize loading of `KeyStore` + */ + protected def loadKeystore(filename: String, password: String): KeyStore = { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + val fin = Files.newInputStream(Paths.get(filename)) + try keyStore.load(fin, password.toCharArray) + finally Try(fin.close()) + keyStore + } + + /** + * Subclass may override to customize `KeyManager` + */ + protected def keyManagers: Array[KeyManager] = { + val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + factory.init(loadKeystore(SSLKeyStore, SSLKeyStorePassword), SSLKeyPassword.toCharArray) + factory.getKeyManagers + } + + /** + * Subclass may override to customize `TrustManager` + */ + protected def trustManagers: Array[TrustManager] = { + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + trustManagerFactory.init(loadKeystore(SSLTrustStore, SSLTrustStorePassword)) + trustManagerFactory.getTrustManagers + } + + def createSecureRandom(): SecureRandom = + SecureRandomFactory.createSecureRandom(SSLRandomNumberGenerator, log) + + override def createServerSSLEngine(hostname: String, port: Int): SSLEngine = + createSSLEngine(akka.stream.Server, hostname, port) + + override def createClientSSLEngine(hostname: String, port: Int): SSLEngine = + createSSLEngine(akka.stream.Client, hostname, port) + + private def createSSLEngine(role: TLSRole, hostname: String, port: Int): SSLEngine = { + createSSLEngine(sslContext, role, hostname, port) + } + + private def createSSLEngine(sslContext: SSLContext, role: TLSRole, hostname: String, port: Int): SSLEngine = { + + val engine = sslContext.createSSLEngine(hostname, port) + + if (HostnameVerification && role == akka.stream.Client) { + val sslParams = sslContext.getDefaultSSLParameters + sslParams.setEndpointIdentificationAlgorithm("HTTPS") + engine.setSSLParameters(sslParams) + } + + engine.setUseClientMode(role == akka.stream.Client) + engine.setEnabledCipherSuites(SSLEnabledAlgorithms.toArray) + engine.setEnabledProtocols(Array(SSLProtocol)) + + if ((role != akka.stream.Client) && SSLRequireMutualAuthentication) + engine.setNeedClientAuth(true) + + engine + } + + override def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] = + None + + override def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] = + None + +} diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala index 29767e1f1d..8e624eb904 100644 --- a/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala @@ -5,35 +5,11 @@ package akka.remote.artery package tcp -import java.io.FileNotFoundException -import java.io.IOException -import java.nio.file.Files -import java.nio.file.Paths -import java.security.GeneralSecurityException -import java.security.KeyStore -import java.security.SecureRandom -import javax.net.ssl.KeyManager -import javax.net.ssl.KeyManagerFactory -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLEngine -import javax.net.ssl.SSLSession -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory - -import scala.util.Try - -import com.typesafe.config.Config - -import akka.actor.ActorSystem import akka.actor.ExtendedActorSystem import akka.actor.setup.Setup -import akka.annotation.InternalApi -import akka.event.LogMarker -import akka.event.Logging -import akka.event.MarkerLoggingAdapter -import akka.japi.Util.immutableSeq -import akka.stream.TLSRole import akka.util.ccompat._ +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession @ccompatUsedUntil213 trait SSLEngineProvider { @@ -60,132 +36,6 @@ trait SSLEngineProvider { class SslTransportException(message: String, cause: Throwable) extends RuntimeException(message, cause) -/** - * Config in akka.remote.artery.ssl.config-ssl-engine - * - * Subclass may override protected methods to replace certain parts, such as key and trust manager. - */ -class ConfigSSLEngineProvider(protected val config: Config, protected val log: MarkerLoggingAdapter) - extends SSLEngineProvider { - - def this(system: ActorSystem) = - this( - system.settings.config.getConfig("akka.remote.artery.ssl.config-ssl-engine"), - Logging.withMarker(system, classOf[ConfigSSLEngineProvider].getName)) - - val SSLKeyStore: String = config.getString("key-store") - val SSLTrustStore: String = config.getString("trust-store") - val SSLKeyStorePassword: String = config.getString("key-store-password") - val SSLKeyPassword: String = config.getString("key-password") - val SSLTrustStorePassword: String = config.getString("trust-store-password") - val SSLEnabledAlgorithms: Set[String] = immutableSeq(config.getStringList("enabled-algorithms")).to(Set) - val SSLProtocol: String = config.getString("protocol") - val SSLRandomNumberGenerator: String = config.getString("random-number-generator") - val SSLRequireMutualAuthentication: Boolean = config.getBoolean("require-mutual-authentication") - val HostnameVerification: Boolean = config.getBoolean("hostname-verification") - - private lazy val sslContext: SSLContext = { - // log hostname verification warning once - if (HostnameVerification) - log.debug("TLS/SSL hostname verification is enabled.") - else - log.info( - LogMarker.Security, - "TLS/SSL hostname verification is disabled. See Akka reference documentation for more information.") - - constructContext() - } - - private def constructContext(): SSLContext = { - try { - val rng = createSecureRandom() - val ctx = SSLContext.getInstance(SSLProtocol) - ctx.init(keyManagers, trustManagers, rng) - ctx - } catch { - case e: FileNotFoundException => - throw new SslTransportException( - "Server SSL connection could not be established because key store could not be loaded", - e) - case e: IOException => - throw new SslTransportException("Server SSL connection could not be established because: " + e.getMessage, e) - case e: GeneralSecurityException => - throw new SslTransportException( - "Server SSL connection could not be established because SSL context could not be constructed", - e) - } - } - - /** - * Subclass may override to customize loading of `KeyStore` - */ - protected def loadKeystore(filename: String, password: String): KeyStore = { - val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) - val fin = Files.newInputStream(Paths.get(filename)) - try keyStore.load(fin, password.toCharArray) - finally Try(fin.close()) - keyStore - } - - /** - * Subclass may override to customize `KeyManager` - */ - protected def keyManagers: Array[KeyManager] = { - val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) - factory.init(loadKeystore(SSLKeyStore, SSLKeyStorePassword), SSLKeyPassword.toCharArray) - factory.getKeyManagers - } - - /** - * Subclass may override to customize `TrustManager` - */ - protected def trustManagers: Array[TrustManager] = { - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) - trustManagerFactory.init(loadKeystore(SSLTrustStore, SSLTrustStorePassword)) - trustManagerFactory.getTrustManagers - } - - def createSecureRandom(): SecureRandom = - SecureRandomFactory.createSecureRandom(SSLRandomNumberGenerator, log) - - override def createServerSSLEngine(hostname: String, port: Int): SSLEngine = - createSSLEngine(akka.stream.Server, hostname, port) - - override def createClientSSLEngine(hostname: String, port: Int): SSLEngine = - createSSLEngine(akka.stream.Client, hostname, port) - - private def createSSLEngine(role: TLSRole, hostname: String, port: Int): SSLEngine = { - createSSLEngine(sslContext, role, hostname, port) - } - - private def createSSLEngine(sslContext: SSLContext, role: TLSRole, hostname: String, port: Int): SSLEngine = { - - val engine = sslContext.createSSLEngine(hostname, port) - - if (HostnameVerification && role == akka.stream.Client) { - val sslParams = sslContext.getDefaultSSLParameters - sslParams.setEndpointIdentificationAlgorithm("HTTPS") - engine.setSSLParameters(sslParams) - } - - engine.setUseClientMode(role == akka.stream.Client) - engine.setEnabledCipherSuites(SSLEnabledAlgorithms.toArray) - engine.setEnabledProtocols(Array(SSLProtocol)) - - if ((role != akka.stream.Client) && SSLRequireMutualAuthentication) - engine.setNeedClientAuth(true) - - engine - } - - override def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] = - None - - override def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] = - None - -} - object SSLEngineProviderSetup { /** @@ -214,31 +64,3 @@ object SSLEngineProviderSetup { * Constructor is *Internal API*, use factories in [[SSLEngineProviderSetup]] */ class SSLEngineProviderSetup private (val sslEngineProvider: ExtendedActorSystem => SSLEngineProvider) extends Setup - -/** - * INTERNAL API - */ -@InternalApi private[akka] object SecureRandomFactory { - def createSecureRandom(randomNumberGenerator: String, log: MarkerLoggingAdapter): SecureRandom = { - val rng = randomNumberGenerator match { - case s @ ("SHA1PRNG" | "NativePRNG") => - log.debug("SSL random number generator set to: {}", s) - // SHA1PRNG 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(s) - - case "" | "SecureRandom" => - log.debug("SSL random number generator set to [SecureRandom]") - new SecureRandom - - case unknown => - log.warning( - LogMarker.Security, - "Unknown SSL random number generator [{}] falling back to SecureRandom", - unknown) - new SecureRandom - } - rng.nextInt() // prevent stall on first access - rng - } -} diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/SecureRandomFactory.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/SecureRandomFactory.scala new file mode 100644 index 0000000000..a9879372dc --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/SecureRandomFactory.scala @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp + +import java.security.SecureRandom + +import akka.annotation.InternalApi +import akka.event.LogMarker +import akka.event.MarkerLoggingAdapter +import com.typesafe.config.Config + +/** + * INTERNAL API + */ +@InternalApi private[akka] object SecureRandomFactory { + + val GeneratorSha1Prng = "SHA1PRNG" + val GeneratorNativePrng = "NativePRNG" + val GeneratorJdkSecureRandom = "SecureRandom" + + /** + * INTERNAL API + */ + @InternalApi + // extracted as a method for testing + private[tcp] def rngConfig(config: Config) = { + config.getString("random-number-generator") + } + + def createSecureRandom(config: Config, log: MarkerLoggingAdapter): SecureRandom = { + createSecureRandom(rngConfig(config), log) + } + + def createSecureRandom(randomNumberGenerator: String, log: MarkerLoggingAdapter): SecureRandom = { + val rng = randomNumberGenerator match { + case s @ (GeneratorSha1Prng | GeneratorNativePrng) => + log.debug("SSL random number generator set to: {}", s) + // SHA1PRNG 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(s) + + case "" | GeneratorJdkSecureRandom => + log.debug("SSL random number generator set to [SecureRandom]") + new SecureRandom + + case unknown => + log.warning( + LogMarker.Security, + "Unknown SSL random number generator [{}] falling back to SecureRandom", + unknown) + new SecureRandom + } + rng.nextInt() // prevent stall on first access + rng + } +} diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/PemManagersProvider.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/PemManagersProvider.scala new file mode 100644 index 0000000000..5b803721c8 --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/PemManagersProvider.scala @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.io.ByteArrayInputStream +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files +import java.security.KeyStore +import java.security.PrivateKey +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +import akka.annotation.InternalApi +import akka.pki.pem.DERPrivateKeyLoader +import akka.pki.pem.PEMDecoder +import javax.net.ssl.KeyManager +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory + +import scala.concurrent.blocking + +/** + * INTERNAL API + */ +@InternalApi +private[ssl] object PemManagersProvider { + + /** + * INTERNAL API + */ + @InternalApi + private[ssl] def buildKeyManagers( + privateKey: PrivateKey, + cert: X509Certificate, + cacert: Certificate): Array[KeyManager] = { + val keyStore = KeyStore.getInstance("JKS") + keyStore.load(null) + + keyStore.setCertificateEntry("cert", cert) + keyStore.setCertificateEntry("cacert", cacert) + keyStore.setKeyEntry("private-key", privateKey, "changeit".toCharArray, Array(cert, cacert)) + + val kmf = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + kmf.init(keyStore, "changeit".toCharArray) + val keyManagers = kmf.getKeyManagers + keyManagers + } + + /** + * INTERNAL API + */ + @InternalApi + private[ssl] def buildTrustManagers(cacert: Certificate): Array[TrustManager] = { + val trustStore = KeyStore.getInstance("JKS") + trustStore.load(null) + trustStore.setCertificateEntry("cacert", cacert) + + val tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + tmf.init(trustStore) + tmf.getTrustManagers + } + + /** + * INTERNAL API + */ + @InternalApi + private[ssl] def loadPrivateKey(filename: String): PrivateKey = blocking { + val bytes = Files.readAllBytes(new File(filename).toPath) + val pemData = new String(bytes, Charset.forName("UTF-8")) + DERPrivateKeyLoader.load(PEMDecoder.decode(pemData)) + } + + private val certFactory = CertificateFactory.getInstance("X.509") + + /** + * INTERNAL API + */ + @InternalApi + private[ssl] def loadCertificate(filename: String): Certificate = blocking { + val bytes = Files.readAllBytes(new File(filename).toPath) + certFactory.generateCertificate(new ByteArrayInputStream(bytes)) + } + +} diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/RotatingKeysSSLEngineProvider.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/RotatingKeysSSLEngineProvider.scala new file mode 100644 index 0000000000..631d8f63a5 --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/RotatingKeysSSLEngineProvider.scala @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.io.FileNotFoundException +import java.io.IOException +import java.security.GeneralSecurityException +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.cert.Certificate +import java.security.cert.X509Certificate + +import akka.actor.ActorSystem +import akka.annotation.ApiMayChange +import akka.annotation.InternalApi +import akka.event.Logging +import akka.event.MarkerLoggingAdapter +import akka.remote.artery.tcp.SSLEngineProvider +import akka.remote.artery.tcp.SecureRandomFactory +import akka.remote.artery.tcp.SslTransportException +import akka.remote.artery.tcp.ssl.RotatingKeysSSLEngineProvider.CachedContext +import akka.remote.artery.tcp.ssl.RotatingKeysSSLEngineProvider.ConfiguredContext +import akka.stream.TLSRole +import com.typesafe.config.Config +import javax.net.ssl.KeyManager +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession +import javax.net.ssl.TrustManager + +import scala.concurrent.duration._ + +/** + * Variation on ConfigSSLEngineProvider that will periodically reload the keys and certificates + * from disk, to facilitate rolling updates of certificates. + * + * This class is still ApiMayChange because it can likely be further harmonized with + * the standard ConfigSSLEngineProvider. Also the location and default values of the + * configuration may change in future versions of Akka. + * + * This provider does not perform hostname verification, but instead allows checking + * that the remote certificate has a subject name that matches the subject name of + * the configured certificate. + */ +@ApiMayChange +final class RotatingKeysSSLEngineProvider(val config: Config, protected val log: MarkerLoggingAdapter) + extends SSLEngineProvider { + + def this(system: ActorSystem) = + this( + system.settings.config.getConfig("akka.remote.artery.ssl.rotating-keys-engine"), + Logging.withMarker(system, classOf[RotatingKeysSSLEngineProvider].getName)) + + // read config + + private val SSLKeyFile: String = config.getString("key-file") + private val SSLCertFile: String = config.getString("cert-file") + private val SSLCACertFile: String = config.getString("ca-cert-file") + + private val sslEngineConfig = new SSLEngineConfig(config) + import sslEngineConfig._ + + // build a PRNG (created once, reused on every instance of SSLContext + private val rng: SecureRandom = SecureRandomFactory.createSecureRandom(SSLRandomNumberGenerator, log) + + // handle caching + @volatile private var cachedContext: Option[CachedContext] = None + + /** INTERNAL API */ + @InternalApi + private[ssl] def getSSLContext() = getContext().context + private def getContext(): ConfiguredContext = { + cachedContext match { + case Some(CachedContext(_, expired)) if expired.isOverdue() => + // Multiple connection requests arriving when the cache is overdue will + // create different CachedContext instances and only the last one will + // be cached. This is fine. + val context = constructContext() + cachedContext = Some(CachedContext(context, SSLContextCacheTime.fromNow)) + context + case Some(CachedContext(cached, _)) => cached + case None => + // Multiple connection requests arriving when the cache is empty will + // create different CachedContext instances. This is fine. + val context = constructContext() + cachedContext = Some(CachedContext(context, SSLContextCacheTime.fromNow)) + context + } + } + + // Construct the cached instance + private def constructContext(): ConfiguredContext = { + val (privateKey, cert, cacert) = readFiles() + try { + val keyManagers: Array[KeyManager] = PemManagersProvider.buildKeyManagers(privateKey, cert, cacert) + val trustManagers: Array[TrustManager] = PemManagersProvider.buildTrustManagers(cacert) + + val sessionVerifier = new PeerSubjectVerifier(cert) + + val ctx = SSLContext.getInstance(SSLProtocol) + ctx.init(keyManagers, trustManagers, rng) + ConfiguredContext(ctx, sessionVerifier) + } catch { + case e: GeneralSecurityException => + throw new SslTransportException( + "Server SSL connection could not be established because SSL context could not be constructed", + e) + case e: IllegalArgumentException => + throw new SslTransportException("Server SSL connection could not be established because: " + e.getMessage, e) + } + } + + private def readFiles(): (PrivateKey, X509Certificate, Certificate) = { + try { + val cacert: Certificate = PemManagersProvider.loadCertificate(SSLCACertFile) + val cert: X509Certificate = PemManagersProvider.loadCertificate(SSLCertFile).asInstanceOf[X509Certificate] + val privateKey: PrivateKey = PemManagersProvider.loadPrivateKey(SSLKeyFile) + (privateKey, cert, cacert) + } catch { + case e: FileNotFoundException => + throw new SslTransportException( + "Server SSL connection could not be established because a key or cert could not be loaded", + e) + case e: IOException => + throw new SslTransportException("Server SSL connection could not be established because: " + e.getMessage, e) + } + } + + // Implement the SSLEngine create methods from the trait + override def createServerSSLEngine(hostname: String, port: Int): SSLEngine = + createSSLEngine(akka.stream.Server, hostname, port)(getContext().context) + + override def createClientSSLEngine(hostname: String, port: Int): SSLEngine = + createSSLEngine(akka.stream.Client, hostname, port)(getContext().context) + + private def createSSLEngine(role: TLSRole, hostname: String, port: Int)(sslContext: SSLContext) = { + + val engine = sslContext.createSSLEngine(hostname, port) + + engine.setUseClientMode(role == akka.stream.Client) + engine.setEnabledCipherSuites(SSLEnabledAlgorithms.toArray) + engine.setEnabledProtocols(Array(SSLProtocol)) + + if (role != akka.stream.Client) engine.setNeedClientAuth(true) + + engine + } + + // Implement the post-handshake verification methods from the trait + override def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] = + getContext().sessionVerifier.verifyClientSession(hostname, session) + + override def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] = + getContext().sessionVerifier.verifyServerSession(hostname, session) + +} + +object RotatingKeysSSLEngineProvider { + + /** + * INTERNAL API + */ + @InternalApi + private case class CachedContext(cached: ConfiguredContext, expires: Deadline) + + /** + * INTERNAL API + */ + @InternalApi + private case class ConfiguredContext(context: SSLContext, sessionVerifier: SessionVerifier) + +} diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/SSLEngineConfig.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/SSLEngineConfig.scala new file mode 100644 index 0000000000..82a475bd5f --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/SSLEngineConfig.scala @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import akka.annotation.InternalApi +import akka.japi.Util.immutableSeq +import com.typesafe.config.Config + +import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration._ + +/** + * INTERNAL API + */ +@InternalApi +private[tcp] class SSLEngineConfig(config: Config) { + private[tcp] val SSLRandomNumberGenerator: String = config.getString("random-number-generator") + + private[tcp] val SSLProtocol: String = config.getString("protocol") + private[tcp] val SSLEnabledAlgorithms: Set[String] = + immutableSeq(config.getStringList("enabled-algorithms")).toSet + private[tcp] val SSLRequireMutualAuthentication: Boolean = { + if (config.hasPath("require-mutual-authentication")) + config.getBoolean("require-mutual-authentication") + else + false + } + private[tcp] val HostnameVerification: Boolean = { + if (config.hasPath("hostname-verification")) + config.getBoolean("hostname-verification") + else + false + } + private[tcp] val SSLContextCacheTime: FiniteDuration = + if (config.hasPath("ssl-context-cache-ttl")) + config.getDuration("ssl-context-cache-ttl").toMillis.millis + else + 1024.days // a lot of days (not Inf, because `Inf` is not a FiniteDuration +} diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/SessionVerifier.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/SessionVerifier.scala new file mode 100644 index 0000000000..2a2771d08e --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/SessionVerifier.scala @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.security.cert.X509Certificate + +import akka.annotation.InternalApi +import javax.net.ssl.SSLSession + +/** + * Allows hooking in extra verification before finishing the SSL handshake. + * + * INTERNAL API + */ +@InternalApi +private[ssl] trait SessionVerifier { + def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] + def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] +} + +/** + * This verifier approves all sessions. + * + * INTERNAL API + */ +@InternalApi +private[ssl] final object NoopSessionVerifier extends SessionVerifier { + override def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] = None + override def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] = None +} + +/** + * This is a TLS session verifier that checks the peer has a subject name that matches + * the subject name of the given certificate. This can be useful to prevent accidentally + * connecting with other nodes that have certificates that, while being signed by the + * same certificate authority, belong to different clusters. + * + * INTERNAL API + */ +@InternalApi +private[ssl] final class PeerSubjectVerifier(peerCertificate: X509Certificate) extends SessionVerifier { + override def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] = + verifyPeerCertificates(session) + override def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] = + verifyPeerCertificates(session) + + private def verifyPeerCertificates(session: SSLSession) = { + val mySubjectNames = X509Readers.getAllSubjectNames(peerCertificate) + if (session.getPeerCertificates.length == 0) { + Some(new IllegalArgumentException("No peer certificates")) + } + session.getPeerCertificates()(0) match { + case x509: X509Certificate => + val peerSubjectNames = + X509Readers.getAllSubjectNames(x509) + if (mySubjectNames.exists(peerSubjectNames)) None + else + Some( + new IllegalArgumentException( + s"None of the peer subject names $peerSubjectNames were in local subject names $mySubjectNames")) + case other => + Some(new IllegalArgumentException(s"Unknown certificate type: ${other.getClass}")) + } + } + +} diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/X509Readers.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/X509Readers.scala new file mode 100644 index 0000000000..51f44374e2 --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/ssl/X509Readers.scala @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.security.cert.X509Certificate +import java.util + +import akka.annotation.InternalApi +import javax.naming.ldap.LdapName +import akka.util.ccompat.JavaConverters._ + +/** + * INTERNAL API + */ +@InternalApi +private[akka] object X509Readers { + + def getAllSubjectNames(cert: X509Certificate): Set[String] = { + val maybeCommonName = + new LdapName(cert.getSubjectX500Principal.getName).getRdns.asScala.collectFirst { + case attr if attr.getType.equalsIgnoreCase("CN") => + attr.getValue.toString + } + + val iterable: Iterable[util.List[_]] = Option(cert.getSubjectAlternativeNames).map(_.asScala).getOrElse(Nil) + val alternates = iterable.collect { + // See the javadocs of cert.getSubjectAlternativeNames for what this list contains, + // first element should be an integer, if that integer is 2, then the second element + // is a String containing the DNS name. + case list if list.size() == 2 && list.get(0) == 2 => + list.get(1) match { + case dnsName: String => dnsName + case other => + throw new IllegalArgumentException( + s"Error reading Subject Alternative Name, expected dns name to be a String, but instead got a ${other.getClass}") + } + } + + maybeCommonName.toSet ++ alternates + } + +} diff --git a/akka-remote/src/test/resources/Makefile b/akka-remote/src/test/resources/Makefile index 5343762ec5..86edcc4eaf 100644 --- a/akka-remote/src/test/resources/Makefile +++ b/akka-remote/src/test/resources/Makefile @@ -1,3 +1,5 @@ +# Documents how truststore and keystore were created + all: truststore keystore truststore: domain.crt @@ -6,12 +8,17 @@ truststore: domain.crt keystore: domain.crt domain.key openssl pkcs12 -export -inkey domain.key -passin pass:changeme -in domain.crt -out keystore -passout pass:changeme -domain.crt: domain.csr domain.key - openssl x509 -req -in domain.csr -sha256 -extfile <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:localhost")) -out domain.crt -extensions SAN -signkey domain.key +domain.crt: domain.csr domain.key domain.cnf + openssl x509 -req -in domain.csr -sha256 -extfile domain.cnf -out domain.crt -extensions SAN -signkey domain.key -domain.csr: - openssl req -new -sha256 -key domain.key -subj "/C=ZA/ST=web/O=Lightbend/CN=akka-remote" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:localhost")) -out domain.csr -passout pass:changeme +domain.cnf: + cat /etc/ssl/openssl.cnf > domain.cnf + echo "[SAN]" >> domain.cnf + echo "subjectAltName=DNS:localhost" >> domain.cnf + +domain.csr: domain.cnf + openssl req -new -newkey rsa:2048 -keyout domain.key -subj "/C=ZA/ST=web/O=Lightbend/CN=akka-remote" -reqexts SAN -config domain.cnf -out domain.csr -passout pass:changeme .PHONY: clean clean: - rm domain.crt domain.csr keystore truststore + rm domain.key domain.crt domain.csr keystore truststore domain.cnf diff --git a/akka-remote/src/test/resources/domain.crt b/akka-remote/src/test/resources/domain.crt new file mode 100644 index 0000000000..facd3f74a6 --- /dev/null +++ b/akka-remote/src/test/resources/domain.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgIULYjj2NGVQ1r1MzK9j03lmw/s9AAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCWkExDDAKBgNVBAgMA3dlYjESMBAGA1UECgwJTGlnaHRi +ZW5kMRQwEgYDVQQDDAtha2thLXJlbW90ZTAeFw0yMDA2MDIxMTA1MDVaFw0yMDA3 +MDIxMTA1MDVaMEUxCzAJBgNVBAYTAlpBMQwwCgYDVQQIDAN3ZWIxEjAQBgNVBAoM +CUxpZ2h0YmVuZDEUMBIGA1UEAwwLYWtrYS1yZW1vdGUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC8bahFbJFC31YWoyJGdOasMnZHE+D3jjnTmGS+E6Ev +YxQ1zR+ja2CpDv1M5VjOS4qzVXBXm/2OM35Er6sE+cAdtO8qXq39hzhNoS5nvHIo +hl881MHIbndohtMZm2NsKM2yHnqnFI4jMnlEK9d8gmn25PsqUwOC9g9h4HOp+qys +mqDzMmyZSS3qotyximOPBIQcRan6xh9i3Zhi3VRIxMl9WNR1gU5sbOeO4G7xsKyY +FjfEeVjyDOG1pYHpnBVtqTDJoNzs5jZIslzpZU/iCW1fF2r5VwCuwMj/fgSxz7Qv +LXqw70QDKeDkebgaTmhqAtYbAT20JXwMCuiE+8Lo4hGbAgMBAAGjGDAWMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAm97sH2qjazMJV66X +wJfxk72qHpZIXyzGIAcORcF8lxDOKaqO8q85cZa9uNhq+CtSOEN41KupBKVk4dfa +ZZ7IWFqptXKsztQ6Ff+ruEX3ZeW3ZsZp72+PuauC6ClNmxZG4/bUA0uKKd8s5yPK +jqJ3KR6ZuYykBvT2dQrHdI4LQPC4Sh+AZtfizTh21dYz4F1HPe/aBoDx0eAO6oyF +S0V6Mm8d/ydCg5wS+s0NmNniia3sww2fud+PyR3AaaubSBKhThQg6pQhFiaxjKSz +IMCg9Yicy8vem5w+HqOJqoyiPDSdxtInyeNKskxcB7ayOpcWj/TX/W379FWABxeo +rt8eZw== +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/keystore b/akka-remote/src/test/resources/keystore index a76d66d7ab..14a0e51f7f 100644 Binary files a/akka-remote/src/test/resources/keystore and b/akka-remote/src/test/resources/keystore differ diff --git a/akka-remote/src/test/resources/ssl/README.md b/akka-remote/src/test/resources/ssl/README.md new file mode 100644 index 0000000000..d305c7aac5 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/README.md @@ -0,0 +1,7 @@ +# Testing Resources + +This folder contains a diverse set of keys, certificates and keystores generated using scripts inspired by +those from https://github.com/playframework/play-samples/tree/2.8.x/play-scala-tls-example/scripts. + +The resources in this folder are build with a few restrictions. See `TlsResourcesSpec.scala` for some +tests asserting the certificates and keys in this folder fulfill the required restrictions. diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.crt b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.crt new file mode 100644 index 0000000000..0b60498c80 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDgjCCAyagAwIBAgIEba7KPTAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDI3WhcNMzAwNTMwMTU1MDI3 +WjCBjzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxIzAhBgNVBAMTGmFydGVyeS1ub2RlMDAxLmV4YW1wbGUu +Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjUrn0eQmYrrDg8gP +CtDOQblPbjVgfKR0SzBUCCBfIZOWy+YYm51LldRSKwg644IkU0O2eTeEDzo4qVf9 +9RWPQ+C2SIdc40fv2AEQTKsmah+tfW3HFQsFsR6DBfFJAYbscHis1vKBl+xo2OZp +YV2K21+UfI44Je2m7fLrPq0rKY9c5zXCATLNMQOeGV3yrtNqX4cTKSGH3nlDkY3L +MpAeYP+agnCMt+wBejxgx44PtQaWOHNwa1A8DwfDB2WIk2dXr90lpODfTREmB5vj +v+eBQ2gM2ObgIy8za2Kr8HQwAV+jtPIZLwAHftqBY5VrVWe3th3wMUEJPdhJ9Sqi +3MGVwwIDAQABo4GyMIGvMB8GA1UdIwQYMBaAFNkRJB4AQDAmCIvHYS0AKD4/AqUU +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAw +PgYDVR0RBDcwNYIaYXJ0ZXJ5LW5vZGUwMDEuZXhhbXBsZS5jb22CF2FydGVyeS1u +b2RlLmV4YW1wbGUuY29tMB0GA1UdDgQWBBSjukxB7Goxnv8D7rthlKtLV8/kCzAM +BggqhkjOPQQDAgUAA0gAMEUCIQCNaZmqT7X/4iUUTXp01qoWHP6rLQnPV1gM7m4Z +C0flhwIgW3xLzxKTEEusX1hOuAxvSXBkKRnKdu1/XeJ9xXj3DrI= +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.p12 b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.p12 new file mode 100644 index 0000000000..271cd410e9 Binary files /dev/null and b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.pem b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.pem new file mode 100644 index 0000000000..501ac638a7 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node001.example.com.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAjUrn0eQmYrrDg8gPCtDOQblPbjVgfKR0SzBUCCBfIZOWy+YY +m51LldRSKwg644IkU0O2eTeEDzo4qVf99RWPQ+C2SIdc40fv2AEQTKsmah+tfW3H +FQsFsR6DBfFJAYbscHis1vKBl+xo2OZpYV2K21+UfI44Je2m7fLrPq0rKY9c5zXC +ATLNMQOeGV3yrtNqX4cTKSGH3nlDkY3LMpAeYP+agnCMt+wBejxgx44PtQaWOHNw +a1A8DwfDB2WIk2dXr90lpODfTREmB5vjv+eBQ2gM2ObgIy8za2Kr8HQwAV+jtPIZ +LwAHftqBY5VrVWe3th3wMUEJPdhJ9Sqi3MGVwwIDAQABAoIBABSX5lHhPvcE4ZpD +fm3FIUrTB5C4lueT0J9k29qTXUo3iLMPRmbn9ixQVemPuYWPYlwAcogYX1cY1UlZ +Wdpu2gK6rdbEY/V8dqi0/vstttug4lh1t56JjLrUB6TGFi3yzGNYM1jg36KVAnMa +oiFe0O5IcAv3bpeYm8vyq/bmpnPYCUwOAyTGJ6JBUs98YEQu5qGdWVYGk20G346m +AW37bAeIE9oTMHD06aXZRBtaFQjvCeCg+aM9FhTo6hOD4mTADeV2qfjUPrhE3jal +c6Ul84m0RG5A2M7isBK/JmQ0hZ9uJIEGBSWdAG6g0sDLN02pdNtTWG0nuA+XHolV +9KznWYECgYEA3Y11PmyiCpZyHLN3y30Gw62aQ+MW41XVNUcb3Dn42UkKbhgnoFbI +rNhZp+jAJapIZTTB3xfE3dwvPlYHHfwwTxcmg20cx0CpNlpqCigVGQqe2FxhWRq8 +ZR9YHS5mseOmmdJYdfG58TsSmjveEmgbC+Q+1roBVwNNpfIeyYUg43cCgYEAo0LU +QsFXZVOTFb6vRaAGrfiSd0bmzFEMWS9H6G6ZJUhNodoECCQaeF0ypJermVg/nfD2 +wobSTP4hA7dtRDHzNecQRS3zfVdaCmIN+50jpfrT+av6Z07k5bxpFHUAyzI0bj/M +gDGjZ9seDo/4vQfdiqxYWjPr8DXYvANazbARuxUCgYAH83uxsdRe7OdLgGVcODB1 +9VUD+rJnlj0AnHyzeqEjqytkqBlD99lb2qfdDs1WjLXsa+hJSWEXVT+czRmUSeix +7fLD5LaTsA5ilPwZQTcAnxD0UtxrhjocpvNSmMe2uqTQAGyMTxCNR8FzJ5LgtjvC +QX6/1g0WQlgXDIluUgjMIQKBgQCPQoFH4qhx/Zg/qIfcrMOvvUOo7spv117ik56h +0wsHsB6PO+P10Nh5bi6WR5EIimuoiF2/7NZ1QTpvLHHxOXOVhSC908ip4BDes5RZ +ilZRu3xuxf6A0LYC8gWzMch0haWEaO9mPiiJZblGRgeauGAq43jUDmOm8VkyAi+X +9jxY0QKBgQDTJHTw1KFklidDFQ1v9V+uORLmyVP4Ms/TPMNJsuTqc3lnBSU9Zxfz +Vw20aBMZdfno1z5Uvirg++Zt1B7hVUj+MB2VslV6HqLjj3fL96xXrYPsjMYtwTOD +//jalt23mVJorMXpKBMfgEUPyw3Ca4R0mL/D4miv88XXZLlUJwwUyA== +-----END RSA PRIVATE KEY----- diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.crt b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.crt new file mode 100644 index 0000000000..db18c33158 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDgTCCAyagAwIBAgIEOqBQVjAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDMwWhcNMzAwNTMwMTU1MDMw +WjCBjzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxIzAhBgNVBAMTGmFydGVyeS1ub2RlMDAyLmV4YW1wbGUu +Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3pjLzJbVB78E4A1D +7AU4mn4XV8aWZHHRfyPf2yL62WtqLLktzdB27c/QdgV8G6oKW50MU4HmyaZqvxK1 +RD4Q0QSVGy/Dz8MFxJz3k38YWFICqyBYNQcU4NsFMwO/yguQHY21Oyxk4liWtA1p +U9ZYn51Tozd7jFtzbYVKC4PucjuV26nb6ufLZZtVZYw+nuz1Uyv4LychKXSZHSqB +XSH9rj4piOyO7t7IPdQqYLDFS1OBkBhk3q/YnfzYBHZjkYApLO1t9Jf4S74mtJ2Q +TpISxIdpKzizXcr9OfVybeoCOwQXeNNs74zX5qa5nd1ZuqC42qBsNFUeSwqcJv2z +HhBScwIDAQABo4GyMIGvMB8GA1UdIwQYMBaAFNkRJB4AQDAmCIvHYS0AKD4/AqUU +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAw +PgYDVR0RBDcwNYIaYXJ0ZXJ5LW5vZGUwMDIuZXhhbXBsZS5jb22CF2FydGVyeS1u +b2RlLmV4YW1wbGUuY29tMB0GA1UdDgQWBBQgG8h6MjX1T0N/vGI+YYqi27oivDAM +BggqhkjOPQQDAgUAA0cAMEQCIBJGrfxFjZFHIr0ZeBlYJylly9CypDJnFOgxEZpY +B0ETAiABn94e6yDwRMVOdC3UgnMq8E94LfqJU+tduEPH30qB3Q== +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.p12 b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.p12 new file mode 100644 index 0000000000..bbc0d4172a Binary files /dev/null and b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.pem b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.pem new file mode 100644 index 0000000000..a126038d82 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node002.example.com.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3pjLzJbVB78E4A1D7AU4mn4XV8aWZHHRfyPf2yL62WtqLLkt +zdB27c/QdgV8G6oKW50MU4HmyaZqvxK1RD4Q0QSVGy/Dz8MFxJz3k38YWFICqyBY +NQcU4NsFMwO/yguQHY21Oyxk4liWtA1pU9ZYn51Tozd7jFtzbYVKC4PucjuV26nb +6ufLZZtVZYw+nuz1Uyv4LychKXSZHSqBXSH9rj4piOyO7t7IPdQqYLDFS1OBkBhk +3q/YnfzYBHZjkYApLO1t9Jf4S74mtJ2QTpISxIdpKzizXcr9OfVybeoCOwQXeNNs +74zX5qa5nd1ZuqC42qBsNFUeSwqcJv2zHhBScwIDAQABAoIBAHdr0iq44SF+wcbS +VxZQ1sVL0Iou6JCK37IuNPMEGUB7+EJ2NrSsmqGLVHN9DdBpsZTk9K/4iTC2L57D +EqzB/5OjubsULSFRp86Lx+dB0HXRycy1VZ1dZz4bQvbTlBl5ip/QXuKYNqeYj4GZ +kGCCJpm7dhuisI3kolCnqcnzxgFSJhgcQHYZkpJH09CZNxUtF7AIWtMRkj2IIKjV +/MzQDJz8EINBQzWIDTviu0Qkd7V2dStJ/GfnFkUeRgxHQUJtXM6p5vRIxObzd+fi +HRYF8c2t3HQcnVlqjRWeHPtdh9jKKzETCVABTc8WzLu7DyONnGgE3w3DoMAJKHVL +77ziqEECgYEA9kSHfagEviqTd1aYkhD1T9Q1FS04SefdQc2RzvF3FIVDNyv45fxk +tbC89X0MoOfdbQbwPoY4p2unjV9tNjwp7WqDnGHSDz56qxbuJEaclq4gPDIciXAM +Qi4PvSp1YZ+3na2j5bDomPmjo54s3mh5nitq0Jw52MQGRBebx8ynyXcCgYEA52TK +d8G8fJKeRxOTN7iEKds6m77MnFDVvONwlM9pQUCuqPm+FT0vb8cMNDLNWouMmmCB +gSP1bc1bNbcN2teH+5lbn5wYQ7AQaF6lbOdgUBV5N49S6AGbvAkT/w2O0Mo7YU5Q +mIWvYxhQpPquevtFOAAbSVQrebDiJhg0Rjy2feUCgYEA3YUBN3eWZJSZt4Quk10r +vJYO9bCKbHhjnxhR6wtq6QuCTbOBHSduU7zaDBxi6q4GkFbobeWAOqDsw48uBtYR +hN0F6/pV4J5760AiEIFvw1534o3U+4/Nhw413BvAIINxwCT8Q9VhNJGBr+DNTXY+ +x5cYavPMWP7jAAcYep3N47sCgYB7evjk0XkhTSizq0mLmabFo1zyUe5kmGqHAyRH +9SspDDhoqeV69gzDbIghrt6RLBkbJNbXMHY/YzACSS5Wk1/Yru0LDsSQEnufBqrm +o85szhjCwnQupPUTchC+seB9oP3xHla6HdULX6VhdPj5Xe+BQ+VLy2Pr662zQIVc +2fdU1QKBgQCWCPSc8byqtYsIg3b59iyTYbR3rW49ZUHNNjm3fKIXkEpHZQSg9/v2 +aNAUpVqm9k/Tt0OJ1+OceDRBlBgVPFIJRuqJ37V9pmicsjVN2biqc+2RQtdpgzcl +lkz3cnp1Jdg9tGDmnip5XplLcjh3gaxMYNsQRAmHE0j19ck6Rcx2rw== +-----END RSA PRIVATE KEY----- diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.crt b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.crt new file mode 100644 index 0000000000..83d4472930 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDgTCCAyagAwIBAgIEPjDg6zAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDMyWhcNMzAwNTMwMTU1MDMy +WjCBjzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxIzAhBgNVBAMTGmFydGVyeS1ub2RlMDAzLmV4YW1wbGUu +Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA24C7726icyAGM/Vp +baY79OaRUihFm6H4iYDuhdZ66QqBOOTNqLXZnetddJ55mS+LndoSxNYNNhD7XiyI +Orcn1ETtzfZmtjJVYb2JB0lHj3awTYN4Lgchny+GGTjb+mEihyGv/6Csu/mMcrxS +PZDcwy2pHO4TJ8lQ0yg9SDFneWxzDTOxIn15cSHSiexunL6s8wzw9WzjR6kNF7Ig +qHWK/cLabvsZr7cLuNmaWBNpGyaAxqXbOhrhBmDxaY1mCtRSKzPCPGAM83F1mtEa +mX64W7WEEw/h71q/u32o9qgmhQqodbZqt5So2RFpU3MVPuYfQQPdTeqkpcwPsDiE +NqfdQQIDAQABo4GyMIGvMB8GA1UdIwQYMBaAFNkRJB4AQDAmCIvHYS0AKD4/AqUU +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAw +PgYDVR0RBDcwNYIaYXJ0ZXJ5LW5vZGUwMDMuZXhhbXBsZS5jb22CF2FydGVyeS1u +b2RlLmV4YW1wbGUuY29tMB0GA1UdDgQWBBSx8rKQxZJ9SjW3ghqxtwZuNMhwhTAM +BggqhkjOPQQDAgUAA0cAMEQCIDr0T5OrW7BRXFWosw8clK6jVafAJHJSmysM7iV5 +4FDIAiBGdRE1iTqcvA1ya3H89gvqAOn2K41RuYf3xSR1tU1zhw== +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.p12 b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.p12 new file mode 100644 index 0000000000..63b32d5ba7 Binary files /dev/null and b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.pem b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.pem new file mode 100644 index 0000000000..bf0154e93e --- /dev/null +++ b/akka-remote/src/test/resources/ssl/artery-nodes/artery-node003.example.com.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA24C7726icyAGM/VpbaY79OaRUihFm6H4iYDuhdZ66QqBOOTN +qLXZnetddJ55mS+LndoSxNYNNhD7XiyIOrcn1ETtzfZmtjJVYb2JB0lHj3awTYN4 +Lgchny+GGTjb+mEihyGv/6Csu/mMcrxSPZDcwy2pHO4TJ8lQ0yg9SDFneWxzDTOx +In15cSHSiexunL6s8wzw9WzjR6kNF7IgqHWK/cLabvsZr7cLuNmaWBNpGyaAxqXb +OhrhBmDxaY1mCtRSKzPCPGAM83F1mtEamX64W7WEEw/h71q/u32o9qgmhQqodbZq +t5So2RFpU3MVPuYfQQPdTeqkpcwPsDiENqfdQQIDAQABAoIBAGKjBNDhPGrTdzYe +D9RQIR06Bw+OPUlkjZTstUK7UNwr9kmkt64amcHXJFXlaOsnbGvwtQJy1dj35J07 +EbSg3WsL1nj5QsqY77lOPKdjjJ6xTSRn8bdtSPSJnI70+BUZVTS4NKiAgV6vEyfz +7FjyIeIrQJVZfo4gbwuUR4WLfd4XwratfkHEi7otHbTpnkU3O7TbV1I6EyqUQtZx +2xhThAV4utTyKJFyYgaqzTQZ5djIk2a93eyaoz4tnouUIKYzG1XWyz5t82UD+ViH +72rHDJGNy39Wok+lOVQ3WpJYJJkdJcf5vfuF4mQ+JdIhRpYsh+q2y68obaYOONPU +sMF6SAECgYEA8wjOxj61aviIFSrHhrZI01u00BfAhil7DcZjarKjykxMSXS3uXsb +1OoScieHXaQWs1xKXuiS3naiRi0CmdVTs9CLAlddzTK9p2e8Uc9H8Ae5U5yugKdD +3ekpbXOih+iHQWYP/9nPN9VEb/q3aMy7iRKYgELC68m6Tp03bZnoeuECgYEA5zaM +nh9NVl7ur5FBN2V7fgEORg6Z3va4RLs7DUkXELWp9HWsbC/9rBSboATzyK6NYCMD +mL1Q3WbdPldhH3b+zkt7CLGoyQluVIyspXBuhpsj8MmImsFbSQJBMEiQ5zgh2Gd3 +xdVFXS1P9H5hXhy1GGePGrf7gQfqnKszQer5DmECgYAHnAr9YhFEHCwGnaRJr4Nw +OrramSPKD5puv/t058sBFop88k6eXCBu9jVFpb8zS2P6kbUya43NsWE7WUVvk6Jf +SvRPSnUBa8lMaI8Y8KiL93HyEEHWfWY+mIJXjvtTzhAOGCgAFs3KLb9K0krT2TU2 +AYMM4QpBX7uZooqNv/frgQKBgQDi4XdIriSgjVUgOKPLLSzp7zVHb4pz7JvS7fq7 +Ra55ehnExTeljc4ZbrtrYZCqqwYVgSZFWfgg2ZBeXTXzvzu3yP94/4RFiZiXJNdB +HDuIoHG7FLeUTAo8cRbwvzRZf45OoPE50tZW4WDk5KK8y+S0huI48LK94bvJcoFA +vMcZ4QKBgFbrlcgi7XcjfKo9+JzXx7Q8p7gC1j1qNevq9qpkGDgAn5Txdi1PFqNM +g39aHjiaYA2+GOB61MEGelaZkhtwCl/4cpCV9uVodhhxTUb3bQLM6KuvuB5gW7Qi +yGPxI9HnFuakK0Cs250rdQMoTriXRwK+NYahDqmvOqRihZdbS6E2 +-----END RSA PRIVATE KEY----- diff --git a/akka-remote/src/test/resources/ssl/client.example.com.crt b/akka-remote/src/test/resources/ssl/client.example.com.crt new file mode 100644 index 0000000000..f29b0b8af9 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/client.example.com.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICkDCCAjWgAwIBAgIEOro4SzAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDIwWhcNMzAwNTMwMTU1MDIw +WjCBhzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxGzAZBgNVBAMTEmNsaWVudC5leGFtcGxlLmNvbTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABAaveLe/GP5gV9905DmI6wCOUrEcK0TCnbUo +4uHCl5lRX6m3v6pY/CyvK/UDLu30qfBvsJhWD5Ggr28wQZYyfOyjgZQwgZEwHwYD +VR0jBBgwFoAU2REkHgBAMCYIi8dhLQAoPj8CpRQwEwYDVR0lBAwwCgYIKwYBBQUH +AwIwDgYDVR0PAQH/BAQDAgWgMCoGA1UdEQQjMCGCEmNsaWVudC5leGFtcGxlLmNv +bYILZXhhbXBsZS5jb20wHQYDVR0OBBYEFOMNZuBCz5J+ar98ruHnFkQAekAQMAwG +CCqGSM49BAMCBQADRwAwRAIgfrnQChNQsf2ft/zA4ln5pUzdy3mGmp0eCD4lBN64 +pVQCIB5ARpTUKetQmj7YkU7etM02TPVpKjAslxmYuMQEpbrt +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/client.example.com.p12 b/akka-remote/src/test/resources/ssl/client.example.com.p12 new file mode 100644 index 0000000000..f3ed429260 Binary files /dev/null and b/akka-remote/src/test/resources/ssl/client.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/exampleca.crt b/akka-remote/src/test/resources/ssl/exampleca.crt new file mode 100644 index 0000000000..6dcee07369 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/exampleca.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICNDCCAdigAwIBAgIEKYhR+DAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDEzWhcNNDcxMDE3MTU1MDEz +WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN +U2FuIEZyYW5jaXNjbzEYMBYGA1UEChMPRXhhbXBsZSBDb21wYW55MRQwEgYDVQQL +EwtFeGFtcGxlIE9yZzESMBAGA1UEAxMJZXhhbXBsZUNBMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAE9K81J2BKaFGTI/4qgUr31mr3Jpl6YK+7pCmy2LNiW0Jufova +p41ERTrT9IynJF3NlJpzuKhmuA+80ZM5NSR4QqNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAgQwHQYDVR0OBBYEFNkRJB4AQDAmCIvHYS0AKD4/AqUU +MAwGCCqGSM49BAMCBQADSAAwRQIhAP2Uf206YbhJ2pb5MvehuLanJzFydOAq5uC4 +AXmspW+EAiAuPLFIAPyB+6gpr/mQ8V+yyh3aCJV3bvFq8aorrQyC3Q== +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/exampleca.p12 b/akka-remote/src/test/resources/ssl/exampleca.p12 new file mode 100644 index 0000000000..b936601bb3 Binary files /dev/null and b/akka-remote/src/test/resources/ssl/exampleca.p12 differ diff --git a/akka-remote/src/test/resources/ssl/gen-artery-nodes.example.com.sh b/akka-remote/src/test/resources/ssl/gen-artery-nodes.example.com.sh new file mode 100755 index 0000000000..c43e1fba5f --- /dev/null +++ b/akka-remote/src/test/resources/ssl/gen-artery-nodes.example.com.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +export PW=`cat password` + +. gen-functions.sh + + +function createArteryNodeKeySet() { + PREFIX=$1 + createExampleRSAKeySet $PREFIX "serverAuth,clientAuth" "DNS:$PREFIX.example.com,DNS:artery-node.example.com" +} + +rm -rf artery-nodes + +## Then create array few artery-nodes (in ./ssl/artery-nodes folder) +createArteryNodeKeySet "artery-node001" +createArteryNodeKeySet "artery-node002" +createArteryNodeKeySet "artery-node003" + +mkdir -p artery-nodes +mv artery-node00* artery-nodes/. diff --git a/akka-remote/src/test/resources/ssl/gen-functions.sh b/akka-remote/src/test/resources/ssl/gen-functions.sh new file mode 100644 index 0000000000..7edd22681f --- /dev/null +++ b/akka-remote/src/test/resources/ssl/gen-functions.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +function createKeySet() { + PREFIX=$1 + KEYALG=$2 + KEYLENGTH=$3 + KeyUsage=$4 + ExtendedKeyUsage=$5 + SubjectAlternativeNames=$6 + + # Create a certificate used for peer-to-peer, under the example.com CA + keytool -genkeypair -v \ + -alias $PREFIX.example.com \ + -dname "CN=$PREFIX.example.com, OU=Example Org, O=Example Company, L=San Francisco, ST=California, C=US" \ + -keystore $PREFIX.example.com.p12 \ + -storetype PKCS12 \ + -keypass:env PW \ + -storepass:env PW \ + -storetype PKCS12 \ + -keyalg $KEYALG \ + -keysize $KEYLENGTH \ + -validity 3650 + + # Create a certificate signing request for $PREFIX.example.com + keytool -certreq -v \ + -alias $PREFIX.example.com \ + -keypass:env PW \ + -storepass:env PW \ + -keystore $PREFIX.example.com.p12 \ + -file $PREFIX.example.com.csr + + # Tell exampleCA to sign the certificate. + keytool -gencert -v \ + -alias exampleca \ + -keypass:env PW \ + -storepass:env PW \ + -keystore exampleca.p12 \ + -infile $PREFIX.example.com.csr \ + -outfile $PREFIX.example.com.crt \ + -ext KeyUsage:critical=$4 \ + -ext EKU=$5 \ + -ext SAN=$6 \ + -rfc \ + -validity 3650 + + rm $PREFIX.example.com.csr + + # Tell $PREFIX.example.com.p12 it can trust exampleca as a signer. + keytool -import -v \ + -alias exampleca \ + -file exampleca.crt \ + -keystore $PREFIX.example.com.p12 \ + -storetype PKCS12 \ + -storepass:env PW << EOF +yes +EOF + + # Import the signed certificate back into $PREFIX.example.com.p12 + keytool -import -v \ + -alias $PREFIX.example.com \ + -file $PREFIX.example.com.crt \ + -keystore $PREFIX.example.com.p12 \ + -storetype PKCS12 \ + -storepass:env PW + +} + +function exportRSAPrivateKeyAsPem { + PREFIX=$1 + # Export the private key as a PEM. This export adds some extra PKCS#12 bag info. + openssl pkcs12 -in $PREFIX.example.com.p12 -nodes -nocerts -out tmp.pem -passin pass:kLnCu3rboe + # A second pass extracts the PKCS#12 bag information and produces a clean PEM file. + openssl rsa -in tmp.pem -out $PREFIX.example.com.pem + rm tmp.pem +} + +function createExampleRSAKeySet() { + PREFIX=$1 + EKU=$2 + SAN=$3 + createKeySet $PREFIX \ + RSA \ + 2048 \ + "digitalSignature,keyEncipherment" \ + $EKU \ + $SAN + exportRSAPrivateKeyAsPem $PREFIX +} + + +function createExampleECKeySet() { + PREFIX=$1 + EKU=$2 + SAN=$3 + createKeySet $PREFIX \ + EC \ + 256 \ + "digitalSignature,keyEncipherment" \ + $EKU \ + $SAN +} + diff --git a/akka-remote/src/test/resources/ssl/genca.sh b/akka-remote/src/test/resources/ssl/genca.sh new file mode 100755 index 0000000000..e7a77cafd0 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/genca.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +export PW=`cat password` + +# Create a self signed key pair root CA certificate. +keytool -genkeypair -v \ + -alias exampleca \ + -dname "CN=exampleCA, OU=Example Org, O=Example Company, L=San Francisco, ST=California, C=US" \ + -keystore exampleca.p12 \ + -storetype PKCS12 \ + -keypass:env PW \ + -storepass:env PW \ + -keyalg EC \ + -keysize 256 \ + -ext KeyUsage:critical="keyCertSign" \ + -ext BasicConstraints:critical="ca:true" \ + -validity 9999 + +# Export the exampleCA public certificate +keytool -export -v \ + -alias exampleca \ + -file exampleca.crt \ + -keypass:env PW \ + -storepass:env PW \ + -keystore exampleca.p12 \ + -storetype PKCS12 \ + -rfc diff --git a/akka-remote/src/test/resources/ssl/gencerts.sh b/akka-remote/src/test/resources/ssl/gencerts.sh new file mode 100755 index 0000000000..beee48132a --- /dev/null +++ b/akka-remote/src/test/resources/ssl/gencerts.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +export PW=`cat password` + +. gen-functions.sh + +rm *.crt +rm *.p12 +rm *.pem + +./genca.sh + +## some server certificates +createExampleECKeySet "one" "serverAuth" "DNS:one.example.com,DNS:example.com" +createExampleECKeySet "two" "serverAuth" "DNS:two.example.com,DNS:example.com" +createExampleECKeySet "island" "serverAuth" "DNS:island.example.com" + +## a client certificate +createExampleECKeySet "client" "clientAuth" "DNS:client.example.com,DNS:example.com" + +## node.example.com is part of the example.com dataset (in ./ssl/ folder) but not the artery-nodes +createExampleRSAKeySet "node" "serverAuth,clientAuth" "DNS:node.example.com,DNS:example.com" +createExampleRSAKeySet "rsa-client" "clientAuth" "DNS:rsa-client.example.com,DNS:example.com" + +## a certificate valid for both server and client (peer-to-peer) +## with RSA keys +./gen-artery-nodes.example.com.sh diff --git a/akka-remote/src/test/resources/ssl/island.example.com.crt b/akka-remote/src/test/resources/ssl/island.example.com.crt new file mode 100644 index 0000000000..33521a8064 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/island.example.com.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIIChDCCAiigAwIBAgIEJnvhJzAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDE4WhcNMzAwNTMwMTU1MDE4 +WjCBhzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxGzAZBgNVBAMTEmlzbGFuZC5leGFtcGxlLmNvbTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABIkVH3HvRjaESvuOGGT/fusdHKWMLIPL9bW8 +xY/Xn0R8vfSd11u4DuXAJyXHXoh9nr446gZEnkIJChp8UiR3F6SjgYcwgYQwHwYD +VR0jBBgwFoAU2REkHgBAMCYIi8dhLQAoPj8CpRQwEwYDVR0lBAwwCgYIKwYBBQUH +AwEwDgYDVR0PAQH/BAQDAgWgMB0GA1UdEQQWMBSCEmlzbGFuZC5leGFtcGxlLmNv +bTAdBgNVHQ4EFgQU18dwseHFcZuh+t7OL5S4qArmonEwDAYIKoZIzj0EAwIFAANI +ADBFAiAG9sYgiBMx8burgPmM7miEdIxVRWSMcoHFYpt7GP9JyAIhAMu0VjMAsvXj +P5ToFhlgEza2dL6spd9rerToqevTBvOm +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/island.example.com.p12 b/akka-remote/src/test/resources/ssl/island.example.com.p12 new file mode 100644 index 0000000000..dfe50c9de8 Binary files /dev/null and b/akka-remote/src/test/resources/ssl/island.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/node.example.com.crt b/akka-remote/src/test/resources/ssl/node.example.com.crt new file mode 100644 index 0000000000..0bf0ff16fa --- /dev/null +++ b/akka-remote/src/test/resources/ssl/node.example.com.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAwagAwIBAgIEfzq/RjAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDIzWhcNMzAwNTMwMTU1MDIz +WjCBhTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxGTAXBgNVBAMTEG5vZGUuZXhhbXBsZS5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt1BnrLQGu6QaqTYilJufamlpVqW43 +fjibKUA62FRzoD56rTST5fnn3XK0PHWcr6DyVIEFUuz22DsnXEJazy0Edt2AwKsu +aP5An+1zMBBYu9mvYEFXTu1VczFIN5PzYIb8pIWHsuchAhHYVFZmUAWbSCvR8hns +VSW1n/bLxnXIfvnrmv5hHMsPgoEdG9gw5V7OA4wDAWwc5nb8fC61E/CSSPnKoWN7 +Vm8f1laTfh/mKOViO66dKy1zeYXvfgt7BoolfmibG8zkrxqeDQRmedzJYyobbVV6 +YHFjsAAtqVqmSj6sLfgKUGq6Oq/0+NJqKp8QhEBdPIwyekh+ijpDGirZAgMBAAGj +gZwwgZkwHwYDVR0jBBgwFoAU2REkHgBAMCYIi8dhLQAoPj8CpRQwHQYDVR0lBBYw +FAYIKwYBBQUHAwEGCCsGAQUFBwMCMA4GA1UdDwEB/wQEAwIFoDAoBgNVHREEITAf +ghBub2RlLmV4YW1wbGUuY29tggtleGFtcGxlLmNvbTAdBgNVHQ4EFgQUuZLgycb7 +6NYNriONSXH0cyN+kXwwDAYIKoZIzj0EAwIFAANIADBFAiEAhHn3vyQz4RzQAz8n +6Xn01+GwKYX9crzifo2N90rcWD4CIDSw8zaDacsYroUhS+Z7UqRSYYVLynv5pK98 +E0Pb8Y9B +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/node.example.com.p12 b/akka-remote/src/test/resources/ssl/node.example.com.p12 new file mode 100644 index 0000000000..ca90e540a1 Binary files /dev/null and b/akka-remote/src/test/resources/ssl/node.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/node.example.com.pem b/akka-remote/src/test/resources/ssl/node.example.com.pem new file mode 100644 index 0000000000..0cb4b4c999 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/node.example.com.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArdQZ6y0BrukGqk2IpSbn2ppaValuN344mylAOthUc6A+eq00 +k+X5591ytDx1nK+g8lSBBVLs9tg7J1xCWs8tBHbdgMCrLmj+QJ/tczAQWLvZr2BB +V07tVXMxSDeT82CG/KSFh7LnIQIR2FRWZlAFm0gr0fIZ7FUltZ/2y8Z1yH7565r+ +YRzLD4KBHRvYMOVezgOMAwFsHOZ2/HwutRPwkkj5yqFje1ZvH9ZWk34f5ijlYjuu +nSstc3mF734LewaKJX5omxvM5K8ang0EZnncyWMqG21VemBxY7AALalapko+rC34 +ClBqujqv9PjSaiqfEIRAXTyMMnpIfoo6Qxoq2QIDAQABAoIBAGvzmNUQckcpuFXz +KLVH4B1GVmt0eVpFLOpPR/BDgM796U9OWaSv4e84+48rLO9NsupLyISB51+1Ead0 +CFFU8GQhXZYkESsVUTDZISGz67LvllTvtiR+g63Zh/WNbnhqreogUjzfweIh4usy +FSYc9B5nR69uZuL6ThzpZt9ONL958U9u6atG6mCDssT3i7ncqbluoQ9qgT1Cqamj +33VVziG5x+jM9j5TyWvy28/yO+uaR46lsPYGgAttYJ2SpEvssniWubsZ9X+BxAV8 +RtGm+0eGgKDzi64qLU/vRq1xBuSBB1zA4XhnIE/jN/ifWpzO5VF3mVtQbHKT554m +8BjW18ECgYEA/YT5CXcJuxhzWApupXNr+mQAsIeaNjuduQCFkPYrirZQyjmSWmhW ++50IPQtQrvzV5mC98XGzBOfnJ9FL+q73GMJQqx2FGwFrenjATLQJ/OcfYxrr1MEq +RdANX7ZTdBRVlR6EF3FCbwVYDyhkPy/W0PGMsgqdZHDwBJc6U+8RutUCgYEAr4eD +1WaPnh75gJ/iJ6+ouGJPYKO2NTPCHouXJ0LWXJp6tD4geiV6O25/2sQYVTlkyhBg +u3XyC0wpDIGoE/nrk0T6wIX+cyFWeSCxjkWilzT0GHj4LVvOnbz4DYD5PEEkqvns +nK8KWxFs9QJ6CVOUBkZv06UCL3YPC1tVJNn9afUCgYB9TKZlVi86CHihr+5F2ckp +ZRmuJidC3K40jJx3LCQTF87QVCoQgvdSvqcevKPxCMeTaIcYeTCYoSFvXZNm3+kC +lK+YEywBT+9WBa5NesJg+75Yliqu6ZXCEXU6s/uFKLOv0vhIOdMy2fpO65C4ZiWO ++YOnT3XA+cy3CCNs7oDdzQKBgQCAKGd/JhTyFBeDbDkJVN3RUiY2nxFoItQ2zSCd +j9VHY5r3guzfggGO5wz+w3Iot3D5f5/A/0qsKP1HnlsDytPPgOu8KZkwokSqx84b +3Ifr8sPOInTBWWiwDsrlwSc4cS++jh/N/peHCmANO7Oyn41ST5dSZgYEdSRi3Fp8 +P5UhCQKBgQCj8Hp7UINSAd6SjM+PT2wLiTtzbxAkl37cCwdLAjuEXOkwicxLCfQ/ +kxcdkHsUnXWsr51gEP24aVcUsODAiEz/0w3ebrgQsFIwZ8S3cZ9/lCVpXmwpyiQv +/zOpqsQFbJ1ags5CfQ4cFehoHR0rk/ZOsf65T7mmedBcbaMFeucflA== +-----END RSA PRIVATE KEY----- diff --git a/akka-remote/src/test/resources/ssl/one.example.com.crt b/akka-remote/src/test/resources/ssl/one.example.com.crt new file mode 100644 index 0000000000..6769ef66bd --- /dev/null +++ b/akka-remote/src/test/resources/ssl/one.example.com.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICjDCCAi+gAwIBAgIETEztzDAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDE0WhcNMzAwNTMwMTU1MDE0 +WjCBhDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxGDAWBgNVBAMTD29uZS5leGFtcGxlLmNvbTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABFWOb1oYjnJHzx1UMQTFbYhFdlEirsWGt0do/ujO +/mXk8kMOQz+p9AtrfKR5AUCsZt19EDAtomXPdmepiOf9+7OjgZEwgY4wHwYDVR0j +BBgwFoAU2REkHgBAMCYIi8dhLQAoPj8CpRQwEwYDVR0lBAwwCgYIKwYBBQUHAwEw +DgYDVR0PAQH/BAQDAgWgMCcGA1UdEQQgMB6CD29uZS5leGFtcGxlLmNvbYILZXhh +bXBsZS5jb20wHQYDVR0OBBYEFC72gRAyHCLjripkjmxa9fLT7Ov5MAwGCCqGSM49 +BAMCBQADSQAwRgIhAP2ltiw9l9mYWpIzoMAP0jBVWBBBKxajHAY0vYA0myaGAiEA +8fPcYmmsb0dfSOnoR5LXiegG33Tih7HBiG86Gai+ozA= +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/one.example.com.p12 b/akka-remote/src/test/resources/ssl/one.example.com.p12 new file mode 100644 index 0000000000..6a641005cc Binary files /dev/null and b/akka-remote/src/test/resources/ssl/one.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/password b/akka-remote/src/test/resources/ssl/password new file mode 100644 index 0000000000..965106c660 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/password @@ -0,0 +1 @@ +kLnCu3rboe diff --git a/akka-remote/src/test/resources/ssl/pem/README.md b/akka-remote/src/test/resources/ssl/pem/README.md new file mode 100644 index 0000000000..9fa4d170f5 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/pem/README.md @@ -0,0 +1,14 @@ +# PEM resources + +The following resources were copy/pasted from the akka-pki/src/test/resources folder. + +- multi-prime-pkcs1.pem +- pkcs1.pem +- pkcs8.pem +- selfsigned-certificate.pem + +Note that these set of certificates are valid PEMs but are invalid for akka-remote +use. Either the key length, certificate usage limitations (via the UsageKeyExtensions), +or the fact that the key's certificate is self-signed cause one of the following +errors: `certificate_unknown`, `certificate verify message signature error`/`bad_certificate` +during the SSLHandshake. diff --git a/akka-remote/src/test/resources/ssl/pem/pkcs1.pem b/akka-remote/src/test/resources/ssl/pem/pkcs1.pem new file mode 100644 index 0000000000..5f23b81867 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/pem/pkcs1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0IQFs9KpS6fpm7Bq5JcAzRzdZnYub7qGBJ9+QZX8F6qUYiXf +jbVPAZSIksNg6G5yMkzBLZ8r0UOm5WJs6sAHKGJOQk9DcwZEt3XpGbYXAnM5V1sb +xd5oNcbXLcHouU8jrEj+O2KvmgCzDDCUOf3SjGnF4dWqhsTT9tMJZvWVB0OjpKnT +zcoxFKO/XCz8Spb1+FgKUgt3afA+JTRpQWtaZJ41fuTg0rm0qtUSeZXPUEYkqqK4 +msoSe7dbu7uiEOkUPoeiP7wzSrwihQSCxOzwdphV2XtF5Xfs24/Ad4WKXTqRVUK/ +yldcQy2orFSsX41KBzS8PI1hnOC6uRA0PiGd/QIDAQABAoIBAQCDbxShGuK326m3 +B2b5m+1XXSB5m3j92FbtxxMwiDgVOuK5UyItEuIwHs5PpHQLTsMQzaze8vwNtlUX +Ngltl4lrfTvTNF9Ru9vIwLwkBtFOLA8y7yz8doq9iw7LuvTVCft0d7Y4/KWvr00t +G9nzC/mRpIKlLaeFt7/cT34XtikwH+Ez8uWGidYnrqkKZ0bsIZvD+e7gae+veohN +BnTcyDIk8P5nXG0vM4hxtjLo3KstemwOt6vCtiKzL2Vq/JAVD3nlec8WPBzft79I +k5tb3Qm/OnxIQaWF5AhAVkXgMsLL3ddJoAn/K6NZ6NtRGZozkwdP+m4nacrKFJVJ +6ew7OdAJAoGBAO65GvteHLXQ3d1NfU+X6RSBatJdpuf2S4Hc7P+sTkPjLDlQPDKL +ZFLVigPlCehMvgGASPOxL8UqPZugwEo83ywp5t/iOUccXZCPSISreSzWJPfOJ+dl +aKP2cSHHPNC/mISDpp/NF+xfgEAUQQ6AdOKwHGlsFWBvkkw810d4zmXvAoGBAN+b +QYv32wNa2IHC1Ri3VgtfHpQ20fMz+UO6TMefBbgkcgBk4L+O7gSCpThaqy9R7UnX +IEH0QyaBEXzQxB7qvCo1pczWiEUcG+vAyyz9U9oO9Hee/UTMOWL4Sj7qoavau2Be +5PFOO6qA+19JTnStuNb3swNrMmxDQpyNDvUkYAbTAoGBALuYkSCJ84vZaBBZrajX +mt13WieYWuocPXf+0euVTyfAJOehKr0ZlywVDNFEssVvUT1Cv5FpYz3QlPtwlsuA +DGzbPMghMZu1Kb3JK1a+nYnjeseVpPwNT+7RYlQGCr+MYOF5x336oNsqrVEt2XX4 +8mGVva4GtsHCy7fHc/GBeMjXAoGBALhEYkytER//okG0xBUdKFwwo6tyTavEndpx +UUqDwpvP9N5cQ1W4vG6dFviMx1s0gX4DOQMA/sFhRX79L1FnEW8bTKmz9RI2qs+p +zgUiMhKVlmJpc79ZKMVlZRHaGybbFuTA7pvoY4ULy5rndy7x5kvITg44LZJID0Gh +gL0Fn9ifAoGAEaWA7yxTA7phDIt0U91HZEVhNSM4cZDurE7614qWhKveSP8f0J4c +3d9y/re4RcCwmss/FtQWSkB+32WbD3qk+QB7hV1oFqJ5ObcfwevGYR8m6vOz1h2L +3pQNi4PcH3U8eeGG1laFKUQ295rBLNqIbOo2y+4hxMnC4tka1X118Ec= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/akka-remote/src/test/resources/ssl/pem/selfsigned-certificate.pem b/akka-remote/src/test/resources/ssl/pem/selfsigned-certificate.pem new file mode 100644 index 0000000000..4fb0ea7b86 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/pem/selfsigned-certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCzCCAfOgAwIBAgIQfEHPfR1p1xuW9TQlfxAugjANBgkqhkiG9w0BAQsFADAv +MS0wKwYDVQQDEyQwZDIwN2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgw +HhcNMTkxMDExMTMyODUzWhcNMjQxMDA5MTQyODUzWjAvMS0wKwYDVQQDEyQwZDIw +N2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDhD0BxlDzEOzcp45lPHL60lnM6k3puEGb2lKHL5/nR +F94FCnZL0FH8EdxWzzAYgys+kUwSdo4QMuWuvjY2Km4Wob6k4uAeYEFTCfBdi4/z +r4kpWzu8xLz+uZWimLQrjqVytNNK3DMv6ebWUJ/92VTDS4yzWk4YV0MVr2b2OgMK +SgMvaFQ8L/CwyML72PBWIqU67+MMvvcTLxQdyEgQTTjP0bbiXMLDvfZDarLJojsW +SNBz7AIkznhGkzIGGdhAa41PnPu9XaBFhaqx9Qe3+MG2/k1l/46eHtmxCqhOUde1 +i0vy6ZfgcGifua1tg1UBI/oT4S0dsq24dq7K1MYLyHTrAgMBAAGjIzAhMA4GA1Ud +DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAa +5YOlvob4wqps3HLaOIi7VzDihNHciP+BI0mzxHa7D3bGaecRPSeG3xEoD/Uxs9o4 +8cByPpYx1Wl8LLRx14wDcK0H+UPpo4gCI6h6q92cJj0YRjcSUUt8EIu3qnFMjtM+ +sl/uc21fGlq6cvMRZXqtVYENoSNTDHi5a5eEXRa2eZ8XntjvOanOhIKWmxvr8r4a +Voz4WdnXx1C8/BzB62UBoMu4QqMGMLk5wXP0D6hECUuespMest+BeoJAVhTq7wZs +rSP9q08n1stZFF4+bEBaxcqIqdhOLQdHcYELN+a5v0Mcwdsy7jJMagmNPfsKoOKC +hLOsmNYKHdmWg37Jib5o +-----END CERTIFICATE----- \ No newline at end of file diff --git a/akka-remote/src/test/resources/ssl/rsa-client.example.com.crt b/akka-remote/src/test/resources/ssl/rsa-client.example.com.crt new file mode 100644 index 0000000000..9e8732c499 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/rsa-client.example.com.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDYzCCAwigAwIBAgIECUHCVTAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDI1WhcNMzAwNTMwMTU1MDI1 +WjCBizELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxHzAdBgNVBAMTFnJzYS1jbGllbnQuZXhhbXBsZS5jb20w +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCEViFvY8pp7M/yv4tyUGyq +knInRW+FtHH7rkgB7nqL6DCItp81yafB2KMQ7NrdRXKr12sHUhcpd81acchqURZM +TqM5GwhK0TieR5qfmT+zQmBXKprIVc2sRvgVC7p4wNseXwky0aLJQRlFWAgB2nZ/ ++EBDNt0I5QGLSoaN5fMH6F9pVC1IpyCnVFisgDg7obCOIkBUfVyswye0+RcjGYRA +PelMzQcy62AnYh/64eBDKdGfLjHczaw14Uuy5OlWjeyVdM6MZBOf+KfxdD8KLlhw +2cLxtzkJk3JedqW4+ufRS7ilGRpUw8e2IgR9TQW0VLYByALAVIfJSMfUEx70K5NB +AgMBAAGjgZgwgZUwHwYDVR0jBBgwFoAU2REkHgBAMCYIi8dhLQAoPj8CpRQwEwYD +VR0lBAwwCgYIKwYBBQUHAwIwDgYDVR0PAQH/BAQDAgWgMC4GA1UdEQQnMCWCFnJz +YS1jbGllbnQuZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tMB0GA1UdDgQWBBSPL7X9 +LBqCjnPi25CIzzkX3OEiITAMBggqhkjOPQQDAgUAA0cAMEQCIDx/fReRIBGXgRkn +zYGi09q7VccGTfw0gCTUa68NLTqpAiA27xcuzNMBS21xtXa7nVEOJJ1BUyi7c9pS +/9KFyHXAXg== +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/rsa-client.example.com.p12 b/akka-remote/src/test/resources/ssl/rsa-client.example.com.p12 new file mode 100644 index 0000000000..0b3e3157da Binary files /dev/null and b/akka-remote/src/test/resources/ssl/rsa-client.example.com.p12 differ diff --git a/akka-remote/src/test/resources/ssl/rsa-client.example.com.pem b/akka-remote/src/test/resources/ssl/rsa-client.example.com.pem new file mode 100644 index 0000000000..48f4475d14 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/rsa-client.example.com.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAhFYhb2PKaezP8r+LclBsqpJyJ0VvhbRx+65IAe56i+gwiLaf +NcmnwdijEOza3UVyq9drB1IXKXfNWnHIalEWTE6jORsIStE4nkean5k/s0JgVyqa +yFXNrEb4FQu6eMDbHl8JMtGiyUEZRVgIAdp2f/hAQzbdCOUBi0qGjeXzB+hfaVQt +SKcgp1RYrIA4O6GwjiJAVH1crMMntPkXIxmEQD3pTM0HMutgJ2If+uHgQynRny4x +3M2sNeFLsuTpVo3slXTOjGQTn/in8XQ/Ci5YcNnC8bc5CZNyXnaluPrn0Uu4pRka +VMPHtiIEfU0FtFS2AcgCwFSHyUjH1BMe9CuTQQIDAQABAoIBAAEuZ6P/5wmlBIIt +NYhysVWgcwZot0MhRjcx6hCiWVOwYeaGgstPHoE7dtEq3BVHjmt8Q8dP80b6e8nH +5DsWuDU8KnbTB/LrBS9cgkPUcV4fRI7ioXkSUwCrrYE6lpKHXi1aOdHLT0GupBQb +Yg7qLU6dH6256qUuriHZK3ROzimO5R3lsHkA8+Lavw6/LJIuLMkl5wmg91AS4Y0d +SYUUf7452hj2Ir/RK7wNrtZ/QQ5g6iIXaH8vKt20DBkUXuiMJI6uP98/w2axwMC6 +/AkSAsFDh0K3ZiJWjeNYQ5KZrLFUNmADMFo6RL5fvcyp1c2e6vLcIbM6JPOtwql1 +SI2S2HUCgYEA2ezDtj4lN61uh8nXk0z0UApc5Cx+dzLMcbGrCLIn67x9EbDtcOvz +oy+GnnD/JLhd/3HRf1Lwd+30iXpJ5rr1nRLwlSXmWsHvFafwGvRQ4NWYt3QyM9TX +98WMchy5m+0pBqBq+0IZPLUB7iY54xgUHMk2/IrbwkkvkYWS4gKUN2sCgYEAm3U1 +t/DwrDuzF4/ZhGTxOI0Y3JSFRGtwF5npkR/6bN/T9JKU4gD8T3fkvkmEZ66xVzUA +iuUIgVs3rSkVi51Sr2xId8KMUPRJtDWAThJYEtY1AAFjY9gYlxYA/kggWQSPWfX+ +CzLHAAB5uIPe2xS3YN3yXZLABefv7ZF6i1aBBwMCgYBNE0NJEoPBRHLCTe4T5/TE +1lVyUhZMfEf4sjjms3QRGTI27pecB6e9AJMhOJ/U0exU62GIIcJw+FUzxm+azmcO +LeOvLJ9jXBH+W849CkoMqx7/S3ZyBIZ52IHK3kP7VQ7cjCIqSX95jB9pplV071A1 +uijbexUsiwvq8Q45J2ZajwKBgCtVNLAdPTkFOxqqQluhN4wn6HI0BCHaQNiTUoPd +ghSvH4nhAhctZydPqDdSjtHH5C8G2yvcQ86q+o4OEa9lHxM+/8RCOpKmRZUyBJ2+ +h0ZY51UlDeta5R/YRlabDElD+CF/bFz6vnXFrCg+ufQfhi4+L7zdlyEOUdbK4nnM +lxK5AoGBALMfsbYejpjNP0Dmcv644barz9SLKPT9Sxv5xjDNxRXWATIy2MblM9WO +WO0hNw6KLfSl1dkrbZYH4/4abw6g0FYCivBe9X7p/iGkIBKx9PWAW18vTnomW7Oq +RfTCzmLHhGVhxl6ByvSzNIV7MCgPeXalC7gUBLdnKuBvQCOvzyPr +-----END RSA PRIVATE KEY----- diff --git a/akka-remote/src/test/resources/ssl/two.example.com.crt b/akka-remote/src/test/resources/ssl/two.example.com.crt new file mode 100644 index 0000000000..fc766d02d3 --- /dev/null +++ b/akka-remote/src/test/resources/ssl/two.example.com.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICizCCAi+gAwIBAgIECNzvNjAMBggqhkjOPQQDAgUAMH4xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUgT3JnMRIw +EAYDVQQDEwlleGFtcGxlQ0EwHhcNMjAwNjAxMTU1MDE2WhcNMzAwNTMwMTU1MDE2 +WjCBhDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEUMBIGA1UE +CxMLRXhhbXBsZSBPcmcxGDAWBgNVBAMTD3R3by5leGFtcGxlLmNvbTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABK2v6kZfa6beqxAXcSjAIUKORvbwivAEwoik38ZN +kQ8X60K5/gFE/QNoozTOZ60pbPzI8QFjcTXlpm4aKcRudwajgZEwgY4wHwYDVR0j +BBgwFoAU2REkHgBAMCYIi8dhLQAoPj8CpRQwEwYDVR0lBAwwCgYIKwYBBQUHAwEw +DgYDVR0PAQH/BAQDAgWgMCcGA1UdEQQgMB6CD3R3by5leGFtcGxlLmNvbYILZXhh +bXBsZS5jb20wHQYDVR0OBBYEFDrOW8faP3xxtC/wd1f6EZwlpF7XMAwGCCqGSM49 +BAMCBQADSAAwRQIhAPr4w+30vBqHWGJOGvBScN3OXQaBnQSlFIc5c0AErLYdAiB+ +JuYU6hy1EmvRZxz2p/r4pz94K/81pNCAWgHKp1pxgQ== +-----END CERTIFICATE----- diff --git a/akka-remote/src/test/resources/ssl/two.example.com.p12 b/akka-remote/src/test/resources/ssl/two.example.com.p12 new file mode 100644 index 0000000000..38b9320938 Binary files /dev/null and b/akka-remote/src/test/resources/ssl/two.example.com.p12 differ diff --git a/akka-remote/src/test/resources/truststore b/akka-remote/src/test/resources/truststore index f70de5dde2..7f6041469c 100644 Binary files a/akka-remote/src/test/resources/truststore and b/akka-remote/src/test/resources/truststore differ diff --git a/akka-remote/src/test/scala/akka/remote/artery/ArterySpecSupport.scala b/akka-remote/src/test/scala/akka/remote/artery/ArterySpecSupport.scala index 099ef92999..2be5d7e3c1 100644 --- a/akka-remote/src/test/scala/akka/remote/artery/ArterySpecSupport.scala +++ b/akka-remote/src/test/scala/akka/remote/artery/ArterySpecSupport.scala @@ -32,6 +32,9 @@ object ArterySpecSupport { // set the test key-store and trust-store properties // TLS only used if transport=tls-tcp, which can be set from specific tests or // System properties (e.g. jenkins job) + // TODO: randomly return a Config using ConfigSSLEngineProvider or + // RotatingKeysSSLEngineProvider and, eventually, run tests twice + // (once for each provider). lazy val tlsConfig: Config = { val trustStore = getClass.getClassLoader.getResource("truststore").getPath val keyStore = getClass.getClassLoader.getResource("keystore").getPath diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/SecureRandomFactorySpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/SecureRandomFactorySpec.scala new file mode 100644 index 0000000000..33e3de6e31 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/SecureRandomFactorySpec.scala @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp + +import java.io.ByteArrayOutputStream +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import java.util.zip.GZIPOutputStream + +import akka.event.NoMarkerLogging +import akka.testkit.AkkaSpec + +class SecureRandomFactorySHA1Spec extends SecureRandomFactorySpec("SHA1PRNG") +class SecureRandomFactoryNativePRNGSpec extends SecureRandomFactorySpec("NativePRNG") +class SecureRandomFactoryJVMChoiceSpec extends SecureRandomFactorySpec("SecureRandom") +class SecureRandomFactoryBlankSpec extends SecureRandomFactorySpec("") +class SecureRandomFactoryInvalidPRNGSpec extends SecureRandomFactorySpec("InvalidPRNG") + +abstract class SecureRandomFactorySpec(alg: String) extends AkkaSpec { + var prng: SecureRandom = null + + def isSupported: Boolean = { + try { + prng = SecureRandomFactory.createSecureRandom(alg, NoMarkerLogging) + prng.nextInt() // Has to work + val sRng = alg + if (prng.getAlgorithm != sRng && sRng != "") + throw new NoSuchAlgorithmException(sRng) + true + } catch { + case e: NoSuchAlgorithmException => + info(e.toString) + false + } + } + + s"Artery's Secure Random support ($alg)" must { + if (isSupported) { + "generate random" in { + val bytes = Array.ofDim[Byte](16) + // Reproducer of the specific issue described at + // https://doc.akka.io/docs/akka/current/security/2018-08-29-aes-rng.html + // awaitAssert just in case we are very unlucky to get same sequence more than once + awaitAssert { + val randomBytes = List + .fill(10) { + prng.nextBytes(bytes) + bytes.toVector + } + .toSet + randomBytes.size should ===(10) + } + } + + "have random numbers that are not compressable, because then they are not random" in { + val randomData = new Array[Byte](1024 * 1024) + prng.nextBytes(randomData) + + val baos = new ByteArrayOutputStream() + val gzipped = new GZIPOutputStream(baos) + try gzipped.write(randomData) + finally gzipped.close() + + val compressed = baos.toByteArray + // random data should not be compressible + // Another reproducer of https://doc.akka.io/docs/akka/current/security/2018-08-29-aes-rng.html + // with the broken implementation the compressed size was <5k + compressed.size should be > randomData.length + } + + } else { + s"not be run when the $alg PRNG provider is not supported by the platform this test is currently being executed on" in { + pending + } + } + } +} diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala index 4925446996..b530ffa0f9 100644 --- a/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala @@ -5,27 +5,23 @@ package akka.remote.artery package tcp -import java.io.ByteArrayOutputStream import java.security.NoSuchAlgorithmException -import java.util.zip.GZIPOutputStream -import javax.net.ssl.SSLEngine -import scala.concurrent.duration._ - -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory - -import akka.actor.ActorIdentity -import akka.actor.ActorPath -import akka.actor.ActorRef -import akka.actor.ExtendedActorSystem -import akka.actor.Identify -import akka.actor.RootActorPath +import akka.actor.{ ActorIdentity, ActorPath, ActorRef, Identify, RootActorPath } import akka.actor.setup.ActorSystemSetup +import akka.remote.artery.tcp.ssl.CipherSuiteSupportCheck import akka.testkit.EventFilter import akka.testkit.ImplicitSender import akka.testkit.TestActors import akka.testkit.TestProbe +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession +import org.scalatest.matchers.should.Matchers + +import scala.concurrent.duration._ +import scala.util.{ Failure, Success } class TlsTcpWithDefaultConfigSpec extends TlsTcpSpec(ConfigFactory.empty()) @@ -53,6 +49,17 @@ class TlsTcpWithCrappyRSAWithMD5OnlyHereToMakeSureThingsWorkSpec } """)) +class TlsTcpWithRotatingKeysSSLEngineSpec extends TlsTcpSpec(ConfigFactory.parseString(s""" + akka.remote.artery.ssl { + ssl-engine-provider = akka.remote.artery.tcp.ssl.RotatingKeysSSLEngineProvider + rotating-keys-engine { + key-file = ${getClass.getClassLoader.getResource("ssl/node.example.com.pem").getPath} + cert-file = ${getClass.getClassLoader.getResource("ssl/node.example.com.crt").getPath} + ca-cert-file = ${getClass.getClassLoader.getResource("ssl/exampleca.crt").getPath} + } + } + """)) + object TlsTcpSpec { lazy val config: Config = { @@ -68,37 +75,31 @@ object TlsTcpSpec { abstract class TlsTcpSpec(config: Config) extends ArteryMultiNodeSpec(config.withFallback(TlsTcpSpec.config)) - with ImplicitSender { + with ImplicitSender + with Matchers { val systemB = newRemoteSystem(name = Some("systemB")) val addressB = address(systemB) val rootB = RootActorPath(addressB) def isSupported: Boolean = { - try { - val provider = new ConfigSSLEngineProvider(system) - - val rng = provider.createSecureRandom() - rng.nextInt() // Has to work - val sRng = provider.SSLRandomNumberGenerator - if (rng.getAlgorithm != sRng && sRng != "") - throw new NoSuchAlgorithmException(sRng) - - val address = system.asInstanceOf[ExtendedActorSystem].provider.getDefaultAddress - val host = address.host.get - val port = address.port.get - - val engine = provider.createServerSSLEngine(host, port) - val gotAllSupported = provider.SSLEnabledAlgorithms.diff(engine.getSupportedCipherSuites.toSet) - val gotAllEnabled = provider.SSLEnabledAlgorithms.diff(engine.getEnabledCipherSuites.toSet) - gotAllSupported.isEmpty || (throw new IllegalArgumentException("Cipher Suite not supported: " + gotAllSupported)) - gotAllEnabled.isEmpty || (throw new IllegalArgumentException("Cipher Suite not enabled: " + gotAllEnabled)) - engine.getSupportedProtocols.contains(provider.SSLProtocol) || - (throw new IllegalArgumentException("Protocol not supported: " + provider.SSLProtocol)) - } catch { - case e @ (_: IllegalArgumentException | _: NoSuchAlgorithmException) => + val checked = system.settings.config.getString("akka.remote.artery.ssl.ssl-engine-provider") match { + case "akka.remote.artery.tcp.ConfigSSLEngineProvider" => + CipherSuiteSupportCheck.isSupported(system, "akka.remote.artery.ssl.config-ssl-engine") + case "akka.remote.artery.tcp.ssl.RotatingKeysSSLEngineProvider" => + CipherSuiteSupportCheck.isSupported(system, "akka.remote.artery.ssl.rotating-keys-engine") + case other => + fail( + s"Don't know how to determine whether the crypto building blocks in [$other] are available on this platform") + } + checked match { + case Success(()) => + true + case Failure(e @ (_: IllegalArgumentException | _: NoSuchAlgorithmException)) => info(e.toString) false + case Failure(other) => + fail("Unexpected failure checking whether the crypto building blocks are available on this platform.", other) } } @@ -122,43 +123,6 @@ abstract class TlsTcpSpec(config: Config) if (isSupported) { - "generate random" in { - val provider = new ConfigSSLEngineProvider(system) - val rng = provider.createSecureRandom() - val bytes = Array.ofDim[Byte](16) - // Reproducer of the specific issue described at - // https://doc.akka.io/docs/akka/current/security/2018-08-29-aes-rng.html - // awaitAssert just in case we are very unlucky to get same sequence more than once - awaitAssert { - val randomBytes = List - .fill(10) { - rng.nextBytes(bytes) - bytes.toVector - } - .toSet - randomBytes.size should ===(10) - } - } - - "have random numbers that are not compressable, because then they are not random" in { - val provider = new ConfigSSLEngineProvider(system) - val rng = provider.createSecureRandom() - - val randomData = new Array[Byte](1024 * 1024) - rng.nextBytes(randomData) - - val baos = new ByteArrayOutputStream() - val gzipped = new GZIPOutputStream(baos) - try gzipped.write(randomData) - finally gzipped.close() - - val compressed = baos.toByteArray - // random data should not be compressible - // Another reproducer of https://doc.akka.io/docs/akka/current/security/2018-08-29-aes-rng.html - // with the broken implementation the compressed size was <5k - compressed.size should be > randomData.length - } - "deliver messages" in { systemB.actorOf(TestActors.echoActorProps, "echo") val echoRef = identify(rootB / "user" / "echo") @@ -257,17 +221,21 @@ class TlsTcpWithActorSystemSetupSpec extends ArteryMultiNodeSpec(TlsTcpSpec.conf val sslProviderClientProbe = TestProbe() val sslProviderSetup = SSLEngineProviderSetup(sys => - new ConfigSSLEngineProvider(sys) { + new SSLEngineProvider { + val delegate = new ConfigSSLEngineProvider(sys) override def createServerSSLEngine(hostname: String, port: Int): SSLEngine = { sslProviderServerProbe.ref ! "createServerSSLEngine" - super.createServerSSLEngine(hostname, port) + delegate.createServerSSLEngine(hostname, port) } override def createClientSSLEngine(hostname: String, port: Int): SSLEngine = { sslProviderClientProbe.ref ! "createClientSSLEngine" - super.createClientSSLEngine(hostname, port) + delegate.createClientSSLEngine(hostname, port) } - + override def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] = + delegate.verifyClientSession(hostname, session) + override def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] = + delegate.verifyServerSession(hostname, session) }) val systemB = newRemoteSystem(name = Some("systemB"), setup = Some(ActorSystemSetup(sslProviderSetup))) diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/CipherSuiteSupportCheck.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/CipherSuiteSupportCheck.scala new file mode 100644 index 0000000000..82941e2d43 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/CipherSuiteSupportCheck.scala @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.security.NoSuchAlgorithmException + +import akka.actor.ActorSystem +import akka.actor.ExtendedActorSystem +import akka.event.NoMarkerLogging +import akka.remote.artery.tcp.SecureRandomFactory +import com.typesafe.config.Config +import javax.net.ssl.SSLEngine + +import scala.util.Try + +object CipherSuiteSupportCheck { + + /** + * Given a `configPath` runs a set of validations over the default protocols, + * cipher suites, PRNG algorithm/provider, etc... + */ + def isSupported(system: ActorSystem, configPath: String): Try[Unit] = Try { + val config = system.settings.config + val subConfig = config.getConfig(configPath) + + isPrngSupported(subConfig) + val engine: SSLEngine = buildSslEngine(system) + val sslFactoryConfig = new SSLEngineConfig(subConfig) + + areAlgorithmsSupported(sslFactoryConfig, engine) + isProtocolSupported(sslFactoryConfig, engine) + } + + private def isPrngSupported(config: Config): Unit = { + val rng = SecureRandomFactory.createSecureRandom(config, NoMarkerLogging) + rng.nextInt() // Has to work + val setting = SecureRandomFactory.rngConfig(config) + if (rng.getAlgorithm != setting && setting != "") + throw new NoSuchAlgorithmException(setting) + } + + private def areAlgorithmsSupported(sslFactoryConfig: SSLEngineConfig, engine: SSLEngine) = { + import sslFactoryConfig._ + val gotAllSupported = SSLEnabledAlgorithms.diff(engine.getSupportedCipherSuites.toSet) + val gotAllEnabled = SSLEnabledAlgorithms.diff(engine.getEnabledCipherSuites.toSet) + gotAllSupported.isEmpty || (throw new IllegalArgumentException("Cipher Suite not supported: " + gotAllSupported)) + gotAllEnabled.isEmpty || (throw new IllegalArgumentException("Cipher Suite not enabled: " + gotAllEnabled)) + } + + private def isProtocolSupported(sslFactoryConfig: SSLEngineConfig, engine: SSLEngine) = { + import sslFactoryConfig._ + engine.getSupportedProtocols.contains(SSLProtocol) || + (throw new IllegalArgumentException("Protocol not supported: " + SSLProtocol)) + + } + + private def buildSslEngine(system: ActorSystem): SSLEngine = { + val address = system.asInstanceOf[ExtendedActorSystem].provider.getDefaultAddress + val host = address.host.get + val port = address.port.get + val provider = new akka.remote.artery.tcp.ConfigSSLEngineProvider(system) + val engine = provider.createServerSSLEngine(host, port) + engine + } + +} diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/PeerSubjectVerifierSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/PeerSubjectVerifierSpec.scala new file mode 100644 index 0000000000..150b8280d8 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/PeerSubjectVerifierSpec.scala @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.security.Principal +import java.security.cert.Certificate +import java.security.cert.X509Certificate + +import javax.net.ssl.SSLSession +import javax.net.ssl.SSLSessionContext +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class PeerSubjectVerifierSpec extends AnyWordSpec with Matchers { + import TlsResourcesSpec._ + "PeerSubjectVerifier" must { + + "accept a peer when both peers have the same subject name" in { + // CN=one.example.com + // SAN=DNS:number-one.example.com,DNS:example.com + // see https://github.com/playframework/play-samples/pull/97 + val exampleOne = loadCert("/ssl/one.example.com.crt") + // CN=two.example.com + // SAN=DNS:number-two.example.com,DNS:example.com + val exampleTwo = loadCert("/ssl/two.example.com.crt") + // verification passes because both certs have `example.com` in the SAN + new PeerSubjectVerifier(exampleOne).verifyServerSession(null, inMemSession(exampleTwo)) mustBe None + } + + "reject a peer when peers don't have the same subject name" in { + // `island.example.com` has no SAN and its only subject is not available on `two.example.com` + // the peer verification must fail. + val client = loadCert("/ssl/island.example.com.crt") + val exampleTwo = loadCert("/ssl/two.example.com.crt") + new PeerSubjectVerifier(client).verifyServerSession(null, inMemSession(exampleTwo)) mustNot be(None) + } + } + + def inMemSession(peerCert: X509Certificate): SSLSession = { + new SSLSession { + override def getPeerCertificates: Array[Certificate] = Array(peerCert) + override def getId: Array[Byte] = throw new UnsupportedOperationException() + override def getSessionContext: SSLSessionContext = throw new UnsupportedOperationException() + override def getCreationTime: Long = throw new UnsupportedOperationException() + override def getLastAccessedTime: Long = throw new UnsupportedOperationException() + override def invalidate(): Unit = throw new UnsupportedOperationException() + override def isValid: Boolean = throw new UnsupportedOperationException() + override def putValue(name: String, value: Any): Unit = throw new UnsupportedOperationException() + override def getValue(name: String): AnyRef = throw new UnsupportedOperationException() + override def removeValue(name: String): Unit = throw new UnsupportedOperationException() + override def getValueNames: Array[String] = throw new UnsupportedOperationException() + override def getLocalCertificates: Array[Certificate] = throw new UnsupportedOperationException() + override def getPeerCertificateChain: Array[javax.security.cert.X509Certificate] = + throw new UnsupportedOperationException() + override def getPeerPrincipal: Principal = throw new UnsupportedOperationException() + override def getLocalPrincipal: Principal = throw new UnsupportedOperationException() + override def getCipherSuite: String = throw new UnsupportedOperationException() + override def getProtocol: String = throw new UnsupportedOperationException() + override def getPeerHost: String = throw new UnsupportedOperationException() + override def getPeerPort: Int = throw new UnsupportedOperationException() + override def getPacketBufferSize: Int = throw new UnsupportedOperationException() + override def getApplicationBufferSize: Int = throw new UnsupportedOperationException() + } + } +} diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/PemManagersProviderSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/PemManagersProviderSpec.scala new file mode 100644 index 0000000000..5973262135 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/PemManagersProviderSpec.scala @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.security.PrivateKey +import java.security.cert.Certificate +import java.security.cert.X509Certificate + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +/** + * + */ +class PemManagersProviderSpec extends AnyWordSpec with Matchers { + + "A PemManagersProvider" must { + + "load stores reading files setup in config (akka-pki samples)" in { + // These set of certificates are valid PEMs but are invalid for akka-remote + // use. Either the key length, certificate usage limitations (via the UsageKeyExtensions), + // or the fact that the key's certificate is self-signed cause one of the following + // errors: `certificate_unknown`, `certificate verify message signature error`/`bad_certificate` + // during the SSLHandshake. + withFiles("ssl/pem/pkcs1.pem", "ssl/pem/selfsigned-certificate.pem", "ssl/pem/selfsigned-certificate.pem") { + (pk, cert, cacert) => + PemManagersProvider.buildKeyManagers(pk, cert, cacert).length must be(1) + PemManagersProvider.buildTrustManagers(cacert).length must be(1) + cert.getSubjectDN.getName must be("CN=0d207b68-9a20-4ee8-92cb-bf9699581cf8") + } + } + + "load stores reading files setup in config (keytool samples)" in { + withFiles("ssl/node.example.com.pem", "ssl/node.example.com.crt", "ssl/exampleca.crt") { (pk, cert, cacert) => + PemManagersProvider.buildKeyManagers(pk, cert, cacert).length must be(1) + PemManagersProvider.buildTrustManagers(cacert).length must be(1) + cert.getSubjectDN.getName must be( + "CN=node.example.com, OU=Example Org, O=Example Company, L=San Francisco, ST=California, C=US") + } + } + + } + + private def withFiles(keyFile: String, certFile: String, caCertFile: String)( + block: (PrivateKey, X509Certificate, Certificate) => Unit) = { + block( + PemManagersProvider.loadPrivateKey(nameToPath(keyFile)), + PemManagersProvider.loadCertificate(nameToPath(certFile)).asInstanceOf[X509Certificate], + PemManagersProvider.loadCertificate(nameToPath(caCertFile))) + } + + private def nameToPath(name: String): String = getClass.getClassLoader.getResource(name).getPath +} diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/RotatingKeysSSLEngineProviderSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/RotatingKeysSSLEngineProviderSpec.scala new file mode 100644 index 0000000000..89eb0e6aa0 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/RotatingKeysSSLEngineProviderSpec.scala @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2016-2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.concurrent.atomic.AtomicReference + +import akka.actor.ActorIdentity +import akka.actor.ActorPath +import akka.actor.ActorRef +import akka.actor.ActorSystem +import akka.actor.Address +import akka.actor.ExtendedActorSystem +import akka.actor.Identify +import akka.actor.RootActorPath +import akka.actor.setup.ActorSystemSetup +import akka.remote.artery.ArteryMultiNodeSpec +import akka.remote.artery.tcp.SSLEngineProvider +import akka.remote.artery.tcp.SSLEngineProviderSetup +import akka.remote.artery.tcp.TlsTcpSpec +import akka.testkit.ImplicitSender +import akka.testkit.TestActors +import akka.testkit.TestProbe +import com.typesafe.config.ConfigFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession + +import scala.concurrent.duration._ +import scala.concurrent.Await +import scala.concurrent.blocking +import scala.util.control.NonFatal + +// This is a simplification Spec. It doesn't rely on changing files. +class RotatingProviderWithStaticKeysSpec + extends RotatingKeysSSLEngineProviderSpec(RotatingKeysSSLEngineProviderSpec.resourcesConfig) { + "Artery with TLS/TCP with RotatingKeysSSLEngine" must { + + "rebuild the SSLContext" in { + if (!arteryTcpTlsEnabled()) + pending + + // an initial connection between sysA (from the testkit) and sysB + // to get sysB up and running + val (remoteSysB, pathEchoB) = buildRemoteWithEchoActor("B-rebuild") + contact(system, pathEchoB) + assertEnginesCreated(remoteSysB) + val before = remoteSysB.sslContextRef.get() + + awaitCacheExpiration() + + // Send message to system C from system B. + // Not using system A because we can't get a reference to the SSLContext in system A + val (remoteSysC, pathEchoC) = buildRemoteWithEchoActor("C-rebuild") + contact(remoteSysB.actorSystem, pathEchoC) + assertEnginesCreated(remoteSysC) + assertEnginesCreated(remoteSysB) + + // the SSLContext references on sysB should differ + val after = remoteSysB.sslContextRef.get() + before shouldNot be(after) + } + + "keep existing connections alive (no new SSLEngine's created after cache expiration)" in { + if (!arteryTcpTlsEnabled()) + pending + + // an initial connection between sysA (from the testkit) and sysB + // to get sysB up and running + val (remoteSysB, pathEchoB) = buildRemoteWithEchoActor("B-reuse-alive") + contact(system, pathEchoB) + assertThreeChannelsAreCreated(remoteSysB) + // once the three channels are created, no new engines are required... (cont'd) + contact(system, pathEchoB) + contact(system, pathEchoB) + assertNoEnginesCreated(remoteSysB) + + awaitCacheExpiration() + + // ... (cont) even when the cache has expired. + // Send message to system B from system A should not require a new SSLEngine + // be created. + contact(system, pathEchoB) + assertNoEnginesCreated(remoteSysB) + } + + } +} + +// This is a the real deal Spec. It relies on changing files on a particular folder. +class RotatingProviderWithChangingKeysSpec + extends RotatingKeysSSLEngineProviderSpec(RotatingKeysSSLEngineProviderSpec.tempFileConfig) { + import RotatingKeysSSLEngineProviderSpec._ + + protected override def atStartup(): Unit = { + super.atStartup() + deployCaCert() + deployKeySet("ssl/artery-nodes/artery-node001.example.com") + } + + "Artery with TLS/TCP with RotatingKeysSSLEngine" must { + "rebuild the SSLContext using new keys" in { + if (!arteryTcpTlsEnabled()) + pending + + // an initial connection between sysA (from the testkit) and sysB + // to get sysB up and running + val (remoteSysB, pathEchoB) = buildRemoteWithEchoActor("B-reread") + contact(system, pathEchoB) + assertEnginesCreated(remoteSysB) + val before = remoteSysB.sslContextRef.get() + + // setup new (invalid) keys + // The `ssl/rsa-client.example.com` keyset can't be used in peer-to-peer connections + // it's only valid for `clientAuth` + + deployKeySet("ssl/rsa-client.example.com") + awaitCacheExpiration() + val (_, pathEchoC) = buildRemoteWithEchoActor("C-reread") + try { + contact(remoteSysB.actorSystem, pathEchoC) + fail("The credentials under `ssl/rsa-client` are not valid for Akka remote so contact() must fail.") + } catch { + case _: java.lang.AssertionError => + // This assertion error is expected because we expect a failure in contact() since + // the SSL credentials are invalid + } + + // deploy a new key set + deployKeySet("ssl/artery-nodes/artery-node003.example.com") + + // Send message to system C from system B. + // Using invalid keys, this should fail + val (remoteSysD, pathEchoD) = buildRemoteWithEchoActor("D-reread") + contact(remoteSysB.actorSystem, pathEchoD) + assertEnginesCreated(remoteSysB) + assertEnginesCreated(remoteSysD) + // the SSLContext references on sysB should differ + val after = remoteSysB.sslContextRef.get() + before shouldNot be(after) + } + + } +} + +object RotatingKeysSSLEngineProviderSpec { + val cacheTtlInSeconds = 1 + + private val arteryNode001Id = "ssl/artery-nodes/artery-node001.example.com" + + private val baseConfig = """ + akka.remote.artery { + ## the large-messages channel in artery is not used for this tests + ## but we're enabling it to test it also creates its own SSLEngine + large-message-destinations = [ "/user/large" ] + } + akka.remote.artery.ssl { + ssl-engine-provider = akka.remote.artery.tcp.ssl.RotatingKeysSSLEngineProvider + } + """ + + val resourcesConfig: String = baseConfig + + s""" + akka.remote.artery.ssl.rotating-keys-engine { + key-file = ${getClass.getClassLoader.getResource(s"$arteryNode001Id.pem").getPath} + cert-file = ${getClass.getClassLoader.getResource(s"$arteryNode001Id.crt").getPath} + ca-cert-file = ${getClass.getClassLoader.getResource("ssl/exampleca.crt").getPath} + ssl-context-cache-ttl = ${cacheTtlInSeconds}s + } + """ + + val temporaryDirectory: Path = Files.createTempDirectory("akka-remote-rotating-keys-spec") + val keyLocation = new File(temporaryDirectory.toFile, "tls.key") + val certLocation = new File(temporaryDirectory.toFile, "tls.crt") + val cacertLocation = new File(temporaryDirectory.toFile, "ca.crt") + val tempFileConfig: String = baseConfig + + s""" + akka.remote.artery.ssl.rotating-keys-engine { + key-file = ${temporaryDirectory.toFile.getAbsolutePath}/tls.key + cert-file = ${temporaryDirectory.toFile.getAbsolutePath}/tls.crt + ca-cert-file = ${temporaryDirectory.toFile.getAbsolutePath}/ca.crt + ssl-context-cache-ttl = ${cacheTtlInSeconds}s + } + """ + + private def deployResource(resourceName: String, to: Path): Unit = blocking { + // manually ensuring files are deleted and copied to prevent races. + try { + val from = new File(getClass.getClassLoader.getResource(resourceName).getPath).toPath + to.toFile.getParentFile.mkdirs() + Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING) + } catch { + case NonFatal(t) => throw new RuntimeException(s"Can't copy resource [$resourceName] to [$to].", t) + } + } + def deployCaCert(): Unit = { + deployResource("ssl/exampleca.crt", cacertLocation.toPath) + } + def deployKeySet(setName: String): Unit = { + deployResource(setName + ".crt", certLocation.toPath) + deployResource(setName + ".pem", keyLocation.toPath) + } + def cleanupTemporaryDirectory(): Unit = { + temporaryDirectory.toFile.listFiles().foreach { _.delete() } + temporaryDirectory.toFile.delete() + } +} + +// Superclass to integration tests to test key rotation. Basic happy-path +// integration tests of `RotatingKeysSSLEngineProvider` as an SSLEngineProvider for Akka Remote +// are in `TlsTcpWithRotatingKeysSSLEngineSpec` +abstract class RotatingKeysSSLEngineProviderSpec(extraConfig: String) + extends ArteryMultiNodeSpec(ConfigFactory.parseString(extraConfig).withFallback(TlsTcpSpec.config)) + with ImplicitSender { + import RotatingKeysSSLEngineProviderSpec._ + + var systemsToTerminate: Seq[ActorSystem] = Nil + + // Assert the RemoteSystem created three pairs of SSLEngines (main channel, + // large messages channel and control channel) + // NOTE: the large message channel is not enabled but default. In this test suite + // it's enabled via adding a value to the `large-message-destinations` setting + def assertThreeChannelsAreCreated(remoteSystem: RemoteSystem) = { + assertEnginesCreated(remoteSystem) + assertEnginesCreated(remoteSystem) + assertEnginesCreated(remoteSystem) + } + def assertEnginesCreated(remoteSystem: RemoteSystem) = { + remoteSystem.sslProviderServerProbe.expectMsg("createServerSSLEngine") + remoteSystem.sslProviderClientProbe.expectMsg("createClientSSLEngine") + } + def assertNoEnginesCreated(remoteSystem: RemoteSystem) = { + remoteSystem.sslProviderServerProbe.expectNoMessage() + remoteSystem.sslProviderClientProbe.expectNoMessage() + } + + // sleep to force the cache in sysB's instance to expire + def awaitCacheExpiration(): Unit = { + Thread.sleep((RotatingKeysSSLEngineProviderSpec.cacheTtlInSeconds + 1) * 1000) + } + + def contact(fromSystem: ActorSystem, toPath: ActorPath): Unit = { + val senderOnSource = TestProbe()(fromSystem) + fromSystem.actorSelection(toPath).tell(Identify(toPath.name), senderOnSource.ref) + val targetRef: ActorRef = senderOnSource.expectMsgType[ActorIdentity].ref.get + targetRef.tell("ping-1", senderOnSource.ref) + senderOnSource.expectMsg("ping-1") + } + + def buildRemoteWithEchoActor(id: String): (RemoteSystem, ActorPath) = { + val remoteSysB = new RemoteSystem(s"system$id", extraConfig, newRemoteSystem, address) + systemsToTerminate :+= remoteSysB.actorSystem + val actorName = s"echo$id" + remoteSysB.actorSystem.actorOf(TestActors.echoActorProps, actorName) + val pathEchoB = remoteSysB.rootActorPath / "user" / actorName + (remoteSysB, pathEchoB) + } + + override def afterTermination(): Unit = { + systemsToTerminate.foreach { systemToTerminate => + system.log.info(s"Terminating $systemToTerminate...") + Await.result(systemToTerminate.terminate(), 10.seconds) + } + // Don't cleanup folder until all systems have terminated + cleanupTemporaryDirectory() + super.afterTermination() + } +} + +class RemoteSystem( + name: String, + configString: String, + newRemoteSystem: (Option[String], Option[String], Option[ActorSystemSetup]) => ActorSystem, + address: (ActorSystem) => Address)(implicit system: ActorSystem) { + + val sslProviderServerProbe: TestProbe = TestProbe() + val sslProviderClientProbe: TestProbe = TestProbe() + val sslContextRef = new AtomicReference[SSLContext]() + + val sslProviderSetup = + SSLEngineProviderSetup( + sys => new ProbedSSLEngineProvider(sys, sslContextRef, sslProviderServerProbe, sslProviderClientProbe)) + + val actorSystem = + newRemoteSystem(Some(configString), Some(name), Some(ActorSystemSetup(sslProviderSetup))) + val remoteAddress = address(actorSystem) + val rootActorPath = RootActorPath(remoteAddress) + +} + +class ProbedSSLEngineProvider( + sys: ExtendedActorSystem, + sslContextRef: AtomicReference[SSLContext], + sslProviderServerProbe: TestProbe, + sslProviderClientProbe: TestProbe) + extends SSLEngineProvider { + val delegate = new RotatingKeysSSLEngineProvider(sys) + + override def createServerSSLEngine(hostname: String, port: Int): SSLEngine = { + val engine = delegate.createServerSSLEngine(hostname, port) + // only report the invocation on the probe when the invocation succeeds + sslProviderServerProbe.ref ! "createServerSSLEngine" + // invoked last to let `createEngine` be the trigger of the SSL context reconstruction + sslContextRef.set(delegate.getSSLContext()) + engine + } + + override def createClientSSLEngine(hostname: String, port: Int): SSLEngine = { + val engine = delegate.createClientSSLEngine(hostname, port) + // only report the invocation on the probe when the invocation succeeds + sslProviderClientProbe.ref ! "createClientSSLEngine" + // invoked last to let `createEngine` be the trigger of the SSL context reconstruction + sslContextRef.set(delegate.getSSLContext()) + engine + } + + override def verifyClientSession(hostname: String, session: SSLSession): Option[Throwable] = + delegate.verifyClientSession(hostname, session) + override def verifyServerSession(hostname: String, session: SSLSession): Option[Throwable] = + delegate.verifyServerSession(hostname, session) +} diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/TlsResourcesSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/TlsResourcesSpec.scala new file mode 100644 index 0000000000..df7516b455 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/TlsResourcesSpec.scala @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import scala.util.Try +import scala.util.control.NonFatal + +import akka.util.ccompat.JavaConverters._ + +/** + * + */ +class TlsResourcesSpec extends AnyWordSpec with Matchers { + + import TlsResourcesSpec._ + + val baseNode = "node" + val baseClient = "client" + val baseRsaClient = "rsa-client" + val baseIslandServer = "island" + val baseServers = Set("one", "two") + val arteryNodeSet = Set("artery-nodes/artery-node001", "artery-nodes/artery-node002", "artery-nodes/artery-node003") + + "example.com certificate family" must { + "all include `example.com` as SAN" in { + val sameSan = baseServers + baseClient + baseNode + baseRsaClient + sameSan.foreach { prefix => + val serverCert = loadCert(s"/ssl/$prefix.example.com.crt") + X509Readers.getAllSubjectNames(serverCert).contains("example.com") mustBe (true) + } + } + + "not add `example.com` as SAN on the `island.example.com` certificate" in { + val notExampleSan = arteryNodeSet + baseIslandServer + notExampleSan.foreach { prefix => + val cert = loadCert(s"/ssl/$prefix.example.com.crt") + X509Readers.getAllSubjectNames(cert).contains("example.com") mustBe (false) + } + } + + val serverAuth = "1.3.6.1.5.5.7.3.1" // TLS Web server authentication + val clientAuth = "1.3.6.1.5.5.7.3.2" // TLS Web client authentication + "have certificates usable for client Auth" in { + val clients = Set(baseClient, baseNode, baseRsaClient) ++ arteryNodeSet + clients.foreach { prefix => + val cert = loadCert(s"/ssl/$prefix.example.com.crt") + cert.getExtendedKeyUsage.asScala.contains(clientAuth) mustBe (true) + } + } + + "have certificates usable for server Auth" in { + val servers = baseServers + baseIslandServer + baseNode ++ arteryNodeSet + servers.foreach { prefix => + val serverCert = loadCert(s"/ssl/$prefix.example.com.crt") + serverCert.getExtendedKeyUsage.asScala.contains(serverAuth) mustBe (true) + } + } + + "have RSA Private Keys in PEM format" in { + val nodes = arteryNodeSet + baseNode + baseRsaClient + nodes.foreach { prefix => + try { + val pk = PemManagersProvider.loadPrivateKey(toAbsolutePath(s"ssl/$prefix.example.com.pem")) + pk.getAlgorithm must be("RSA") + } catch { + case NonFatal(t) => fail(s"Failed test for $prefix", t) + } + } + } + + } + +} + +object TlsResourcesSpec { + + def toAbsolutePath(resourceName: String): String = + getClass.getClassLoader.getResource(resourceName).getPath + + private val certFactory = CertificateFactory.getInstance("X.509") + def loadCert(resourceName: String): X509Certificate = { + val fin = classOf[X509ReadersSpec].getResourceAsStream(resourceName) + try certFactory.generateCertificate(fin).asInstanceOf[X509Certificate] + finally Try(fin.close()) + } +} diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/X509ReadersSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/X509ReadersSpec.scala new file mode 100644 index 0000000000..20e1cc4a25 --- /dev/null +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/ssl/X509ReadersSpec.scala @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.remote.artery.tcp.ssl + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +/** + * + */ +class X509ReadersSpec extends AnyWordSpec with Matchers { + import TlsResourcesSpec._ + + "X509Readers" must { + "read a certificate's name from the CN" in { + val island = loadCert("/ssl/island.example.com.crt") + X509Readers.getAllSubjectNames(island) mustBe (Set("island.example.com")) + } + + "read both the CN and the subject alternative names" in { + val serverCert = loadCert("/domain.crt") + X509Readers.getAllSubjectNames(serverCert) mustBe (Set("akka-remote", "localhost")) + } + + "read a certificate that has no SAN extension" in { + // a self-signed CA without SAN + val island = loadCert("/ssl/pem/selfsigned-certificate.pem") + X509Readers.getAllSubjectNames(island) mustBe (Set("0d207b68-9a20-4ee8-92cb-bf9699581cf8")) + } + + } + +} diff --git a/build.sbt b/build.sbt index df345dbc1f..638cb65e30 100644 --- a/build.sbt +++ b/build.sbt @@ -336,6 +336,7 @@ lazy val remote = .dependsOn( actor, stream, + pki, protobuf % "test", actorTests % "test->test", testkit % "test->test", diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 6c7aa30173..d1f684c0d8 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -83,7 +83,7 @@ object Dependencies { // Added explicitly for when artery tcp is used val agrona = "org.agrona" % "agrona" % agronaVersion // ApacheV2 - val asnOne = "com.hierynomus" % "asn-one" % "0.4.0" // ApacheV2 + val asnOne = ("com.hierynomus" % "asn-one" % "0.4.0").exclude("org.slf4j", "slf4j-api") // ApacheV2 val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % jacksonVersion // ApacheV2 val jacksonAnnotations = "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion // ApacheV2 @@ -202,7 +202,7 @@ object Dependencies { Seq( asnOne, // pull up slf4j version from the one provided transitively in asnOne to fix unidoc - Compile.slf4jApi % "provided", + Compile.slf4jApi, Test.scalatest) val remoteDependencies = Seq(netty, aeronDriver, aeronClient)