001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2009 Parametric Technology Corporation (PTC)
016 * Portions Copyright 2011-2016 ForgeRock AS.
017 */
018package org.opends.admin.ads.util;
019
020import java.security.KeyStore;
021import java.security.KeyStoreException;
022import java.security.NoSuchAlgorithmException;
023import java.security.NoSuchProviderException;
024import java.security.cert.CertificateException;
025import java.security.cert.X509Certificate;
026import java.util.ArrayList;
027import java.util.List;
028
029import javax.naming.ldap.LdapName;
030import javax.naming.ldap.Rdn;
031import javax.net.ssl.TrustManager;
032import javax.net.ssl.TrustManagerFactory;
033import javax.net.ssl.X509TrustManager;
034
035import org.forgerock.i18n.LocalizableMessage;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.opends.server.util.Platform;
038
039/**
040 * This class is in charge of checking whether the certificates that are
041 * presented are trusted or not.
042 * This implementation tries to check also that the subject DN of the
043 * certificate corresponds to the host passed using the setHostName method.
044 *
045 * The constructor tries to use a default TrustManager from the system and if
046 * it cannot be retrieved this class will only accept the certificates
047 * explicitly accepted by the user (and specified by calling acceptCertificate).
048 *
049 * NOTE: this class is not aimed to be used when we have connections in parallel.
050 */
051public class ApplicationTrustManager implements X509TrustManager
052{
053  /**
054   * The enumeration for the different causes for which the trust manager can
055   * refuse to accept a certificate.
056   */
057  public enum Cause
058  {
059    /** The certificate was not trusted. */
060    NOT_TRUSTED,
061    /** The certificate's subject DN's value and the host name we tried to connect to do not match. */
062    HOST_NAME_MISMATCH
063  }
064  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
065
066  private X509TrustManager trustManager;
067  private String lastRefusedAuthType;
068  private X509Certificate[] lastRefusedChain;
069  private Cause lastRefusedCause;
070  private final KeyStore keystore;
071
072  /**
073   * The following ArrayList contain information about the certificates
074   * explicitly accepted by the user.
075   */
076  private final List<X509Certificate[]> acceptedChains = new ArrayList<>();
077  private final List<String> acceptedAuthTypes = new ArrayList<>();
078  private final List<String> acceptedHosts = new ArrayList<>();
079
080  private String host;
081
082  /**
083   * The default constructor.
084   *
085   * @param keystore The keystore to use for this trustmanager.
086   */
087  public ApplicationTrustManager(KeyStore keystore)
088  {
089    this.keystore = keystore;
090    String userSpecifiedAlgo = System.getProperty("org.opends.admin.trustmanageralgo");
091    String userSpecifiedProvider = System.getProperty("org.opends.admin.trustmanagerprovider");
092
093    //Handle IBM specific cases if the user did not specify a algorithm and/or provider.
094    if(userSpecifiedAlgo == null && Platform.isVendor("IBM"))
095    {
096      userSpecifiedAlgo = "IbmX509";
097    }
098    if(userSpecifiedProvider == null && Platform.isVendor("IBM"))
099    {
100      userSpecifiedProvider = "IBMJSSE2";
101    }
102
103    // Have some fallbacks to choose the provider and algorithm of the key manager.
104    // First see if the user wanted to use something specific,
105    // then try with the SunJSSE provider and SunX509 algorithm.
106    // Finally,fallback to the default algorithm of the JVM.
107    String[] preferredProvider =
108        { userSpecifiedProvider, "SunJSSE", null, null };
109    String[] preferredAlgo =
110        { userSpecifiedAlgo, "SunX509", "SunX509",
111          TrustManagerFactory.getDefaultAlgorithm() };
112
113      for (int i=0; i<preferredProvider.length && trustManager == null; i++)
114      {
115        String provider = preferredProvider[i];
116        String algo = preferredAlgo[i];
117        if (algo == null)
118        {
119          continue;
120        }
121        try
122        {
123          TrustManagerFactory tmf = null;
124          if (provider != null)
125          {
126            tmf = TrustManagerFactory.getInstance(algo, provider);
127          }
128          else
129          {
130            tmf = TrustManagerFactory.getInstance(algo);
131          }
132          tmf.init(keystore);
133          for (TrustManager tm : tmf.getTrustManagers())
134          {
135            if (tm instanceof X509TrustManager)
136            {
137              trustManager = (X509TrustManager) tm;
138              break;
139            }
140          }
141        }
142        catch (NoSuchProviderException e)
143        {
144          logger.warn(LocalizableMessage.raw("Error with the provider: "+provider, e));
145        }
146        catch (NoSuchAlgorithmException e)
147        {
148          logger.warn(LocalizableMessage.raw("Error with the algorithm: "+algo, e));
149        }
150        catch (KeyStoreException e)
151        {
152          logger.warn(LocalizableMessage.raw("Error with the keystore", e));
153        }
154      }
155  }
156
157  @Override
158  public void checkClientTrusted(X509Certificate[] chain, String authType)
159  throws CertificateException
160  {
161    boolean explicitlyAccepted = false;
162    try
163    {
164      if (trustManager != null)
165      {
166        try
167        {
168          trustManager.checkClientTrusted(chain, authType);
169        }
170        catch (CertificateException ce)
171        {
172          verifyAcceptedCertificates(chain, authType);
173          explicitlyAccepted = true;
174        }
175      }
176      else
177      {
178        verifyAcceptedCertificates(chain, authType);
179        explicitlyAccepted = true;
180      }
181    }
182    catch (CertificateException ce)
183    {
184      manageException(chain, authType, ce, Cause.NOT_TRUSTED);
185    }
186
187    if (!explicitlyAccepted)
188    {
189      try
190      {
191        verifyHostName(chain);
192      }
193      catch (CertificateException ce)
194      {
195        manageException(chain, authType, ce, Cause.HOST_NAME_MISMATCH);
196      }
197    }
198  }
199
200  @Override
201  public void checkServerTrusted(X509Certificate[] chain,
202      String authType) throws CertificateException
203  {
204    boolean explicitlyAccepted = false;
205    try
206    {
207      if (trustManager != null)
208      {
209        try
210        {
211          trustManager.checkServerTrusted(chain, authType);
212        }
213        catch (CertificateException ce)
214        {
215          verifyAcceptedCertificates(chain, authType);
216          explicitlyAccepted = true;
217        }
218      }
219      else
220      {
221        verifyAcceptedCertificates(chain, authType);
222        explicitlyAccepted = true;
223      }
224    }
225    catch (CertificateException ce)
226    {
227      manageException(chain, authType, ce, Cause.NOT_TRUSTED);
228    }
229
230    if (!explicitlyAccepted)
231    {
232      try
233      {
234        verifyHostName(chain);
235      }
236      catch (CertificateException ce)
237      {
238        manageException(chain, authType, ce, Cause.HOST_NAME_MISMATCH);
239      }
240    }
241  }
242
243  private void manageException(final X509Certificate[] chain,
244      final String authType, final CertificateException ce, final Cause cause)
245      throws OpendsCertificateException
246  {
247    lastRefusedChain = chain;
248    lastRefusedAuthType = authType;
249    lastRefusedCause = cause;
250    throw new OpendsCertificateException(chain, ce);
251  }
252
253  @Override
254  public X509Certificate[] getAcceptedIssuers()
255  {
256    if (trustManager != null)
257    {
258      return trustManager.getAcceptedIssuers();
259    }
260    return new X509Certificate[0];
261  }
262
263  /**
264   * This method is called when the user accepted a certificate.
265   * @param chain the certificate chain accepted by the user.
266   * @param authType the authentication type.
267   * @param host the host we tried to connect and that presented the certificate.
268   */
269  public void acceptCertificate(X509Certificate[] chain, String authType,
270      String host)
271  {
272    acceptedChains.add(chain);
273    acceptedAuthTypes.add(authType);
274    acceptedHosts.add(host);
275  }
276
277  /**
278   * Sets the host name we are trying to contact in a secure mode.  This
279   * method is used if we want to verify the correspondence between the
280   * hostname and the subject DN of the certificate that is being presented.
281   * If this method is never called (or called passing null) no verification
282   * will be made on the host name.
283   * @param host the host name we are trying to contact in a secure mode.
284   */
285  public void setHost(String host)
286  {
287    this.host = host;
288  }
289
290  /**
291   * This is a method used to set to null the different members that provide
292   * information about the last refused certificate.  It is recommended to
293   * call this method before trying to establish a connection using this
294   * trust manager.
295   */
296  public void resetLastRefusedItems()
297  {
298    lastRefusedAuthType = null;
299    lastRefusedChain = null;
300    lastRefusedCause = null;
301  }
302
303  /**
304   * Creates a copy of this ApplicationTrustManager.
305   * @return a copy of this ApplicationTrustManager.
306   */
307  public ApplicationTrustManager createCopy()
308  {
309    ApplicationTrustManager copy = new ApplicationTrustManager(keystore);
310    copy.lastRefusedAuthType = lastRefusedAuthType;
311    copy.lastRefusedChain = lastRefusedChain;
312    copy.lastRefusedCause = lastRefusedCause;
313    copy.acceptedChains.addAll(acceptedChains);
314    copy.acceptedAuthTypes.addAll(acceptedAuthTypes);
315    copy.acceptedHosts.addAll(acceptedHosts);
316
317    copy.host = host;
318
319    return copy;
320  }
321
322  /**
323   * Verifies whether the provided chain and authType have been already accepted
324   * by the user or not.  If they have not a CertificateException is thrown.
325   * @param chain the certificate chain to analyze.
326   * @param authType the authentication type.
327   * @throws CertificateException if the provided certificate chain and the
328   * authentication type have not been accepted explicitly by the user.
329   */
330  private void verifyAcceptedCertificates(X509Certificate[] chain,
331      String authType) throws CertificateException
332  {
333    boolean found = false;
334    for (int i=0; i<acceptedChains.size() && !found; i++)
335    {
336      if (authType.equals(acceptedAuthTypes.get(i)))
337      {
338        X509Certificate[] current = acceptedChains.get(i);
339        found = current.length == chain.length;
340        for (int j=0; j<chain.length && found; j++)
341        {
342          found = chain[j].equals(current[j]);
343        }
344      }
345    }
346    if (!found)
347    {
348      throw new OpendsCertificateException(
349          "Certificate not in list of accepted certificates", chain);
350    }
351  }
352
353  /**
354   * Verifies that the provided certificate chains subject DN corresponds to the
355   * host name specified with the setHost method.
356   * @param chain the certificate chain to analyze.
357   * @throws CertificateException if the subject DN of the certificate does
358   * not match with the host name specified with the method setHost.
359   */
360  private void verifyHostName(X509Certificate[] chain) throws CertificateException
361  {
362    if (host != null)
363    {
364      boolean matches = false;
365      try
366      {
367        LdapName dn =
368          new LdapName(chain[0].getSubjectX500Principal().getName());
369        Rdn rdn = dn.getRdn(dn.getRdns().size() - 1);
370        String value = rdn.getValue().toString();
371        matches = hostMatch(value, host);
372        if (!matches)
373        {
374          logger.warn(LocalizableMessage.raw("Subject DN RDN value is: "+value+
375              " and does not match host value: "+host));
376          // Try with the accepted hosts names
377          for (int i =0; i<acceptedHosts.size() && !matches; i++)
378          {
379            if (hostMatch(acceptedHosts.get(i), host))
380            {
381              X509Certificate[] current = acceptedChains.get(i);
382              matches = current.length == chain.length;
383              for (int j=0; j<chain.length && matches; j++)
384              {
385                matches = chain[j].equals(current[j]);
386              }
387            }
388          }
389        }
390      }
391      catch (Throwable t)
392      {
393        logger.warn(LocalizableMessage.raw("Error parsing subject dn: "+
394            chain[0].getSubjectX500Principal(), t));
395      }
396
397      if (!matches)
398      {
399        throw new OpendsCertificateException(
400            "Hostname mismatch between host name " + host
401                + " and subject DN: " + chain[0].getSubjectX500Principal(),
402            chain);
403      }
404    }
405  }
406
407  /**
408   * Returns the authentication type for the last refused certificate.
409   * @return the authentication type for the last refused certificate.
410   */
411  public String getLastRefusedAuthType()
412  {
413    return lastRefusedAuthType;
414  }
415
416  /**
417   * Returns the last cause for refusal of a certificate.
418   * @return the last cause for refusal of a certificate.
419   */
420  public Cause getLastRefusedCause()
421  {
422    return lastRefusedCause;
423  }
424
425  /**
426   * Returns the certificate chain for the last refused certificate.
427   * @return the certificate chain for the last refused certificate.
428   */
429  public X509Certificate[] getLastRefusedChain()
430  {
431    return lastRefusedChain;
432  }
433
434  /**
435   * Checks whether two host names match.  It accepts the use of wildcard in the
436   * host name.
437   * @param host1 the first host name.
438   * @param host2 the second host name.
439   * @return <CODE>true</CODE> if the host match and <CODE>false</CODE>
440   * otherwise.
441   */
442  private boolean hostMatch(String host1, String host2)
443  {
444    if (host1 == null)
445    {
446      throw new IllegalArgumentException("The host1 parameter cannot be null");
447    }
448    if (host2 == null)
449    {
450      throw new IllegalArgumentException("The host2 parameter cannot be null");
451    }
452    String[] h1 = host1.split("\\.");
453    String[] h2 = host2.split("\\.");
454
455    boolean hostMatch = h1.length == h2.length;
456    for (int i=0; i<h1.length && hostMatch; i++)
457    {
458      if (!"*".equals(h1[i]) && !"*".equals(h2[i]))
459      {
460        hostMatch = h1[i].equalsIgnoreCase(h2[i]);
461      }
462    }
463    return hostMatch;
464  }
465}