From 5ced345128dde870c1087f7a398608e860072238 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 28 Sep 2026 13:19:19 +0000
Subject: [PATCH] [#1111] Refuse an HTTPS configuration without a key the handler can present, and leave a running HTTP handler as it is when one is applied (#1114)

---
 opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java |  300 +++++++++++++++++++++++++++++++++++++
 opendj-server-legacy/src/messages/org/opends/messages/protocol.properties                                  |    4 
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java             |  127 +++++++++++----
 3 files changed, 388 insertions(+), 43 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
index 2b118d5..9f94105 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -24,6 +24,7 @@
 
 import java.io.IOException;
 import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -211,9 +212,10 @@
     }
 
     // Reconfigure SSL if needed.
+    final List<LocalizableMessage> noKeyReasons;
     try
     {
-      configureSSL(config);
+      noKeyReasons = configureSSL(config);
     }
     catch (DirectoryException e)
     {
@@ -222,6 +224,20 @@
       ccr.addMessage(e.getMessageObject());
       return ccr;
     }
+    if (!noKeyReasons.isEmpty())
+    {
+      // The check refuses such a configuration, but the key store may have changed since.
+      // The SSL settings take effect only when the handler restarts, and it will not start with these.
+      for (LocalizableMessage reason : noKeyReasons)
+      {
+        logger.error(reason);
+        ccr.addMessage(reason);
+      }
+      final LocalizableMessage warning = WARN_CONNHANDLER_NO_KEY_UNTIL_RESTART.get(friendlyName);
+      logger.warn(warning);
+      ccr.addMessage(warning);
+      ccr.setAdminActionRequired(true);
+    }
 
     if (config.isEnabled() && this.currentConfig.isEnabled() && isListening())
     {
@@ -242,7 +258,9 @@
 
     this.initConfig = config;
     this.currentConfig = config;
-    this.enabled = this.currentConfig.isEnabled();
+    // Without a key it can present, a running handler keeps serving with the SSL settings it started with,
+    // and a handler that is down does not start.
+    this.enabled = config.isEnabled() && (noKeyReasons.isEmpty() || isListening());
 
     return ccr;
   }
@@ -277,18 +295,22 @@
     return b1 == b2;
   }
 
-  private void configureSSL(HTTPConnectionHandlerCfg config)
+  /**
+   * Sets the SSL engine configurator that the handler starts its HTTP server with.
+   *
+   * @param config
+   *          the configuration to take the SSL settings from
+   * @return why the configuration leaves the handler without a key it can present, empty when it has one
+   * @throws DirectoryException
+   *           if the SSL context cannot be created
+   */
+  private List<LocalizableMessage> configureSSL(HTTPConnectionHandlerCfg config)
       throws DirectoryException
   {
     protocol = config.isUseSSL() ? "HTTPS" : "HTTP";
-    if (config.isUseSSL())
-    {
-      sslEngineConfigurator = createSSLEngineConfigurator(config, true);
-    }
-    else
-    {
-      sslEngineConfigurator = null;
-    }
+    final List<LocalizableMessage> noKeyReasons = new ArrayList<>();
+    sslEngineConfigurator = createSSLEngineConfigurator(config, noKeyReasons);
+    return noKeyReasons;
   }
 
   @Override
@@ -435,16 +457,26 @@
     handlerName = getHandlerName(config);
 
     // Configure SSL if needed.
+    final List<LocalizableMessage> noKeyReasons;
     try
     {
-      // This call may disable the connector if wrong SSL settings
-      configureSSL(config);
+      noKeyReasons = configureSSL(config);
     }
     catch (DirectoryException e)
     {
       logger.traceException(e);
       throw new InitializationException(e.getMessageObject());
     }
