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