001/* 002 * Copyright 2019-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2019-2023 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-2023 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}