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 }