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