+    if (!noKeyReasons.isEmpty())
+    {
+      // A handler without a key it can present does not start.
+      for (LocalizableMessage reason : noKeyReasons)
+      {
+        logger.error(reason);
+      }
+      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
+      this.enabled = false;
+    }
 
     // Create and register monitors.
     statTracker = new HTTPStatistics(handlerName + " Statistics");
@@ -495,9 +527,10 @@
 
     if (config.isEnabled() && config.isUseSSL())
     {
+      final List<LocalizableMessage> noKeyReasons = new ArrayList<>();
       try
       {
-        createSSLEngineConfigurator(config, false);
+        createSSLEngineConfigurator(config, noKeyReasons);
       }
       catch (DirectoryException e)
       {
@@ -505,6 +538,12 @@
         unacceptableReasons.add(e.getMessageObject());
         return false;
       }
+      if (!noKeyReasons.isEmpty())
+      {
+        // The SSL settings take effect only when the handler restarts, and it would not start with these.
+        unacceptableReasons.addAll(noKeyReasons);
+        return false;
+      }
     }
 
     return true;
@@ -822,17 +861,15 @@
    *
    * @param config
    *          the configuration to create the SSL engine configurator for
-   * @param forUse
-   *          {@code true} when the handler is going to use the configurator: at its start a handler
-   *          without a usable key is disabled ({@link #applyConfigurationChange} sets {@code enabled}
-   *          from the configuration afterwards); {@code false} when the configurator only checks a
-   *          proposed configuration, which must leave the running handler as it is
+   * @param noKeyReasons
+   *          receives why the configuration leaves the handler without a key it can present; the
+   *          caller decides what that means for the handler, which this method leaves as it is
    * @return the SSL engine configurator, or {@code null} if the configuration does not use SSL
    * @throws DirectoryException
    *           if the SSL context cannot be created
    */
-  private SSLEngineConfigurator createSSLEngineConfigurator(HTTPConnectionHandlerCfg config, boolean forUse)
-      throws DirectoryException
+  private SSLEngineConfigurator createSSLEngineConfigurator(HTTPConnectionHandlerCfg config,
+      List<LocalizableMessage> noKeyReasons) throws DirectoryException
   {
     if (!config.isUseSSL())
     {
@@ -841,7 +878,7 @@
 
     try
     {
-      SSLContext sslContext = createSSLContext(config, forUse);
+      SSLContext sslContext = createSSLContext(config, noKeyReasons);
       SSLEngineConfigurator configurator = new SSLEngineConfigurator(sslContext);
       configurator.setClientMode(false);
 
@@ -889,16 +926,20 @@
     }
   }
 
-  private void disableAndWarn(boolean forUse)
-  {
-    if (forUse)
-    {
-      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
-      enabled = false;
-    }
-  }
-
-  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config, boolean forUse) throws Exception
+  /**
+   * Creates the SSL context for the provided configuration. The configuration is named by its own name
+   * rather than by {@code friendlyName}, which is not set yet when the check runs on a new instance.
+   *
+   * @param config
+   *          the configuration to create the SSL context for
+   * @param noKeyReasons
+   *          receives why the configuration leaves the handler without a key it can present
+   * @return the SSL context, or {@code null} if the configuration does not use SSL
+   * @throws Exception
+   *           if the SSL context cannot be created
+   */
+  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config, List<LocalizableMessage> noKeyReasons)
+      throws Exception
   {
     if (!config.isUseSSL())
     {
@@ -909,14 +950,12 @@
     KeyManagerProvider<?> keyManagerProvider = serverContext.getKeyManagerProvider(keyMgrDN);
     if (keyManagerProvider == null)
     {
-      logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName);
+      noKeyReasons.add(ERR_NULL_KEY_PROVIDER_MANAGER.get(keyMgrDN, config.name()));
       keyManagerProvider = new NullKeyManagerProvider();
-      disableAndWarn(forUse);
     }
     else if (!keyManagerProvider.containsAtLeastOneKey())
     {
-      logger.error(ERR_INVALID_KEYSTORE, friendlyName);
-      disableAndWarn(forUse);
+      noKeyReasons.add(ERR_INVALID_KEYSTORE.get(config.name()));
     }
 
     final SortedSet<String> aliases = new TreeSet<>(config.getSSLCertNickname());
@@ -927,18 +966,28 @@
     }
     else
     {
+      final List<LocalizableMessage> missingAliases = new ArrayList<>();
       final Iterator<String> it = aliases.iterator();
       while (it.hasNext())
       {
-        if (!keyManagerProvider.containsKeyWithAlias(it.next()))
+        final String alias = it.next();
+        if (!keyManagerProvider.containsKeyWithAlias(alias))
         {
-          logger.error(ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS, aliases, friendlyName);
+          missingAliases.add(ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS.get(alias, config.name()));
           it.remove();
         }
       }
       if (aliases.isEmpty())
       {
-        disableAndWarn(forUse);
+        noKeyReasons.addAll(missingAliases);
+      }
+      else
+      {
+        // One of the other aliases is there, so the handler still has a key it can present.
+        for (LocalizableMessage missingAlias : missingAliases)
+        {
+          logger.error(missingAlias);
+        }
       }
       keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), aliases, friendlyName);
     }
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties b/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
index 60dccc8..28f7d03 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
@@ -830,6 +830,10 @@
   Verify that the keystore is properly configured
 ERR_INVALID_KEYSTORE_1527=No usable key was found for '%s'. Verify the keystore content
 INFO_DISABLE_CONNECTION_1528=Disabling %s
