This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new bf8adfb50e feat: add optional TLS hostname verification for classic 
remoting (#3164)
bf8adfb50e is described below

commit bf8adfb50e4cc4fb94ef60082df0a45efcad6973
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sat Jun 27 10:06:51 2026 +0800

    feat: add optional TLS hostname verification for classic remoting (#3164)
    
    * feat: add optional TLS hostname verification for classic remoting
    
    Motivation:
    Classic remoting's ConfigSSLEngineProvider never called
    setEndpointIdentificationAlgorithm, meaning TLS certificate hostname
    verification was not performed. An attacker with any certificate signed
    by a trusted CA could perform MITM attacks. Artery already supports
    hostname verification via a configurable option.
    
    Modification:
    - SSLSettings: add hostname-verification config option (default off)
    - SSLEngineProvider trait: add createClientSSLEngine(hostname, port)
      default method for backward compatibility
    - ConfigSSLEngineProvider: implement hostname-aware engine creation with
      setEndpointIdentificationAlgorithm("HTTPS") when enabled
    - NettyTransport: pass remote address hostname to SSL handler on client
      side when hostname verification is enabled
    - NettySSLSupport: accept optional hostname/port parameters
    
    Result:
    Classic remoting can now optionally verify TLS certificate hostnames,
    matching Artery's behavior. Disabled by default for backward
    compatibility. Enable with hostname-verification = on in SSL security
    config.
    
    Tests:
    - remote / Test / testOnly org.apache.pekko.remote.Ticket1978ConfigSpec
    
    References:
    Fixes #3161
    
    * fix: address review - add hostname-verification to reference.conf and 
security warning log
    
    - Add hostname-verification config entry with documentation in
      reference.conf for discoverability
    - Add security info log when hostname verification is disabled,
      matching Artery's behavior
---
 remote/src/main/resources/reference.conf           |  5 +++
 .../remote/transport/netty/NettySSLSupport.scala   | 18 +++++++--
 .../remote/transport/netty/NettyTransport.scala    | 13 ++++--
 .../remote/transport/netty/SSLEngineProvider.scala | 46 ++++++++++++++++++----
 .../apache/pekko/remote/Ticket1978ConfigSpec.scala | 10 +++++
 5 files changed, 79 insertions(+), 13 deletions(-)

diff --git a/remote/src/main/resources/reference.conf 
b/remote/src/main/resources/reference.conf
index ab71456d3c..c1ecc59fc0 100644
--- a/remote/src/main/resources/reference.conf
+++ b/remote/src/main/resources/reference.conf
@@ -733,6 +733,11 @@ pekko {
           #
           # To prevent man-in-the-middle attacks this setting is enabled by 
default.
           require-mutual-authentication = on
+
+          # Set this to `on` to verify the hostname in the server's TLS 
certificate
+          # against the hostname used to connect. When `off`, any certificate 
signed
+          # by a trusted CA will be accepted, which is vulnerable to MITM 
attacks.
+          hostname-verification = off
         }
       }
 
diff --git 
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
 
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
index 06d723655e..2b6cc31cc2 100644
--- 
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
+++ 
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
@@ -49,6 +49,9 @@ private[pekko] class SSLSettings(config: Config) {
 
   val SSLRequireMutualAuthentication = 
getBoolean("require-mutual-authentication")
 
+  val SSLHostnameVerification: Boolean =
+    if (config.hasPath("hostname-verification")) 
config.getBoolean("hostname-verification")
+    else false
 }
 
 /**
@@ -63,10 +66,19 @@ private[pekko] object NettySSLSupport {
   /**
    * Construct a SSLHandler which can be inserted into a Netty server/client 
pipeline
    */
-  def apply(sslEngineProvider: SSLEngineProvider, isClient: Boolean, log: 
MarkerLoggingAdapter): SslHandler = {
+  def apply(
+      sslEngineProvider: SSLEngineProvider,
+      isClient: Boolean,
+      log: MarkerLoggingAdapter,
+      hostname: Option[String] = None,
+      port: Int = -1): SslHandler = {
     val sslEngine =
-      if (isClient) sslEngineProvider.createClientSSLEngine()
-      else sslEngineProvider.createServerSSLEngine()
+      if (isClient) {
+        hostname match {
+          case Some(h) => sslEngineProvider.createClientSSLEngine(h, port)
+          case None    => sslEngineProvider.createClientSSLEngine()
+        }
+      } else sslEngineProvider.createServerSSLEngine()
     val handler = new SslHandler(sslEngine)
     handler.handshakeFuture().addListener((future: Future[Channel]) => {
       if (!future.isSuccess) {
diff --git 
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
 
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
index bba302ce03..28005094a9 100644
--- 
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
+++ 
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
@@ -420,10 +420,17 @@ class NettyTransport(val settings: 
NettyTransportSettings, val system: ExtendedA
         .get)
     } else OptionVal.None
 
-  private def sslHandler(isClient: Boolean): SslHandler = {
+  private def sslHandler(isClient: Boolean, remoteAddress: Option[Address] = 
None): SslHandler = {
     sslEngineProvider match {
       case OptionVal.Some(sslProvider) =>
-        NettySSLSupport(sslProvider, isClient, log)
+        val hostnameVerification = 
SslSettings.exists(_.SSLHostnameVerification)
+        if (isClient && hostnameVerification && remoteAddress.isDefined) {
+          val addr = remoteAddress.get
+          val port = addr.port.getOrElse(-1)
+          NettySSLSupport(sslProvider, isClient, log, addr.host, port)
+        } else {
+          NettySSLSupport(sslProvider, isClient, log)
+        }
       case _ =>
         throw new IllegalStateException("Expected enable-ssl=on")
     }
@@ -440,7 +447,7 @@ class NettyTransport(val settings: NettyTransportSettings, 
val system: ExtendedA
   private def clientPipelineInitializer(remoteAddress: Address): 
ChannelInitializer[SocketChannel] =
     (ch: SocketChannel) => {
       val pipeline = newPipeline(ch)
-      if (EnableSsl) pipeline.addFirst("SslHandler", sslHandler(isClient = 
true))
+      if (EnableSsl) pipeline.addFirst("SslHandler", sslHandler(isClient = 
true, Some(remoteAddress)))
       val handler = new TcpClientHandler(NettyTransport.this, remoteAddress, 
log)
       pipeline.addLast("clienthandler", handler)
     }
diff --git 
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
 
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
index 88619cb8fa..39a7bd3752 100644
--- 
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
+++ 
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
@@ -27,10 +27,12 @@ import javax.net.ssl.SSLEngine
 import javax.net.ssl.TrustManager
 import javax.net.ssl.TrustManagerFactory
 
+import scala.annotation.nowarn
 import scala.util.Try
 
 import org.apache.pekko
 import pekko.actor.ActorSystem
+import pekko.event.LogMarker
 import pekko.event.Logging
 import pekko.event.MarkerLoggingAdapter
 import pekko.remote.RemoteTransportException
@@ -44,6 +46,13 @@ trait SSLEngineProvider {
 
   def createClientSSLEngine(): SSLEngine
 
+  /**
+   * Create a client SSLEngine with the target hostname and port for hostname 
verification.
+   * Default implementation delegates to [[createClientSSLEngine()]].
+   */
+  @nowarn("msg=never used")
+  def createClientSSLEngine(hostname: String, port: Int): SSLEngine = 
createClientSSLEngine()
+
 }
 
 /**
@@ -62,7 +71,15 @@ class ConfigSSLEngineProvider(protected val log: 
MarkerLoggingAdapter, private v
 
   import settings._
 
-  private val sslContext: SSLContext = {
+  if (SSLHostnameVerification)
+    log.debug("TLS/SSL hostname verification is enabled.")
+  else
+    log.info(
+      LogMarker.Security,
+      "TLS/SSL hostname verification is disabled. " +
+      "Set pekko.remote.classic.netty.ssl.security.hostname-verification=on to 
enable.")
+
+  private lazy val sslContext: SSLContext = {
     try {
       val rng = createSecureRandom()
       val ctx = SSLContext.getInstance(SSLProtocol)
@@ -115,18 +132,33 @@ class ConfigSSLEngineProvider(protected val log: 
MarkerLoggingAdapter, private v
     SecureRandomFactory.createSecureRandom(SSLRandomNumberGenerator, log)
 
   override def createServerSSLEngine(): SSLEngine =
-    createSSLEngine(pekko.stream.Server)
+    createSSLEngine(pekko.stream.Server, None)
 
   override def createClientSSLEngine(): SSLEngine =
-    createSSLEngine(pekko.stream.Client)
+    createSSLEngine(pekko.stream.Client, None)
 
-  private def createSSLEngine(role: TLSRole): SSLEngine = {
-    createSSLEngine(sslContext, role)
+  override def createClientSSLEngine(hostname: String, port: Int): SSLEngine =
+    createSSLEngine(pekko.stream.Client, Some((hostname, port)))
+
+  private def createSSLEngine(role: TLSRole, peerHost: Option[(String, Int)]): 
SSLEngine = {
+    createSSLEngine(sslContext, role, peerHost)
   }
 
-  private def createSSLEngine(sslContext: SSLContext, role: TLSRole): 
SSLEngine = {
+  private def createSSLEngine(
+      sslContext: SSLContext,
+      role: TLSRole,
+      peerHost: Option[(String, Int)]): SSLEngine = {
+
+    val engine = peerHost match {
+      case Some((hostname, port)) => sslContext.createSSLEngine(hostname, port)
+      case None                   => sslContext.createSSLEngine()
+    }
 
-    val engine = sslContext.createSSLEngine()
+    if (SSLHostnameVerification && role == pekko.stream.Client) {
+      val sslParams = sslContext.getDefaultSSLParameters
+      sslParams.setEndpointIdentificationAlgorithm("HTTPS")
+      engine.setSSLParameters(sslParams)
+    }
 
     engine.setUseClientMode(role == pekko.stream.Client)
     engine.setEnabledCipherSuites(SSLEnabledAlgorithms.toArray)
diff --git 
a/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala 
b/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala
index 2fc434d6c2..b7cb9934b4 100644
--- a/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala
+++ b/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala
@@ -36,6 +36,16 @@ class Ticket1978ConfigSpec extends PekkoSpec("""
       settings.SSLEnabledAlgorithms should ===(
         Set("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"))
       settings.SSLRandomNumberGenerator should ===("SecureRandom")
+      settings.SSLHostnameVerification should ===(false)
+    }
+
+    "support hostname-verification setting" in {
+      val settings = new SSLSettings(
+        com.typesafe.config.ConfigFactory
+          .parseString("hostname-verification = on")
+          
.withFallback(system.settings.config.getConfig("pekko.remote.classic.netty.ssl.security")))
+
+      settings.SSLHostnameVerification should ===(true)
     }
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to