/*
|
* 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));
|
}
|
}
|