001/*
002 * Copyright 2008-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-2024 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) 2008-2024 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
039import java.io.BufferedReader;
040import java.io.BufferedWriter;
041import java.io.File;
042import java.io.FileReader;
043import java.io.FileWriter;
044import java.io.InputStream;
045import java.io.InputStreamReader;
046import java.io.IOException;
047import java.io.PrintStream;
048import java.nio.file.Files;
049import java.security.cert.Certificate;
050import java.security.cert.CertificateException;
051import java.security.cert.X509Certificate;
052import java.util.ArrayList;
053import java.util.Collection;
054import java.util.Collections;
055import java.util.List;
056import java.util.concurrent.ConcurrentHashMap;
057import javax.net.ssl.X509TrustManager;
058
059import com.unboundid.util.Debug;
060import com.unboundid.util.NotMutable;
061import com.unboundid.util.NotNull;
062import com.unboundid.util.Nullable;
063import com.unboundid.util.ObjectPair;
064import com.unboundid.util.StaticUtils;
065import com.unboundid.util.ThreadSafety;
066import com.unboundid.util.ThreadSafetyLevel;
067import com.unboundid.util.ssl.cert.CertException;
068
069import static com.unboundid.util.ssl.SSLMessages.*;
070
071
072
073/**
074 * This class provides an SSL trust manager that will interactively prompt the
075 * user to determine whether to trust any certificate that is presented to it.
076 * It provides the ability to cache information about certificates that had been
077 * previously trusted so that the user is not prompted about the same
078 * certificate repeatedly, and it can be configured to store trusted
079 * certificates in a file so that the trust information can be persisted.
080 */
081@NotMutable()
082@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
083public final class PromptTrustManager
084       implements X509TrustManager
085{
086  /**
087   * A pre-allocated empty certificate array.
088   */
089  @NotNull private static final X509Certificate[] NO_CERTIFICATES =
090       new X509Certificate[0];
091
092
093
094  // Indicates whether to examine the validity dates for the certificate in
095  // addition to whether the certificate has been previously trusted.
096  private final boolean examineValidityDates;
097
098  // The set of previously-accepted certificates.  The certificates will be
099  // mapped from an all-lowercase hexadecimal string representation of the
100  // certificate signature to a flag that indicates whether the certificate has
101  // already been manually trusted even if it is outside of the validity window.
102  @NotNull private final ConcurrentHashMap<String,Boolean> acceptedCerts;
103
104  // The input stream from which the user input will be read.
105  @NotNull private final InputStream in;
106
107  // A list of the addresses that the client is expected to use to connect to
108  // one of the target servers.
109  @NotNull private final List<String> expectedAddresses;
110
111  // The print stream that will be used to display the prompt.
112  @NotNull private final PrintStream out;
113
114  // The path to the file to which the set of accepted certificates should be
115  // persisted.
116  @Nullable private final String acceptedCertsFile;
117
118
119
120  /**
121   * Creates a new instance of this prompt trust manager.  It will cache trust
122   * information in memory but not on disk.
123   */
124  public PromptTrustManager()
125  {
126    this(null, true, null, null);
127  }
128
129
130
131  /**
132   * Creates a new instance of this prompt trust manager.  It may optionally
133   * cache trust information on disk.
134   *
135   * @param  acceptedCertsFile  The path to a file in which the certificates
136   *                            that have been previously accepted will be
137   *                            cached.  It may be {@code null} if the cache
138   *                            should only be maintained in memory.
139   */
140  public PromptTrustManager(@Nullable final String acceptedCertsFile)
141  {
142    this(acceptedCertsFile, true, null, null);
143  }
144
145
146
147  /**
148   * Creates a new instance of this prompt trust manager.  It may optionally
149   * cache trust information on disk, and may also be configured to examine or
150   * ignore validity dates.
151   *
152   * @param  acceptedCertsFile     The path to a file in which the certificates
153   *                               that have been previously accepted will be
154   *                               cached.  It may be {@code null} if the cache
155   *                               should only be maintained in memory.
156   * @param  examineValidityDates  Indicates whether to reject certificates if
157   *                               the current time is outside the validity
158   *                               window for the certificate.
159   * @param  in                    The input stream that will be used to read
160   *                               input from the user.  If this is {@code null}
161   *                               then {@code System.in} will be used.
162   * @param  out                   The print stream that will be used to display
163   *                               the prompt to the user.  If this is
164   *                               {@code null} then System.out will be used.
165   */
166  public PromptTrustManager(@Nullable final String acceptedCertsFile,
167                            final boolean examineValidityDates,
168                            @Nullable final InputStream in,
169                            @Nullable final PrintStream out)
170  {
171    this(acceptedCertsFile, examineValidityDates,
172         Collections.<String>emptyList(), in, out);
173  }
174
175
176
177  /**
178   * Creates a new instance of this prompt trust manager.  It may optionally
179   * cache trust information on disk, and may also be configured to examine or
180   * ignore validity dates.
181   *
182   * @param  acceptedCertsFile     The path to a file in which the certificates
183   *                               that have been previously accepted will be
184   *                               cached.  It may be {@code null} if the cache
185   *                               should only be maintained in memory.
186   * @param  examineValidityDates  Indicates whether to reject certificates if
187   *                               the current time is outside the validity
188   *                               window for the certificate.
189   * @param  expectedAddress       An optional address that the client is
190   *                               expected to use to connect to the target
191   *                               server.  This may be {@code null} if no
192   *                               expected address is available, if this trust
193   *                               manager is only expected to be used to
194   *                               validate client certificates, or if no server
195   *                               address validation should be performed.  If a
196   *                               non-{@code null} value is provided, then the
197   *                               trust manager may issue a warning if the
198   *                               certificate does not contain that address.
199   * @param  in                    The input stream that will be used to read
200   *                               input from the user.  If this is {@code null}
201   *                               then {@code System.in} will be used.
202   * @param  out                   The print stream that will be used to display
203   *                               the prompt to the user.  If this is
204   *                               {@code null} then System.out will be used.
205   */
206  public PromptTrustManager(@Nullable final String acceptedCertsFile,
207                            final boolean examineValidityDates,
208                            @Nullable final String expectedAddress,
209                            @Nullable final InputStream in,
210                            @Nullable final PrintStream out)
211  {
212    this(acceptedCertsFile, examineValidityDates,
213         (expectedAddress == null)
214              ? Collections.<String>emptyList()
215              : Collections.singletonList(expectedAddress),
216         in, out);
217  }
218
219
220
221  /**
222   * Creates a new instance of this prompt trust manager.  It may optionally
223   * cache trust information on disk, and may also be configured to examine or
224   * ignore validity dates.
225   *
226   * @param  acceptedCertsFile     The path to a file in which the certificates
227   *                               that have been previously accepted will be
228   *                               cached.  It may be {@code null} if the cache
229   *                               should only be maintained in memory.
230   * @param  examineValidityDates  Indicates whether to reject certificates if
231   *                               the current time is outside the validity
232   *                               window for the certificate.
233   * @param  expectedAddresses     An optional collection of the addresses that
234   *                               the client is expected to use to connect to
235   *                               one of the target servers.  This may be
236   *                               {@code null} or empty if no expected
237   *                               addresses are available, if this trust
238   *                               manager is only expected to be used to
239   *                               validate client certificates, or if no server
240   *                               address validation should be performed.  If a
241   *                               non-empty collection is provided, then the
242   *                               trust manager may issue a warning if the
243   *                               certificate does not contain any of these
244   *                               addresses.
245   * @param  in                    The input stream that will be used to read
246   *                               input from the user.  If this is {@code null}
247   *                               then {@code System.in} will be used.
248   * @param  out                   The print stream that will be used to display
249   *                               the prompt to the user.  If this is
250   *                               {@code null} then System.out will be used.
251   */
252  public PromptTrustManager(@Nullable final String acceptedCertsFile,
253              final boolean examineValidityDates,
254              @Nullable final Collection<String> expectedAddresses,
255              @Nullable final InputStream in,
256              @Nullable final PrintStream out)
257  {
258    this.acceptedCertsFile    = acceptedCertsFile;
259    this.examineValidityDates = examineValidityDates;
260
261    if (expectedAddresses == null)
262    {
263      this.expectedAddresses = Collections.emptyList();
264    }
265    else
266    {
267      this.expectedAddresses =
268           Collections.unmodifiableList(new ArrayList<>(expectedAddresses));
269    }
270
271    if (in == null)
272    {
273      this.in = System.in;
274    }
275    else
276    {
277      this.in = in;
278    }
279
280    if (out == null)
281    {
282      this.out = System.out;
283    }
284    else
285    {
286      this.out = out;
287    }
288
289    acceptedCerts = new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(20));
290
291    if (acceptedCertsFile != null)
292    {
293      BufferedReader r = null;
294      try
295      {
296        final File f = new File(acceptedCertsFile);
297        if (f.exists())
298        {
299          r = new BufferedReader(new FileReader(f));
300          while (true)
301          {
302            final String line = r.readLine();
303            if (line == null)
304            {
305              break;
306            }
307            acceptedCerts.put(line, false);
308          }
309        }
310      }
311      catch (final Exception e)
312      {
313        Debug.debugException(e);
314      }
315      finally
316      {
317        if (r != null)
318        {
319          try
320          {
321            r.close();
322          }
323          catch (final Exception e)
324          {
325            Debug.debugException(e);
326          }
327        }
328      }
329    }
330  }
331
332
333
334  /**
335   * Writes an updated copy of the trusted certificate cache to disk.
336   *
337   * @throws  IOException  If a problem occurs.
338   */
339  private void writeCacheFile()
340          throws IOException
341  {
342    final File tempFile = new File(acceptedCertsFile + ".new");
343
344    BufferedWriter w = null;
345    try
346    {
347      w = new BufferedWriter(new FileWriter(tempFile));
348
349      for (final String certBytes : acceptedCerts.keySet())
350      {
351        w.write(certBytes);
352        w.newLine();
353      }
354    }
355    finally
356    {
357      if (w != null)
358      {
359        w.close();
360      }
361    }
362
363    final File cacheFile = new File(acceptedCertsFile);
364    if (cacheFile.exists())
365    {
366      final File oldFile = new File(acceptedCertsFile + ".previous");
367      if (oldFile.exists())
368      {
369        Files.delete(oldFile.toPath());
370      }
371
372      Files.move(cacheFile.toPath(), oldFile.toPath());
373    }
374
375    Files.move(tempFile.toPath(), cacheFile.toPath());
376  }
377
378
379
380  /**
381   * Indicates whether this trust manager would interactively prompt the user
382   * about whether to trust the provided certificate chain.
383   *
384   * @param  chain  The chain of certificates for which to make the
385   *                determination.
386   *
387   * @return  {@code true} if this trust manger would interactively prompt the
388   *          user about whether to trust the certificate chain, or
389   *          {@code false} if not (e.g., because the certificate is already
390   *          known to be trusted).
391   */
392  public synchronized boolean wouldPrompt(
393                                   @NotNull final X509Certificate[] chain)
394  {
395    try
396    {
397      final String cacheKey = getCacheKey(chain[0]);
398      return PromptTrustManagerProcessor.shouldPrompt(cacheKey,
399           convertChain(chain), false, examineValidityDates, acceptedCerts,
400           null).getFirst();
401    }
402    catch (final Exception e)
403    {
404      Debug.debugException(e);
405      return false;
406    }
407  }
408
409
410
411  /**
412   * Performs the necessary validity check for the provided certificate array.
413   *
414   * @param  chain       The chain of certificates for which to make the
415   *                     determination.
416   * @param  serverCert  Indicates whether the certificate was presented as a
417   *                     server certificate or as a client certificate.
418   *
419   * @throws  CertificateException  If the provided certificate chain should not
420   *                                be trusted.
421   */
422  private synchronized void checkCertificateChain(
423                                 @NotNull final X509Certificate[] chain,
424                                 final boolean serverCert)
425          throws CertificateException
426  {
427    final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain =
428         convertChain(chain);
429
430    final String cacheKey = getCacheKey(chain[0]);
431    final ObjectPair<Boolean,List<String>> shouldPromptResult =
432         PromptTrustManagerProcessor.shouldPrompt(cacheKey, convertedChain,
433              serverCert, examineValidityDates, acceptedCerts,
434              expectedAddresses);
435
436    if (! shouldPromptResult.getFirst())
437    {
438      return;
439    }
440
441    if (serverCert)
442    {
443      out.println(INFO_PROMPT_SERVER_HEADING.get());
444    }
445    else
446    {
447      out.println(INFO_PROMPT_CLIENT_HEADING.get());
448    }
449
450    out.println();
451    out.println("     " +
452         INFO_PROMPT_SUBJECT.get(convertedChain[0].getSubjectDN()));
453    out.println("     " +
454         INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate(
455              convertedChain[0].getNotBeforeDate())));
456    out.println("     " +
457         INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate(
458              convertedChain[0].getNotAfterDate())));
459
460    try
461    {
462      final byte[] sha1Fingerprint = convertedChain[0].getSHA1Fingerprint();
463      final StringBuilder buffer = new StringBuilder();
464      StaticUtils.toHex(sha1Fingerprint, ":", buffer);
465      out.println("     " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer));
466    }
467    catch (final Exception e)
468    {
469      Debug.debugException(e);
470    }
471    try
472    {
473      final byte[] sha256Fingerprint = convertedChain[0].getSHA256Fingerprint();
474      final StringBuilder buffer = new StringBuilder();
475      StaticUtils.toHex(sha256Fingerprint, ":", buffer);
476      out.println("     " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer));
477    }
478    catch (final Exception e)
479    {
480      Debug.debugException(e);
481    }
482
483
484    for (int i=1; i < chain.length; i++)
485    {
486      out.println("     -");
487      out.println("     " +
488           INFO_PROMPT_ISSUER_SUBJECT.get(i, convertedChain[i].getSubjectDN()));
489      out.println("     " +
490           INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate(
491                convertedChain[i].getNotBeforeDate())));
492      out.println("     " +
493           INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate(
494                convertedChain[i].getNotAfterDate())));
495
496      try
497      {
498        final byte[] sha1Fingerprint = convertedChain[i].getSHA1Fingerprint();
499        final StringBuilder buffer = new StringBuilder();
500        StaticUtils.toHex(sha1Fingerprint, ":", buffer);
501        out.println("     " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer));
502      }
503      catch (final Exception e)
504      {
505        Debug.debugException(e);
506      }
507      try
508      {
509        final byte[] sha256Fingerprint =
510             convertedChain[i].getSHA256Fingerprint();
511        final StringBuilder buffer = new StringBuilder();
512        StaticUtils.toHex(sha256Fingerprint, ":", buffer);
513        out.println("     " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer));
514      }
515      catch (final Exception e)
516      {
517        Debug.debugException(e);
518      }
519    }
520
521    for (final String warningMessage : shouldPromptResult.getSecond())
522    {
523      out.println();
524      for (final String line :
525           StaticUtils.wrapLine(warningMessage,
526                (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)))
527      {
528        out.println(line);
529      }
530    }
531
532    final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
533    while (true)
534    {
535      try
536      {
537        out.println();
538        out.print(INFO_PROMPT_MESSAGE.get() + ' ');
539        out.flush();
540        final String line = reader.readLine();
541        if (line == null)
542        {
543          // The input stream has been closed, so we can't prompt for trust,
544          // and should assume it is not trusted.
545          throw new CertificateException(
546               ERR_CERTIFICATE_REJECTED_BY_END_OF_STREAM.get(
547                    SSLUtil.certificateToString(chain[0])));
548        }
549        else if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes"))
550        {
551          // The certificate should be considered trusted.
552          break;
553        }
554        else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no"))
555        {
556          // The certificate should not be trusted.
557          throw new CertificateException(
558               ERR_CERTIFICATE_REJECTED_BY_USER.get(
559                    SSLUtil.certificateToString(chain[0])));
560        }
561      }
562      catch (final CertificateException ce)
563      {
564        throw ce;
565      }
566      catch (final Exception e)
567      {
568        Debug.debugException(e);
569      }
570    }
571
572    boolean isOutsideValidityWindow = false;
573    for (final com.unboundid.util.ssl.cert.X509Certificate c : convertedChain)
574    {
575      if (! c.isWithinValidityWindow())
576      {
577        isOutsideValidityWindow = true;
578        break;
579      }
580    }
581
582    acceptedCerts.put(cacheKey, isOutsideValidityWindow);
583
584    if (acceptedCertsFile != null)
585    {
586      try
587      {
588        writeCacheFile();
589      }
590      catch (final Exception e)
591      {
592        Debug.debugException(e);
593      }
594    }
595  }
596
597
598
599  /**
600   * Indicate whether to prompt about certificates contained in the cache if the
601   * current time is outside the validity window for the certificate.
602   *
603   * @return  {@code true} if the certificate validity time should be examined
604   *          for cached certificates and the user should be prompted if they
605   *          are expired or not yet valid, or {@code false} if cached
606   *          certificates should be accepted even outside of the validity
607   *          window.
608   */
609  public boolean examineValidityDates()
610  {
611    return examineValidityDates;
612  }
613
614
615
616  /**
617   * Retrieves a list of the addresses that the client is expected to use to
618   * communicate with the server, if available.
619   *
620   * @return  A list of the addresses that the client is expected to use to
621   *          communicate with the server, or an empty list if this is not
622   *          available or applicable.
623   */
624  @NotNull()
625  public List<String> getExpectedAddresses()
626  {
627    return expectedAddresses;
628  }
629
630
631
632  /**
633   * Checks to determine whether the provided client certificate chain should be
634   * trusted.
635   *
636   * @param  chain     The client certificate chain for which to make the
637   *                   determination.
638   * @param  authType  The authentication type based on the client certificate.
639   *
640   * @throws  CertificateException  If the provided client certificate chain
641   *                                should not be trusted.
642   */
643  @Override()
644  public void checkClientTrusted(@NotNull final X509Certificate[] chain,
645                                 @NotNull final String authType)
646         throws CertificateException
647  {
648    checkCertificateChain(chain, false);
649  }
650
651
652
653  /**
654   * Checks to determine whether the provided server certificate chain should be
655   * trusted.
656   *
657   * @param  chain     The server certificate chain for which to make the
658   *                   determination.
659   * @param  authType  The key exchange algorithm used.
660   *
661   * @throws  CertificateException  If the provided server certificate chain
662   *                                should not be trusted.
663   */
664  @Override()
665  public void checkServerTrusted(@NotNull final X509Certificate[] chain,
666                                 @NotNull final String authType)
667         throws CertificateException
668  {
669    checkCertificateChain(chain, true);
670  }
671
672
673
674  /**
675   * Retrieves the accepted issuer certificates for this trust manager.  This
676   * will always return an empty array.
677   *
678   * @return  The accepted issuer certificates for this trust manager.
679   */
680  @Override()
681  @NotNull()
682  public X509Certificate[] getAcceptedIssuers()
683  {
684    return NO_CERTIFICATES;
685  }
686
687
688
689  /**
690   * Retrieves the cache key used to identify the provided certificate in the
691   * map of accepted certificates.
692   *
693   * @param  certificate  The certificate for which to get the cache key.
694   *
695   * @return  The generated cache key.
696   */
697  @NotNull()
698  static String getCacheKey(@NotNull final Certificate certificate)
699  {
700    final X509Certificate x509Certificate = (X509Certificate) certificate;
701    return StaticUtils.toLowerCase(
702         StaticUtils.toHex(x509Certificate.getSignature()));
703  }
704
705
706
707  /**
708   * Converts the provided certificate chain from Java's representation of
709   * X.509 certificates to the LDAP SDK's version.
710   *
711   * @param  chain  The chain to be converted.
712   *
713   * @return  The converted certificate chain.
714   *
715   * @throws  CertificateException  If a problem occurs while performing the
716   *                                conversion.
717   */
718  @NotNull()
719  static com.unboundid.util.ssl.cert.X509Certificate[] convertChain(
720              @NotNull final Certificate[] chain)
721         throws CertificateException
722  {
723    final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain =
724         new com.unboundid.util.ssl.cert.X509Certificate[chain.length];
725    for (int i=0; i < chain.length; i++)
726    {
727      try
728      {
729        convertedChain[i] = new com.unboundid.util.ssl.cert.X509Certificate(
730             chain[i].getEncoded());
731      }
732      catch (final CertException ce)
733      {
734        Debug.debugException(ce);
735        throw new CertificateException(ce.getMessage(), ce);
736      }
737    }
738
739    return convertedChain;
740  }
741}