From 70d9a179cdd8d975b44e1815c249e20d9f91097f Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 16 Sep 2026 08:10:15 +0000
Subject: [PATCH] [#912] Provision the ads-truststore from an existing key store at setup time (#984)
---
opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java | 264 ++++++
opendj-server-legacy/src/main/java/org/opends/quicksetup/SecurityOptions.java | 46 +
opendj-server-legacy/src/main/java/org/opends/server/util/CertificateManager.java | 124 ++
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-change-certs.adoc | 77 +
opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/AdsTrustStoreProvisionerTest.java | 549 ++++++++++++
opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDSArgumentParser.java | 65 +
opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSArgumentParserTestCase.java | 205 ++++
opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java | 41
opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java | 115 ++
opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java | 22
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java | 94 ++
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/AdsTrustStoreProvisioner.java | 254 +++++
opendj-server-legacy/src/messages/org/opends/messages/tool.properties | 31
opendj-server-legacy/src/messages/org/opends/messages/utility.properties | 9
opendj-server-legacy/src/test/java/org/opends/server/util/CertificateFixture.java | 235 +++++
opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties | 24
opendj-server-legacy/src/test/java/org/opends/quicksetup/AdsTrustStoreInstallTestCase.java | 447 ++++++++++
17 files changed, 2,597 insertions(+), 5 deletions(-)
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-change-certs.adoc b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-change-certs.adoc
index 8a12955..6566a9e 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-change-certs.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-change-certs.adoc
@@ -29,6 +29,8 @@
* Replace a key pair used for replication
+* Install a server with a CA-signed certificate for replication
+
* Use a CA-signed certificate for replication
OpenDJ uses keystores (for private keys) and truststores (for public, signed certificates). Up to three sets of keystores are used, as shown in the following illustration.
@@ -56,7 +58,7 @@
This Java Keystore holds public key certificates of all servers replicating with the current server. It also includes the `ads-certificate` key pair of the current server. The password is stored in `ads-truststore.pin`.
+
-Do not change this keystore directly, except to install a CA-signed certificate for replication as described in xref:#replace-ads-cert-ca["To Use a CA-Signed Certificate for Replication"].
+Do not change this keystore directly, except to install a CA-signed certificate for replication as described in xref:#replace-ads-cert-ca["To Use a CA-Signed Certificate for Replication"]. A server which is installed with the `--useKeyStoreForReplication` option of the `setup` command gets its CA-signed key pair at installation time instead, as described in xref:#install-ads-cert-ca["To Install a Server With a CA-Signed Replication Certificate"].
`keystore`::
This Java Keystore holds the private key and server certificate, `server-cert`, used to protect TLS/SSL communications with client applications. The password, stored in `keystore.pin`, is also the key password for `server-cert`.
@@ -424,11 +426,84 @@
====
+[#install-ads-cert-ca]
+.To Install a Server With a CA-Signed Replication Certificate
+====
+The `setup` command can provision the `ads-truststore` from the key store you already hold, so that a server presents a certificate signed by your own Certificate Authority (CA) on the replication port from its first start. Use this when installing a server which is to join a topology secured with your CA: it replaces the manual procedure of xref:#replace-ads-cert-ca["To Use a CA-Signed Certificate for Replication"], which otherwise has to be repeated on every server that joins.
+
+. Install the server with the key store holding your CA-signed key pair, adding the `--useKeyStoreForReplication` option:
++
+
+[source, console]
+----
+$ /path/to/opendj/setup \
+ --cli \
+ --no-prompt \
+ --hostname opendj.example.com \
+ --ldapPort 1389 \
+ --adminConnectorPort 4444 \
+ --rootUserDN "cn=Directory Manager" \
+ --rootUserPassword password \
+ --baseDN dc=example,dc=com \
+ --usePkcs12keyStore /path/to/server.p12 \
+ --keyStorePasswordFile /path/to/keystore.pin \
+ --certNickname server-cert \
+ --enableStartTLS \
+ --ldapsPort 1636 \
+ --useKeyStoreForReplication
+----
++
+The `server-cert` key pair is copied into the `ads-truststore`, the certificates which issued it are trusted there, and the `ssl-cert-nickname` property of the crypto manager is set to `server-cert`. The same applies to the `--useJavaKeystore`, `--useJCEKS` and `--useBcfksKeystore` options. The key store is the one given for LDAPS or StartTLS, so the command has to enable one of them, with `--ldapsPort` or `--enableStartTLS`. A certificate generated by the installer and a key held in a PKCS#11 token cannot be used, the first because it would be self-signed like the one it replaces, the second because its private key cannot be exported.
+
+. If your key store holds the issued certificate on its own, rather than its whole chain, add the certificates to trust to the same command:
++
+
+[source, console]
+----
+ --replicationCaCertFile /path/to/ca.crt
+----
++
+Every certificate held in the file is trusted, so a file holding a chain of authorities is given once; repeat the option for certificates held in separate files. The certificate of a key pair is not a trust anchor on its own: the trust managers read the certificate a key belongs to and none of its issuers, so a server whose trust store holds no certificate to trust would trust no peer. Rather than install such a server, the `setup` command stops and says so.
+
+. Once the server has started for the first time, check what the trust store holds:
++
+
+[source, console]
+----
+$ cd /path/to/opendj/config
+$ keytool -list -keystore ads-truststore -storepass `cat ads-truststore.pin`
+
+Keystore type: JKS
+Keystore provider: SUN
+
+Your keystore contains 3 entries
+
+ads-ca-1, Sep 8, 2026, trustedCertEntry,
+Certificate fingerprint (SHA-256): 21:9F:...
+server-cert, Sep 8, 2026, PrivateKeyEntry,
+Certificate fingerprint (SHA-256): 8D:22:...
+ads-certificate, Sep 8, 2026, PrivateKeyEntry,
+Certificate fingerprint (SHA-256): 3B:F0:...
+----
++
+The certificates to trust are imported under the `ads-ca-1`, `ads-ca-2`... aliases. The `ads-certificate` key pair is generated by the server when it first starts and stays in the store: it is the crypto manager instance key, published to the topology under `cn=instance keys,cn=admin data`. It is simply no longer the key pair presented on the replication port.
+
++
+[NOTE]
+======
+Every server of a topology has to trust your CA before any of them presents a certificate it signed. Servers installed this way trust it from the start. Servers which are already running do not, and are updated with the procedure below, on all of them, before the first server installed this way joins.
+
+Trusting your CA on the replication port means trusting every certificate it issues, as the certificate is the only credential a peer presents and its subject is not checked. Use a CA dedicated to the replication topology, or one whose issuance you constrain, rather than a corporate CA which also signs certificates for unrelated hosts.
+======
+====
+
[#replace-ads-cert-ca]
.To Use a CA-Signed Certificate for Replication
====
Replication connections use neither the `keystore` nor the `admin-keystore`. Both the key pair presented on the replication port and the certificates trusted on that port are read from the `ads-truststore`, and the alias to present is the `ssl-cert-nickname` property of the crypto manager, `ads-certificate` by default. To secure replication with certificates signed by your own Certificate Authority (CA), install the CA-signed key pair in the `ads-truststore` of every server.
+This procedure applies to servers which are already installed. A server yet to be installed gets the same result from the `setup` command, as described in xref:#install-ads-cert-ca["To Install a Server With a CA-Signed Replication Certificate"].
+
Servers authenticate each other on the replication port, so import the CA certificate everywhere first, in a separate pass. A server whose certificate changes before the other servers trust your CA can no longer connect to the topology.
Each `keytool` command applies to a single server, and is run while that server is stopped. A running server writes the whole `ads-truststore` back to disk when it adds the certificate of another server to it, which would discard the changes made meanwhile by `keytool`.
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/SecurityOptions.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/SecurityOptions.java
index 5c356ea..c11fdcd 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/SecurityOptions.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/SecurityOptions.java
@@ -17,8 +17,11 @@
*/
package org.opends.quicksetup;
+import java.io.File;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Set;
import java.util.TreeSet;
@@ -58,6 +61,8 @@
private String keyStorePath;
private String keyStorePassword;
private final Set<String> aliasesToUse = new TreeSet<>();
+ private boolean replicationUsesKeyStore;
+ private final List<File> replicationCaCertFiles = new ArrayList<>();
private SecurityOptions()
{
@@ -437,4 +442,45 @@
this.aliasesToUse.addAll(aliasesToUse);
}
+ /**
+ * Tells whether the key pairs of this key store are to secure replication as well.
+ * Replication reads the key pair it presents, and the certificates it trusts, from the
+ * trust store used for server to server communication and from nowhere else, so the key
+ * pairs have to be copied there.
+ * @return {@code true} if replication is to present the key pairs of this key store.
+ */
+ public boolean getReplicationUsesKeyStore()
+ {
+ return replicationUsesKeyStore;
+ }
+
+ /**
+ * Sets whether the key pairs of this key store are to secure replication as well.
+ * @param replicationUsesKeyStore whether replication is to present these key pairs.
+ */
+ public void setReplicationUsesKeyStore(boolean replicationUsesKeyStore)
+ {
+ this.replicationUsesKeyStore = replicationUsesKeyStore;
+ }
+
+ /**
+ * Returns the files holding certificates to trust on the replication port, on top of
+ * the issuers found in the certificate chains of the key pairs to use.
+ * @return the files holding certificates to trust, empty if there is none.
+ */
+ public List<File> getReplicationCaCertFiles()
+ {
+ return replicationCaCertFiles;
+ }
+
+ /**
+ * Sets the files holding certificates to trust on the replication port.
+ * @param replicationCaCertFiles the files holding certificates to trust.
+ */
+ public void setReplicationCaCertFiles(Collection<File> replicationCaCertFiles)
+ {
+ this.replicationCaCertFiles.clear();
+ this.replicationCaCertFiles.addAll(replicationCaCertFiles);
+ }
+
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/AdsTrustStoreProvisioner.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/AdsTrustStoreProvisioner.java
new file mode 100644
index 0000000..bbd7b8a
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/AdsTrustStoreProvisioner.java
@@ -0,0 +1,254 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.quicksetup.installer;
+
+import static org.opends.messages.QuickSetupMessages.*;
+import static org.opends.quicksetup.util.Utils.createProtectedFile;
+import static org.opends.server.config.ConfigConstants.ADS_CERTIFICATE_ALIAS;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+
+import org.opends.quicksetup.ApplicationException;
+import org.opends.quicksetup.ReturnCode;
+import org.opends.server.util.CertificateManager;
+import org.opends.server.util.SetupUtils;
+
+/**
+ * Provisions the trust store used for server to server communication, {@code
+ * ads-truststore}, from a key store the operator already holds.
+ * <p>
+ * Replication reads both the key pair it presents on the replication port and the
+ * certificates it trusts there from that file, and from nowhere else: neither the key
+ * store configured for LDAPS nor the one of the administration connector is consulted.
+ * Without this, a server installs with the self-signed {@code ads-certificate} the trust
+ * store backend generates on the first start, and securing replication with an
+ * organisation's own certificates means stopping every server afterwards and repeating a
+ * {@code keytool} procedure by hand on each of them.
+ * <p>
+ * Two properties of the trust store shape what this class does:
+ * <ul>
+ * <li>Only a trusted certificate entry is a trust anchor. The certificate chain of a key
+ * entry is not: the trust managers take the certificate the key belongs to and none of
+ * its issuers. The issuing certificates are therefore imported as trusted certificate
+ * entries of their own, and a key pair which comes without any, and without certificates
+ * named separately, is reported rather than left to fail as a handshake later on.</li>
+ * <li>The {@code ads-certificate} key pair is not provisioned. It is the crypto manager
+ * instance key, published to the topology under {@code cn=instance keys,cn=admin data}
+ * and read by its alias, and the trust store backend generates it on the first start when
+ * the alias is free.</li>
+ * </ul>
+ */
+final class AdsTrustStoreProvisioner
+{
+ /** The prefix of the aliases the trusted certificates are imported under. */
+ private static final String CA_ALIAS_PREFIX = "ads-ca-";
+
+ private final String trustStorePath;
+ private final String pinFilePath;
+
+ /**
+ * Creates a provisioner for the provided trust store.
+ *
+ * @param trustStorePath
+ * The path of the trust store file to create.
+ * @param pinFilePath
+ * The path of the file to write the generated PIN of the trust store to.
+ */
+ AdsTrustStoreProvisioner(String trustStorePath, String pinFilePath)
+ {
+ this.trustStorePath = trustStorePath;
+ this.pinFilePath = pinFilePath;
+ }
+
+ /**
+ * Creates the trust store, holding the provided key pairs and the certificates to trust
+ * on the replication port, and writes its PIN file.
+ * <p>
+ * Everything is read and checked before anything is written: a refusal leaves no file
+ * behind, and a failure while writing removes the partial trust store.
+ *
+ * @param source
+ * The key store holding the key pairs to import.
+ * @param aliases
+ * The aliases of the key pairs to import, as the key store spells them, which
+ * become the certificate nicknames the crypto manager presents.
+ * @param caCertificateFiles
+ * The files holding certificates to trust, on top of the issuers found in the
+ * certificate chains of the imported key pairs. Every certificate of a file is
+ * trusted. May be empty.
+ * @throws ApplicationException
+ * If the key pairs cannot be read, if an alias is one the trust store reserves,
+ * if the trust store would end up trusting no certificate at all, if it
+ * already exists, or if it cannot be written.
+ */
+ void provision(CertificateManager source, Collection<String> aliases, Collection<File> caCertificateFiles)
+ throws ApplicationException
+ {
+ final List<Certificate> trusted;
+ try
+ {
+ checkReservedAliases(aliases);
+ trusted = issuersOf(source, aliases);
+ for (File caCertificateFile : caCertificateFiles)
+ {
+ for (Certificate certificate : certificatesOf(caCertificateFile))
+ {
+ if (!trusted.contains(certificate))
+ {
+ trusted.add(certificate);
+ }
+ }
+ }
+ }
+ catch (ApplicationException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE.get(trustStorePath, String.valueOf(e)), e);
+ }
+ if (trusted.isEmpty())
+ {
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE_NO_TRUST_ANCHOR.get(
+ source.getKeyStorePath(), joinAliases(aliases)), null);
+ }
+ if (new File(trustStorePath).exists())
+ {
+ // Only what this run writes is removed on failure, so a store which is already
+ // there is left alone rather than overwritten or deleted.
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE_EXISTS.get(trustStorePath), null);
+ }
+
+ try
+ {
+ // The trust store is a JKS whatever the type of the key store the key pairs come
+ // from: ds-cfg-trust-store-type of the ads-truststore backend says JKS.
+ final String pin = new String(SetupUtils.createSelfSignedCertificatePwd());
+ final CertificateManager trustStore =
+ new CertificateManager(trustStorePath, CertificateManager.KEY_STORE_TYPE_JKS, pin);
+ for (String alias : aliases)
+ {
+ trustStore.importKeyEntry(alias, source, alias);
+ }
+ int trustedCertificates = 0;
+ for (Certificate certificate : trusted)
+ {
+ trustStore.addTrustedCertificate(CA_ALIAS_PREFIX + ++trustedCertificates, certificate);
+ }
+ createProtectedFile(pinFilePath, pin);
+ }
+ catch (Throwable t)
+ {
+ deletePartialTrustStore();
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE.get(trustStorePath, String.valueOf(t)), t);
+ }
+ }
+
+ /**
+ * Refuses the aliases the trust store keeps for itself: {@code ads-certificate} is the
+ * instance key the server generates on its first start, which it would skip if the alias
+ * were taken, and {@code ads-ca-N} are the certificates trusted here.
+ */
+ private void checkReservedAliases(Collection<String> aliases) throws ApplicationException
+ {
+ for (String alias : aliases)
+ {
+ if (alias.equalsIgnoreCase(ADS_CERTIFICATE_ALIAS)
+ || alias.toLowerCase(Locale.ENGLISH).startsWith(CA_ALIAS_PREFIX))
+ {
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE_RESERVED_ALIAS.get(alias, ADS_CERTIFICATE_ALIAS, CA_ALIAS_PREFIX), null);
+ }
+ }
+ }
+
+ /**
+ * Returns the issuers of the certificate chains of the provided key pairs, one entry
+ * per distinct certificate: the key pairs of one server are usually issued by the same
+ * authority, which is then to be trusted once rather than under one alias each.
+ */
+ private List<Certificate> issuersOf(CertificateManager source, Collection<String> aliases) throws Exception
+ {
+ final List<Certificate> issuers = new ArrayList<>();
+ for (String alias : aliases)
+ {
+ final Certificate[] chain = source.getCertificateChain(alias);
+ if (chain == null || chain.length == 0)
+ {
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE_NO_KEY_PAIR.get(source.getKeyStorePath(), alias), null);
+ }
+ for (int i = 1; i < chain.length; i++)
+ {
+ if (!issuers.contains(chain[i]))
+ {
+ issuers.add(chain[i]);
+ }
+ }
+ }
+ return issuers;
+ }
+
+ /**
+ * Returns every certificate held in the provided file, which may be a single DER or PEM
+ * certificate, a PEM bundle or a PKCS#7 chain: a file which holds a chain of authorities
+ * is trusted whole, rather than up to its first certificate only.
+ */
+ private Collection<? extends Certificate> certificatesOf(File caCertificateFile) throws Exception
+ {
+ final Collection<? extends Certificate> certificates;
+ try (InputStream in = new FileInputStream(caCertificateFile))
+ {
+ certificates = CertificateFactory.getInstance("X.509").generateCertificates(in);
+ }
+ catch (CertificateException e)
+ {
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE_CA_CERT_FILE_UNREADABLE.get(caCertificateFile.getPath(), e.getMessage()), e);
+ }
+ if (certificates.isEmpty())
+ {
+ throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
+ ERR_INSTALL_ADS_TRUSTSTORE_CA_CERT_FILE_EMPTY.get(caCertificateFile.getPath()), null);
+ }
+ return certificates;
+ }
+
+ private void deletePartialTrustStore()
+ {
+ new File(trustStorePath).delete();
+ new File(pinFilePath).delete();
+ }
+
+ private String joinAliases(Collection<String> aliases)
+ {
+ return String.join(", ", aliases);
+ }
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
index 661af28..91a495e 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
@@ -1214,19 +1214,27 @@
*/
private void configureServer() throws ApplicationException
{
- notifyListeners(getFormattedWithPoints(INFO_PROGRESS_CONFIGURING.get()));
copyTemplateInstance();
writeOpenDSJavaHome();
writeHostName();
checkAbort();
+ final SecurityOptions sec = getUserData().getSecurityOptions();
+ if (sec.getReplicationUsesKeyStore())
+ {
+ // Before the configuration and the certificates are written: the provisioning
+ // refuses key stores and certificate files it cannot use, and a refusal then
+ // leaves the configuration as the template had it.
+ provisionAdsTrustStore(sec);
+ }
+
+ notifyListeners(getFormattedWithPoints(INFO_PROGRESS_CONFIGURING.get()));
List<String> argList = CollectionUtils.newArrayList(
"-c", getConfigurationFile(),
"-h", getUserData().getHostName(),
"-p", String.valueOf(getUserData().getServerPort()),
"--adminConnectorPort", String.valueOf(getUserData().getAdminConnectorPort()));
- final SecurityOptions sec = getUserData().getSecurityOptions();
// TODO: even if the user does not configure SSL maybe we should choose
// a secure port that is not being used and that we can actually use.
if (sec.getEnableSSL())
@@ -1241,6 +1249,7 @@
}
addCertificateArguments(sec, argList);
+ addAdsCertificateArguments(sec, argList);
// For the moment do not enable JMX
if (getUserData().getServerJMXPort() > 0)
{
@@ -1341,6 +1350,47 @@
configureCertificate(sec);
}
+ /**
+ * Provisions the trust store used for server to server communication from the key store
+ * the server certificate comes from. Replication reads both the key pair it presents on
+ * its port and the certificates it trusts there from that trust store, and from nowhere
+ * else, so the key pair has to be copied into it while the server is stopped: the
+ * nickname to present is read once, when the crypto manager is created at startup.
+ *
+ * @param sec
+ * the security options holding the key store to provision the trust store from.
+ * @throws ApplicationException
+ * if the trust store cannot be provisioned.
+ */
+ private void provisionAdsTrustStore(SecurityOptions sec) throws ApplicationException
+ {
+ notifyListeners(getFormattedWithPoints(INFO_PROGRESS_UPDATING_ADS_TRUSTSTORE.get()));
+ final CertificateManager keyStore = new CertificateManager(
+ sec.getKeystorePath(), keyStoreTypeOf(sec), sec.getKeystorePassword());
+ new AdsTrustStoreProvisioner(getAdsTrustStorePath(), getAdsTrustStorePinPath())
+ .provision(keyStore, sec.getAliasesToUse(), sec.getReplicationCaCertFiles());
+ notifyListeners(getFormattedDoneWithLineBreak());
+ }
+
+ /** Returns the key store type of the provided security options, as CertificateManager names it. */
+ private static String keyStoreTypeOf(SecurityOptions sec)
+ {
+ switch (sec.getCertificateType())
+ {
+ case JKS:
+ return CertificateManager.KEY_STORE_TYPE_JKS;
+ case JCEKS:
+ return CertificateManager.KEY_STORE_TYPE_JCEKS;
+ case PKCS12:
+ return CertificateManager.KEY_STORE_TYPE_PKCS12;
+ case BCFKS:
+ return CertificateManager.KEY_STORE_TYPE_BCFKS;
+ default:
+ throw new IllegalStateException(
+ "No key store to read a key pair from: " + sec.getCertificateType());
+ }
+ }
+
private void configureCertificate(SecurityOptions sec) throws ApplicationException
{
try
@@ -1545,6 +1595,24 @@
}
}
+ /**
+ * Adds the certificate nicknames the crypto manager is to present on the replication
+ * port. The property is read once, when the crypto manager is created, so it is written
+ * to the configuration before the server is started for the first time rather than set
+ * with dsconfig afterwards.
+ */
+ private static void addAdsCertificateArguments(SecurityOptions sec, List<String> argList)
+ {
+ if (sec.getReplicationUsesKeyStore())
+ {
+ for (String alias : sec.getAliasesToUse())
+ {
+ argList.add("--adsCertNickName");
+ argList.add(alias);
+ }
+ }
+ }
+
private static void addCertificateArguments(List<String> argList, SecurityOptions sec,
Collection<String> aliasesInKeyStore, String keyStoreDN, String trustStoreDN)
{
@@ -4122,6 +4190,28 @@
return getPath2("keystore.pin");
}
+ /**
+ * Returns the path of the trust store used for server to server communication, the one
+ * the replication port reads its key pair and its trusted certificates from.
+ *
+ * @return the path of the ads-truststore.
+ */
+ private String getAdsTrustStorePath()
+ {
+ return getPath2("ads-truststore");
+ }
+
+ /**
+ * Returns the path of the file holding the PIN of the trust store used for server to
+ * server communication.
+ *
+ * @return the path of the ads-truststore PIN file.
+ */
+ private String getAdsTrustStorePinPath()
+ {
+ return getPath2("ads-truststore.pin");
+ }
+
private String getPath2(String relativePath)
{
String parentFile = getPath(getInstancePath(), Installation.CONFIG_PATH_RELATIVE);
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java
index 771574a..02efe2c 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java
@@ -267,6 +267,7 @@
private StringArgument keyManagerProviderDN;
private StringArgument trustManagerProviderDN;
private StringArgument certNickNames;
+ private StringArgument adsCertNickNames;
private StringArgument keyManagerPath;
private StringArgument serverRoot;
private StringArgument backendType;
@@ -324,6 +325,7 @@
updateStartTLS();
updateKeyManager();
updateTrustManager();
+ updateCryptoManagerCertNickname();
updateRootUser(rootDN, rootPW);
addFQDNDigestMD5();
updateCryptoCipher();
@@ -440,6 +442,12 @@
.multiValued()
.valuePlaceholder(INFO_NICKNAME_PLACEHOLDER.get())
.buildAndAddToParser(argParser);
+ adsCertNickNames =
+ StringArgument.builder("adsCertNickName")
+ .description(INFO_CONFIGDS_DESCRIPTION_ADS_CERTNICKNAME.get())
+ .multiValued()
+ .valuePlaceholder(INFO_NICKNAME_PLACEHOLDER.get())
+ .buildAndAddToParser(argParser);
baseDNString =
StringArgument.builder(OPTION_LONG_BASEDN)
.shortIdentifier(OPTION_SHORT_BASEDN)
@@ -1159,6 +1167,39 @@
}
/**
+ * Sets the certificate nicknames the crypto manager presents for server to server
+ * communication, that is on the replication port. The property replaces the value the
+ * template configuration holds, {@code ads-certificate}, which is the self-signed key
+ * pair the trust store backend generates: with a key pair of its own provisioned into
+ * the trust store, the server presents that one instead.
+ * <p>
+ * The value is written to the configuration rather than set with dsconfig once the
+ * server runs because the crypto manager reads the property once, when it is created at
+ * startup, and replication caches the value when a replication server or a replicated
+ * domain is created.
+ */
+ private void updateCryptoManagerCertNickname() throws ConfigureDSException
+ {
+ if (!adsCertNickNames.isPresent())
+ {
+ return;
+ }
+ final List<String> attrValues = adsCertNickNames.getValues();
+ try
+ {
+ updateConfigEntryWithAttribute(
+ DN_CRYPTO_MANAGER,
+ ATTR_SSL_CERT_NICKNAME,
+ CoreSchema.getDirectoryStringSyntax(),
+ attrValues.toArray(new Object[attrValues.size()]));
+ }
+ catch (final Exception e)
+ {
+ throw new ConfigureDSException(e, ERR_CONFIGDS_CANNOT_UPDATE_CRYPTO_MANAGER_CERT_NICKNAME.get(e));
+ }
+ }
+
+ /**
* Check that the cipher specified is supported. This is intended to fix
* issues with JVM that do not support the default cipher (see issue 3075 for
* instance).
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java
index ee1de64..b652428 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java
@@ -855,9 +855,20 @@
final SecurityOptions securityOptions = SecurityOptions.createOptionsForCertificatType(
certType, pathToCertificat, pwd, enableSSL, enableStartTLS, sslPort, certNicknames);
+ securityOptions.setReplicationUsesKeyStore(argParser.useKeyStoreForReplicationArg.isPresent());
+ securityOptions.setReplicationCaCertFiles(getReplicationCaCertFiles());
uData.setSecurityOptions(securityOptions);
}
+ private List<File> getReplicationCaCertFiles() {
+ final List<File> caCertFiles = new ArrayList<>();
+ for (String path : argParser.replicationCaCertFileArg.getValues())
+ {
+ caCertFiles.add(new File(path));
+ }
+ return caCertFiles;
+ }
+
private List<String> getCertNickNames() {
List<String> certNicknames = argParser.certNicknameArg.getValues();
if ((certNicknames == null) || (certNicknames.size() == 0)) {
@@ -1747,6 +1758,8 @@
throw new IllegalStateException("Unexpected cert type: "+ certType);
}
}
+ securityOptions.setReplicationUsesKeyStore(argParser.useKeyStoreForReplicationArg.isPresent());
+ securityOptions.setReplicationCaCertFiles(getReplicationCaCertFiles());
return securityOptions;
}
@@ -1934,11 +1947,16 @@
}
for (String certNickname : certNicknames)
{
- // Check if the certificate alias is in the list.
+ // Check if the certificate alias is in the list. JKS, JCEKS and PKCS#12 key
+ // stores fold aliases to lower case, a BCFKS key store looks them up exactly:
+ // a nickname which differs in case from the alias would pass here and fail
+ // once the certificate is read from the key store.
boolean found = false;
for (int i = 0; i < aliases.length && !found; i++)
{
- found = aliases[i].equalsIgnoreCase(certNickname);
+ found = type == SecurityOptions.CertificateType.BCFKS
+ ? aliases[i].equals(certNickname)
+ : aliases[i].equalsIgnoreCase(certNickname);
}
if (!found)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDSArgumentParser.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDSArgumentParser.java
index f46da3c..f406843 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDSArgumentParser.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDSArgumentParser.java
@@ -13,6 +13,7 @@
*
* Copyright 2008-2010 Sun Microsystems, Inc.
* Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.tools;
@@ -20,6 +21,7 @@
import static com.forgerock.opendj.cli.CliMessages.INFO_JMXPORT_PLACEHOLDER;
import static com.forgerock.opendj.cli.CliMessages.INFO_KEYSTORE_PWD_FILE_PLACEHOLDER;
import static com.forgerock.opendj.cli.CliMessages.INFO_NUM_ENTRIES_PLACEHOLDER;
+import static com.forgerock.opendj.cli.CliMessages.INFO_PATH_PLACEHOLDER;
import static com.forgerock.opendj.cli.CliMessages.INFO_PORT_PLACEHOLDER;
import static com.forgerock.opendj.cli.CliMessages.INFO_ROOT_USER_PWD_FILE_PLACEHOLDER;
import static com.forgerock.opendj.cli.CommonArguments.*;
@@ -28,6 +30,7 @@
import static org.opends.messages.ToolMessages.*;
+import java.io.File;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -94,6 +97,8 @@
StringArgument directoryManagerDNArg;
private StringArgument directoryManagerPwdStringArg;
StringArgument useJavaKeyStoreArg;
+ BooleanArgument useKeyStoreForReplicationArg;
+ StringArgument replicationCaCertFileArg;
StringArgument useJCEKSArg;
StringArgument usePkcs12Arg;
private StringArgument keyStorePasswordArg;
@@ -396,6 +401,20 @@
.buildArgument();
addDefaultArgument(certNicknameArg);
+ useKeyStoreForReplicationArg =
+ BooleanArgument.builder("useKeyStoreForReplication")
+ .description(INFO_INSTALLDS_DESCRIPTION_USE_KEYSTORE_FOR_REPLICATION.get())
+ .buildArgument();
+ addArgument(useKeyStoreForReplicationArg);
+
+ replicationCaCertFileArg =
+ StringArgument.builder("replicationCaCertFile")
+ .description(INFO_INSTALLDS_DESCRIPTION_REPLICATION_CA_CERT_FILE.get())
+ .multiValued()
+ .valuePlaceholder(INFO_PATH_PLACEHOLDER.get())
+ .buildArgument();
+ addArgument(replicationCaCertFileArg);
+
connectTimeoutArg = connectTimeOutArgument();
addArgument(connectTimeoutArg);
@@ -658,6 +677,52 @@
enableStartTLSArg.getLongIdentifier()));
}
}
+
+ checkReplicationCertificateArguments(errorMessages);
+ }
+
+ /**
+ * Checks the arguments which provision the trust store used for server to server
+ * communication. The key pair presented on the replication port is copied out of an
+ * existing key store, so there has to be one, and its private key has to be readable:
+ * neither a certificate generated by the installer nor a key held in a PKCS#11 token
+ * qualifies.
+ * @param errorMessages the list of messages to which we add the error messages
+ * describing the problems encountered during the execution of the checking.
+ */
+ private void checkReplicationCertificateArguments(Collection<LocalizableMessage> errorMessages)
+ {
+ if (useKeyStoreForReplicationArg.isPresent()
+ && !useJavaKeyStoreArg.isPresent()
+ && !useJCEKSArg.isPresent()
+ && !usePkcs12Arg.isPresent()
+ && !useBcfksArg.isPresent())
+ {
+ errorMessages.add(ERR_INSTALLDS_REPLICATION_KEYSTORE_REQUIRED.get(
+ useKeyStoreForReplicationArg.getLongIdentifier(),
+ useJavaKeyStoreArg.getLongIdentifier(),
+ useJCEKSArg.getLongIdentifier(),
+ usePkcs12Arg.getLongIdentifier(),
+ useBcfksArg.getLongIdentifier()));
+ }
+
+ if (replicationCaCertFileArg.isPresent())
+ {
+ if (!useKeyStoreForReplicationArg.isPresent())
+ {
+ errorMessages.add(ERR_INSTALLDS_REPLICATION_CA_CERT_FILE_REQUIRES.get(
+ replicationCaCertFileArg.getLongIdentifier(),
+ useKeyStoreForReplicationArg.getLongIdentifier()));
+ }
+ for (String path : replicationCaCertFileArg.getValues())
+ {
+ final File certFile = new File(path);
+ if (!certFile.exists() || !certFile.isFile())
+ {
+ errorMessages.add(ERR_INSTALLDS_REPLICATION_CA_CERT_FILE_INVALID.get(path));
+ }
+ }
+ }
}
/**
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/util/CertificateManager.java b/opendj-server-legacy/src/main/java/org/opends/server/util/CertificateManager.java
index 4b10875..cc3e997 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/util/CertificateManager.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/util/CertificateManager.java
@@ -13,13 +13,17 @@
*
* Copyright 2008-2010 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.util;
import java.io.File;
import java.io.FileInputStream;
+import java.security.GeneralSecurityException;
+import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
+import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
@@ -81,6 +85,8 @@
private static final String CERT_ALIAS_MSG = "certificate alias";
private static final String CERT_REQUEST_FILE_MSG =
"certificate request file";
+ private static final String SOURCE_KEYSTORE_MSG = "source key store";
+ private static final String CERT_MSG = "certificate";
/** The parsed key store backing this certificate manager. */
private KeyStore keyStore;
@@ -191,6 +197,17 @@
/**
+ * Retrieves the path of the key store this certificate manager works on.
+ *
+ * @return The path of the key store.
+ */
+ public String getKeyStorePath() {
+ return keyStorePath;
+ }
+
+
+
+ /**
* Indicates whether the provided alias is in use in the key store.
*
* @param alias The alias for which to make the determination. It must not
@@ -268,6 +285,113 @@
/**
+ * Retrieves the certificate chain of the key entry with the specified alias from the
+ * key store, the certificate the key belongs to first and its issuers next.
+ *
+ * @param alias The alias of the key entry whose chain to retrieve. It must not be
+ * {@code null} or empty.
+ *
+ * @return The certificate chain, or {@code null} if the key store holds no key entry
+ * under the specified alias.
+ *
+ * @throws KeyStoreException If a problem occurs while interacting with the key store,
+ * or the key store does not exist.
+ */
+ public Certificate[] getCertificateChain(String alias)
+ throws KeyStoreException {
+ ensureValid(alias, CERT_ALIAS_MSG);
+ KeyStore ks = getKeyStore();
+ if (ks == null) {
+ LocalizableMessage msg = ERR_CERTMGR_KEYSTORE_NONEXISTANT.get();
+ throw new KeyStoreException(msg.toString());
+ }
+ return ks.getCertificateChain(alias);
+ }
+
+
+ /**
+ * Copies the key entry with the specified alias from the provided key store into this
+ * one, with its whole certificate chain. The private key is re-encrypted with the
+ * password of this key store: the key managers of the server are initialised with the
+ * store password only, so a key which kept the password of the key store it comes from
+ * could not be read back.
+ * <p>
+ * The certificates of the chain are not added as trusted certificates, as only a
+ * trusted certificate entry is a trust anchor. Use {@link #addTrustedCertificate} for
+ * the issuers which have to be trusted.
+ *
+ * @param alias The alias to store the key entry under in this key store. It
+ * must not be {@code null} or empty.
+ * @param sourceManager The certificate manager of the key store holding the key entry
+ * to copy. It must not be {@code null}.
+ * @param sourceAlias The alias of the key entry to copy. It must not be
+ * {@code null} or empty.
+ *
+ * @throws KeyStoreException If the source key store holds no key entry under the
+ * provided alias, if its private key is protected by a
+ * password other than the one of the source key store, if
+ * the alias is already in use in this key store, or a
+ * problem occurs while interacting with either key store.
+ */
+ public void importKeyEntry(String alias, CertificateManager sourceManager, String sourceAlias)
+ throws KeyStoreException {
+ ensureValid(alias, CERT_ALIAS_MSG);
+ ensureValid(sourceAlias, CERT_ALIAS_MSG);
+ if (sourceManager == null) {
+ LocalizableMessage msg = ERR_CERTMGR_VALUE_INVALID.get(SOURCE_KEYSTORE_MSG);
+ throw new NullPointerException(msg.toString());
+ }
+
+ final Certificate[] chain = sourceManager.getCertificateChain(sourceAlias);
+ final Key privateKey;
+ try {
+ privateKey = sourceManager.getKeyStore().getKey(sourceAlias, sourceManager.password);
+ } catch (UnrecoverableKeyException e) {
+ // The key is protected by a password of its own. The key managers of the server
+ // unlock private keys with the store password only, so this is the same limitation
+ // the key store already has for LDAPS: say so rather than "Cannot recover key".
+ throw new KeyStoreException(
+ ERR_CERTMGR_KEY_PASSWORD_DIFFERS.get(sourceAlias, sourceManager.keyStorePath).toString(), e);
+ } catch (GeneralSecurityException e) {
+ throw new KeyStoreException(
+ ERR_CERTMGR_IMPORT_KEY_ENTRY.get(sourceAlias, e.getMessage()).toString(), e);
+ }
+ if (privateKey == null || chain == null || chain.length == 0) {
+ LocalizableMessage msg =
+ ERR_CERTMGR_NO_KEY_ENTRY.get(sourceAlias, sourceManager.keyStorePath);
+ throw new KeyStoreException(msg.toString());
+ }
+
+ keyStore = null;
+ Platform.importKeyEntry(getKeyStore(), keyStoreType, keyStorePath, alias, password, privateKey, chain);
+ }
+
+
+ /**
+ * Adds the provided certificate to the key store as a trusted certificate entry. Only
+ * such an entry is a trust anchor: of a key entry, the trust managers take the
+ * certificate the key belongs to and none of its issuers.
+ *
+ * @param alias The alias to use for the certificate. It must not be
+ * {@code null} or empty.
+ * @param certificate The certificate to trust. It must not be {@code null}.
+ *
+ * @throws KeyStoreException If the alias is already in use, or a problem occurs while
+ * interacting with the key store.
+ */
+ public void addTrustedCertificate(String alias, Certificate certificate)
+ throws KeyStoreException {
+ ensureValid(alias, CERT_ALIAS_MSG);
+ if (certificate == null) {
+ LocalizableMessage msg = ERR_CERTMGR_VALUE_INVALID.get(CERT_MSG);
+ throw new NullPointerException(msg.toString());
+ }
+ keyStore = null;
+ Platform.addTrustedCertificate(getKeyStore(), keyStoreType, keyStorePath, alias, password, certificate);
+ }
+
+
+ /**
* Generates a self-signed certificate using the provided information.
*
* @param keyType Specifies the key size, key and signature algorithms.
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java b/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java
index ea304f8..d4267e5 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java
@@ -23,6 +23,7 @@
import java.io.FileOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
+import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
@@ -56,6 +57,7 @@
import static org.opends.messages.UtilityMessages.ERR_CERTMGR_CERT_REPLIES_INVALID;
import static org.opends.messages.UtilityMessages.ERR_CERTMGR_DELETE_ALIAS;
import static org.opends.messages.UtilityMessages.ERR_CERTMGR_GEN_SELF_SIGNED_CERT;
+import static org.opends.messages.UtilityMessages.ERR_CERTMGR_IMPORT_KEY_ENTRY;
import static org.opends.messages.UtilityMessages.ERR_CERTMGR_KEYSTORE_NONEXISTANT;
import static org.opends.messages.UtilityMessages.ERR_CERTMGR_TRUSTED_CERT;
@@ -190,6 +192,62 @@
}
}
+ private final void importKeyEntry(KeyStore ks, String ksType, String ksPath, String alias, char[] pwd,
+ Key privateKey, Certificate[] chain) throws KeyStoreException
+ {
+ try
+ {
+ if (ks == null)
+ {
+ ks = KeyStore.getInstance(ksType);
+ ks.load(null, pwd);
+ }
+ else if (ks.containsAlias(alias))
+ {
+ // setKeyEntry would silently replace whatever the alias holds.
+ LocalizableMessage msg = ERR_CERTMGR_ALIAS_ALREADY_EXISTS.get(alias);
+ throw new KeyStoreException(msg.toString());
+ }
+ // The key is re-encrypted with the password of this key store: the key managers of
+ // the server are initialised with the store password only, so a key which kept the
+ // password of the key store it comes from could not be read back.
+ ks.setKeyEntry(alias, privateKey, pwd, chain);
+ try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
+ ks.store(fileOutStream, pwd);
+ }
+ }
+ catch (Exception e)
+ {
+ throw new KeyStoreException(ERR_CERTMGR_IMPORT_KEY_ENTRY.get(alias, e.getMessage()).toString(), e);
+ }
+ }
+
+ private final void addTrustedCertificate(KeyStore ks, String ksType, String ksPath, String alias, char[] pwd,
+ Certificate certificate) throws KeyStoreException
+ {
+ try
+ {
+ if (ks == null)
+ {
+ ks = KeyStore.getInstance(ksType);
+ ks.load(null, pwd);
+ }
+ else if (ks.containsAlias(alias))
+ {
+ LocalizableMessage msg = ERR_CERTMGR_ALIAS_ALREADY_EXISTS.get(alias);
+ throw new KeyStoreException(msg.toString());
+ }
+ ks.setCertificateEntry(alias, certificate);
+ try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
+ ks.store(fileOutStream, pwd);
+ }
+ }
+ catch (Exception e)
+ {
+ throw new KeyStoreException(ERR_CERTMGR_TRUSTED_CERT.get(alias, e.getMessage()).toString(), e);
+ }
+ }
+
private static final KeyStore generateSelfSignedCertificate(KeyStore ks,
String ksType, String ksPath, KeyType keyType, String alias, char[] pwd, String dn,
int validity) throws KeyStoreException
@@ -333,6 +391,63 @@
}
/**
+ * Copy a key entry, that is a private key and its certificate chain, into the provided
+ * keystore; creating the keystore with the provided type and path if it doesn't exist.
+ * The private key is re-encrypted with the password of the destination keystore.
+ *
+ * @param ks
+ * The keystore to add the key entry to, may be null if it doesn't exist.
+ * @param ksType
+ * The type to use if the keystore is created.
+ * @param ksPath
+ * The path to the keystore.
+ * @param alias
+ * The alias to store the key entry under.
+ * @param pwd
+ * The keystore password, used for the private key as well.
+ * @param privateKey
+ * The private key to store.
+ * @param chain
+ * The certificate chain of the private key, the certificate it belongs to first.
+ * @throws KeyStoreException
+ * If the alias is already in use, or an error occurred adding the key entry to
+ * the keystore.
+ */
+ public static void importKeyEntry(KeyStore ks, String ksType, String ksPath, String alias, char[] pwd,
+ Key privateKey, Certificate[] chain) throws KeyStoreException
+ {
+ IMPL.importKeyEntry(ks, ksType, ksPath, alias, pwd, privateKey, chain);
+ }
+
+ /**
+ * Add the provided certificate to the provided keystore as a trusted certificate entry;
+ * creating the keystore with the provided type and path if it doesn't exist. Only such
+ * an entry is a trust anchor: the certificate chain of a key entry is not, the trust
+ * managers take the certificate the key belongs to and none of its issuers.
+ *
+ * @param ks
+ * The keystore to add the certificate to, may be null if it doesn't exist.
+ * @param ksType
+ * The type to use if the keystore is created.
+ * @param ksPath
+ * The path to the keystore.
+ * @param alias
+ * The alias to store the certificate under.
+ * @param pwd
+ * The keystore password.
+ * @param certificate
+ * The certificate to trust.
+ * @throws KeyStoreException
+ * If the alias is already in use, or an error occurred adding the certificate
+ * to the keystore.
+ */
+ public static void addTrustedCertificate(KeyStore ks, String ksType, String ksPath, String alias, char[] pwd,
+ Certificate certificate) throws KeyStoreException
+ {
+ IMPL.addTrustedCertificate(ks, ksType, ksPath, alias, pwd, certificate);
+ }
+
+ /**
* Delete the specified alias from the provided keystore.
*
* @param ks
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties
index 4645220..9a304f6 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
@@ -255,6 +256,28 @@
INFO_ERROR_BROWSER_DISPLAY_TITLE=Error
INFO_ERROR_CONFIGURING=Error Configuring Directory Server.
INFO_ERROR_CONFIGURING_CERTIFICATE=Error Configuring Certificates.
+ERR_INSTALL_ADS_TRUSTSTORE=An error occurred while provisioning the trust store \
+ %s, used for server to server communication.%nThe error is: %s
+ERR_INSTALL_ADS_TRUSTSTORE_NO_KEY_PAIR=The key store %s holds no key pair, that is \
+ no private key with its certificate, under the alias %s.
+ERR_INSTALL_ADS_TRUSTSTORE_NO_TRUST_ANCHOR=The key store %s holds no issuing \
+ certificate for the key pair or pairs %s: the certificate chain stops at the \
+ certificate itself. A server trusts its peers on the replication port through the \
+ certificate authorities held in its trust store, and the chain of a key pair is not \
+ one of them, so this server would trust no peer and could join no topology. Use a \
+ key store which holds the whole certificate chain, or name the certificates to trust \
+ with the --replicationCaCertFile argument.
+ERR_INSTALL_ADS_TRUSTSTORE_RESERVED_ALIAS=The certificate nickname %s cannot be \
+ presented on the replication port: the trust store used for server to server \
+ communication reserves the alias %s for the instance key the server generates when \
+ it first starts, and the aliases starting with %s for the certificates it trusts. \
+ Store the key pair under another alias.
+ERR_INSTALL_ADS_TRUSTSTORE_EXISTS=The trust store %s, used for server to server \
+ communication, already exists and is left as it is.
+ERR_INSTALL_ADS_TRUSTSTORE_CA_CERT_FILE_EMPTY=The certificate file %s holds no \
+ certificate.
+ERR_INSTALL_ADS_TRUSTSTORE_CA_CERT_FILE_UNREADABLE=The certificate file %s cannot \
+ be read as X.509 certificates, DER or PEM encoded: %s
INFO_ERROR_CONFIGURING_REMOTE_GENERIC=An unexpected error occurred \
configuring server %s.%nThe error is: %s
INFO_ERROR_CONNECTING_TO_LOCAL=An error occurred connecting to the server.
@@ -584,6 +607,7 @@
INFO_PROGRESS_UNCONFIGURING_REPLICATION_REMOTE=Unconfiguring Replication on \
%s
INFO_PROGRESS_UPDATING_CERTIFICATES=Configuring Certificates
+INFO_PROGRESS_UPDATING_ADS_TRUSTSTORE=Configuring Replication Certificates
INFO_PROGRESSBAR_INITIAL_LABEL=Starting...
INFO_PROGRESSBAR_TOOLTIP=Progress Bar
INFO_PWD_TOO_SHORT=The minimum length required for the Root User \
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties b/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
index c03e0bc..930df01 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
@@ -1424,12 +1424,19 @@
INFO_CONFIGDS_DESCRIPTION_CERTNICKNAME_871=Nickname of the \
certificate that the connection handler should use when accepting SSL-based \
connections or performing StartTLS negotiation
+INFO_CONFIGDS_DESCRIPTION_ADS_CERTNICKNAME=Nickname of the certificate that \
+ the crypto manager should present for server to server communication, that is on \
+ the replication port. It replaces the nickname of the self-signed key pair the \
+ trust store backend generates
ERR_CONFIGDS_KEYMANAGER_PROVIDER_DN_REQUIRED_872=ERROR: You must \
provide the %s argument when providing the %s argument
ERR_CONFIGDS_CANNOT_UPDATE_CERT_NICKNAME_873=An error occurred while \
attempting to update the nickname of the certificate that the connection \
handler should use when accepting SSL-based connections or performing \
StartTLS negotiation: %s
+ERR_CONFIGDS_CANNOT_UPDATE_CRYPTO_MANAGER_CERT_NICKNAME=An error occurred while \
+ attempting to update the nickname of the certificate that the crypto manager \
+ should present for server to server communication: %s
INFO_LDAPMODIFY_DESCRIPTION_FILENAME_874=LDIF file containing \
the changes to apply
ERR_MAKELDIF_TEMPLATE_INVALID_PARENT_TEMPLATE_875=The parent template %s \
@@ -1842,6 +1849,30 @@
INFO_INSTALLDS_DESCRIPTION_CERT_NICKNAME_1405=Nickname of the \
certificate that the server should use when accepting SSL-based \
connections or performing StartTLS negotiation
+INFO_INSTALLDS_DESCRIPTION_USE_KEYSTORE_FOR_REPLICATION=Secure replication \
+ with the key pair given for SSL-based connections instead of a self-signed \
+ certificate generated by the server. The key pair, and the certificates it is \
+ issued by, are copied into the trust store used for server to server \
+ communication, and the certificate nickname is set on the crypto manager. \
+ Requires a key store the private key can be read from, given for LDAPS or \
+ StartTLS: the key store is only accepted along with --ldapsPort or \
+ --enableStartTLS
+INFO_INSTALLDS_DESCRIPTION_REPLICATION_CA_CERT_FILE=Path of a file holding \
+ certificates to trust on the replication port, typically the certificate of \
+ your Certificate Authority or its chain; every certificate of the file is \
+ trusted. Needed only when the key store does not hold the whole certificate \
+ chain of the key pair, as the issuing certificates found in that chain are \
+ trusted as well. This argument can be used several times
+ERR_INSTALLDS_REPLICATION_KEYSTORE_REQUIRED=The --%s argument requires an \
+ existing key store: use it together with --%s, --%s, --%s or --%s. The key pair \
+ presented on the replication port is copied out of that key store, so it can be \
+ neither a certificate generated by the installer nor a key held in a PKCS#11 \
+ token, whose private keys cannot be exported
+ERR_INSTALLDS_REPLICATION_CA_CERT_FILE_REQUIRES=The --%s argument can only be \
+ used together with --%s: certificates are trusted on the replication port for \
+ the key pair which is presented there
+ERR_INSTALLDS_REPLICATION_CA_CERT_FILE_INVALID=The certificate file %s does not \
+ exist or is not a file
ERR_INSTALLDS_SEVERAL_CERTIFICATE_TYPE_SPECIFIED_1406=You have \
specified several certificate types to be used. Only one certificate type \
(self-signed, JKS, JCEKS, PKCS#12 or PCKS#11) is allowed
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/utility.properties b/opendj-server-legacy/src/messages/org/opends/messages/utility.properties
index 428e26b..cf74391 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/utility.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/utility.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2009 Sun Microsystems, Inc.
# Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
@@ -348,6 +349,14 @@
ERR_CERTMGR_TRUSTED_CERT_292=The trusted certificate associated \
with alias %s could not be added to keystore because of the following \
reason: %s
+ERR_CERTMGR_NO_KEY_ENTRY_344=There is no key entry, that is a private key \
+and its certificate chain, under the alias %s in the key store %s
+ERR_CERTMGR_IMPORT_KEY_ENTRY_345=The key entry with alias %s could not be \
+imported because of the following reason: %s
+ERR_CERTMGR_KEY_PASSWORD_DIFFERS_346=The private key with alias %s in the key \
+store %s is protected by a password other than the password of the key store. \
+The server unlocks private keys with the key store password only, so protect the \
+key with that password, for instance with keytool -keypasswd
ERR_CERTMGR_FILE_NAME_INVALID_293=The %s is invalid because it is \
null
ERR_CERTMGR_VALUE_INVALID_294=The argument %s is invalid because it \
diff --git a/opendj-server-legacy/src/test/java/org/opends/quicksetup/AdsTrustStoreInstallTestCase.java b/opendj-server-legacy/src/test/java/org/opends/quicksetup/AdsTrustStoreInstallTestCase.java
new file mode 100644
index 0000000..6fd8e58
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/quicksetup/AdsTrustStoreInstallTestCase.java
@@ -0,0 +1,447 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.quicksetup;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.security.KeyStore;
+import java.security.Provider;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
+import org.opends.quicksetup.util.ServerController;
+import org.opends.quicksetup.util.ZipExtractor;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.util.CertificateFixture;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.util.OperatingSystem;
+
+/**
+ * Installs a server with {@code setup} and the arguments which provision the trust store
+ * used for server to server communication, and checks what the installed instance holds:
+ * this is the whole point of the arguments, and the pieces which carry the values from
+ * the command line to the trust store and to the crypto manager sit in three different
+ * tools.
+ * <p>
+ * Each installation uses a different key store type, so that every arm of the mapping
+ * from the key store argument to the key store type is walked by one of them.
+ */
+public class AdsTrustStoreInstallTestCase extends DirectoryServerTestCase
+{
+ private static final String KEY_STORE_PASSWORD = "keyStorePassword";
+ private static final String CERT_NICKNAME = "server-cert";
+ private static final String SECOND_CERT_NICKNAME = "server-cert-2";
+ /** How long one run of setup, or one stop of the server it started, is given. */
+ private static final long TIMEOUT_MINUTES = 5;
+
+ /**
+ * The installed server presents the CA-signed key pairs on the replication port and
+ * trusts the authority which issued them, with no manual {@code keytool} pass. Two key
+ * pairs are named, so that both nicknames reach the crypto manager.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testSetupProvisionsTheAdsTrustStore() throws Exception
+ {
+ final File workspace = TestCaseUtils.createTemporaryDirectory("adsTrustStoreInstall");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File keyStore = new File(workspace, "server.p12");
+ ca.addKeyEntry(keyStore, "PKCS12", KEY_STORE_PASSWORD, CERT_NICKNAME, "CN=host.example.com", true);
+ ca.addKeyEntry(keyStore, "PKCS12", KEY_STORE_PASSWORD, SECOND_CERT_NICKNAME, "CN=host.example.com", true);
+
+ final File serverRoot = installServer(workspace, "--usePkcs12keyStore", keyStore, "-O",
+ "--certNickname", CERT_NICKNAME, "--certNickname", SECOND_CERT_NICKNAME, "--useKeyStoreForReplication");
+
+ final KeyStore keys = loadAdsTrustStore(serverRoot);
+ assertTrue(keys.isKeyEntry(CERT_NICKNAME), "the key pair to present was not imported");
+ assertTrue(keys.isKeyEntry(SECOND_CERT_NICKNAME), "the second key pair to present was not imported");
+ assertEquals(keys.getCertificateChain(CERT_NICKNAME).length, 2);
+ assertEquals(keys.getCertificateAlias(ca.getCaCertificate()), "ads-ca-1",
+ "the issuing certificate is not trusted");
+ assertFalse(keys.containsAlias("ads-ca-2"), "the shared issuer is trusted twice");
+
+ assertEquals(cryptoManagerCertNicknames(serverRoot), List.of(CERT_NICKNAME, SECOND_CERT_NICKNAME),
+ "the crypto manager does not present both key pairs");
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(workspace);
+ }
+ }
+
+ /**
+ * A key store which holds the issued certificate alone is enough when the certificates
+ * of the authorities are named separately, and every certificate of the named file is
+ * trusted. With no nickname given, the only key pair of the key store is the one
+ * presented.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testSetupTrustsTheNamedCaCertificates() throws Exception
+ {
+ final File workspace = TestCaseUtils.createTemporaryDirectory("adsTrustStoreCaFile");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final CertificateFixture otherCa = new CertificateFixture("CN=Other CA,O=Example");
+ final File keyStore = new File(workspace, "server.jks");
+ ca.addKeyEntry(keyStore, "JKS", KEY_STORE_PASSWORD, CERT_NICKNAME, "CN=host.example.com", false);
+ final File caChainFile = new File(workspace, "ca-chain.crt");
+ CertificateFixture.writeCertificates(caChainFile, ca.getCaCertificate(), otherCa.getCaCertificate());
+
+ final File serverRoot = installServer(workspace, "--useJavaKeystore", keyStore, "-O",
+ "--useKeyStoreForReplication", "--replicationCaCertFile", caChainFile.getAbsolutePath());
+
+ final KeyStore keys = loadAdsTrustStore(serverRoot);
+ assertTrue(keys.isKeyEntry(CERT_NICKNAME), "the key pair to present was not imported");
+ assertEquals(keys.getCertificateAlias(ca.getCaCertificate()), "ads-ca-1",
+ "the first named certificate is not trusted");
+ assertEquals(keys.getCertificateAlias(otherCa.getCaCertificate()), "ads-ca-2",
+ "the second certificate of the named file is not trusted");
+ assertEquals(cryptoManagerCertNicknames(serverRoot), List.of(CERT_NICKNAME),
+ "the only key pair of the key store is not the one presented");
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(workspace);
+ }
+ }
+
+ /**
+ * A key store which holds the issued certificate alone, with no certificate to trust
+ * named either, would install a server which trusts no peer. The installation stops
+ * and says which key pair is at fault, rather than leave the failure to show up as a
+ * handshake error once the server joins a topology, and it stops before the
+ * configuration and the certificates of the server are written.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testSetupRefusesAKeyStoreWithNoCertificateToTrust() throws Exception
+ {
+ final File workspace = TestCaseUtils.createTemporaryDirectory("adsTrustStoreNoAnchor");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File keyStore = new File(workspace, "server.jceks");
+ ca.addKeyEntry(keyStore, "JCEKS", KEY_STORE_PASSWORD, CERT_NICKNAME, "CN=host.example.com", false);
+
+ final SetupResult result = runSetup(workspace, "--useJCEKS", keyStore, "-O",
+ "--certNickname", CERT_NICKNAME, "--useKeyStoreForReplication");
+ assertNotEquals(result.exitCode, 0, "setup installed a server which trusts no peer:\n" + result.output);
+ assertTrue(result.output.contains(CERT_NICKNAME), result.output);
+ assertTrue(result.output.contains("--replicationCaCertFile"), result.output);
+ assertFalse(configFile(result.serverRoot, "ads-truststore").exists(), "a trust store was left behind");
+ assertFalse(configFile(result.serverRoot, "ads-truststore.pin").exists(), "a PIN file was left behind");
+ assertEquals(cryptoManagerCertNicknames(result.serverRoot), List.of("ads-certificate"),
+ "the configuration was written before the refusal");
+ assertFalse(configFile(result.serverRoot, "truststore").exists(),
+ "the certificates were configured before the refusal");
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(workspace);
+ }
+ }
+
+ /**
+ * A BCFKS key store is provisioned like the others, and the nickname has to match its
+ * alias exactly: a BCFKS key store looks aliases up exactly where JKS, JCEKS and PKCS#12
+ * fold them to lower case, so a nickname differing in case names a key pair the
+ * installer cannot read, and is refused when the arguments are checked rather than
+ * reported as a missing key pair half way through.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testSetupProvisionsABcfksKeyStoreUnderItsExactAlias() throws Exception
+ {
+ final File workspace = TestCaseUtils.createTemporaryDirectory("adsTrustStoreBcfks");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File keyStore = new File(workspace, "server.bcfks");
+ final Provider bcFips = new BouncyCastleFipsProvider();
+ final boolean registered = Security.addProvider(bcFips) != -1;
+ try
+ {
+ ca.addKeyEntry(keyStore, "BCFKS", KEY_STORE_PASSWORD, "Server-Cert", "CN=host.example.com", true);
+ }
+ finally
+ {
+ if (registered)
+ {
+ Security.removeProvider(bcFips.getName());
+ }
+ }
+
+ final SetupResult refused = runSetup(workspace, "--useBcfksKeystore", keyStore, "-O",
+ "--certNickname", CERT_NICKNAME, "--useKeyStoreForReplication");
+ assertNotEquals(refused.exitCode, 0, "setup accepted a nickname the BCFKS key store does not hold:\n"
+ + refused.output);
+ assertTrue(refused.output.contains("Server-Cert"), "the aliases of the key store are not listed:\n"
+ + refused.output);
+ assertFalse(configFile(refused.serverRoot, "config.ldif").exists(), "the refusal came after the arguments");
+ TestCaseUtils.deleteDirectory(refused.serverRoot);
+
+ final File serverRoot = installServer(workspace, "--useBcfksKeystore", keyStore, "-O",
+ "--certNickname", "Server-Cert", "--useKeyStoreForReplication");
+ final KeyStore keys = loadAdsTrustStore(serverRoot);
+ assertTrue(keys.isKeyEntry("Server-Cert"), "the key pair to present was not imported");
+ assertEquals(keys.getCertificateAlias(ca.getCaCertificate()), "ads-ca-1",
+ "the issuing certificate is not trusted");
+ assertEquals(cryptoManagerCertNicknames(serverRoot), List.of("Server-Cert"));
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(workspace);
+ }
+ }
+
+ /**
+ * Without the arguments, the installation is left as it was: the trust store is created
+ * by the server on its first start and the crypto manager keeps presenting the
+ * self-signed key pair the trust store backend generates.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testSetupLeavesTheSelfSignedInstanceKeyByDefault() throws Exception
+ {
+ final File workspace = TestCaseUtils.createTemporaryDirectory("adsTrustStoreDefault");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File keyStore = new File(workspace, "server.p12");
+ ca.addKeyEntry(keyStore, "PKCS12", KEY_STORE_PASSWORD, CERT_NICKNAME, "CN=host.example.com", true);
+
+ final File serverRoot = installServer(workspace, "--usePkcs12keyStore", keyStore, "-O",
+ "--certNickname", CERT_NICKNAME);
+
+ assertFalse(configFile(serverRoot, "ads-truststore").exists(),
+ "the trust store is provisioned without being asked for");
+ assertEquals(cryptoManagerCertNicknames(serverRoot), List.of("ads-certificate"));
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(workspace);
+ }
+ }
+
+ /**
+ * The installed server starts on the provisioned trust store: the trust store backend
+ * opens it with the PIN setup wrote, generates the instance key next to the imported
+ * entries, and the crypto manager finds the nickname it is configured with. This is
+ * the road from the provisioned store to the replication port, which no other case
+ * walks.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testInstalledServerStartsOnTheProvisionedTrustStore() throws Exception
+ {
+ final File workspace = TestCaseUtils.createTemporaryDirectory("adsTrustStoreStart");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File keyStore = new File(workspace, "server.p12");
+ ca.addKeyEntry(keyStore, "PKCS12", KEY_STORE_PASSWORD, CERT_NICKNAME, "CN=host.example.com", true);
+
+ final File serverRoot;
+ final SetupResult started = runSetup(workspace, "--usePkcs12keyStore", keyStore,
+ "--certNickname", CERT_NICKNAME, "--useKeyStoreForReplication");
+ serverRoot = started.serverRoot;
+ try
+ {
+ assertEquals(started.exitCode, 0, "setup failed to start the server:\n" + started.output);
+ }
+ finally
+ {
+ stopServer(serverRoot);
+ }
+
+ final File errorLog = new File(serverRoot, "logs" + File.separator + "errors");
+ for (String line : Files.readAllLines(errorLog.toPath(), StandardCharsets.UTF_8))
+ {
+ assertFalse(line.contains("severity=ERROR") && line.contains("ads-truststore"),
+ "the first start could not use the provisioned trust store: " + line);
+ assertFalse(line.contains("severity=ERROR") && line.contains(CERT_NICKNAME),
+ "the first start could not find the nickname: " + line);
+ }
+
+ final KeyStore keys = loadAdsTrustStore(serverRoot);
+ assertTrue(keys.isKeyEntry(CERT_NICKNAME), "the imported key pair did not survive the first start");
+ assertTrue(keys.isCertificateEntry("ads-ca-1"), "the trusted certificate did not survive the first start");
+ assertTrue(keys.isKeyEntry("ads-certificate"), "the server did not generate its instance key");
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(workspace);
+ }
+ }
+
+ /** Loads the provisioned trust store with the PIN setup wrote for it. */
+ private KeyStore loadAdsTrustStore(File serverRoot) throws Exception
+ {
+ final File trustStore = configFile(serverRoot, "ads-truststore");
+ final File pinFile = configFile(serverRoot, "ads-truststore.pin");
+ assertTrue(trustStore.exists(), "setup left no " + trustStore);
+ assertTrue(pinFile.exists(), "setup left no " + pinFile);
+
+ final KeyStore keys = KeyStore.getInstance("JKS");
+ final String pin = new String(Files.readAllBytes(pinFile.toPath()), StandardCharsets.UTF_8).trim();
+ try (final FileInputStream in = new FileInputStream(trustStore))
+ {
+ keys.load(in, pin.toCharArray());
+ }
+ return keys;
+ }
+
+ private File configFile(File serverRoot, String name)
+ {
+ return new File(serverRoot, "config" + File.separator + name);
+ }
+
+ /** Extracts the built package and runs setup on it, returning the server root. */
+ private File installServer(File workspace, String keyStoreArgument, File keyStore, String... extraArgs)
+ throws Exception
+ {
+ final SetupResult result = runSetup(workspace, keyStoreArgument, keyStore, extraArgs);
+ assertEquals(result.exitCode, 0, "setup failed:\n" + result.output);
+ return result.serverRoot;
+ }
+
+ /**
+ * Extracts the built package and runs setup on it, whether it succeeds or not. The
+ * server is started unless {@code -O} is among the extra arguments.
+ */
+ private SetupResult runSetup(File workspace, String keyStoreArgument, File keyStore, String... extraArgs)
+ throws Exception
+ {
+ final File serverRoot = new File(workspace, "opendj");
+ new ZipExtractor(TestUtilities.getInstallPackageFile()).extract(serverRoot);
+
+ final int[] ports = TestCaseUtils.findFreePorts(3);
+ final List<String> args = new ArrayList<>();
+ args.add(new File(serverRoot, OperatingSystem.isWindows() ? "setup.bat" : "setup").getPath());
+ args.add("--cli");
+ args.add("-n");
+ args.add("-w");
+ args.add("password");
+ args.add("-b");
+ args.add("dc=example,dc=com");
+ args.add("-p");
+ args.add(String.valueOf(ports[0]));
+ args.add("--adminConnectorPort");
+ args.add(String.valueOf(ports[1]));
+ args.add("-x");
+ args.add(String.valueOf(ports[2]));
+ args.add(keyStoreArgument);
+ args.add(keyStore.getAbsolutePath());
+ args.add("--keyStorePassword");
+ args.add(KEY_STORE_PASSWORD);
+ args.add("--enableStartTLS");
+ args.addAll(List.of(extraArgs));
+
+ // The output goes to a file rather than a pipe, so that a hung setup is killed on
+ // expiry instead of holding the suite until the failsafe timeout.
+ final File output = new File(workspace, "setup.out");
+ final Process process = new ProcessBuilder(args).redirectErrorStream(true).redirectOutput(output).start();
+ if (!process.waitFor(TIMEOUT_MINUTES, TimeUnit.MINUTES))
+ {
+ process.destroyForcibly();
+ fail("setup did not finish within " + TIMEOUT_MINUTES + " minutes:\n" + readOutput(output));
+ }
+ return new SetupResult(serverRoot, process.exitValue(), readOutput(output));
+ }
+
+ private String readOutput(File output) throws Exception
+ {
+ return new String(Files.readAllBytes(output.toPath()), StandardCharsets.UTF_8);
+ }
+
+ /** Stops the server setup started, if it is running. */
+ private void stopServer(File serverRoot) throws Exception
+ {
+ final Installation installation = new Installation(serverRoot, serverRoot);
+ if (installation.getStatus().isServerRunning())
+ {
+ new ServerController(installation).stopServer();
+ }
+ }
+
+ /** What one run of the setup command produced. */
+ private static final class SetupResult
+ {
+ private final File serverRoot;
+ private final int exitCode;
+ private final String output;
+
+ private SetupResult(File serverRoot, int exitCode, String output)
+ {
+ this.serverRoot = serverRoot;
+ this.exitCode = exitCode;
+ this.output = output;
+ }
+ }
+
+ /** Returns the ssl-cert-nickname values of the crypto manager entry of the written configuration. */
+ private List<String> cryptoManagerCertNicknames(File serverRoot) throws Exception
+ {
+ final File configFile = configFile(serverRoot, "config.ldif");
+ final List<String> nicknames = new ArrayList<>();
+ boolean inCryptoManager = false;
+ for (String line : Files.readAllLines(configFile.toPath(), StandardCharsets.UTF_8))
+ {
+ if (line.isEmpty())
+ {
+ inCryptoManager = false;
+ }
+ else if (line.equalsIgnoreCase("dn: cn=Crypto Manager,cn=config"))
+ {
+ inCryptoManager = true;
+ }
+ else if (inCryptoManager && line.startsWith("ds-cfg-ssl-cert-nickname:"))
+ {
+ nicknames.add(line.substring("ds-cfg-ssl-cert-nickname:".length()).trim());
+ }
+ }
+ return nicknames;
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/AdsTrustStoreProvisionerTest.java b/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/AdsTrustStoreProvisionerTest.java
new file mode 100644
index 0000000..0e48205
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/AdsTrustStoreProvisionerTest.java
@@ -0,0 +1,549 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.quicksetup.installer;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.security.KeyStore;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+import com.forgerock.opendj.util.OperatingSystem;
+import org.opends.quicksetup.ApplicationException;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.util.CertificateFixture;
+import org.opends.server.util.CertificateManager;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the provisioning of the {@code ads-truststore} from a key store an operator
+ * already holds, which is what lets {@code setup} secure the replication port with a
+ * CA-signed certificate instead of the self-signed {@code ads-certificate}.
+ */
+public class AdsTrustStoreProvisionerTest extends DirectoryServerTestCase
+{
+ private static final String SOURCE_PASSWORD = "sourcePassword";
+
+ /**
+ * The key pair is imported with its chain, the issuers of that chain are trusted, and
+ * the generated PIN is the one written to the PIN file: a PIN file which does not open
+ * the trust store leaves the server unable to read it.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionImportsKeyPairIssuersAndWritesPin() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionAdsTrustStore");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.<File> emptyList());
+
+ final KeyStore keyStore = loadTrustStore(trustStore, pinFile);
+ assertTrue(keyStore.isKeyEntry("server-cert"));
+ assertEquals(keyStore.getCertificateChain("server-cert").length, 2);
+ assertNotNull(keyStore.getKey("server-cert", pinOf(pinFile).toCharArray()));
+ assertEquals(keyStore.getCertificateAlias(ca.getCaCertificate()), "ads-ca-1");
+ assertTrue(keyStore.isCertificateEntry("ads-ca-1"));
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * The instance key of the server is not provisioned: the server generates
+ * {@code ads-certificate} itself when it first starts, and it has to stay the key pair
+ * published to the topology as the crypto manager instance key.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionLeavesTheInstanceKeyToTheServer() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionInstanceKey");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.<File> emptyList());
+
+ assertFalse(loadTrustStore(trustStore, pinFile).containsAlias("ads-certificate"));
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * A key store which holds the issued certificate alone gives the trust store no trust
+ * anchor: the trust managers take the certificate a key belongs to and none of its
+ * issuers, so such a server would trust no peer. The provisioning reports it and leaves
+ * no half-written trust store behind.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionFailsWithoutTrustAnchorAndWritesNothing() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionNoAnchor");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", false);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ try
+ {
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.<File> emptyList());
+ fail("Expected the provisioning to report that no certificate is trusted");
+ }
+ catch (ApplicationException e)
+ {
+ assertTrue(e.getMessageObject().toString().contains("server-cert"), e.getMessageObject().toString());
+ }
+ assertFalse(trustStore.exists());
+ assertFalse(pinFile.exists());
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * A key store which holds the issued certificate alone is enough when the certificates
+ * to trust are provided separately, as the issuing certificate is then imported from
+ * its file.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionTrustsCaCertificateFromFile() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionCaFile");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", false);
+ final File caFile = new File(tmpDir, "ca.crt");
+ ca.writeCaCertificate(caFile);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.singletonList(caFile));
+
+ final KeyStore keyStore = loadTrustStore(trustStore, pinFile);
+ assertTrue(keyStore.isKeyEntry("server-cert"));
+ assertTrue(keyStore.isCertificateEntry("ads-ca-1"));
+ assertEquals(keyStore.getCertificate("ads-ca-1"), ca.getCaCertificate());
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * Two key pairs issued by the same authority, as the key pairs of one server usually
+ * are, trust that authority once rather than under one alias each.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionTrustsASharedIssuerOnce() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionSharedIssuer");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert-2", "CN=host.example.com", true);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Arrays.asList("server-cert", "server-cert-2"), Collections.<File> emptyList());
+
+ final KeyStore keyStore = loadTrustStore(trustStore, pinFile);
+ assertTrue(keyStore.isKeyEntry("server-cert"));
+ assertTrue(keyStore.isKeyEntry("server-cert-2"));
+ assertTrue(keyStore.isCertificateEntry("ads-ca-1"));
+ assertFalse(keyStore.containsAlias("ads-ca-2"));
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * A file holding a chain of authorities, as a CA publishes it, is trusted whole: a
+ * server signed by another authority of the chain would otherwise fail the handshake
+ * while the installation reported nothing.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionTrustsEveryCertificateOfAFile() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionCaChainFile");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final CertificateFixture otherCa = new CertificateFixture("CN=Other CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", false);
+ final File caChainFile = new File(tmpDir, "ca-chain.crt");
+ CertificateFixture.writeCertificates(caChainFile, ca.getCaCertificate(), otherCa.getCaCertificate());
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.singletonList(caChainFile));
+
+ final KeyStore keyStore = loadTrustStore(trustStore, pinFile);
+ assertEquals(keyStore.getCertificateAlias(ca.getCaCertificate()), "ads-ca-1");
+ assertEquals(keyStore.getCertificateAlias(otherCa.getCaCertificate()), "ads-ca-2",
+ "the second certificate of the file is not trusted");
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * A certificate named in a file which the chain of the key pair holds as well, or
+ * which two files hold, is trusted once: two aliases for one certificate would be two
+ * entries to explain in the trust store, for nothing.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionTrustsACertificateNamedTwiceOnce() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionCaTwice");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+ final File caFile = new File(tmpDir, "ca.crt");
+ ca.writeCaCertificate(caFile);
+ final File caFileAgain = new File(tmpDir, "ca-again.crt");
+ ca.writeCaCertificate(caFileAgain);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Arrays.asList(caFile, caFileAgain));
+
+ final KeyStore keyStore = loadTrustStore(trustStore, pinFile);
+ assertEquals(keyStore.getCertificateAlias(ca.getCaCertificate()), "ads-ca-1");
+ assertFalse(keyStore.containsAlias("ads-ca-2"), "the same certificate is trusted under a second alias");
+ assertEquals(keyStore.size(), 2);
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * A certificate file which holds no certificate is refused before anything is written:
+ * either it is not a certificate at all, or it is empty, which the certificate factory
+ * reads as no certificate rather than as an error.
+ *
+ * @param content
+ * What the file holds.
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test(dataProvider = "filesHoldingNoCertificate")
+ public void testProvisionWritesNothingWhenACaFileHoldsNoCertificate(String content) throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionCaFileInvalid");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+ final File notACertificate = new File(tmpDir, "ca.pem");
+ Files.write(notACertificate.toPath(), content.getBytes(StandardCharsets.UTF_8));
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ try
+ {
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.singletonList(notACertificate));
+ fail("Expected the provisioning to refuse a file which holds no certificate");
+ }
+ catch (ApplicationException expected)
+ {
+ assertTrue(expected.getMessageObject().toString().contains(notACertificate.getName()),
+ expected.getMessageObject().toString());
+ }
+ assertFalse(trustStore.exists());
+ assertFalse(pinFile.exists());
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ @DataProvider
+ public Object[][] filesHoldingNoCertificate()
+ {
+ return new Object[][] { { "" }, { "not a certificate" } };
+ }
+
+ /**
+ * A failure once the trust store is being written, here a second key pair whose private
+ * key has a password of its own, removes the partial trust store: a store which holds
+ * the first key pair only would let the server start and present it, and the failure
+ * would only show when the second nickname is looked up.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionRemovesThePartialTrustStoreWhenAKeyCannotBeRead() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionPartial");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.jks");
+ ca.addKeyEntry(source, "JKS", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+ ca.addKeyEntry(source, "JKS", SOURCE_PASSWORD, "keyPassword", "server-cert-2", "CN=host.example.com", true);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ try
+ {
+ newProvisioner(trustStore, pinFile).provision(
+ new CertificateManager(source.getAbsolutePath(), CertificateManager.KEY_STORE_TYPE_JKS, SOURCE_PASSWORD),
+ Arrays.asList("server-cert", "server-cert-2"), Collections.<File> emptyList());
+ fail("Expected the provisioning to fail on a key it cannot read");
+ }
+ catch (ApplicationException expected)
+ {
+ assertTrue(expected.getMessageObject().toString().contains("server-cert-2"),
+ expected.getMessageObject().toString());
+ }
+ assertFalse(trustStore.exists(), "the partial trust store was left behind");
+ assertFalse(pinFile.exists());
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * A trust store which is already there is left as it is: only what this run writes is
+ * removed on failure, so a store this run did not create is neither overwritten nor
+ * deleted.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionLeavesAnExistingTrustStoreAlone() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionExisting");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final byte[] existing = "an existing trust store".getBytes(StandardCharsets.UTF_8);
+ Files.write(trustStore.toPath(), existing);
+
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ try
+ {
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.<File> emptyList());
+ fail("Expected the provisioning to refuse to run over an existing trust store");
+ }
+ catch (ApplicationException expected)
+ {
+ assertTrue(expected.getMessageObject().toString().contains(trustStore.getAbsolutePath()),
+ expected.getMessageObject().toString());
+ }
+ assertEquals(Files.readAllBytes(trustStore.toPath()), existing, "the existing trust store was touched");
+ assertFalse(pinFile.exists());
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * The aliases the trust store keeps for itself are refused: a key pair imported as
+ * {@code ads-certificate} would be taken for the instance key the server generates,
+ * and one imported as {@code ads-ca-1} would collide with the first trusted certificate.
+ *
+ * @param alias
+ * The reserved alias, in whatever case.
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test(dataProvider = "reservedAliases")
+ public void testProvisionRefusesAReservedAlias(String alias) throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionReservedAlias");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, alias, "CN=host.example.com", true);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ try
+ {
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList(alias), Collections.<File> emptyList());
+ fail("Expected the provisioning to refuse the reserved alias " + alias);
+ }
+ catch (ApplicationException expected)
+ {
+ assertTrue(expected.getMessageObject().toString().contains(alias), expected.getMessageObject().toString());
+ }
+ assertFalse(trustStore.exists());
+ assertFalse(pinFile.exists());
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ @DataProvider
+ public Object[][] reservedAliases()
+ {
+ return new Object[][] { { "ads-certificate" }, { "ADS-Certificate" }, { "ads-ca-1" }, { "ADS-CA-7" } };
+ }
+
+ /**
+ * The PIN file is readable by its owner only, as it opens the private key presented on
+ * the replication port.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testProvisionProtectsThePinFile() throws Exception
+ {
+ if (OperatingSystem.isWindows())
+ {
+ return;
+ }
+
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("provisionPinPermissions");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", SOURCE_PASSWORD, "server-cert", "CN=host.example.com", true);
+
+ final File trustStore = new File(tmpDir, "ads-truststore");
+ final File pinFile = new File(tmpDir, "ads-truststore.pin");
+ newProvisioner(trustStore, pinFile).provision(
+ sourceManager(source), Collections.singletonList("server-cert"), Collections.<File> emptyList());
+
+ final Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(pinFile.toPath());
+ assertEquals(permissions, PosixFilePermissions.fromString("rw-------"));
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ private AdsTrustStoreProvisioner newProvisioner(File trustStore, File pinFile)
+ {
+ return new AdsTrustStoreProvisioner(trustStore.getAbsolutePath(), pinFile.getAbsolutePath());
+ }
+
+ private CertificateManager sourceManager(File source)
+ {
+ return new CertificateManager(source.getAbsolutePath(), CertificateManager.KEY_STORE_TYPE_PKCS12,
+ SOURCE_PASSWORD);
+ }
+
+ private String pinOf(File pinFile) throws Exception
+ {
+ return new String(Files.readAllBytes(pinFile.toPath()), StandardCharsets.UTF_8).trim();
+ }
+
+ private KeyStore loadTrustStore(File trustStore, File pinFile) throws Exception
+ {
+ final KeyStore keyStore = KeyStore.getInstance(CertificateManager.KEY_STORE_TYPE_JKS);
+ try (final FileInputStream in = new FileInputStream(trustStore))
+ {
+ keyStore.load(in, pinOf(pinFile).toCharArray());
+ }
+ return keyStore;
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSArgumentParserTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSArgumentParserTestCase.java
new file mode 100644
index 0000000..6490c06
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSArgumentParserTestCase.java
@@ -0,0 +1,205 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.tools;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.File;
+
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.cli.ArgumentException;
+
+/**
+ * Tests the arguments which let {@code setup} provision the trust store used for server
+ * to server communication from a key store the operator already holds.
+ */
+public class InstallDSArgumentParserTestCase extends DirectoryServerTestCase
+{
+ /** A fragment of the message reporting that the key pair to present has to come from a key store. */
+ private static final String KEY_STORE_REQUIRED = "requires an existing key store";
+ /** A fragment of the message reporting a certificate file which cannot be read. */
+ private static final String CERT_FILE_INVALID = "does not exist";
+ /** A fragment of the message reporting an argument the parser does not know. */
+ private static final String UNKNOWN_ARGUMENT = "is not allowed for use with this program";
+
+ /**
+ * The key pair presented on the replication port is imported from an existing key
+ * store, so asking for a self-signed certificate to be generated cannot satisfy it.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testUseKeyStoreForReplicationRejectsAGeneratedCertificate() throws Exception
+ {
+ assertReportsUseKeyStoreForReplication(true,
+ "--cli", "-n", "-w", "password", "-b", "dc=example,dc=com",
+ "--generateSelfSignedCertificate", "--enableStartTLS", "--useKeyStoreForReplication");
+ }
+
+ /**
+ * The private key of a PKCS#11 token cannot be exported, so it cannot be copied into
+ * the trust store used for replication.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testUseKeyStoreForReplicationRejectsAPkcs11Token() throws Exception
+ {
+ assertReportsUseKeyStoreForReplication(true,
+ "--cli", "-n", "-w", "password", "-b", "dc=example,dc=com",
+ "--usePkcs11Keystore", "--keyStorePassword", "password", "--enableStartTLS",
+ "--useKeyStoreForReplication");
+ }
+
+ /**
+ * A key store the installer can read the key pair from is accepted, whichever of the
+ * four key store arguments names it.
+ *
+ * @param keyStoreArgument
+ * The argument naming the key store.
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test(dataProvider = "keyStoreArguments")
+ public void testUseKeyStoreForReplicationAcceptsAKeyStore(String keyStoreArgument) throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("useKeyStoreForReplication");
+ try
+ {
+ final File keyStore = new File(tmpDir, "server.keystore");
+ assertTrue(keyStore.createNewFile());
+ assertReportsUseKeyStoreForReplication(false,
+ "--cli", "-n", "-w", "password", "-b", "dc=example,dc=com",
+ keyStoreArgument, keyStore.getAbsolutePath(), "--keyStorePassword", "password",
+ "--enableStartTLS", "--useKeyStoreForReplication");
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ @DataProvider
+ public Object[][] keyStoreArguments()
+ {
+ return new Object[][] {
+ { "--useJavaKeystore" }, { "--useJCEKS" }, { "--usePkcs12keyStore" }, { "--useBcfksKeystore" } };
+ }
+
+ /**
+ * Certificates to trust on the replication port are only meaningful together with the
+ * key pair to present there: a server which trusts an authority but keeps presenting
+ * its self-signed certificate is the half-configured state this provisioning exists to
+ * avoid.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testReplicationCaCertFileRequiresUseKeyStoreForReplication() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("replicationCaCertFile");
+ try
+ {
+ final File caFile = new File(tmpDir, "ca.crt");
+ assertTrue(caFile.createNewFile());
+ final String error = parseAndReturnError(
+ "--cli", "-n", "-w", "password", "-b", "dc=example,dc=com",
+ "--usePkcs12keyStore", new File(tmpDir, "server.p12").getAbsolutePath(),
+ "--keyStorePassword", "password", "--enableStartTLS",
+ "--replicationCaCertFile", caFile.getAbsolutePath());
+ assertTrue(error.contains("replicationCaCertFile") && error.contains("useKeyStoreForReplication"), error);
+ assertFalse(error.contains(UNKNOWN_ARGUMENT), error);
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * A certificate file which does not exist is reported while the arguments are checked,
+ * rather than half way through the installation.
+ *
+ * @throws Exception
+ * If a problem occurs.
+ */
+ @Test
+ public void testReplicationCaCertFileMustExist() throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("replicationCaCertMissing");
+ try
+ {
+ final File missing = new File(tmpDir, "nonexistent.crt");
+ final String error = parseAndReturnError(
+ "--cli", "-n", "-w", "password", "-b", "dc=example,dc=com",
+ "--usePkcs12keyStore", new File(tmpDir, "server.p12").getAbsolutePath(),
+ "--keyStorePassword", "password", "--enableStartTLS", "--useKeyStoreForReplication",
+ "--replicationCaCertFile", missing.getAbsolutePath());
+ assertTrue(error.contains(missing.getAbsolutePath()) && error.contains(CERT_FILE_INVALID), error);
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /**
+ * Asserts whether the arguments are rejected because {@code --useKeyStoreForReplication}
+ * cannot be satisfied. Other errors, such as a port which happens to be in use on the
+ * machine running the tests, are ignored: only the message under test is looked for. The
+ * wording is matched as well as the argument name, so that an argument the parser does
+ * not know at all, which is reported by name too, does not pass for the check under test.
+ */
+ private void assertReportsUseKeyStoreForReplication(boolean expected, String... args) throws Exception
+ {
+ final String error = parseAndReturnError(args);
+ final boolean reported =
+ error.contains("useKeyStoreForReplication") && error.contains(KEY_STORE_REQUIRED);
+ if (expected)
+ {
+ assertTrue(reported, error);
+ }
+ else
+ {
+ assertFalse(reported, error);
+ assertFalse(error.contains(UNKNOWN_ARGUMENT), error);
+ }
+ }
+
+ /** Parses the provided arguments and returns the reported errors, empty if there is none. */
+ private String parseAndReturnError(String... args) throws Exception
+ {
+ final InstallDSArgumentParser parser = new InstallDSArgumentParser(InstallDS.class.getName());
+ parser.initializeArguments();
+ try
+ {
+ parser.parseArguments(args);
+ return "";
+ }
+ catch (ArgumentException e)
+ {
+ return e.getMessage();
+ }
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateFixture.java b/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateFixture.java
new file mode 100644
index 0000000..e8401f2
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateFixture.java
@@ -0,0 +1,235 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.Date;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+/**
+ * A miniature Certificate Authority for tests which need a CA-signed key pair rather
+ * than a self-signed one: the key stores {@code setup} is given by an operator who runs
+ * their own PKI.
+ * <p>
+ * The class name deliberately avoids the {@code Test} prefix and the {@code Test},
+ * {@code TestCase} suffixes: those are the patterns the failsafe configuration picks up
+ * as test classes, and every test class is required to extend {@code DirectoryServerTestCase}.
+ */
+public final class CertificateFixture
+{
+ private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
+ private static final int VALIDITY_DAYS = 365;
+
+ private final KeyPair caKeyPair;
+ private final X509Certificate caCertificate;
+ private final String caSubject;
+
+ /**
+ * Creates a certificate authority whose certificate is self-signed, as a root CA is.
+ *
+ * @param caSubject
+ * The subject DN of the CA certificate, for instance {@code "CN=Example CA"}.
+ * @throws Exception
+ * If the CA key pair or certificate cannot be generated.
+ */
+ public CertificateFixture(String caSubject) throws Exception
+ {
+ this.caSubject = caSubject;
+ this.caKeyPair = newKeyPair();
+ this.caCertificate = sign(caSubject, caKeyPair.getPublic(), caSubject, caKeyPair, true);
+ }
+
+ /**
+ * Returns the certificate of this authority, the one which has to be trusted for the
+ * certificates it issues to be accepted.
+ *
+ * @return The CA certificate.
+ */
+ public X509Certificate getCaCertificate()
+ {
+ return caCertificate;
+ }
+
+ /**
+ * Writes the CA certificate to the provided file, DER encoded, as {@code keytool
+ * -exportcert} does.
+ *
+ * @param file
+ * The file to write the certificate to.
+ * @throws Exception
+ * If the file cannot be written.
+ */
+ public void writeCaCertificate(File file) throws Exception
+ {
+ try (final OutputStream out = new FileOutputStream(file))
+ {
+ out.write(caCertificate.getEncoded());
+ }
+ }
+
+ /**
+ * Writes certificates to the provided file, PEM encoded one after the other, as a CA
+ * publishes the chain of its authorities in one {@code ca-chain.crt} file.
+ *
+ * @param file
+ * The file to write the certificates to.
+ * @param certificates
+ * The certificates to write, in order.
+ * @throws Exception
+ * If the file cannot be written.
+ */
+ public static void writeCertificates(File file, Certificate... certificates) throws Exception
+ {
+ final Base64.Encoder encoder = Base64.getMimeEncoder(64, new byte[] { '\n' });
+ try (final Writer out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.US_ASCII))
+ {
+ for (Certificate certificate : certificates)
+ {
+ out.write("-----BEGIN CERTIFICATE-----\n");
+ out.write(encoder.encodeToString(certificate.getEncoded()));
+ out.write("\n-----END CERTIFICATE-----\n");
+ }
+ }
+ }
+
+ /**
+ * Adds one key pair signed by this authority to a key store, creating the key store if
+ * it does not exist yet.
+ *
+ * @param file
+ * The key store file to create or extend.
+ * @param storeType
+ * The key store type, for instance {@code "PKCS12"} or {@code "JKS"}.
+ * @param password
+ * The password protecting both the store and the private key, as the key
+ * managers of the server require them to be identical.
+ * @param alias
+ * The alias to store the key pair under.
+ * @param subject
+ * The subject DN of the issued certificate.
+ * @param withChain
+ * {@code true} to store the CA certificate along with the issued certificate,
+ * as a properly built key store does, {@code false} to store the issued
+ * certificate on its own.
+ * @throws Exception
+ * If the key store cannot be written.
+ */
+ public void addKeyEntry(File file, String storeType, String password, String alias, String subject,
+ boolean withChain) throws Exception
+ {
+ addKeyEntry(file, storeType, password, password, alias, subject, withChain);
+ }
+
+ /**
+ * Adds one key pair signed by this authority to a key store, with a private key
+ * protected by a password of its own: what {@code keytool -genkeypair -keypass} leaves,
+ * and what the key managers of the server cannot unlock.
+ *
+ * @param file
+ * The key store file to create or extend.
+ * @param storeType
+ * The key store type, for instance {@code "PKCS12"} or {@code "JKS"}.
+ * @param storePassword
+ * The password protecting the store.
+ * @param keyPassword
+ * The password protecting the private key.
+ * @param alias
+ * The alias to store the key pair under.
+ * @param subject
+ * The subject DN of the issued certificate.
+ * @param withChain
+ * {@code true} to store the CA certificate along with the issued certificate,
+ * {@code false} to store the issued certificate on its own.
+ * @throws Exception
+ * If the key store cannot be written.
+ */
+ public void addKeyEntry(File file, String storeType, String storePassword, String keyPassword, String alias,
+ String subject, boolean withChain) throws Exception
+ {
+ final KeyPair keyPair = newKeyPair();
+ final X509Certificate certificate = sign(subject, keyPair.getPublic(), caSubject, caKeyPair, false);
+ final Certificate[] chain = withChain
+ ? new Certificate[] { certificate, caCertificate }
+ : new Certificate[] { certificate };
+
+ final KeyStore keyStore = KeyStore.getInstance(storeType);
+ if (file.exists())
+ {
+ try (final InputStream in = new FileInputStream(file))
+ {
+ keyStore.load(in, storePassword.toCharArray());
+ }
+ }
+ else
+ {
+ keyStore.load(null, storePassword.toCharArray());
+ }
+ keyStore.setKeyEntry(alias, keyPair.getPrivate(), keyPassword.toCharArray(), chain);
+ try (final OutputStream out = new FileOutputStream(file))
+ {
+ keyStore.store(out, storePassword.toCharArray());
+ }
+ }
+
+ private static KeyPair newKeyPair() throws Exception
+ {
+ final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ return generator.generateKeyPair();
+ }
+
+ private static X509Certificate sign(String subject, java.security.PublicKey subjectKey, String issuer,
+ KeyPair issuerKeyPair, boolean isCa) throws Exception
+ {
+ final Instant now = Instant.now();
+ final JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
+ new X500Name(issuer),
+ new BigInteger(64, new SecureRandom()),
+ Date.from(now.minus(1, ChronoUnit.DAYS)),
+ Date.from(now.plus(VALIDITY_DAYS, ChronoUnit.DAYS)),
+ new X500Name(subject),
+ subjectKey);
+ builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(isCa));
+
+ final ContentSigner signer =
+ new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).build(issuerKeyPair.getPrivate());
+ return new JcaX509CertificateConverter().getCertificate(builder.build(signer));
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java
index 013ed48..774d735 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java
@@ -20,7 +20,9 @@
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.util.Arrays;
@@ -1152,6 +1154,268 @@
/**
+ * Tests that {@code importKeyEntry} copies the whole certificate chain of the source
+ * key entry and re-encrypts the private key with the password of the destination key
+ * store: the key managers of the server are initialised with the store password only,
+ * so a key which kept the password of the source key store could not be read back.
+ *
+ * @throws Exception If a problem occurs.
+ */
+ @Test
+ public void testImportKeyEntryCopiesChainAndReEncryptsKey()
+ throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("importKeyEntry");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", "sourcePassword", "server-cert", "CN=host.example.com", true);
+
+ final File destination = new File(tmpDir, "ads-truststore");
+ final CertificateManager destinationManager =
+ new CertificateManager(destination.getAbsolutePath(), "JKS", "destinationPassword");
+ destinationManager.importKeyEntry("server-cert",
+ new CertificateManager(source.getAbsolutePath(), "PKCS12", "sourcePassword"), "server-cert");
+
+ final KeyStore keyStore = KeyStore.getInstance("JKS");
+ try (final FileInputStream in = new FileInputStream(destination))
+ {
+ keyStore.load(in, "destinationPassword".toCharArray());
+ }
+ assertTrue(keyStore.isKeyEntry("server-cert"));
+ assertNotNull(keyStore.getKey("server-cert", "destinationPassword".toCharArray()));
+ assertEquals(keyStore.getCertificateChain("server-cert").length, 2);
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+
+
+ /**
+ * Tests that {@code importKeyEntry} reports an alias which the source key store does
+ * not hold, rather than silently importing nothing.
+ *
+ * @throws Exception If a problem occurs.
+ */
+ @Test
+ public void testImportKeyEntryNonexistentSourceAlias()
+ throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("importKeyEntryMissing");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", "sourcePassword", "server-cert", "CN=host.example.com", true);
+
+ final CertificateManager destinationManager = new CertificateManager(
+ new File(tmpDir, "ads-truststore").getAbsolutePath(), "JKS", "destinationPassword");
+ try
+ {
+ destinationManager.importKeyEntry("nonexistent",
+ new CertificateManager(source.getAbsolutePath(), "PKCS12", "sourcePassword"), "nonexistent");
+ fail("Expected a key store exception due to a nonexistent source alias");
+ } catch (KeyStoreException kse) {}
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+
+
+ /**
+ * Tests that {@code getCertificateChain} returns the issuers of a key entry, which is
+ * where the certificates to trust are taken from when a key pair is provisioned.
+ *
+ * @throws Exception If a problem occurs.
+ */
+ @Test
+ public void testGetCertificateChainReturnsIssuers()
+ throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("getCertificateChain");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", "sourcePassword", "server-cert", "CN=host.example.com", true);
+
+ final CertificateManager sourceManager =
+ new CertificateManager(source.getAbsolutePath(), "PKCS12", "sourcePassword");
+ final Certificate[] chain = sourceManager.getCertificateChain("server-cert");
+ assertEquals(chain.length, 2);
+ assertEquals(chain[1], ca.getCaCertificate());
+ assertNull(sourceManager.getCertificateChain("nonexistent"));
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+
+
+ /**
+ * Tests that {@code addTrustedCertificate} stores a certificate held in memory as a
+ * trusted certificate entry: only such an entry is a trust anchor, the certificate
+ * chain of a key entry is not.
+ *
+ * @throws Exception If a problem occurs.
+ */
+ @Test
+ public void testAddTrustedCertificateStoresTrustAnchor()
+ throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("addTrustedCertificate");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File destination = new File(tmpDir, "ads-truststore");
+
+ final CertificateManager destinationManager =
+ new CertificateManager(destination.getAbsolutePath(), "JKS", "destinationPassword");
+ destinationManager.addTrustedCertificate("ads-ca-1", ca.getCaCertificate());
+
+ final KeyStore keyStore = KeyStore.getInstance("JKS");
+ try (final FileInputStream in = new FileInputStream(destination))
+ {
+ keyStore.load(in, "destinationPassword".toCharArray());
+ }
+ assertTrue(keyStore.isCertificateEntry("ads-ca-1"));
+ assertEquals(keyStore.getCertificate("ads-ca-1"), ca.getCaCertificate());
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+
+
+ /**
+ * Tests that {@code importKeyEntry} names the cause when the private key is protected
+ * by a password of its own: the key managers of the server unlock keys with the store
+ * password only, and "Cannot recover key" says neither that nor what to do about it.
+ *
+ * @throws Exception If a problem occurs.
+ */
+ @Test
+ public void testImportKeyEntryReportsAKeyPasswordWhichDiffers()
+ throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("importKeyEntryKeyPassword");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.jks");
+ ca.addKeyEntry(source, "JKS", "sourcePassword", "keyPassword", "server-cert", "CN=host.example.com", true);
+
+ final File destination = new File(tmpDir, "ads-truststore");
+ final CertificateManager destinationManager =
+ new CertificateManager(destination.getAbsolutePath(), "JKS", "destinationPassword");
+ try
+ {
+ destinationManager.importKeyEntry("server-cert",
+ new CertificateManager(source.getAbsolutePath(), "JKS", "sourcePassword"), "server-cert");
+ fail("Expected a key store exception due to a key password which differs from the store password");
+ }
+ catch (KeyStoreException kse)
+ {
+ assertTrue(kse.getMessage().contains("server-cert"), kse.getMessage());
+ assertTrue(kse.getMessage().contains(source.getAbsolutePath()), kse.getMessage());
+ assertTrue(kse.getMessage().contains("password"), kse.getMessage());
+ assertFalse(kse.getMessage().contains("Cannot recover key"), kse.getMessage());
+ }
+ assertFalse(destination.exists(), "a destination key store was written for a key which could not be read");
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+
+
+ /**
+ * Tests that neither {@code importKeyEntry} nor {@code addTrustedCertificate} replaces
+ * an entry which is already there, as {@code generateSelfSignedCertificate} and
+ * {@code addCertificate} do not: the key store methods they wrap overwrite silently.
+ *
+ * @throws Exception If a problem occurs.
+ */
+ @Test
+ public void testImportKeyEntryAndAddTrustedCertificateRefuseAnAliasInUse()
+ throws Exception
+ {
+ final File tmpDir = TestCaseUtils.createTemporaryDirectory("aliasInUse");
+ try
+ {
+ final CertificateFixture ca = new CertificateFixture("CN=Example CA,O=Example");
+ final File source = new File(tmpDir, "server.p12");
+ ca.addKeyEntry(source, "PKCS12", "sourcePassword", "server-cert", "CN=host.example.com", true);
+ ca.addKeyEntry(source, "PKCS12", "sourcePassword", "other-cert", "CN=other.example.com", true);
+ final CertificateManager sourceManager =
+ new CertificateManager(source.getAbsolutePath(), "PKCS12", "sourcePassword");
+
+ final File destination = new File(tmpDir, "ads-truststore");
+ final CertificateManager destinationManager =
+ new CertificateManager(destination.getAbsolutePath(), "JKS", "destinationPassword");
+ destinationManager.importKeyEntry("server-cert", sourceManager, "server-cert");
+ destinationManager.addTrustedCertificate("ads-ca-1", ca.getCaCertificate());
+
+ try
+ {
+ destinationManager.importKeyEntry("server-cert", sourceManager, "other-cert");
+ fail("Expected a key store exception due to a key entry alias already in use");
+ }
+ catch (KeyStoreException kse)
+ {
+ assertTrue(kse.getMessage().contains("server-cert"), kse.getMessage());
+ }
+ try
+ {
+ destinationManager.addTrustedCertificate("ads-ca-1", sourceManager.getCertificate("other-cert"));
+ fail("Expected a key store exception due to a trusted certificate alias already in use");
+ }
+ catch (KeyStoreException kse)
+ {
+ assertTrue(kse.getMessage().contains("ads-ca-1"), kse.getMessage());
+ }
+ try
+ {
+ destinationManager.importKeyEntry("ads-ca-1", sourceManager, "other-cert");
+ fail("Expected a key store exception due to an alias held by a trusted certificate");
+ }
+ catch (KeyStoreException kse)
+ {
+ assertTrue(kse.getMessage().contains("ads-ca-1"), kse.getMessage());
+ }
+
+ final KeyStore keyStore = KeyStore.getInstance("JKS");
+ try (final FileInputStream in = new FileInputStream(destination))
+ {
+ keyStore.load(in, "destinationPassword".toCharArray());
+ }
+ assertEquals(keyStore.getCertificate("server-cert"), sourceManager.getCertificate("server-cert"),
+ "the key entry was replaced");
+ assertEquals(keyStore.getCertificate("ads-ca-1"), ca.getCaCertificate(), "the trusted certificate was replaced");
+ assertEquals(keyStore.size(), 2);
+ }
+ finally
+ {
+ TestCaseUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+
+
+ /**
* Exports a certificate to a temporary file.
*
* @throws Exception If a problem occurs.
--
Gitblit v1.10.0