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}