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}