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