From 1414f8993a616a8dc2d5430e3a646e3075b87b57 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 03 Sep 2026 08:07:08 +0000
Subject: [PATCH] [#905] Warn when a replication handshake fails and document CA-signed certificates (#906)
---
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java | 85 ++++++++++++++++++++++++++++++++++++++++++
1 files changed, 84 insertions(+), 1 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java b/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
index 9d87dfe..84c3896 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
@@ -43,6 +43,8 @@
import java.util.SortedSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -188,6 +190,17 @@
*/
private static final int CIPHERTEXT_PROLOGUE_VERSION = 1 ;
+ /**
+ * Minimum interval between two errors about the same certificate nickname missing from
+ * the trust store. A new SSL context is built for every connection attempt and a server
+ * which cannot present its certificate reconnects every 500 ms, so the error cannot be
+ * logged on every attempt; it cannot be logged once and never again either, as the error
+ * log is rotated while the misconfiguration outlives it.
+ * <p>
+ * Package private for testing.
+ */
+ static final long CERT_NICKNAME_CHECK_INTERVAL_NANOS = TimeUnit.MINUTES.toNanos(5);
+
private final CipherKeyManager cipherCryptoManager = new CipherKeyManager();
private final MacKeyManager macCryptoManager = new MacKeyManager();
@@ -217,6 +230,11 @@
/** The names of the local certificates to use for SSL. */
private final SortedSet<String> sslCertNicknames;
+ /**
+ * Value of {@link System#nanoTime()} at which each certificate nickname was last looked
+ * up in the trust store, keyed by "component:nickname".
+ */
+ private final ConcurrentMap<String, Long> certNicknameChecks = new ConcurrentHashMap<>();
/** Whether replication sessions use SSL encryption. */
private final boolean sslEncryption;
/** The set of SSL protocols enabled or null for the default set. */
@@ -2679,12 +2697,15 @@
TrustManager[] trustManagers = trustStoreBackend.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
- if (sslCertNicknames == null)
+ if (sslCertNicknames == null || sslCertNicknames.isEmpty())
{
+ // No nickname is configured: let the key manager choose, as wrapping it with an
+ // empty set of aliases would present no certificate at all.
sslContext.init(keyManagers, trustManagers, null);
}
else
{
+ logMissingCertNicknames(componentName, sslCertNicknames, trustStoreBackend);
KeyManager[] extendedKeyManagers =
SelectableCertificateKeyManager.wrap(keyManagers, sslCertNicknames, componentName);
sslContext.init(extendedKeyManagers, trustManagers, null);
@@ -2702,6 +2723,68 @@
}
}
+ /**
+ * Logs an error for each configured certificate nickname which the trust store
+ * does not hold, so that a misconfigured nickname is reported for what it is
+ * instead of only showing up as a failed handshake. A new SSL context is built
+ * for every connection attempt, so each nickname is looked up at most once per
+ * {@link #CERT_NICKNAME_CHECK_INTERVAL_NANOS} interval and per component: a
+ * reconnection loop neither floods the error log nor reads the trust store an
+ * extra time on every attempt, while a nickname which stays missing is reported
+ * again for as long as it is missing.
+ *
+ * @param componentName
+ * The name of the component the SSL context is built for.
+ * @param sslCertNicknames
+ * The configured certificate nicknames.
+ * @param trustStoreBackend
+ * The trust store backend holding the key pairs.
+ * @throws DirectoryException
+ * If the trust store cannot be read.
+ */
+ private void logMissingCertNicknames(String componentName, SortedSet<String> sslCertNicknames,
+ TrustStoreBackend trustStoreBackend) throws DirectoryException
+ {
+ final long nowNanos = System.nanoTime();
+ for (String nickname : sslCertNicknames)
+ {
+ if (isCertNicknameCheckDue(componentName + ":" + nickname, nowNanos)
+ && !trustStoreBackend.containsKeyWithAlias(nickname))
+ {
+ logger.error(ERR_CRYPTOMGR_SSL_CERT_NICKNAME_NOT_FOUND,
+ nickname, trustStoreBackend.getTrustStoreFile(), componentName);
+ }
+ }
+ }
+
+ /**
+ * Indicates whether the provided certificate nickname is to be looked up in the trust
+ * store now, and records the look up if it is. Only one connection attempt at a time is
+ * given the look up and the next one comes a whole interval later, so that the trust
+ * store is read, and the error logged, once per interval however often a peer which
+ * cannot present its certificate reconnects. A look up which then fails to read the
+ * trust store only delays the next one by an interval, and cannot go unnoticed: the SSL
+ * context is not built at all when the trust store cannot be read.
+ * <p>
+ * Package private for testing.
+ *
+ * @param checked
+ * The "component:nickname" pair to look up.
+ * @param nowNanos
+ * The value of {@link System#nanoTime()} at which the look up would happen.
+ * @return {@code true} if the nickname is to be looked up now, {@code false} otherwise.
+ */
+ boolean isCertNicknameCheckDue(String checked, long nowNanos)
+ {
+ final Long lastCheck = certNicknameChecks.get(checked);
+ if (lastCheck == null)
+ {
+ return certNicknameChecks.putIfAbsent(checked, nowNanos) == null;
+ }
+ return nowNanos - lastCheck >= CERT_NICKNAME_CHECK_INTERVAL_NANOS
+ && certNicknameChecks.replace(checked, lastCheck, nowNanos);
+ }
+
@Override
public SortedSet<String> getSslCertNicknames()
{
--
Gitblit v1.10.0