+WARN_CONNHANDLER_NO_KEY_UNTIL_RESTART_1541=The configuration applied to '%s' \
+ leaves it without a key it can present. Its SSL settings take effect only when \
+ it restarts, and it will not start with this configuration. Fix the keystore \
+ or the configuration before it is restarted
 ERR_INITIALIZE_HTTP_CONNECTION_HANDLER_1529=Failed to initialize Http Connection Handler
 ERR_TRANSACTION_ID_CONTROL_HAS_NO_VALUE_1530=No value was provided for the transaction \
  id control, whereas an UTF-8 encoded value is expected
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java
index 22ce5c3..2c9912b 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java
@@ -15,17 +15,19 @@
  */
 package org.opends.server.protocols;
 
-import static org.opends.messages.ProtocolMessages.ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE;
+import static org.opends.messages.ProtocolMessages.*;
 import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
 import static org.opends.server.util.StaticUtils.*;
 import static org.testng.Assert.*;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.net.ConnectException;
 import java.net.Socket;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
+import java.security.KeyStore;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -68,7 +70,9 @@
 /**
  * A change to an SSL connection handler that is rejected because its key store cannot be loaded
  * must leave the running handler as it was: listening, with the SSL context it had. The start of a handler, and a
- * change applied to an LDAP handler, still disable it when it has no key it can present.
+ * change applied to an LDAP handler, still disable it when it has no key it can present. The SSL settings of the HTTP
+ * handler take effect only when it restarts, so its check refuses a configuration without such a key, and a change
+ * applied without one leaves the handler as it is.
  */
 @SuppressWarnings("javadoc")
 @Test(groups = { "precommit" }, sequential = true)
@@ -81,12 +85,25 @@
   private static final long KEEPS_SERVING_MS = TimeUnit.SECONDS.toMillis(3);
   /** How long a handler that has been disabled may take to stop listening: its thread checks every second. */
   private static final long STOPS_LISTENING_MS = TimeUnit.SECONDS.toMillis(10);
+  /** How long a handler that has been enabled may take to start listening: its thread checks every second. */
+  private static final long STARTS_LISTENING_MS = TimeUnit.SECONDS.toMillis(10);
 
   private enum Kind
   {
     LDAP2, LDAP_LEGACY, HTTP
   }
 
