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}