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