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}