/* * 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. *

* 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 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 cryptoManagerCertNicknames(File serverRoot) throws Exception { final File configFile = configFile(serverRoot, "config.ldif"); final List 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; } }