001/*
002 * Copyright 2019-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2019-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2019-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.listener;
037
038
039
040import java.io.ByteArrayOutputStream;
041import java.io.File;
042import java.net.InetAddress;
043import java.text.SimpleDateFormat;
044import java.util.ArrayList;
045import java.util.Date;
046import java.util.Set;
047
048import com.unboundid.ldap.sdk.DN;
049import com.unboundid.ldap.sdk.LDAPConnectionOptions;
050import com.unboundid.ldap.sdk.NameResolver;
051import com.unboundid.ldap.sdk.RDN;
052import com.unboundid.ldap.sdk.ResultCode;
053import com.unboundid.util.Base64;
054import com.unboundid.util.Debug;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.ObjectPair;
057import com.unboundid.util.StaticUtils;
058import com.unboundid.util.ThreadLocalSecureRandom;
059import com.unboundid.util.ThreadSafety;
060import com.unboundid.util.ThreadSafetyLevel;
061import com.unboundid.util.args.DNSHostNameArgumentValueValidator;
062import com.unboundid.util.ssl.cert.CertException;
063import com.unboundid.util.ssl.cert.ManageCertificates;
064
065import static com.unboundid.ldap.listener.ListenerMessages.*;
066
067
068
069/**
070 * This class provides a mechanism for generating a self-signed certificate for
071 * use by a listener that supports SSL or StartTLS.
072 */
073@ThreadSafety(level= ThreadSafetyLevel.NOT_THREADSAFE)
074public final class SelfSignedCertificateGenerator
075{
076  /**
077   * Prevent this utility class from being instantiated.
078   */
079  private SelfSignedCertificateGenerator()
080  {
081    // No implementation is required.
082  }
083
084
085
086  /**
087   * Generates a temporary keystore containing a self-signed certificate for
088   * use by a listener that supports SSL or StartTLS.
089   *
090   * @param  toolName      The name of the tool for which the certificate is to
091   *                       be generated.
092   * @param  keyStoreType  The key store type for the keystore to be created.
093   *                       It must not be {@code null}.
094   *
095   * @return  An {@code ObjectPair} containing the path and PIN for the keystore
096   *          that was generated.
097   *
098   * @throws  CertException  If a problem occurs while trying to generate the
099   *                         temporary keystore containing the self-signed
100   *                         certificate.
101   */
102  @NotNull()
103  public static ObjectPair<File,char[]> generateTemporarySelfSignedCertificate(
104                                             @NotNull final String toolName,
105                                             @NotNull final String keyStoreType)
106         throws CertException
107  {
108    final File keyStoreFile;
109    try
110    {
111      keyStoreFile = File.createTempFile("temp-keystore-", ".jks");
112    }
113    catch (final Exception e)
114    {
115      Debug.debugException(e);
116      throw new CertException(
117           ERR_SELF_SIGNED_CERT_GENERATOR_CANNOT_CREATE_FILE.get(
118                StaticUtils.getExceptionMessage(e)),
119           e);
120    }
121
122    keyStoreFile.delete();
123
124    final byte[] randomBytes = new byte[50];
125    ThreadLocalSecureRandom.get().nextBytes(randomBytes);
126    final String keyStorePIN = Base64.encode(randomBytes);
127
128    generateSelfSignedCertificate(toolName, keyStoreFile, keyStorePIN,
129         keyStoreType, "server-cert");
130    return new ObjectPair<>(keyStoreFile, keyStorePIN.toCharArray());
131  }
132
133
134
135  /**
136   * Generates a self-signed certificate in the specified keystore.
137   *
138   * @param  toolName      The name of the tool for which the certificate is to
139   *                       be generated.
140   * @param  keyStoreFile  The path to the keystore file in which the
141   *                       certificate is to be generated.  This must not be
142   *                       {@code null}, and if the target file exists, then it
143   *                       must be a JKS or PKCS #12 keystore.  If it does not
144   *                       exist, then at least the parent directory must exist.
145   * @param  keyStorePIN   The PIN needed to access the keystore.  It must not
146   *                       be {@code null}.
147   * @param  keyStoreType  The key store type for the keystore to be created, if
148   *                       it does not already exist.  It must not be
149   *                       {@code null}.
150   * @param  alias         The alias to use for the certificate in the keystore.
151   *                       It must not be {@code null}.
152   *
153   * @throws  CertException  If a problem occurs while trying to generate
154   *                         self-signed certificate.
155   */
156  public static void generateSelfSignedCertificate(
157                          @NotNull final String toolName,
158                          @NotNull final File keyStoreFile,
159                          @NotNull final String keyStorePIN,
160                          @NotNull final String keyStoreType,
161                          @NotNull final String alias)
162         throws CertException
163  {
164    // Try to get a set of all addresses associated with the local system and
165    // their corresponding canonical hostnames.
166    final NameResolver nameResolver =
167         LDAPConnectionOptions.DEFAULT_NAME_RESOLVER;
168    Set<InetAddress> localAddresses =
169         StaticUtils.getAllLocalAddresses(nameResolver, false);
170    if (localAddresses.isEmpty())
171    {
172      localAddresses = StaticUtils.getAllLocalAddresses(nameResolver, true);
173
174    }
175
176    final Set<String> canonicalHostNames =
177         StaticUtils.getAvailableCanonicalHostNames(nameResolver,
178              localAddresses);
179
180
181    // Construct a subject DN for the certificate.
182    final DN subjectDN;
183    if (localAddresses.isEmpty())
184    {
185      subjectDN = new DN(new RDN("CN", toolName));
186    }
187    else
188    {
189      subjectDN = new DN(
190           new RDN("CN",
191                nameResolver.getCanonicalHostName(
192                     localAddresses.iterator().next())),
193           new RDN("OU", toolName));
194    }
195
196
197    // Generate a timestamp that corresponds to one day ago.
198    final long oneDayAgoTime = System.currentTimeMillis() - 86_400_000L;
199    final Date oneDayAgoDate = new Date(oneDayAgoTime);
200    final SimpleDateFormat dateFormatter =
201         new SimpleDateFormat("yyyyMMddHHmmss");
202    final String yesterdayTimeStamp = dateFormatter.format(oneDayAgoDate);
203
204
205    // Build the list of arguments to provide to the manage-certificates tool.
206    final ArrayList<String> argList = new ArrayList<>(30);
207    argList.add("generate-self-signed-certificate");
208
209    argList.add("--keystore");
210    argList.add(keyStoreFile.getAbsolutePath());
211
212    argList.add("--keystore-password");
213    argList.add(keyStorePIN);
214
215    argList.add("--keystore-type");
216    argList.add(keyStoreType);
217
218    argList.add("--alias");
219    argList.add(alias);
220
221    argList.add("--subject-dn");
222    argList.add(subjectDN.toString());
223
224    argList.add("--days-valid");
225    argList.add("366");
226
227    argList.add("--validityStartTime");
228    argList.add(yesterdayTimeStamp);
229
230    argList.add("--key-algorithm");
231    argList.add("RSA");
232
233    argList.add("--key-size-bits");
234    argList.add("2048");
235
236    argList.add("--signature-algorithm");
237    argList.add("SHA256withRSA");
238
239    for (final String hostName : canonicalHostNames)
240    {
241      try
242      {
243        DNSHostNameArgumentValueValidator.validateDNSHostName(hostName,
244             false, true, true, null);
245        argList.add("--subject-alternative-name-dns");
246        argList.add(hostName);
247      }
248      catch (final Exception e)
249      {
250        // This indicates that it's not actually a valid hostname, so exclude
251        // it from the set of subject alternative names.
252        Debug.debugException(e);
253      }
254    }
255
256    for (final InetAddress address : localAddresses)
257    {
258      argList.add("--subject-alternative-name-ip-address");
259      argList.add(StaticUtils.trimInterfaceNameFromHostAddress(
260           address.getHostAddress()));
261    }
262
263    argList.add("--key-usage");
264    argList.add("digitalSignature");
265    argList.add("--key-usage");
266    argList.add("keyEncipherment");
267
268    argList.add("--extended-key-usage");
269    argList.add("server-auth");
270    argList.add("--extended-key-usage");
271    argList.add("client-auth");
272
273    final ByteArrayOutputStream output = new ByteArrayOutputStream();
274    final ResultCode resultCode = ManageCertificates.main(null, output, output,
275         argList.toArray(StaticUtils.NO_STRINGS));
276    if (resultCode != ResultCode.SUCCESS)
277    {
278      throw new CertException(
279           ERR_SELF_SIGNED_CERT_GENERATOR_ERROR_GENERATING_CERT.get(
280                StaticUtils.toUTF8String(output.toByteArray())));
281    }
282  }
283}