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