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