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.util.ssl; 022 023 024 025import java.io.OutputStream; 026import java.io.PrintStream; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036import java.util.SortedMap; 037import java.util.SortedSet; 038import java.util.TreeMap; 039import java.util.TreeSet; 040import javax.net.ssl.SSLContext; 041import javax.net.ssl.SSLParameters; 042 043import com.unboundid.ldap.sdk.LDAPException; 044import com.unboundid.ldap.sdk.LDAPRuntimeException; 045import com.unboundid.ldap.sdk.ResultCode; 046import com.unboundid.ldap.sdk.Version; 047import com.unboundid.util.CommandLineTool; 048import com.unboundid.util.Debug; 049import com.unboundid.util.NotMutable; 050import com.unboundid.util.ObjectPair; 051import com.unboundid.util.StaticUtils; 052import com.unboundid.util.ThreadSafety; 053import com.unboundid.util.ThreadSafetyLevel; 054import com.unboundid.util.args.ArgumentException; 055import com.unboundid.util.args.ArgumentParser; 056 057import static com.unboundid.util.ssl.SSLMessages.*; 058 059 060 061/** 062 * This class provides a utility for selecting the cipher suites that should be 063 * supported for TLS communication. The logic used to select the recommended 064 * TLS cipher suites is as follows: 065 * <UL> 066 * <LI> 067 * Only cipher suites that use the TLS protocol will be recommended. Legacy 068 * SSL suites will not be recommended, nor will any suites that use an 069 * unrecognized protocol. 070 * </LI> 071 * 072 * <LI> 073 * Any cipher suite that uses a NULL key exchange, authentication, bulk 074 * encryption, or digest algorithm will not be recommended. 075 * </LI> 076 * 077 * <LI> 078 * Any cipher suite that uses anonymous authentication will not be 079 * recommended. 080 * </LI> 081 * 082 * <LI> 083 * Any cipher suite that uses weakened export-grade encryption will not be 084 * recommended. 085 * </LI> 086 * 087 * <LI> 088 * Only cipher suites that use ECDHE, DHE, or RSA key exchange algorithms 089 * will be recommended. Other key agreement algorithms, including ECDH, 090 * DH, and KRB5, will not be recommended. Cipher suites that use a 091 * pre-shared key or password will not be recommended. 092 * </LI> 093 * 094 * <LI> 095 * Only cipher suites that use AES or ChaCha20 bulk encryption ciphers will 096 * be recommended. Other bulk cipher algorithms, including RC4, DES, 3DES, 097 * IDEA, Camellia, and ARIA, will not be recommended. 098 * </LI> 099 * 100 * <LI> 101 * Only cipher suites that use SHA-1 or SHA-2 digests will be recommended 102 * (although SHA-1 digests are de-prioritized). Other digest algorithms, 103 * like MD5, will not be recommended. 104 * </LI> 105 * </UL> 106 * <BR><BR> 107 * Also note that this class can be used as a command-line tool for debugging 108 * purposes. 109 */ 110@NotMutable() 111@ThreadSafety(level= ThreadSafetyLevel.COMPLETELY_THREADSAFE) 112public final class TLSCipherSuiteSelector 113 extends CommandLineTool 114{ 115 /** 116 * The singleton instance of this TLS cipher suite selector. 117 */ 118 private static final TLSCipherSuiteSelector INSTANCE = 119 new TLSCipherSuiteSelector(); 120 121 122 123 // Retrieves a map of the supported cipher suites that are not recommended 124 // for use, mapped to a list of the reasons that the cipher suites are not 125 // recommended. 126 private final SortedMap<String,List<String>> nonRecommendedCipherSuites; 127 128 // The set of TLS cipher suites enabled in the JVM by default, sorted in 129 // order of most preferred to least preferred. 130 private final SortedSet<String> defaultCipherSuites; 131 132 // The recommended set of TLS cipher suites selected by this class, sorted in 133 // order of most preferred to least preferred. 134 private final SortedSet<String> recommendedCipherSuites; 135 136 // The full set of TLS cipher suites supported in the JVM, sorted in order of 137 // most preferred to least preferred. 138 private final SortedSet<String> supportedCipherSuites; 139 140 // The recommended set of TLS cipher suites as an array rather than a set. 141 private final String[] recommendedCipherSuiteArray; 142 143 144 145 /** 146 * Invokes this command-line program with the provided set of arguments. 147 * 148 * @param args The command-line arguments provided to this program. 149 */ 150 public static void main(final String... args) 151 { 152 final ResultCode resultCode = main(System.out, System.err, args); 153 if (resultCode != ResultCode.SUCCESS) 154 { 155 System.exit(resultCode.intValue()); 156 } 157 } 158 159 160 161 /** 162 * Invokes this command-line program with the provided set of arguments. 163 * 164 * @param out The output stream to use for standard output. It may be 165 * {@code null} if standard output should be suppressed. 166 * @param err The output stream to use for standard error. It may be 167 * {@code null} if standard error should be suppressed. 168 * @param args The command-line arguments provided to this program. 169 * 170 * @return A result code that indicates whether the processing was 171 * successful. 172 */ 173 public static ResultCode main(final OutputStream out, final OutputStream err, 174 final String... args) 175 { 176 final TLSCipherSuiteSelector tool = new TLSCipherSuiteSelector(out, err); 177 return tool.runTool(args); 178 } 179 180 181 182 /** 183 * Creates a new instance of this TLS cipher suite selector that will suppress 184 * all output. 185 */ 186 private TLSCipherSuiteSelector() 187 { 188 this(null, null); 189 } 190 191 192 193 194 /** 195 * Creates a new instance of this TLS cipher suite selector that will use the 196 * provided output streams. Note that this constructor should only be used 197 * when invoking it as a command-line tool. 198 * 199 * @param out The output stream to use for standard output. It may be 200 * {@code null} if standard output should be suppressed. 201 * @param err The output stream to use for standard error. It may be 202 * {@code null} if standard error should be suppressed. 203 */ 204 public TLSCipherSuiteSelector(final OutputStream out, 205 final OutputStream err) 206 { 207 super(out, err); 208 209 try 210 { 211 final SSLContext sslContext = SSLContext.getDefault(); 212 213 final SSLParameters supportedParameters = 214 sslContext.getSupportedSSLParameters(); 215 final TreeSet<String> supportedSet = 216 new TreeSet<>(TLSCipherSuiteComparator.getInstance()); 217 supportedSet.addAll(Arrays.asList(supportedParameters.getCipherSuites())); 218 supportedCipherSuites = Collections.unmodifiableSortedSet(supportedSet); 219 220 final SSLParameters defaultParameters = 221 sslContext.getDefaultSSLParameters(); 222 final TreeSet<String> defaultSet = 223 new TreeSet<>(TLSCipherSuiteComparator.getInstance()); 224 defaultSet.addAll(Arrays.asList(defaultParameters.getCipherSuites())); 225 defaultCipherSuites = Collections.unmodifiableSortedSet(supportedSet); 226 227 final ObjectPair<SortedSet<String>,SortedMap<String,List<String>>> 228 selectedPair = selectCipherSuites( 229 supportedParameters.getCipherSuites()); 230 recommendedCipherSuites = 231 Collections.unmodifiableSortedSet(selectedPair.getFirst()); 232 nonRecommendedCipherSuites = 233 Collections.unmodifiableSortedMap(selectedPair.getSecond()); 234 235 recommendedCipherSuiteArray = 236 recommendedCipherSuites.toArray(StaticUtils.NO_STRINGS); 237 } 238 catch (final Exception e) 239 { 240 Debug.debugException(e); 241 242 // This should never happen. 243 throw new LDAPRuntimeException(new LDAPException(ResultCode.LOCAL_ERROR, 244 ERR_TLS_CIPHER_SUITE_SELECTOR_INIT_ERROR.get( 245 StaticUtils.getExceptionMessage(e)), 246 e)); 247 } 248 249 250 // If the JVM's TLS debugging support is enabled, then invoke the tool 251 // and send its output to standard error. 252 final String debugProperty = 253 StaticUtils.getSystemProperty("javax.net.debug"); 254 if ((debugProperty != null) && debugProperty.equals("all")) 255 { 256 System.err.println(); 257 System.err.println(getClass().getName() + " Results:"); 258 generateOutput(System.err); 259 System.err.println(); 260 } 261 } 262 263 264 265 /** 266 * Retrieves the set of all TLS cipher suites supported by the JVM. The set 267 * will be sorted in order of most preferred to least preferred, as determined 268 * by the {@link TLSCipherSuiteComparator}. 269 * 270 * @return The set of all TLS cipher suites supported by the JVM. 271 */ 272 public static SortedSet<String> getSupportedCipherSuites() 273 { 274 return INSTANCE.supportedCipherSuites; 275 } 276 277 278 279 /** 280 * Retrieves the set of TLS cipher suites enabled by default in the JVM. The 281 * set will be sorted in order of most preferred to least preferred, as 282 * determined by the {@link TLSCipherSuiteComparator}. 283 * 284 * @return The set of TLS cipher suites enabled by default in the JVM. 285 */ 286 public static SortedSet<String> getDefaultCipherSuites() 287 { 288 return INSTANCE.defaultCipherSuites; 289 } 290 291 292 293 /** 294 * Retrieves the recommended set of TLS cipher suites as selected by this 295 * class. The set will be sorted in order of most preferred to least 296 * preferred, as determined by the {@link TLSCipherSuiteComparator}. 297 * 298 * @return The recommended set of TLS cipher suites as selected by this 299 * class. 300 */ 301 public static SortedSet<String> getRecommendedCipherSuites() 302 { 303 return INSTANCE.recommendedCipherSuites; 304 } 305 306 307 308 /** 309 * Retrieves an array containing the recommended set of TLS cipher suites as 310 * selected by this class. The array will be sorted in order of most 311 * preferred to least preferred, as determined by the 312 * {@link TLSCipherSuiteComparator}. 313 * 314 * @return An array containing the recommended set of TLS cipher suites as 315 * selected by this class. 316 */ 317 public static String[] getRecommendedCipherSuiteArray() 318 { 319 return INSTANCE.recommendedCipherSuiteArray.clone(); 320 } 321 322 323 324 /** 325 * Retrieves a map containing the TLS cipher suites that are supported by the 326 * JVM but are not recommended for use. The keys of the map will be the names 327 * of the non-recommended cipher suites, sorted in order of most preferred to 328 * least preferred, as determined by the {@link TLSCipherSuiteComparator}. 329 * Each TLS cipher suite name will be mapped to a list of the reasons it is 330 * not recommended for use. 331 * 332 * @return A map containing the TLS cipher suites that are supported by the 333 * JVM but are not recommended for use 334 */ 335 public static SortedMap<String,List<String>> getNonRecommendedCipherSuites() 336 { 337 return INSTANCE.nonRecommendedCipherSuites; 338 } 339 340 341 342 /** 343 * Organizes the provided set of cipher suites into recommended and 344 * non-recommended sets. 345 * 346 * @param cipherSuiteArray An array of the cipher suites to be organized. 347 * 348 * @return An object pair in which the first element is the sorted set of 349 * recommended cipher suites, and the second element is the sorted 350 * map of non-recommended cipher suites and the reasons they are not 351 * recommended for use. 352 */ 353 static ObjectPair<SortedSet<String>,SortedMap<String,List<String>>> 354 selectCipherSuites(final String[] cipherSuiteArray) 355 { 356 final SortedSet<String> recommendedSet = 357 new TreeSet<>(TLSCipherSuiteComparator.getInstance()); 358 final SortedMap<String,List<String>> nonRecommendedMap = 359 new TreeMap<>(TLSCipherSuiteComparator.getInstance()); 360 361 for (final String cipherSuiteName : cipherSuiteArray) 362 { 363 final String name = 364 StaticUtils.toUpperCase(cipherSuiteName).replace('-', '_'); 365 366 // Signalling cipher suite values (which indicate capabilities of the 367 // implementation and aren't really cipher suites on their own) will 368 // always be accepted. 369 if (name.endsWith("_SCSV")) 370 { 371 recommendedSet.add(cipherSuiteName); 372 continue; 373 } 374 375 376 // Only cipher suites using the TLS protocol will be accepted. 377 final List<String> nonRecommendedReasons = new ArrayList<>(5); 378 if (name.startsWith("SSL_")) 379 { 380 nonRecommendedReasons.add( 381 ERR_TLS_CIPHER_SUITE_SELECTOR_LEGACY_SSL_PROTOCOL.get()); 382 } 383 else if (name.startsWith("TLS_")) 384 { 385 // Only TLS cipher suites using a recommended key exchange algorithm 386 // will be accepted. 387 if (name.startsWith("TLS_AES_") || 388 name.startsWith("TLS_CHACHA20_") || 389 name.startsWith("TLS_ECDHE_") || 390 name.startsWith("TLS_DHE_") || 391 name.startsWith("TLS_RSA_")) 392 { 393 // These are recommended key exchange algorithms. 394 } 395 else if (name.startsWith("TLS_ECDH_")) 396 { 397 nonRecommendedReasons.add( 398 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_KE_ALG.get( 399 "ECDH")); 400 } 401 else if (name.startsWith("TLS_DH_")) 402 { 403 nonRecommendedReasons.add( 404 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_KE_ALG.get( 405 "DH")); 406 } 407 else if (name.startsWith("TLS_KRB5_")) 408 { 409 nonRecommendedReasons.add( 410 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_KE_ALG.get( 411 "KRB5")); 412 } 413 else 414 { 415 nonRecommendedReasons.add( 416 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_UNKNOWN_KE_ALG. 417 get()); 418 } 419 } 420 else 421 { 422 nonRecommendedReasons.add( 423 ERR_TLS_CIPHER_SUITE_SELECTOR_UNRECOGNIZED_PROTOCOL.get()); 424 } 425 426 427 // Cipher suites that rely on pre-shared keys will not be accepted. 428 if (name.contains("_PSK")) 429 { 430 nonRecommendedReasons.add(ERR_TLS_CIPHER_SUITE_SELECTOR_PSK.get()); 431 } 432 433 434 // Cipher suites that use a null component will not be accepted. 435 if (name.contains("_NULL")) 436 { 437 nonRecommendedReasons.add( 438 ERR_TLS_CIPHER_SUITE_SELECTOR_NULL_COMPONENT.get()); 439 } 440 441 442 // Cipher suites that use anonymous authentication will not be accepted. 443 if (name.contains("_ANON")) 444 { 445 nonRecommendedReasons.add( 446 ERR_TLS_CIPHER_SUITE_SELECTOR_ANON_AUTH.get()); 447 } 448 449 450 // Cipher suites that use export-grade encryption will not be accepted. 451 if (name.contains("_EXPORT")) 452 { 453 nonRecommendedReasons.add( 454 ERR_TLS_CIPHER_SUITE_SELECTOR_EXPORT_ENCRYPTION.get()); 455 } 456 457 458 // Only cipher suites that use AES or ChaCha20 will be accepted. 459 if (name.contains("_AES") || name.contains("_CHACHA20")) 460 { 461 // These are recommended bulk cipher algorithms. 462 } 463 else if (name.contains("_RC4")) 464 { 465 nonRecommendedReasons.add( 466 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_BE_ALG.get( 467 "RC4")); 468 } 469 else if (name.contains("_3DES")) 470 { 471 nonRecommendedReasons.add( 472 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_BE_ALG.get( 473 "3DES")); 474 } 475 else if (name.contains("_DES")) 476 { 477 nonRecommendedReasons.add( 478 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_BE_ALG.get( 479 "DES")); 480 } 481 else if (name.contains("_IDEA")) 482 { 483 nonRecommendedReasons.add( 484 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_BE_ALG.get( 485 "IDEA")); 486 } 487 else if (name.contains("_CAMELLIA")) 488 { 489 nonRecommendedReasons.add( 490 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_BE_ALG.get( 491 "Camellia")); 492 } 493 else if (name.contains("_ARIA")) 494 { 495 nonRecommendedReasons.add( 496 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_BE_ALG.get( 497 "ARIA")); 498 } 499 else 500 { 501 nonRecommendedReasons.add( 502 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_UNKNOWN_BE_ALG. 503 get()); 504 } 505 506 507 // Only cipher suites that use a SHA-1 or SHA-2 digest algorithm will be 508 // accepted. 509 if (name.endsWith("_SHA512") || 510 name.endsWith("_SHA384") || 511 name.endsWith("_SHA256") || 512 name.endsWith("_SHA")) 513 { 514 // These are recommended digest algorithms. 515 } 516 else if (name.endsWith("_MD5")) 517 { 518 nonRecommendedReasons.add( 519 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_KNOWN_DIGEST_ALG.get( 520 "MD5")); 521 } 522 else 523 { 524 nonRecommendedReasons.add( 525 ERR_TLS_CIPHER_SUITE_SELECTOR_NON_RECOMMENDED_UNKNOWN_DIGEST_ALG. 526 get()); 527 } 528 529 530 // Determine whether to recommend the cipher suite based on whether there 531 // are any non-recommended reasons. 532 if (nonRecommendedReasons.isEmpty()) 533 { 534 recommendedSet.add(cipherSuiteName); 535 } 536 else 537 { 538 nonRecommendedMap.put(cipherSuiteName, 539 Collections.unmodifiableList(nonRecommendedReasons)); 540 } 541 } 542 543 return new ObjectPair<>(recommendedSet, nonRecommendedMap); 544 } 545 546 547 548 /** 549 * {@inheritDoc} 550 */ 551 @Override() 552 public String getToolName() 553 { 554 return "tls-cipher-suite-selector"; 555 } 556 557 558 559 /** 560 * {@inheritDoc} 561 */ 562 @Override() 563 public String getToolDescription() 564 { 565 return INFO_TLS_CIPHER_SUITE_SELECTOR_TOOL_DESC.get(); 566 } 567 568 569 570 /** 571 * {@inheritDoc} 572 */ 573 @Override() 574 public String getToolVersion() 575 { 576 return Version.NUMERIC_VERSION_STRING; 577 } 578 579 580 581 /** 582 * {@inheritDoc} 583 */ 584 @Override() 585 public void addToolArguments(final ArgumentParser parser) 586 throws ArgumentException 587 { 588 // This tool does not require any arguments. 589 } 590 591 592 593 /** 594 * {@inheritDoc} 595 */ 596 @Override() 597 public ResultCode doToolProcessing() 598 { 599 generateOutput(getOut()); 600 return ResultCode.SUCCESS; 601 } 602 603 604 605 /** 606 * Writes the output to the provided print stream. 607 * 608 * @param s The print stream to which the output should be written. 609 */ 610 private void generateOutput(final PrintStream s) 611 { 612 s.println("Supported TLS Cipher Suites:"); 613 for (final String cipherSuite : supportedCipherSuites) 614 { 615 s.println("* " + cipherSuite); 616 } 617 618 s.println(); 619 s.println("JVM-Default TLS Cipher Suites:"); 620 for (final String cipherSuite : defaultCipherSuites) 621 { 622 s.println("* " + cipherSuite); 623 } 624 625 s.println(); 626 s.println("Non-Recommended TLS Cipher Suites:"); 627 for (final Map.Entry<String,List<String>> e : 628 nonRecommendedCipherSuites.entrySet()) 629 { 630 s.println("* " + e.getKey()); 631 for (final String reason : e.getValue()) 632 { 633 s.println(" - " + reason); 634 } 635 } 636 637 s.println(); 638 s.println("Recommended TLS Cipher Suites:"); 639 for (final String cipherSuite : recommendedCipherSuites) 640 { 641 s.println("* " + cipherSuite); 642 } 643 } 644 645 646 647 /** 648 * Filters the provided collection of potential cipher suite names to retrieve 649 * a set of the suites that are supported by the JVM. 650 * 651 * @param potentialSuiteNames The collection of cipher suite names to be 652 * filtered. 653 * 654 * @return The set of provided cipher suites that are supported by the JVM, 655 * or an empty set if none of the potential provided suite names are 656 * supported by the JVM. 657 */ 658 public static Set<String> selectSupportedCipherSuites( 659 final Collection<String> potentialSuiteNames) 660 { 661 if (potentialSuiteNames == null) 662 { 663 return Collections.emptySet(); 664 } 665 666 final int capacity = 667 StaticUtils.computeMapCapacity(INSTANCE.supportedCipherSuites.size()); 668 final Map<String,String> supportedMap = new HashMap<>(capacity); 669 for (final String supportedSuite : INSTANCE.supportedCipherSuites) 670 { 671 supportedMap.put( 672 StaticUtils.toUpperCase(supportedSuite).replace('-', '_'), 673 supportedSuite); 674 } 675 676 final Set<String> selectedSet = new LinkedHashSet<>(capacity); 677 for (final String potentialSuite : potentialSuiteNames) 678 { 679 final String supportedName = supportedMap.get( 680 StaticUtils.toUpperCase(potentialSuite).replace('-', '_')); 681 if (supportedName != null) 682 { 683 selectedSet.add(supportedName); 684 } 685 } 686 687 return Collections.unmodifiableSet(selectedSet); 688 } 689}