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 }