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