/* * 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 2009-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. * Portions Copyright 2025 Wren Security. * Portions Copyright 2025-2026 3A Systems LLC. */ package org.opends.server.util; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.PrivateKey; import java.security.SecureRandom; import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; import com.forgerock.opendj.util.FipsStaticUtils; import com.forgerock.opendj.util.StaticUtils; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.util.BigIntegers; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.util.Reject; import static org.opends.messages.UtilityMessages.ERR_CERTMGR_ADD_CERT; import static org.opends.messages.UtilityMessages.ERR_CERTMGR_ALIAS_ALREADY_EXISTS; import static org.opends.messages.UtilityMessages.ERR_CERTMGR_ALIAS_INVALID; 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; /** * Provides a wrapper class that collects all of the JVM vendor and JDK version * specific code in a single place. */ public final class Platform { private static final PlatformIMPL IMPL; /** Puts a key store written next to its file in the place of that file. */ interface KeyStoreReplacer { /** * Puts the new file in the place of the old one at once. * * @param temp * the new file, next to the old one * @param target * the old file * @throws IOException * if the old file cannot be replaced that way */ void replace(Path temp, Path target) throws IOException; } /** Replaces a key store file; a test makes it fail to reach the write in place. */ static volatile KeyStoreReplacer keyStoreReplacer = (temp, target) -> Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); /** Gives a key store file written next to the old one the group of that one. */ interface KeyStoreGroupSetter { /** * Gives the new file the group of the old one. * * @param view * the attributes of the new file * @param group * the group of the old file * @throws IOException * if the new file cannot be given that group */ void setGroup(PosixFileAttributeView view, GroupPrincipal group) throws IOException; } /** Gives the new key store file its group; a test makes it fail to reach the write in place. */ static volatile KeyStoreGroupSetter keyStoreGroupSetter = PosixFileAttributeView::setGroup; /** The minimum java supported version. */ public static final String JAVA_MINIMUM_VERSION_NUMBER = "11"; static { IMPL = new DefaultPlatformIMPL(); } /** Key size, key algorithm and signature algorithms used. */ public static enum KeyType { /** RSA key algorithm with 2048 bits size and SHA256withRSA signing algorithm. */ RSA("RSA", 2048, "SHA256withRSA"), /** Elliptic Curve key algorithm with 256 bits size and SHA256withECDSA signing algorithm. */ EC("EC", 256, "SHA256withECDSA"); /** Default key type used when none can be determined. */ public final static KeyType DEFAULT = RSA; final String keyAlgorithm; final int keySize; final String signatureAlgorithm; private KeyType(String keyAlgorithm, int keySize, String signatureAlgorithm) { this.keySize = keySize; this.keyAlgorithm = keyAlgorithm; this.signatureAlgorithm = signatureAlgorithm; } /** * Get a KeyType based on the alias name. * * @param alias * certificate alias * @return KeyTpe deduced from the alias. */ public static KeyType getTypeOrDefault(String alias) { try { return KeyType.valueOf(alias.substring(alias.lastIndexOf('-') + 1).toUpperCase()); } catch (Exception e) { return KeyType.DEFAULT; } } } /** * Platform base class. Performs all of the certificate management functions. */ private static abstract class PlatformIMPL { protected PlatformIMPL() { } /** * Writes the key store over its file. On a POSIX file system it is written to a file of its * own next to it first, which then takes the place of the old one at once: a reader of the * key store, which the server reads from its file for each SSL context, never finds it half * written, and a process stopped part way through leaves the old file whole (#1188). The new * file keeps the mode, the owner and the group of the old one; it does not keep its ACLs. *

* The key store is written in place, as it was before, where it cannot be replaced that way * without changing what the file is: a new key store, one this process may not write to * (refused, as before), one with other hard links, one whose owner or group the new file * cannot take, one whose links, owner or group cannot be told, one in a directory this * process cannot write to, and one which cannot be replaced, such as a file mounted on its own * into a container. On a file system without POSIX attributes (Windows), where neither the * owner, the ACLs nor the hard links of the old file would reach a new one, it is always * written in place. */ private static void store(KeyStore ks, String ksPath, char[] pwd) throws Exception { Path target = Paths.get(ksPath); if (!Files.exists(target) || !target.getFileSystem().supportedFileAttributeViews().contains("posix")) { storeInPlace(ks, target, pwd); return; } // A link goes on pointing where it did: the file it points to is the one replaced. target = target.toRealPath(); if (!Files.isWritable(target) || hasOtherLinks(target)) { storeInPlace(ks, target, pwd); return; } final Path temp; try { temp = Files.createTempFile(target.getParent(), "." + target.getFileName() + ".", ".tmp"); } catch (IOException e) { storeInPlace(ks, target, pwd); return; } try { try (OutputStream out = Files.newOutputStream(temp)) { ks.store(out, pwd); } if (!takeOwnerAndMode(temp, target)) { storeInPlace(ks, target, pwd); return; } try { keyStoreReplacer.replace(temp, target); } catch (IOException e) { storeInPlace(ks, target, pwd); } } finally { Files.deleteIfExists(temp); } } /** * Whether the file has other names, which a new file in its place would not have; a file * whose links cannot be told is taken to have some. */ private static boolean hasOtherLinks(Path target) { try { return ((Number) Files.getAttribute(target, "unix:nlink")).intValue() > 1; } catch (IOException | UnsupportedOperationException e) { return true; } } /** * Gives the new file the group and the mode of the old one, and tells whether it has its * owner and its group: a process may give a file only the groups it is a member of, and only * its own user as owner. The mode comes last, so that the new file is never open to a group * the old one was not, and only when the file can take the place of the old one; a file whose * attributes cannot be read or set cannot. */ private static boolean takeOwnerAndMode(Path temp, Path target) { try { final PosixFileAttributes old = Files.readAttributes(target, PosixFileAttributes.class); final PosixFileAttributeView view = Files.getFileAttributeView(temp, PosixFileAttributeView.class); keyStoreGroupSetter.setGroup(view, old.group()); final PosixFileAttributes now = view.readAttributes(); if (!now.owner().equals(old.owner()) || !now.group().equals(old.group())) { return false; } view.setPermissions(old.permissions()); return true; } catch (IOException | UnsupportedOperationException e) { return false; } } private static void storeInPlace(KeyStore ks, Path target, char[] pwd) throws Exception { try (OutputStream out = new FileOutputStream(target.toFile())) { ks.store(out, pwd); } } private final void deleteAlias(KeyStore ks, String ksPath, String alias, char[] pwd) throws KeyStoreException { try { if (ks == null) { LocalizableMessage msg = ERR_CERTMGR_KEYSTORE_NONEXISTANT.get(); throw new KeyStoreException(msg.toString()); } ks.deleteEntry(alias); store(ks, ksPath, pwd); } catch (Exception e) { throw new KeyStoreException(ERR_CERTMGR_DELETE_ALIAS.get(alias, e.getMessage()).toString(), e); } } private final void addCertificate(KeyStore ks, String ksType, String ksPath, String alias, char[] pwd, String certPath) throws KeyStoreException { try { CertificateFactory cf = CertificateFactory.getInstance("X509"); if (ks == null) { ks = KeyStore.getInstance(ksType); ks.load(null, pwd); } // Do not support certificate replies. if (ks.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) { LocalizableMessage msg = ERR_CERTMGR_CERT_REPLIES_INVALID.get(alias); throw new KeyStoreException(msg.toString()); } else if (!ks.containsAlias(alias) || ks.entryInstanceOf(alias, KeyStore.TrustedCertificateEntry.class)) { try (InputStream inStream = new FileInputStream(certPath)) { trustedCert(alias, cf, ks, inStream); } } else { LocalizableMessage msg = ERR_CERTMGR_ALIAS_INVALID.get(alias); throw new KeyStoreException(msg.toString()); } store(ks, ksPath, pwd); } catch (Exception e) { throw new KeyStoreException(ERR_CERTMGR_ADD_CERT.get(alias, e.getMessage()).toString(), e); } } 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); store(ks, ksPath, 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); store(ks, ksPath, 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 { boolean isFips = StaticUtils.isFips(); try { if(!isFips) { FipsStaticUtils.registerBcProvider(true); } 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()); } KeyPair keyPair = newKeyPair(keyType); PrivateKey privateKey = keyPair.getPrivate(); X500Name subject = new X500Name(dn); Certificate[] certificateChain = new Certificate[] { generateSelfCertificate(keyPair, keyType, subject, validity) }; ks.setKeyEntry(alias, privateKey, pwd, certificateChain); store(ks, ksPath, pwd); return ks; } catch (Exception e) { throw new KeyStoreException(ERR_CERTMGR_GEN_SELF_SIGNED_CERT.get(alias, e.getMessage()).toString(), e); } finally { if(!isFips) { Security.removeProvider(BouncyCastleFipsProvider.PROVIDER_NAME); } } } private static KeyPair newKeyPair(KeyType keyType) throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance(keyType.keyAlgorithm, BouncyCastleFipsProvider.PROVIDER_NAME); generator.initialize(keyType.keySize); return generator.generateKeyPair(); } private static Certificate generateSelfCertificate(KeyPair keyPair, KeyType keyType, X500Name subject, int days) throws Exception { BigInteger serial = BigIntegers.createRandomBigInteger(64, new SecureRandom()); Instant now = Instant.now(); Date notBeforeDate = Date.from(now); Date notAfterDate = Date.from(now.plus(days, ChronoUnit.DAYS)); JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( subject, serial, notBeforeDate, notAfterDate, subject, keyPair.getPublic() ); ContentSigner signer = new JcaContentSignerBuilder(keyType.signatureAlgorithm) .setProvider(BouncyCastleFipsProvider.PROVIDER_NAME) .build(keyPair.getPrivate()); X509CertificateHolder holder = builder.build(signer); JcaX509CertificateConverter converter = new JcaX509CertificateConverter() .setProvider(BouncyCastleFipsProvider.PROVIDER_NAME); return converter.getCertificate(holder); } /** * Generate a x509 certificate from the input stream. Verification is done * only if it is self-signed. */ private void trustedCert(String alias, CertificateFactory cf, KeyStore ks, InputStream in) throws KeyStoreException { try { if (ks.containsAlias(alias)) { LocalizableMessage msg = ERR_CERTMGR_ALIAS_ALREADY_EXISTS.get(alias); throw new KeyStoreException(msg.toString()); } X509Certificate cert = (X509Certificate) cf.generateCertificate(in); if (isSelfSigned(cert)) { cert.verify(cert.getPublicKey()); } ks.setCertificateEntry(alias, cert); } catch (Exception e) { throw new KeyStoreException(ERR_CERTMGR_TRUSTED_CERT.get(alias, e.getMessage()).toString(), e); } } /** * Check that the issuer and subject DNs match. */ private boolean isSelfSigned(X509Certificate cert) { return cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()); } } /** Prevent instantiation. */ private Platform() { } /** * Add the certificate in the specified path to the provided keystore; * creating the keystore with the provided type and path if it doesn't exist. * * @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 if it is created. * @param alias * The alias to store the certificate under. * @param pwd * The password to use in saving the certificate. * @param certPath * The path to the file containing the certificate. * @throws KeyStoreException * If an error occurred adding the certificate to the keystore. */ public static void addCertificate(KeyStore ks, String ksType, String ksPath, String alias, char[] pwd, String certPath) throws KeyStoreException { IMPL.addCertificate(ks, ksType, ksPath, alias, pwd, certPath); } /** * 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 * The keystore to delete the alias from. * @param ksPath * The path to the keystore. * @param alias * The alias to use in the request generation. * @param pwd * The keystore password to use. * @throws KeyStoreException * If an error occurred deleting the alias. */ public static void deleteAlias(KeyStore ks, String ksPath, String alias, char[] pwd) throws KeyStoreException { IMPL.deleteAlias(ks, ksPath, alias, pwd); } /** * Generate a self-signed certificate using the specified alias, dn string and * validity period. If the keystore does not exist, it will be created using * the specified keystore type and path. * * @param ks * The keystore to save the certificate in. May be null if it does * not exist. * @param keyType * The keystore type to use if the keystore is created. * @param ksPath * The path to the keystore if the keystore is created. * @param ksType * Specify the key size, key algorithm and signature algorithms used. * @param alias * The alias to store the certificate under. * @param pwd * The password to us in saving the certificate. * @param dn * The dn string used as the certificate subject. * @param validity * The validity of the certificate in days. * @throws KeyStoreException * If the self-signed certificate cannot be generated. */ public static void generateSelfSignedCertificate(KeyStore ks, String ksType, String ksPath, KeyType keyType, String alias, char[] pwd, String dn, int validity) throws KeyStoreException { PlatformIMPL.generateSelfSignedCertificate(ks, ksType, ksPath, keyType, alias, pwd, dn, validity); } /** * Default platform class. */ private static class DefaultPlatformIMPL extends PlatformIMPL { } /** * Test if a platform java vendor property starts with the specified vendor * string. * * @param vendor * The vendor to check for. * @return {@code true} if the java vendor starts with the specified vendor * string. */ public static boolean isVendor(String vendor) { String javaVendor = System.getProperty("java.vendor"); return javaVendor.startsWith(vendor); } /** * Computes the number of replay/worker/cleaner threads based on the number of cpus in the system. * Allows for a multiplier to be specified and a minimum value to be returned if not enough processors * are present in the system. * * @param minimumValue at least this value should be returned. * @param cpuMultiplier the scaling multiplier of the number of threads to return * @return the number of threads based on the number of cpus in the system. * @throws IllegalArgumentException if {@code cpuMultiplier} is a non positive number */ public static int computeNumberOfThreads(int minimumValue, float cpuMultiplier) { Reject.ifTrue(cpuMultiplier < 0, "Multiplier must be a positive number"); return Math.max(minimumValue, (int)(Runtime.getRuntime().availableProcessors() * cpuMultiplier)); } }