+  /** Why a configuration leaves the handler without a key it can present. */
+  private enum MissingKey
+  {
+    /** The key store holds no key with the configured {@code ssl-cert-nickname}. */
+    ALIAS,
+    /** The key store can be loaded, but holds no key. */
+    EMPTY_KEY_STORE,
+    /** No key manager provider is registered under the configured DN. */
+    NO_PROVIDER
+  }
+
   private static final class NoChanges<C extends Configuration> implements ConfigurationChangeListener<C>
   {
     @Override
@@ -253,7 +270,8 @@
    * An applied change that leaves an LDAP handler without the certificate it is configured to present still disables
    * it. The check accepts such a change, so dsconfig reaches this road. Only {@link LDAPConnectionHandler2} is
    * covered: the legacy handler keeps accepting TCP connections while it is disabled, so a refused connect cannot
-   * tell, and the HTTP handler sets {@code enabled} from the configuration after the SSL context is built.
+   * tell, and the SSL settings of the HTTP handler take effect only when it restarts
+   * ({@link #httpAppliedChangeWithoutAKeyKeepsServingUntilTheRestart}).
    */
   @SuppressWarnings("unchecked")
   @Test
@@ -292,6 +310,276 @@
     }
   }
 
+  @DataProvider
+  public Object[][] missingKeys()
+  {
+    return new Object[][] {
+      { MissingKey.ALIAS, true }, { MissingKey.ALIAS, false },
+      { MissingKey.EMPTY_KEY_STORE, true }, { MissingKey.EMPTY_KEY_STORE, false },
+      { MissingKey.NO_PROVIDER, true }, { MissingKey.NO_PROVIDER, false },
+    };
+  }
+
+  /**
+   * The SSL settings of the HTTP handler take effect only when it restarts, and a handler without a key it can present
+   * does not start. So its check refuses a configuration without such a key, with the reason, both as a change to a
+   * running handler and as a handler being added or enabled, which is checked on a new instance.
+   */
+  @Test(dataProvider = "missingKeys")
+  public void httpCheckRefusesAConfigurationWithoutAKey(MissingKey missing, boolean running) throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final HTTPConnectionHandler handler = running
+        ? (HTTPConnectionHandler) start(Kind.HTTP, configuration(Kind.HTTP, port, null, "5 megabytes", true))
+        : new HTTPConnectionHandler();
+    try
+    {
+      if (running)
+      {
+        assertServesTLS(port);
+      }
+
+      final ConnectionHandlerCfg change = configuration(
+          Kind.HTTP, port, missing == MissingKey.ALIAS ? "no-such-cert" : null, "6 megabytes", true);
+      final List<LocalizableMessage> reasons = new ArrayList<>();
+      final boolean accepted;
+      switch (missing)
+      {
+      case EMPTY_KEY_STORE:
+        writeEmptyKeyStore();
+        accepted = handler.isConfigurationAcceptable(change, reasons);
+        break;
+      case NO_PROVIDER:
+        final KeyManagerProvider<?> provider = DirectoryServer.getKeyManagerProvider(KEY_MANAGER_DN);
+        assertNotNull(provider, "the key manager provider of the test is not registered");
+        DirectoryServer.deregisterKeyManagerProvider(KEY_MANAGER_DN);
+        try
+        {
+          accepted = handler.isConfigurationAcceptable(change, reasons);
+        }
+        finally
+        {
+          DirectoryServer.registerKeyManagerProvider(KEY_MANAGER_DN, provider);
+        }
+        break;
+      default:
+        accepted = handler.isConfigurationAcceptable(change, reasons);
+        break;
+      }
+
+      assertFalse(accepted, "a configuration without a key the handler can present was accepted");
+      assertEquals(reasons.size(), 1, String.valueOf(reasons));
+      final int expected;
+      switch (missing)
+      {
+      case ALIAS:
+        expected = ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS.ordinal();
+        break;
+      case EMPTY_KEY_STORE:
+        expected = ERR_INVALID_KEYSTORE.ordinal();
+        break;
+      default:
+        expected = ERR_NULL_KEY_PROVIDER_MANAGER.ordinal();
+        break;
+      }
+      assertEquals(reasons.get(0).ordinal(), expected, String.valueOf(reasons));
+      assertTrue(reasons.get(0).toString().contains("'Rejected Change Handler'"),
+          "the reason does not name the handler: " + reasons);
+
+      if (running)
+      {
+        assertKeepsServingTLS(port);
+      }
+    }
+    finally
+    {
+      if (running)
+      {
+        stop(handler);
+      }
+    }
+  }
+
+  @DataProvider
+  public Object[][] appliedMissingKeys()
+  {
+    return new Object[][] { { MissingKey.ALIAS }, { MissingKey.EMPTY_KEY_STORE } };
+  }
+
+  /**
+   * A configuration of the HTTP handler that names several certificates, only some of which are in the key store,
+   * still leaves it a key it can present, so the check accepts it.
+   */
+  @Test
+  public void httpCheckAcceptsAConfigurationWithOneOfItsCertificates() throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final HTTPConnectionHandler handler =
+        (HTTPConnectionHandler) start(Kind.HTTP, configuration(Kind.HTTP, port, null, "5 megabytes", true));
+    try
+    {
+      final List<LocalizableMessage> reasons = new ArrayList<>();
+      assertTrue(handler.isConfigurationAcceptable(
+          configuration(Kind.HTTP, port, "no-such-cert,server-cert", "5 megabytes", true), reasons),
+          String.valueOf(reasons));
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  /**
+   * A change applied to a running HTTP handler that leaves it without a key it can present does not stop it: its SSL
+   * settings take effect only when it restarts. The check refuses such a change, but the key store can change between
+   * the check and the apply. The handler keeps serving with the key it has, does not log that it is disabled, and the
+   * result asks for administrative action and warns that it will not start with this configuration. With
+   * {@link MissingKey#EMPTY_KEY_STORE} the applied configuration is the one the handler runs with, so nothing but the
+   * missing key asks for that action.
+   */
+  @Test(dataProvider = "appliedMissingKeys")
+  public void httpAppliedChangeWithoutAKeyKeepsServingUntilTheRestart(MissingKey missing) throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final HTTPConnectionHandler handler =
+        (HTTPConnectionHandler) start(Kind.HTTP, configuration(Kind.HTTP, port, null, "5 megabytes", true));
+    try
+    {
+      assertServesTLS(port);
+
+      if (missing == MissingKey.EMPTY_KEY_STORE)
+      {
+        writeEmptyKeyStore();
+      }
+      TestCaseUtils.ERROR_TEXT_WRITER.clear();
+      final ConfigChangeResult result = handler.applyConfigurationChange((HTTPConnectionHandlerCfg) configuration(
+          Kind.HTTP, port, missing == MissingKey.ALIAS ? "no-such-cert" : null, "5 megabytes", true));
+
+      assertEquals(result.getResultCode(), ResultCode.SUCCESS, String.valueOf(result.getMessages()));
+      assertTrue(result.adminActionRequired(), String.valueOf(result.getMessages()));
+      assertTrue(containsOrdinal(result.getMessages(), WARN_CONNHANDLER_NO_KEY_UNTIL_RESTART.ordinal()),
+          "the result does not warn that the handler will not start: " + result.getMessages());
+      final int reason = missing == MissingKey.ALIAS
+          ? ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS.ordinal() : ERR_INVALID_KEYSTORE.ordinal();
+      assertTrue(containsOrdinal(result.getMessages(), reason),
+          "the result does not say why the key is missing: " + result.getMessages());
+      assertKeepsServingTLS(port);
+      final String disabling = "msgID=" + INFO_DISABLE_CONNECTION.ordinal() + " ";
+      for (String logged : TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
+      {
+        assertFalse(logged.contains(disabling), "logged although the handler keeps serving: " + logged);
+      }
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  /**
+   * An HTTP handler that its start disabled for want of a key stays down when a change that still leaves it without one
+   * is applied: it does not start listening without a key it can present.
+   */
+  @Test
+  public void httpHandlerDisabledAtItsStartStaysDownOnAChangeStillWithoutAKey() throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final HTTPConnectionHandler handler =
+        (HTTPConnectionHandler) start(Kind.HTTP, configuration(Kind.HTTP, port, "no-such-cert", "5 megabytes", true));
+    try
+    {
+      assertDoesNotListen(port);
+
+      handler.applyConfigurationChange(
+          (HTTPConnectionHandlerCfg) configuration(Kind.HTTP, port, "no-such-cert", "6 megabytes", true));
+
+      final long deadline = System.currentTimeMillis() + KEEPS_SERVING_MS;
+      do
+      {
+        assertDoesNotListen(port);
+        Thread.sleep(250);
+      }
+      while (System.currentTimeMillis() < deadline);
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  /** An HTTP handler that its start disabled for want of a key starts once a change that gives it one is applied. */
+  @Test
+  public void httpHandlerDisabledAtItsStartStartsOnAChangeThatGivesItAKey() throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final HTTPConnectionHandler handler =
+        (HTTPConnectionHandler) start(Kind.HTTP, configuration(Kind.HTTP, port, "no-such-cert", "5 megabytes", true));
+    try
+    {
+      assertDoesNotListen(port);
+
+      final HTTPConnectionHandlerCfg change =
+          (HTTPConnectionHandlerCfg) configuration(Kind.HTTP, port, "server-cert", "5 megabytes", true);
+      final List<LocalizableMessage> reasons = new ArrayList<>();
+      assertTrue(handler.isConfigurationAcceptable(change, reasons), String.valueOf(reasons));
+      handler.applyConfigurationChange(change);
+
+      final long deadline = System.currentTimeMillis() + STARTS_LISTENING_MS;
+      while (true)
+      {
+        try (Socket socket = new Socket("127.0.0.1", port))
+        {
+          break;
+        }
+        catch (ConnectException e)
+        {
+          assertTrue(System.currentTimeMillis() < deadline,
+              "the handler does not listen on port " + port + " after a change gave it its certificate");
+        }
+        Thread.sleep(250);
+      }
+      assertServesTLS(port);
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  private static boolean containsOrdinal(List<LocalizableMessage> messages, int ordinal)
+  {
+    for (LocalizableMessage message : messages)
+    {
+      if (message.ordinal() == ordinal)
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void writeEmptyKeyStore() throws Exception
+  {
+    final KeyStore empty = KeyStore.getInstance("JKS");
+    empty.load(null, null);
+    try (OutputStream out = Files.newOutputStream(keyStore.toPath()))
+    {
+      empty.store(out, "password".toCharArray());
+    }
+  }
+
+  private static void assertDoesNotListen(int port) throws IOException
+  {
+    try (Socket socket = new Socket("127.0.0.1", port))
+    {
+      fail("the handler listens on port " + port + " without a key it can present");
+    }
+    catch (ConnectException expected)
+    {
+      // not listening
+    }
+  }
+
   private static ConnectionHandlerCfg configuration(Kind kind, int port, String certNickname, String maxRequestSize,
       boolean useSSL) throws Exception
   {
@@ -317,7 +605,11 @@
     }
     if (certNickname != null)
     {
-      lines.add("ds-cfg-ssl-cert-nickname: " + certNickname);
+      // several nicknames are separated by commas
+      for (String nickname : certNickname.split(","))
+      {
+        lines.add("ds-cfg-ssl-cert-nickname: " + nickname);
+      }
     }
     if (kind == Kind.HTTP)
     {

--
Gitblit v1.10.0