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

gavinchou pushed a commit to branch bdbje
in repository https://gitbox.apache.org/repos/asf/doris-thirdparty.git


The following commit(s) were added to refs/heads/bdbje by this push:
     new 7ed72b255f3 [enhance](bdbje)support ssl/tls cert reload (#359)
7ed72b255f3 is described below

commit 7ed72b255f3933f542f7cbff393385894656015a
Author: koarz <[email protected]>
AuthorDate: Tue Oct 14 23:10:59 2025 +0800

    [enhance](bdbje)support ssl/tls cert reload (#359)
---
 pom.xml                                            |   2 +-
 .../com/sleepycat/je/rep/ReplicationSSLConfig.java | 316 +++++++++++
 .../java/com/sleepycat/je/rep/impl/RepParams.java  |  64 +++
 .../je/rep/utilint/net/SSLChannelFactory.java      | 592 +++++++++++++++++++--
 .../je/rep/utilint/net/SSLMirrorMatcher.java       |  50 +-
 5 files changed, 972 insertions(+), 52 deletions(-)

diff --git a/pom.xml b/pom.xml
index 41a55c39054..805f13d10e7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
     </parent>
     <groupId>org.apache.doris</groupId>
     <artifactId>je</artifactId>
-    <version>18.3.15-doris-SNAPSHOT</version>
+    <version>18.3.16-doris-SNAPSHOT</version>
     <name>bdb-je apache doris release</name>
     <url>https://doris.apache.org/</url>
     <description>fork from bdb-je 18.3.12 from maven with starrocks bdbje 
patches</description>
diff --git a/src/main/java/com/sleepycat/je/rep/ReplicationSSLConfig.java 
b/src/main/java/com/sleepycat/je/rep/ReplicationSSLConfig.java
index 061c9d21292..3060eadc048 100644
--- a/src/main/java/com/sleepycat/je/rep/ReplicationSSLConfig.java
+++ b/src/main/java/com/sleepycat/je/rep/ReplicationSSLConfig.java
@@ -510,6 +510,123 @@ public class ReplicationSSLConfig extends 
ReplicationNetworkConfig {
     public static final String SSL_HOST_VERIFIER_PARAMS =
         EnvironmentParams.REP_PARAM_PREFIX + "ssl.hostVerifierParams";
 
+    /**
+     * The interval in seconds for checking certificate file changes.
+     * The certificate file watcher will check for file modifications at this
+     * frequency. A smaller value provides faster certificate reload response
+     * but consumes more system resources. A value of 0 disables certificate
+     * file monitoring completely.
+     *
+     * <p><table border="1"
+     *           summary="Information about configuration option">
+     * <tr><td>Name</td><td>Type</td><td>Mutable</td><td>Default</td></tr>
+     * <tr>
+     * <td>{@value}</td>
+     * <td>Long</td>
+     * <td>No</td>
+     * <td>30</td>
+     * </tr>
+     * </table>
+     */
+    public static final String SSL_CERT_REFRESH_INTERVAL_SECONDS =
+        EnvironmentParams.REP_PARAM_PREFIX + "ssl.certRefreshIntervalSeconds";
+
+    /**
+     * The timeout in seconds for smooth certificate transition.
+     * During certificate reload, the system will keep backup certificates
+     * for this duration to ensure smooth transition without connection
+     * disruption. After this timeout, backup certificates will be cleaned up.
+     *
+     * <p><table border="1"
+     *           summary="Information about configuration option">
+     * <tr><td>Name</td><td>Type</td><td>Mutable</td><td>Default</td></tr>
+     * <tr>
+     * <td>{@value}</td>
+     * <td>Long</td>
+     * <td>No</td>
+     * <td>30</td>
+     * </tr>
+     * </table>
+     */
+    public static final String SSL_CERT_TRANSITION_TIMEOUT_SECONDS =
+        EnvironmentParams.REP_PARAM_PREFIX + 
"ssl.certTransitionTimeoutSeconds";
+
+    /**
+     * The path to the PEM certificate file for SSL data channel factories.
+     * The specified path must be absolute.
+     * When both PEM and P12 configurations are present, P12 takes precedence.
+     *
+     * <p><table border="1"
+     *           summary="Information about configuration option">
+     * <tr><td>Name</td><td>Type</td><td>Mutable</td><td>Default</td></tr>
+     * <tr>
+     * <td>{@value}</td>
+     * <td>String</td>
+     * <td>No</td>
+     * <td>""</td>
+     * </tr>
+     * </table>
+     */
+    public static final String SSL_PEM_CERT_FILE =
+        EnvironmentParams.REP_PARAM_PREFIX + "ssl.pemCertFile";
+
+    /**
+     * The path to the PEM private key file for SSL data channel factories.
+     * The specified path must be absolute.
+     * When both PEM and P12 configurations are present, P12 takes precedence.
+     *
+     * <p><table border="1"
+     *           summary="Information about configuration option">
+     * <tr><td>Name</td><td>Type</td><td>Mutable</td><td>Default</td></tr>
+     * <tr>
+     * <td>{@value}</td>
+     * <td>String</td>
+     * <td>No</td>
+     * <td>""</td>
+     * </tr>
+     * </table>
+     */
+    public static final String SSL_PEM_KEY_FILE =
+        EnvironmentParams.REP_PARAM_PREFIX + "ssl.pemKeyFile";
+
+    /**
+     * The password for the PEM private key file (if encrypted).
+     * If this parameter is not set or has an empty value, the key is
+     * assumed to be unencrypted.
+     *
+     * <p><table border="1"
+     *           summary="Information about configuration option">
+     * <tr><td>Name</td><td>Type</td><td>Mutable</td><td>Default</td></tr>
+     * <tr>
+     * <td>{@value}</td>
+     * <td>String</td>
+     * <td>No</td>
+     * <td>""</td>
+     * </tr>
+     * </table>
+     */
+    public static final String SSL_PEM_KEY_PASSWORD =
+        EnvironmentParams.REP_PARAM_PREFIX + "ssl.pemKeyPassword";
+
+    /**
+     * The path to the PEM CA certificate file for SSL trust verification.
+     * The specified path must be absolute.
+     * When both PEM and P12 configurations are present, P12 takes precedence.
+     *
+     * <p><table border="1"
+     *           summary="Information about configuration option">
+     * <tr><td>Name</td><td>Type</td><td>Mutable</td><td>Default</td></tr>
+     * <tr>
+     * <td>{@value}</td>
+     * <td>String</td>
+     * <td>No</td>
+     * <td>""</td>
+     * </tr>
+     * </table>
+     */
+    public static final String SSL_PEM_CA_CERT_FILE =
+        EnvironmentParams.REP_PARAM_PREFIX + "ssl.pemCaCertFile";
+
     /* The set of Replication properties specific to this class */
     private static Set<String> repSSLProperties;
     static {
@@ -532,6 +649,12 @@ public class ReplicationSSLConfig extends 
ReplicationNetworkConfig {
         repSSLProperties.add(SSL_HOST_VERIFIER);
         repSSLProperties.add(SSL_HOST_VERIFIER_CLASS);
         repSSLProperties.add(SSL_HOST_VERIFIER_PARAMS);
+        repSSLProperties.add(SSL_CERT_REFRESH_INTERVAL_SECONDS);
+        repSSLProperties.add(SSL_CERT_TRANSITION_TIMEOUT_SECONDS);
+        repSSLProperties.add(SSL_PEM_CERT_FILE);
+        repSSLProperties.add(SSL_PEM_KEY_FILE);
+        repSSLProperties.add(SSL_PEM_KEY_PASSWORD);
+        repSSLProperties.add(SSL_PEM_CA_CERT_FILE);
         /* Nail the set down */
         repSSLProperties = Collections.unmodifiableSet(repSSLProperties);
     }
@@ -1229,6 +1352,199 @@ public class ReplicationSSLConfig extends 
ReplicationNetworkConfig {
                                hostVerifierParams, validateParams);
     }
 
+    /**
+     * Returns the certificate file refresh interval in seconds.
+     *
+     * @return the refresh interval in seconds, or 0 if monitoring is disabled
+     */
+    public long getSSLCertRefreshIntervalSeconds() {
+        return DbConfigManager.getLongVal(props, 
RepParams.SSL_CERT_REFRESH_INTERVAL_SECONDS);
+    }
+
+    /**
+     * Sets the certificate file refresh interval in seconds.
+     * The certificate file watcher will check for file modifications at this
+     * frequency. A smaller value provides faster certificate reload response
+     * but consumes more system resources. A value of 0 disables certificate
+     * file monitoring completely.
+     *
+     * @param intervalSeconds the refresh interval in seconds (0 to disable)
+     *
+     * @return this
+     *
+     * @throws IllegalArgumentException if intervalSeconds is negative
+     */
+    public ReplicationNetworkConfig setSSLCertRefreshIntervalSeconds(long 
intervalSeconds) {
+        setSSLCertRefreshIntervalSecondsVoid(intervalSeconds);
+        return this;
+    }
+
+    /**
+     * @hidden
+     * The void return setter for use by Bean editors.
+     */
+    public void setSSLCertRefreshIntervalSecondsVoid(long intervalSeconds) {
+        if (intervalSeconds < 0) {
+            throw new IllegalArgumentException("Certificate refresh interval 
cannot be negative");
+        }
+        DbConfigManager.setVal(props, 
RepParams.SSL_CERT_REFRESH_INTERVAL_SECONDS,
+                               Long.toString(intervalSeconds), validateParams);
+    }
+
+    /**
+     * Returns the certificate transition timeout in seconds.
+     *
+     * @return the transition timeout in seconds
+     */
+    public long getSSLCertTransitionTimeoutSeconds() {
+        return DbConfigManager.getLongVal(props, 
RepParams.SSL_CERT_TRANSITION_TIMEOUT_SECONDS);
+    }
+
+    /**
+     * Sets the certificate transition timeout in seconds.
+     * During certificate reload, the system will keep backup certificates
+     * for this duration to ensure smooth transition without connection
+     * disruption. After this timeout, backup certificates will be cleaned up.
+     *
+     * @param timeoutSeconds the transition timeout in seconds
+     *
+     * @return this
+     *
+     * @throws IllegalArgumentException if timeoutSeconds is negative
+     */
+    public ReplicationNetworkConfig setSSLCertTransitionTimeoutSeconds(long 
timeoutSeconds) {
+        setSSLCertTransitionTimeoutSecondsVoid(timeoutSeconds);
+        return this;
+    }
+
+    /**
+     * @hidden
+     * The void return setter for use by Bean editors.
+     */
+    public void setSSLCertTransitionTimeoutSecondsVoid(long timeoutSeconds) {
+        if (timeoutSeconds < 0) {
+            throw new IllegalArgumentException("Certificate transition timeout 
cannot be negative");
+        }
+        DbConfigManager.setVal(props, 
RepParams.SSL_CERT_TRANSITION_TIMEOUT_SECONDS,
+                               Long.toString(timeoutSeconds), validateParams);
+    }
+
+    /**
+     * Returns the path to the PEM certificate file.
+     *
+     * @return the PEM certificate file path
+     */
+    public String getSSLPemCertFile() {
+        return DbConfigManager.getVal(props, RepParams.SSL_PEM_CERT_FILE);
+    }
+
+    /**
+     * Sets the path to the PEM certificate file.
+     *
+     * @param pemCertFile the PEM certificate file path
+     *
+     * @return this
+     */
+    public ReplicationNetworkConfig setSSLPemCertFile(String pemCertFile) {
+        setSSLPemCertFileVoid(pemCertFile);
+        return this;
+    }
+
+    /**
+     * @hidden
+     * The void return setter for use by Bean editors.
+     */
+    public void setSSLPemCertFileVoid(String pemCertFile) {
+        DbConfigManager.setVal(props, RepParams.SSL_PEM_CERT_FILE, 
pemCertFile, validateParams);
+    }
+
+    /**
+     * Returns the path to the PEM private key file.
+     *
+     * @return the PEM private key file path
+     */
+    public String getSSLPemKeyFile() {
+        return DbConfigManager.getVal(props, RepParams.SSL_PEM_KEY_FILE);
+    }
+
+    /**
+     * Sets the path to the PEM private key file.
+     *
+     * @param pemKeyFile the PEM private key file path
+     *
+     * @return this
+     */
+    public ReplicationNetworkConfig setSSLPemKeyFile(String pemKeyFile) {
+        setSSLPemKeyFileVoid(pemKeyFile);
+        return this;
+    }
+
+    /**
+     * @hidden
+     * The void return setter for use by Bean editors.
+     */
+    public void setSSLPemKeyFileVoid(String pemKeyFile) {
+        DbConfigManager.setVal(props, RepParams.SSL_PEM_KEY_FILE, pemKeyFile, 
validateParams);
+    }
+
+    /**
+     * Returns the password for the PEM private key file.
+     *
+     * @return the PEM private key password
+     */
+    public String getSSLPemKeyPassword() {
+        return DbConfigManager.getVal(props, RepParams.SSL_PEM_KEY_PASSWORD);
+    }
+
+    /**
+     * Sets the password for the PEM private key file.
+     *
+     * @param pemKeyPassword the PEM private key password
+     *
+     * @return this
+     */
+    public ReplicationNetworkConfig setSSLPemKeyPassword(String 
pemKeyPassword) {
+        setSSLPemKeyPasswordVoid(pemKeyPassword);
+        return this;
+    }
+
+    /**
+     * @hidden
+     * The void return setter for use by Bean editors.
+     */
+    public void setSSLPemKeyPasswordVoid(String pemKeyPassword) {
+        DbConfigManager.setVal(props, RepParams.SSL_PEM_KEY_PASSWORD, 
pemKeyPassword, validateParams);
+    }
+
+    /**
+     * Returns the path to the PEM CA certificate file.
+     *
+     * @return the PEM CA certificate file path
+     */
+    public String getSSLPemCaCertFile() {
+        return DbConfigManager.getVal(props, RepParams.SSL_PEM_CA_CERT_FILE);
+    }
+
+    /**
+     * Sets the path to the PEM CA certificate file.
+     *
+     * @param pemCaCertFile the PEM CA certificate file path
+     *
+     * @return this
+     */
+    public ReplicationNetworkConfig setSSLPemCaCertFile(String pemCaCertFile) {
+        setSSLPemCaCertFileVoid(pemCaCertFile);
+        return this;
+    }
+
+    /**
+     * @hidden
+     * The void return setter for use by Bean editors.
+     */
+    public void setSSLPemCaCertFileVoid(String pemCaCertFile) {
+        DbConfigManager.setVal(props, RepParams.SSL_PEM_CA_CERT_FILE, 
pemCaCertFile, validateParams);
+    }
+
     /**
      * Returns a copy of this configuration object.
      */
diff --git a/src/main/java/com/sleepycat/je/rep/impl/RepParams.java 
b/src/main/java/com/sleepycat/je/rep/impl/RepParams.java
index 69c2a71348e..857183a3f27 100644
--- a/src/main/java/com/sleepycat/je/rep/impl/RepParams.java
+++ b/src/main/java/com/sleepycat/je/rep/impl/RepParams.java
@@ -1518,6 +1518,70 @@ public class RepParams {
                         false,               // mutable
                         true);               // forReplication
 
+    /**
+     * SSL certificate refresh interval in seconds
+     * @see ReplicationSSLConfig#SSL_CERT_REFRESH_INTERVAL_SECONDS
+     */
+    public static final LongConfigParam SSL_CERT_REFRESH_INTERVAL_SECONDS =
+        new 
LongConfigParam(ReplicationSSLConfig.SSL_CERT_REFRESH_INTERVAL_SECONDS,
+                            0L,        // min
+                            Long.valueOf(Long.MAX_VALUE), // max
+                            30L,         // default
+                            false,           // mutable
+                            true);           // forReplication
+
+    /**
+     * SSL certificate transition timeout in seconds
+     * @see ReplicationSSLConfig#SSL_CERT_TRANSITION_TIMEOUT_SECONDS
+     */
+    public static final LongConfigParam SSL_CERT_TRANSITION_TIMEOUT_SECONDS =
+        new 
LongConfigParam(ReplicationSSLConfig.SSL_CERT_TRANSITION_TIMEOUT_SECONDS,
+                            0L,        // min
+                            Long.valueOf(Long.MAX_VALUE), // max
+                            30L,         // default
+                            false,           // mutable
+                            true);           // forReplication
+
+    /**
+     * SSL PEM certificate file path
+     * @see ReplicationSSLConfig#SSL_PEM_CERT_FILE
+     */
+    public static final ConfigParam SSL_PEM_CERT_FILE =
+        new ConfigParam(ReplicationSSLConfig.SSL_PEM_CERT_FILE,
+                        "",             // default
+                        false,          // mutable
+                        true);          // forReplication
+
+    /**
+     * SSL PEM private key file path
+     * @see ReplicationSSLConfig#SSL_PEM_KEY_FILE
+     */
+    public static final ConfigParam SSL_PEM_KEY_FILE =
+        new ConfigParam(ReplicationSSLConfig.SSL_PEM_KEY_FILE,
+                        "",             // default
+                        false,          // mutable
+                        true);          // forReplication
+
+    /**
+     * SSL PEM private key password
+     * @see ReplicationSSLConfig#SSL_PEM_KEY_PASSWORD
+     */
+    public static final ConfigParam SSL_PEM_KEY_PASSWORD =
+        new ConfigParam(ReplicationSSLConfig.SSL_PEM_KEY_PASSWORD,
+                        "",             // default
+                        false,          // mutable
+                        true);          // forReplication
+
+    /**
+     * SSL PEM CA certificate file path
+     * @see ReplicationSSLConfig#SSL_PEM_CA_CERT_FILE
+     */
+    public static final ConfigParam SSL_PEM_CA_CERT_FILE =
+        new ConfigParam(ReplicationSSLConfig.SSL_PEM_CA_CERT_FILE,
+                        "",             // default
+                        false,          // mutable
+                        true);          // forReplication
+
     /**
      * Override the current JE version, for testing only.
      */
diff --git 
a/src/main/java/com/sleepycat/je/rep/utilint/net/SSLChannelFactory.java 
b/src/main/java/com/sleepycat/je/rep/utilint/net/SSLChannelFactory.java
index f6f13b01a5f..55a2b78a068 100644
--- a/src/main/java/com/sleepycat/je/rep/utilint/net/SSLChannelFactory.java
+++ b/src/main/java/com/sleepycat/je/rep/utilint/net/SSLChannelFactory.java
@@ -16,18 +16,48 @@ package com.sleepycat.je.rep.utilint.net;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.nio.channels.SocketChannel;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.Key;
 import java.security.KeyManagementException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
+import java.security.KeyFactory;
+import java.security.AlgorithmParameters;
 import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
 import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
-
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Level.FINE;
+
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.crypto.spec.IvParameterSpec;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
@@ -61,7 +91,7 @@ public class SSLChannelFactory implements DataChannelFactory {
      * a connection is established based on enabled protocol settings for
      * both client and server.
      */
-    private static final String SSL_CONTEXT_PROTOCOL = "TLS";
+    private static final String SSL_CONTEXT_PROTOCOL = "TLSv1.2";
 
     /**
      * A system property to allow users to specify the correct X509 certificate
@@ -79,13 +109,13 @@ public class SSLChannelFactory implements 
DataChannelFactory {
      * An SSLContext that will hold all the interesting connection parameter
      * information for session creation in server mode.
      */
-    private final SSLContext serverSSLContext;
+    private volatile SSLContext serverSSLContext;
 
     /**
      * An SSLContext that will hold all the interesting connection parameter
      * information for session creation in client mode.
      */
-    private final SSLContext clientSSLContext;
+    private volatile SSLContext clientSSLContext;
 
     /**
      * The base SSLParameters for use in channel creation.
@@ -106,10 +136,21 @@ public class SSLChannelFactory implements 
DataChannelFactory {
 
     private final InstanceLogger logger;
 
+    /**
+     * Instance parameters used for SSL context reconstruction during 
certificate reload
+     */
+    private final InstanceParams instanceParams;
+
+    /**
+     * Scheduled executor service for periodic certificate checking
+     */
+    private ScheduledExecutorService certificateCheckExecutor;
+
     /**
      * Constructor for use during creating based on access configuration
      */
     public SSLChannelFactory(InstanceParams params) {
+        this.instanceParams = params;
         serverSSLContext = constructSSLContext(params, false);
         clientSSLContext = constructSSLContext(params, true);
         baseSSLParameters =
@@ -118,6 +159,9 @@ public class SSLChannelFactory implements 
DataChannelFactory {
         sslAuthenticator = constructSSLAuthenticator(params);
         sslHostVerifier = constructSSLHostVerifier(params);
         logger = params.getContext().getLoggerFactory().getLogger(getClass());
+
+        // Initialize certificate monitoring
+        initializeCertificateMonitoring();
     }
 
     /**
@@ -131,6 +175,7 @@ public class SSLChannelFactory implements 
DataChannelFactory {
                              HostnameVerifier sslHostVerifier,
                              InstanceLogger logger) {
 
+        this.instanceParams = null; // No automatic reloading for 
pre-configured contexts
         this.serverSSLContext = serverSSLContext;
         this.clientSSLContext = clientSSLContext;
         this.baseSSLParameters =
@@ -263,7 +308,323 @@ public class SSLChannelFactory implements 
DataChannelFactory {
     }
 
     /**
-     * Builds an SSLContext object for the specified access mode.
+     * Initialize certificate monitoring using scheduled executor for periodic 
checks.
+     */
+    private void initializeCertificateMonitoring() {
+        if (instanceParams == null) {
+            return;
+        }
+
+        try {
+            final ReplicationSSLConfig config =
+                (ReplicationSSLConfig) 
instanceParams.getContext().getRepNetConfig();
+
+            // Get the refresh interval from configuration
+            long refreshInterval = config.getSSLCertRefreshIntervalSeconds();
+
+            // If refresh interval is 0, disable certificate monitoring
+            if (refreshInterval <= 0) {
+                logger.log(INFO, "Certificate monitoring disabled (refresh 
interval = 0)");
+                return;
+            }
+
+            // Only monitor PEM configuration
+            final String pemCert = config.getSSLPemCertFile();
+            final String pemKey = config.getSSLPemKeyFile();
+            final String pemCa = config.getSSLPemCaCertFile();
+
+            // Create scheduled executor for periodic certificate checking
+            certificateCheckExecutor = 
Executors.newSingleThreadScheduledExecutor(r -> {
+                Thread t = new Thread(r, "BDB-SSL-Certificate-Monitor");
+                t.setDaemon(true);
+                return t;
+            });
+
+            // Schedule periodic certificate checks
+            certificateCheckExecutor.scheduleAtFixedRate(
+                this::checkAndReloadCertificates,
+                refreshInterval,
+                refreshInterval,
+                TimeUnit.SECONDS
+            );
+
+            // Initialize file modification times
+            initializeFileModificationTimes(config);
+
+            logger.log(INFO, "Certificate monitoring initialized with " + 
refreshInterval + " second interval");
+
+        } catch (Exception e) {
+            logger.log(WARNING, "Failed to initialize certificate monitoring: 
" + e.getMessage());
+        }
+    }
+
+    /**
+     * Initialize file modification times on startup.
+     */
+    private void initializeFileModificationTimes(ReplicationSSLConfig config) {
+        try {
+            caLastModified = 
Files.getLastModifiedTime(Paths.get(config.getSSLPemCaCertFile())).toMillis();
+            keyLastModified = 
Files.getLastModifiedTime(Paths.get(config.getSSLPemKeyFile())).toMillis();
+            certLastModified = 
Files.getLastModifiedTime(Paths.get(config.getSSLPemCertFile())).toMillis();
+            logger.log(INFO, "Certificate timestamps initialized");
+        } catch (Exception e) {
+            logger.log(WARNING, "Failed to initialize file modification times: 
" + e.getMessage());
+        }
+    }
+
+    /**
+     * Check and reload certificates if any PEM files have changed.
+     * Uses bit operations to determine reload necessity:
+     * - If CA changes: check_num |= 1 << 2 (bit 2)
+     * - If key changes: check_num |= 1 << 1 (bit 1)
+     * - If cert changes: check_num |= 1 (bit 0)
+     *
+     * Valid reload scenarios:
+     * - CA + cert + key changed: check_num = 7 (111)
+     * - CA + cert changed: check_num = 5 (101)
+     * - Key + cert changed: check_num = 3 (011)
+     * - Only cert changed: check_num = 1 (001)
+     *
+     * Rule: Reload when check_num is odd (cert file changed)
+     * Only update modification times after successful reload.
+     */
+    private void checkAndReloadCertificates() {
+        if (instanceParams == null) {
+            return;
+        }
+
+        try {
+            final ReplicationSSLConfig config =
+                (ReplicationSSLConfig) 
instanceParams.getContext().getRepNetConfig();
+
+            // Use AtomicReference to hold temporary modification times
+            AtomicReference<Long> caModified = new 
AtomicReference<>(caLastModified);
+            AtomicReference<Long> keyModified = new 
AtomicReference<>(keyLastModified);
+            AtomicReference<Long> certModified = new 
AtomicReference<>(certLastModified);
+
+            int check_num = 0;
+
+            // Check CA certificate file
+            if (checkCertificateFile(config.getSSLPemCaCertFile(), 
caModified.get(),
+                    caModified::set, "CA certificate")) {
+                check_num |= 1 << 2;
+            }
+
+            // Check private key file
+            if (checkCertificateFile(config.getSSLPemKeyFile(), 
keyModified.get(),
+                    keyModified::set, "private key")) {
+                check_num |= 1 << 1;
+            }
+
+            // Check certificate file
+            if (checkCertificateFile(config.getSSLPemCertFile(), 
certModified.get(),
+                    certModified::set, "certificate")) {
+                check_num |= 1;
+            }
+
+            // Reload certificates when check_num is odd (certificate file 
changed)
+            if ((check_num & 1) == 1) {
+                // Try to reload certificates
+                if (reloadSSLContexts()) {
+                    // Only update modification times after successful reload
+                    caLastModified = caModified.get();
+                    keyLastModified = keyModified.get();
+                    certLastModified = certModified.get();
+                    logger.log(INFO, "Certificate modification times updated 
after successful reload");
+                } else {
+                    logger.log(WARNING, "Certificate reload failed, keeping 
previous modification times for retry");
+                }
+            }
+
+        } catch (Exception e) {
+            logger.log(WARNING, "Error checking certificate files: " + 
e.getMessage());
+        }
+    }
+
+    /**
+     * File modification time tracking for change detection.
+     * Initialize to -1 to distinguish from 0 (which could be a valid 
timestamp)
+     */
+    private volatile long caLastModified = -1;
+    private volatile long keyLastModified = -1;
+    private volatile long certLastModified = -1;
+
+    /**
+     * Check if a certificate file has been modified and update the stored 
modification time.
+     */
+    private boolean checkCertificateFile(String filePath, long lastModified,
+                                       java.util.function.Consumer<Long> 
timeUpdater,
+                                       String fileType) {
+        if (filePath == null || filePath.isEmpty()) {
+            return false;
+        }
+
+        try {
+            java.io.File file = new java.io.File(filePath);
+
+            // Check if file was deleted
+            if (!file.exists()) {
+                logger.log(WARNING, fileType + " file was deleted: " + 
filePath +
+                          ", waiting for recreation...");
+                if (waitForFileRecreation(file, fileType, filePath, 
timeUpdater)) {
+                    return true; // File was recreated, trigger reload
+                } else {
+                    return false; // File not recreated, skip reload
+                }
+            }
+
+            long currentModTime = 
Files.getLastModifiedTime(Paths.get(filePath)).toMillis();
+
+            if (lastModified == -1 || currentModTime != lastModified) {
+                timeUpdater.accept(currentModTime);
+                if (lastModified != -1) { // Don't trigger reload on first 
check
+                    logger.log(INFO, fileType + " file changed: " + filePath +
+                              " (old: " + lastModified + ", new: " + 
currentModTime + ")");
+                    return true;
+                } else {
+                    logger.log(INFO, fileType + " file initialized: " + 
filePath +
+                              " (timestamp: " + currentModTime + ")");
+                }
+            }
+            return false;
+        } catch (Exception e) {
+            logger.log(WARNING, "Failed to check " + fileType + " file 
modification time for " +
+                      filePath + ": " + e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Wait for file recreation when a certificate file is deleted.
+     * This handles the case where certificates are updated by deleting the 
old file
+     * and creating a new one.
+     */
+    private boolean waitForFileRecreation(java.io.File file, String fileType, 
String filePath,
+                                        java.util.function.Consumer<Long> 
updateTime) {
+        int waitSeconds = 30;
+        int checkInterval = 1;
+
+        for (int i = 0; i < waitSeconds; i += checkInterval) {
+            try {
+                Thread.sleep(checkInterval * 1000);
+                if (file.exists()) {
+                    long newModified = 
Files.getLastModifiedTime(file.toPath()).toMillis();
+                    updateTime.accept(newModified);
+                    logger.log(INFO, fileType + " file recreated and detected: 
" + filePath);
+                    return true;
+                }
+            } catch (Exception e) {
+                logger.log(WARNING, "Error while waiting for " + fileType + " 
file recreation: " +
+                          filePath + ", " + e.getMessage());
+            }
+        }
+
+        logger.log(WARNING, fileType + " file was deleted and not recreated 
within " + waitSeconds +
+                  " seconds, certificate reload cancelled: " + filePath);
+        return false;
+    }
+
+    /**
+     * Reload SSL contexts using only PEM configuration.
+     * @return true if reload was successful, false otherwise
+     */
+    private boolean reloadSSLContexts() {
+        try {
+            logger.log(INFO, "Reloading SSL contexts with new PEM 
certificates");
+
+            // Construct new SSL contexts
+            SSLContext newServerContext = constructSSLContext(instanceParams, 
false);
+            SSLContext newClientContext = constructSSLContext(instanceParams, 
true);
+
+            // Validate new contexts
+            if (newServerContext != null && newClientContext != null) {
+                // Atomically switch to new contexts
+                this.serverSSLContext = newServerContext;
+                this.clientSSLContext = newClientContext;
+
+                // Update mirror matcher principals for new certificates
+                reloadMirrorMatcherPrincipals();
+
+                logger.log(INFO, "SSL certificate reload completed 
successfully");
+                return true;
+            } else {
+                logger.log(WARNING, "Failed to create new SSL contexts during 
reload");
+                return false;
+            }
+
+        } catch (Exception e) {
+            logger.log(WARNING, "Failed to reload SSL certificates: " + 
e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Reload principals for mirror authenticators and verifiers.
+     * Note: DN-based authenticators/verifiers don't need explicit reloading
+     * as they re-evaluate peer certificates on each SSL handshake.
+     */
+    private void reloadMirrorMatcherPrincipals() {
+        try {
+            // Reload authenticator principal if it's a mirror authenticator
+            if (sslAuthenticator instanceof SSLMirrorAuthenticator) {
+                ((SSLMirrorAuthenticator) sslAuthenticator).reloadPrincipal();
+            }
+
+            // Reload host verifier principal if it's a mirror host verifier
+            if (sslHostVerifier instanceof SSLMirrorHostVerifier) {
+                ((SSLMirrorHostVerifier) sslHostVerifier).reloadPrincipal();
+            }
+
+            // DN-based authenticators and verifiers automatically handle
+            // certificate changes during SSL handshake validation, no action 
needed
+        } catch (Exception e) {
+            logger.log(WARNING, "Failed to reload mirror matcher principals: " 
+ e.getMessage());
+        }
+    }
+
+    /**
+     * Stop the certificate monitoring executor and clean up resources.
+     */
+    public void shutdown() {
+        if (certificateCheckExecutor != null) {
+            certificateCheckExecutor.shutdown();
+            try {
+                if (!certificateCheckExecutor.awaitTermination(5, 
TimeUnit.SECONDS)) {
+                    certificateCheckExecutor.shutdownNow();
+                }
+            } catch (InterruptedException e) {
+                certificateCheckExecutor.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+            certificateCheckExecutor = null;
+        }
+    }
+
+    /**
+     * Manually trigger certificate reload.
+     * This can be called programmatically to reload certificates without
+     * waiting for periodic checks.
+     *
+     * @return true if reload was initiated successfully, false otherwise
+     */
+    public boolean triggerCertificateReload() {
+        if (instanceParams == null) {
+            logger.log(WARNING, "Cannot trigger certificate reload: instance 
parameters not available");
+            return false;
+        }
+
+        try {
+            logger.log(INFO, "Manually triggered certificate reload");
+            reloadSSLContexts();
+            return true;
+        } catch (Exception e) {
+            logger.log(WARNING, "Failed to trigger certificate reload: " + 
e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Builds an SSLContext object using only PEM configuration.
      * @param params general instantiation information
      * @param clientMode set to true if the SSLContext is being created for
      * the client side of an SSL connection and false otherwise
@@ -414,7 +775,7 @@ public class SSLChannelFactory implements 
DataChannelFactory {
     }
 
     /**
-     * Reads a KeyStore into memory based on the config.
+     * Reads a KeyStore into memory based on PEM configuration only.
      */
     private static KeyStoreInfo readKeyStoreInfo(InstanceContext context) {
 
@@ -422,39 +783,63 @@ public class SSLChannelFactory implements 
DataChannelFactory {
             (ReplicationSSLConfig) context.getRepNetConfig();
 
         /*
-         * Determine what KeyStore file to access
+         * Determine what KeyStore file to access. P12 configuration takes
+         * precedence over PEM.
          */
         String ksProp = config.getSSLKeyStore();
         if (ksProp == null || ksProp.isEmpty()) {
             ksProp = System.getProperty("javax.net.ssl.keyStore");
         }
 
-        if (ksProp == null) {
-            return null;
+        if (ksProp != null && !ksProp.isEmpty()) {
+
+            /*
+             * Determine what type of keystore to assume. If not specified
+             * loadStore determines the default
+             */
+            final String ksTypeProp = config.getSSLKeyStoreType();
+
+            final char[] ksPw = getKeyStorePassword(context);
+            try {
+                if (ksPw == null) {
+                    throw new IllegalArgumentException(
+                        "Unable to open keystore without a password");
+                }
+
+                /*
+                 * Get a KeyStore instance
+                 */
+                final KeyStore ks =
+                    loadStore(ksProp, ksPw, "keystore", ksTypeProp);
+
+                return new KeyStoreInfo(ksProp, ks, ksPw);
+            } finally {
+                if (ksPw != null) {
+                    Arrays.fill(ksPw, ' ');
+                }
+            }
         }
 
         /*
-         * Determine what type of keystore to assume.  If not specified
-         * loadStore determines the default
+         * No keystore file configured, fall back to PEM material if provided.
          */
-        final String ksTypeProp = config.getSSLKeyStoreType();
+        final String pemCert = config.getSSLPemCertFile();
+        final String pemKey = config.getSSLPemKeyFile();
+        if (pemCert == null || pemCert.isEmpty() ||
+            pemKey == null || pemKey.isEmpty()) {
+            return null;
+        }
 
-        final char[] ksPw = getKeyStorePassword(context);
+        final char[] pemPassword = 
pemPasswordToChars(config.getSSLPemKeyPassword());
         try {
-            if (ksPw == null) {
-                throw new IllegalArgumentException(
-                    "Unable to open keystore without a password");
-            }
-
-            /*
-             * Get a KeyStore instance
-             */
-            final KeyStore ks = loadStore(ksProp, ksPw, "keystore", 
ksTypeProp);
-
-            return new KeyStoreInfo(ksProp, ks, ksPw);
+            final KeyStore ks = loadKeyStore(pemCert, pemKey, pemPassword);
+            return new KeyStoreInfo(pemCert, ks, pemPassword);
+        } catch (Exception e) {
+            throw new IllegalStateException(
+                "Error loading PEM key material from " + pemCert, e);
         } finally {
-            if (ksPw != null) {
-                Arrays.fill(ksPw, ' ');
+            if (pemPassword != null) {
+                Arrays.fill(pemPassword, ' ');
             }
         }
     }
@@ -553,7 +938,7 @@ public class SSLChannelFactory implements 
DataChannelFactory {
     }
 
     /**
-     * Based on the input config, read the configured TrustStore into memory.
+     * Based on PEM configuration, read the configured TrustStore into memory.
      */
     private static KeyStoreInfo readTrustStoreInfo(InstanceContext context) {
 
@@ -561,33 +946,28 @@ public class SSLChannelFactory implements 
DataChannelFactory {
             (ReplicationSSLConfig) context.getRepNetConfig();
 
         /*
-         * Determine what truststore file, if any, to use
+         * Only use PEM CA configuration for truststore.
          */
-        String tsProp = config.getSSLTrustStore();
-        if (tsProp == null || tsProp.isEmpty()) {
-            tsProp = System.getProperty("javax.net.ssl.trustStore");
+        final String pemCa = config.getSSLPemCaCertFile();
+        if (pemCa == null || pemCa.isEmpty()) {
+            return null;
         }
 
-        /*
-         * Determine what type of truststore to assume
-         */
-        String tsTypeProp = config.getSSLTrustStoreType();
-        if (tsTypeProp == null || tsTypeProp.isEmpty()) {
-            tsTypeProp = KeyStore.getDefaultType();
+        try {
+            final KeyStore ts = loadTrustStore(pemCa);
+            return new KeyStoreInfo(pemCa, ts, null);
+        } catch (Exception e) {
+            throw new IllegalStateException(
+                "Error loading PEM CA certificate from " + pemCa, e);
         }
+    }
 
-        /*
-         * Build a TrustStore, if specified
-         */
-        final char[] tsPw = getTrustStorePassword(context);
-
-        if (tsProp != null) {
-            final KeyStore ts = loadStore(tsProp, tsPw, "truststore", 
tsTypeProp);
+    private static char[] pemPasswordToChars(String password) {
 
-            return new KeyStoreInfo(tsProp, ts, tsPw);
+        if (password == null || password.isEmpty()) {
+            return new char[0];
         }
-
-        return null;
+        return password.toCharArray();
     }
 
     /**
@@ -1000,4 +1380,122 @@ public class SSLChannelFactory implements 
DataChannelFactory {
             }
         }
     }
+
+    public static KeyStore loadKeyStore(String certPath,
+                                        String keyPath,
+                                        char[] password)
+        throws Exception {
+        CertificateFactory cf = CertificateFactory.getInstance("X.509");
+        Certificate cert;
+        try (InputStream in = new FileInputStream(certPath)) {
+            cert = cf.generateCertificate(in);
+        }
+
+        PrivateKey privateKey = loadPrivateKey(keyPath, password);
+
+        KeyStore ks = KeyStore.getInstance("PKCS12");
+        ks.load(null, null);
+        ks.setKeyEntry("cert", privateKey, password, new Certificate[]{cert});
+        return ks;
+    }
+
+    public static KeyStore loadTrustStore(String caCertPath) throws Exception {
+        CertificateFactory cf = CertificateFactory.getInstance("X.509");
+        Certificate ca;
+        try (InputStream in = new FileInputStream(caCertPath)) {
+            ca = cf.generateCertificate(in);
+        }
+        KeyStore ts = KeyStore.getInstance("PKCS12");
+        ts.load(null, null);
+        ts.setCertificateEntry("ca", ca);
+        return ts;
+    }
+
+
+    public static PrivateKey loadPrivateKey(String keyPath, char[] password)
+        throws Exception {
+        if (password == null) {
+            password = new char[0];
+        }
+        String pem = new String(Files.readAllBytes(Paths.get(keyPath)));
+        pem = pem.replaceAll("-----BEGIN (.*)-----", "")
+                 .replaceAll("-----END (.*)-----", "")
+                 .replaceAll("\\s", "");
+        byte[] decoded = Base64.getDecoder().decode(pem);
+
+        try {
+            EncryptedPrivateKeyInfo encryptedInfo =
+                new EncryptedPrivateKeyInfo(decoded);
+            Cipher cipher = buildDecryptCipher(encryptedInfo, password);
+            PKCS8EncodedKeySpec keySpec = encryptedInfo.getKeySpec(cipher);
+            return generatePrivateKey(keySpec);
+        } catch (IOException e) {
+            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
+            return generatePrivateKey(keySpec);
+        }
+    }
+
+    private static PrivateKey generatePrivateKey(PKCS8EncodedKeySpec keySpec)
+        throws Exception {
+        String[] keyAlgorithms = {"RSA", "EC", "DSA"};
+        Exception lastException = null;
+
+        for (String keyAlg : keyAlgorithms) {
+            try {
+                return KeyFactory.getInstance(keyAlg).generatePrivate(keySpec);
+            } catch (Exception e) {
+                lastException = e;
+            }
+        }
+
+        if (lastException != null) {
+            throw lastException;
+        }
+
+        throw new Exception(
+            "Unable to parse private key with any supported algorithm");
+    }
+
+    private static Cipher buildDecryptCipher(EncryptedPrivateKeyInfo info,
+                                             char[] password)
+        throws Exception {
+        final String algName = info.getAlgName();
+        if (algName != null && algName.toUpperCase().contains("PBES2")) {
+            AlgorithmParameters params = info.getAlgParameters();
+            if (params == null) {
+                throw new IllegalArgumentException(
+                    "Missing PBES2 algorithm parameters");
+            }
+
+            PBEParameterSpec pbeSpec =
+                params.getParameterSpec(PBEParameterSpec.class);
+            if (pbeSpec == null) {
+                throw new IllegalArgumentException(
+                    "Unsupported PBES2 parameters");
+            }
+
+            final String transformation = params.toString();
+            if (transformation == null || transformation.isEmpty()) {
+                throw new IllegalArgumentException(
+                    "Missing PBES2 transformation");
+            }
+
+            SecretKeyFactory skf = 
SecretKeyFactory.getInstance(transformation);
+            SecretKey secret = skf.generateSecret(new PBEKeySpec(password,
+                                                                
pbeSpec.getSalt(),
+                                                                
pbeSpec.getIterationCount()));
+
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(Cipher.DECRYPT_MODE, secret, pbeSpec);
+            return cipher;
+        }
+
+        Cipher cipher = Cipher.getInstance(algName);
+        PBEKeySpec pbeKeySpec = new PBEKeySpec(password);
+        SecretKeyFactory skf = SecretKeyFactory.getInstance(algName);
+        Key key = skf.generateSecret(pbeKeySpec);
+        cipher.init(Cipher.DECRYPT_MODE, key, info.getAlgParameters());
+        return cipher;
+    }
+
 }
diff --git 
a/src/main/java/com/sleepycat/je/rep/utilint/net/SSLMirrorMatcher.java 
b/src/main/java/com/sleepycat/je/rep/utilint/net/SSLMirrorMatcher.java
index ac7c6179f89..f7145463c37 100644
--- a/src/main/java/com/sleepycat/je/rep/utilint/net/SSLMirrorMatcher.java
+++ b/src/main/java/com/sleepycat/je/rep/utilint/net/SSLMirrorMatcher.java
@@ -13,9 +13,14 @@
 
 package com.sleepycat.je.rep.utilint.net;
 
+import static java.util.logging.Level.FINE;
 import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
 
+import java.io.InputStream;
+import java.io.IOException;
 import java.security.KeyStore;
+import java.security.GeneralSecurityException;
 import java.security.KeyStoreException;
 import java.security.Principal;
 import java.security.cert.Certificate;
@@ -40,8 +45,10 @@ class SSLMirrorMatcher {
     /*
      * The Principal that represents us when in the expected peer's ssl mode.
      */
-    final private Principal ourPrincipal;
+    private volatile Principal ourPrincipal;
     final private InstanceLogger logger;
+    final private InstanceContext context;
+    final private boolean clientMode;
 
     /**
      * Construct an SSLMirrorMatcher
@@ -56,13 +63,15 @@ class SSLMirrorMatcher {
     public SSLMirrorMatcher(InstanceParams params, boolean clientMode)
         throws IllegalArgumentException {
 
-        ourPrincipal = determinePrincipal(params.getContext(), clientMode);
+        this.context = params.getContext();
+        this.clientMode = clientMode;
+        logger = params.getContext().getLoggerFactory().getLogger(getClass());
+        ourPrincipal = determinePrincipal(context, clientMode);
         if (ourPrincipal == null) {
             throw new IllegalArgumentException(
                 "Unable to determine a local principal for comparison " +
                 "with peer principals");
         }
-        logger = params.getContext().getLoggerFactory().getLogger(getClass());
     }
 
     /**
@@ -85,6 +94,7 @@ class SSLMirrorMatcher {
         try {
             peerPrincipal = sslSession.getPeerPrincipal();
         } catch (SSLPeerUnverifiedException pue) {
+            logger.log(INFO, String.format("Error getting peer principal: %s", 
pue));
             return false;
         }
 
@@ -100,6 +110,36 @@ class SSLMirrorMatcher {
         return ourPrincipal.equals(peerPrincipal);
     }
 
+    /**
+     * Reload the local principal when certificates change.
+     * This method provides graceful certificate principal updates during 
smooth transitions.
+     */
+    public void reloadPrincipal() {
+        try {
+            Principal oldPrincipal = ourPrincipal;
+            Principal newPrincipal = determinePrincipal(context, clientMode);
+
+            if (newPrincipal != null) {
+                if (!newPrincipal.equals(oldPrincipal)) {
+                    logger.log(INFO, String.format(
+                        "SSL mirror matcher principal updated from %s to %s",
+                        oldPrincipal != null ? oldPrincipal.getName() : "null",
+                        newPrincipal.getName()));
+                    ourPrincipal = newPrincipal;
+                } else {
+                    logger.log(FINE, "SSL mirror matcher principal unchanged 
after reload");
+                }
+            } else {
+                // Maintain current principal if new one cannot be determined
+                logger.log(WARNING,
+                    "Failed to reload SSL mirror matcher principal: no 
principal found, keeping current principal");
+            }
+        } catch (Exception e) {
+            logger.log(WARNING,
+                "Failed to reload SSL mirror matcher principal: " + 
e.getMessage() + ", keeping current principal");
+        }
+    }
+
     /**
      * Attempt to determine the Principal that we take on when connecting
      * in client or server context based on the ReplicationNetworkConfig.
@@ -128,7 +168,9 @@ class SSLMirrorMatcher {
             try {
                 if (keyStore.size() < 1) {
                     logger.log(INFO, "KeyStore is empty");
-                    return null;
+                    throw new IllegalArgumentException(
+                        "Unable to determine a local principal for" +
+                        " comparison with peer principals");
                 } else if (keyStore.size() > 1) {
                     logger.log(INFO, "KeyStore has multiple entries but no " +
                                "alias was specified.  Using the first one " +


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

Reply via email to