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