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 2010 Sun Microsystems, Inc. 015 * Portions copyright 2012-2016 ForgeRock AS. 016 */ 017package org.forgerock.opendj.ldap; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.net.InetAddress; 023import java.net.UnknownHostException; 024import java.security.GeneralSecurityException; 025import java.security.KeyStore; 026import java.security.NoSuchAlgorithmException; 027import java.security.cert.CertificateException; 028import java.security.cert.CertificateExpiredException; 029import java.security.cert.CertificateNotYetValidException; 030import java.security.cert.CertificateParsingException; 031import java.security.cert.X509Certificate; 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.Date; 035import java.util.List; 036import java.util.Set; 037 038import javax.net.ssl.TrustManager; 039import javax.net.ssl.TrustManagerFactory; 040import javax.net.ssl.X509TrustManager; 041import javax.security.auth.x500.X500Principal; 042 043import org.forgerock.i18n.LocalizableMessage; 044import org.forgerock.i18n.slf4j.LocalizedLogger; 045import org.forgerock.opendj.ldap.schema.AttributeType; 046import org.forgerock.opendj.ldap.schema.Schema; 047import org.forgerock.util.Reject; 048 049import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_IP; 050import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_DNS; 051import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_ALLOTHERS; 052import static com.forgerock.opendj.ldap.CoreMessages.ERR_CERT_NO_MATCH_SUBJECT; 053 054 055/** This class contains methods for creating common types of trust manager. */ 056public final class TrustManagers { 057 058 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 059 060 /** 061 * An X509TrustManager which rejects certificate chains whose subject alternative names do not match the specified 062 * host name or IP address. The check may fall back to checking a hostname in the left-most CN of the certificate 063 * subject for backwards compatibility. 064 */ 065 private static final class CheckHostName implements X509TrustManager { 066 067 private final X509TrustManager trustManager; 068 069 private final String hostName; 070 071 private CheckHostName(final X509TrustManager trustManager, final String hostName) { 072 this.trustManager = trustManager; 073 this.hostName = hostName; 074 } 075 076 @Override 077 public void checkClientTrusted(final X509Certificate[] chain, final String authType) 078 throws CertificateException { 079 verifyHostName(chain); 080 trustManager.checkClientTrusted(chain, authType); 081 } 082 083 @Override 084 public void checkServerTrusted(final X509Certificate[] chain, final String authType) 085 throws CertificateException { 086 verifyHostName(chain); 087 trustManager.checkServerTrusted(chain, authType); 088 } 089 090 @Override 091 public X509Certificate[] getAcceptedIssuers() { 092 return trustManager.getAcceptedIssuers(); 093 } 094 095 /** 096 * Look in the SubjectAlternativeName for DNS names (wildcards are allowed) and IP addresses, and potentially 097 * fall back to checking CN in the subjectDN. 098 * <p> 099 * If DNS names and IP addresses do not match, and other SubjectAlternativeNames are present and critical, do 100 * not fall back checking CN. 101 * </p> 102 * <p> 103 * If DNS names and IP addresses do not match and the SubjectAlternativeNames are non-critical, fall back to 104 * checking CN. 105 * </p> 106 * @param chain X.509 certificate chain from the server 107 */ 108 private void verifyHostName(final X509Certificate[] chain) throws CertificateException { 109 final X500Principal principal = chain[0].getSubjectX500Principal(); 110 try { 111 final List<String> dnsNamePatterns = new ArrayList<>(0); 112 final List<String> ipAddresses = new ArrayList<>(0); 113 final List<Object> allOthers = new ArrayList<>(0); 114 getSanGeneralNames(chain[0], dnsNamePatterns, ipAddresses, allOthers); 115 final boolean sanIsCritical = getSanCriticality(chain[0]); 116 117 final InetAddress hostAddress = toIpAddress(hostName); 118 if (hostAddress != null) { 119 if (verifyIpAddresses(hostAddress, ipAddresses, principal, sanIsCritical)) { 120 return; 121 } 122 } else { 123 if (verifyDnsNamePatterns(hostName, dnsNamePatterns, principal, sanIsCritical)) { 124 return; 125 } 126 } 127 if (!allOthers.isEmpty() && sanIsCritical) { 128 throw new CertificateException(ERR_CERT_NO_MATCH_ALLOTHERS.get(principal, hostName).toString()); 129 } 130 131 final DN dn = DN.valueOf(principal.getName(), Schema.getCoreSchema()); 132 final String certSubjectHostName = getLowestCommonName(dn); 133 /* Backwards compatibility: check wildcards in cn */ 134 if (hostNameMatchesPattern(hostName, certSubjectHostName)) { 135 return; 136 } 137 throw new CertificateException(ERR_CERT_NO_MATCH_SUBJECT.get(principal, hostName).toString()); 138 } catch (final CertificateException e) { 139 logger.warn(LocalizableMessage.raw("Certificate verification problem for: %s", principal), e); 140 throw e; 141 } 142 } 143 144 /** 145 * Collect the general names from a certificate's SubjectAlternativeName extension. 146 * 147 * General Names can contain: dnsNames, ipAddresses, rfc822Names, x400Addresses, directoryNames, ediPartyNames, 148 * uniformResourceIdentifiers, registeredIDs (OID), or otherNames (anything). See 149 * {@link X509Certificate#getSubjectAlternativeNames()} for details on how these values are encoded. We separate 150 * the dnsNames and ipAddresses (which we can try to match) from everything else (which we do not try to match.) 151 * 152 * @param subject certificate 153 * @param dnsNames list where the dnsNames will be added (may be empty) 154 * @param ipAddresses list where the ipAddresses will be added (may be empty) 155 * @param allOthers list where all other general names will be added (may be empty) 156 */ 157 private void getSanGeneralNames(X509Certificate subject, 158 List<String> dnsNames, List<String> ipAddresses, 159 List<Object> allOthers) { 160 try { 161 Collection<List<?>> sans = subject.getSubjectAlternativeNames(); 162 if (sans == null) { 163 return; 164 } 165 for (List<?> san : sans) { 166 switch ((Integer) san.get(0)) { 167 case 2: 168 dnsNames.add((String) san.get(1)); 169 break; 170 case 7: 171 ipAddresses.add((String) san.get(1)); 172 break; 173 default: 174 allOthers.add(san.get(1)); 175 break; 176 } 177 } 178 } catch (CertificateParsingException e) { 179 /* do nothing */ 180 } 181 } 182 183 /** 184 * Get the ASN.1 criticality of the SubjectAlternativeName extension. 185 * 186 * @param subject X509Certificate to check 187 * @return {@code true} if a subject alt name was found and was marked critical, {@code false} otherwise. 188 */ 189 private boolean getSanCriticality(X509Certificate subject) { 190 Set<String> critSet = subject.getCriticalExtensionOIDs(); 191 return critSet != null && critSet.contains("2.5.29.17"); 192 } 193 194 /** 195 * Convert to an IP address without performing a DNS lookup. 196 * 197 * @param hostName either an IP address string, or a host name 198 * @return {@code InetAddress} if hostName was an IPv4 or IPv6 address, or {@code null} 199 */ 200 private static InetAddress toIpAddress(String hostName) { 201 try { 202 if (InetAddressValidator.isValid(hostName)) { 203 return InetAddress.getByName(hostName); 204 } 205 } catch (UnknownHostException e) { 206 /* do nothing */ 207 } 208 return null; 209 } 210 211 /** 212 * Verify an IP address in the list of IP addresses. 213 * 214 * @param hostAddress IP address from the user 215 * @param ipAddresses List of IP addresses from the certificate (may be empty) 216 * @param principal Subject name from the certificate 217 * @param failureIsCritical Should a verification failure throw a {@link CertificateException} 218 * @return {@code true} if the address is verified, {@code false} if the address was not verified. 219 * @throws CertificateException if verification fails and {@code failureIsCritical} is {@code true}. 220 */ 221 private boolean verifyIpAddresses(InetAddress hostAddress, List<String> ipAddresses, X500Principal principal, 222 boolean failureIsCritical) throws CertificateException { 223 if (!ipAddresses.isEmpty()) { 224 for (String address : ipAddresses) { 225 try { 226 if (InetAddress.getByName(address).equals(hostAddress)) { 227 return true; 228 } 229 } catch (UnknownHostException e) { 230 // do nothing 231 } 232 } 233 if (failureIsCritical) { 234 // RFC 5280 mentions: 235 /* If the subject field 236 * contains an empty sequence, then the issuing CA MUST include a 237 * subjectAltName extension that is marked as critical. When including 238 * the subjectAltName extension in a certificate that has a non-empty 239 * subject distinguished name, conforming CAs SHOULD mark the 240 * subjectAltName extension as non-critical. 241 */ 242 // Since SAN is critical, the subject is empty, so we cannot perform the next check anyway 243 throw new CertificateException(ERR_CERT_NO_MATCH_IP.get(principal, hostName).toString()); 244 } 245 } 246 return false; 247 } 248 249 /** 250 * Verify a hostname in the list of DNS name patterns. 251 * 252 * @param hostName Host name from the user 253 * @param dnsNamePatterns List of DNS name patterns from the certificate (may be empty) 254 * @param principal Subject name from the certificate 255 * @param failureIsCritical Should a verification failure throw a {@link CertificateException} 256 * @return {@code true} if the address is verified, {@code false} if the address was not verified. 257 * @throws CertificateException If verification fails and {@code failureIsCritical} is {@code true} 258 */ 259 private boolean verifyDnsNamePatterns(String hostName, List<String> dnsNamePatterns, X500Principal principal, 260 boolean failureIsCritical) throws CertificateException { 261 for (String namePattern : dnsNamePatterns) { 262 if (hostNameMatchesPattern(hostName, namePattern)) { 263 return true; 264 } 265 } 266 if (failureIsCritical) { 267 throw new CertificateException(ERR_CERT_NO_MATCH_DNS.get(principal, hostName).toString()); 268 } 269 return false; 270 } 271 272 /** 273 * Checks whether a host name matches the provided pattern. It accepts the use of wildcards in the pattern, 274 * e.g. {@code *.example.com}. 275 * 276 * @param hostName 277 * The host name. 278 * @param pattern 279 * The host name pattern, which may contain wildcards. 280 * @return {@code true} if the host name matched the pattern, otherwise {@code false}. 281 */ 282 private boolean hostNameMatchesPattern(final String hostName, final String pattern) { 283 final String[] nameElements = hostName.split("\\."); 284 final String[] patternElements = pattern.split("\\."); 285 286 boolean hostMatch = nameElements.length == patternElements.length; 287 for (int i = 0; i < nameElements.length && hostMatch; i++) { 288 final String ne = nameElements[i]; 289 final String pe = patternElements[i]; 290 if (!pe.equals("*")) { 291 hostMatch = ne.equalsIgnoreCase(pe); 292 } 293 } 294 return hostMatch; 295 } 296 297 /** 298 * Find the lowest (left-most) cn in the DN, and return its value. 299 * 300 * @param subject the DN being searched 301 * @return the cn value, or {@code null} if no cn was found 302 */ 303 private String getLowestCommonName(DN subject) { 304 AttributeType cn = Schema.getDefaultSchema().getAttributeType("cn"); 305 for (RDN rdn : subject) { 306 for (AVA ava : rdn) { 307 if (ava.getAttributeType().equals(cn)) { 308 return ava.getAttributeValue().toString(); 309 } 310 } 311 } 312 return null; 313 } 314 } 315 316 /** An X509TrustManager which rejects certificates which have expired or are not yet valid. */ 317 private static final class CheckValidityDates implements X509TrustManager { 318 319 private final X509TrustManager trustManager; 320 321 private CheckValidityDates(final X509TrustManager trustManager) { 322 this.trustManager = trustManager; 323 } 324 325 @Override 326 public void checkClientTrusted(final X509Certificate[] chain, final String authType) 327 throws CertificateException { 328 verifyExpiration(chain); 329 trustManager.checkClientTrusted(chain, authType); 330 } 331 332 @Override 333 public void checkServerTrusted(final X509Certificate[] chain, final String authType) 334 throws CertificateException { 335 verifyExpiration(chain); 336 trustManager.checkServerTrusted(chain, authType); 337 } 338 339 @Override 340 public X509Certificate[] getAcceptedIssuers() { 341 return trustManager.getAcceptedIssuers(); 342 } 343 344 private void verifyExpiration(final X509Certificate[] chain) throws CertificateException { 345 final Date currentDate = new Date(); 346 for (final X509Certificate c : chain) { 347 try { 348 c.checkValidity(currentDate); 349 } catch (final CertificateExpiredException e) { 350 logger.warn(LocalizableMessage.raw( 351 "Refusing to trust security certificate \'%s\' because it expired on %s", 352 c.getSubjectDN().getName(), c.getNotAfter())); 353 throw e; 354 } catch (final CertificateNotYetValidException e) { 355 logger.warn(LocalizableMessage.raw( 356 "Refusing to trust security certificate \'%s\' because it is not valid until %s", 357 c.getSubjectDN().getName(), c.getNotBefore())); 358 throw e; 359 } 360 } 361 } 362 } 363 364 /** An X509TrustManager which does not trust any certificates. */ 365 private static final class DistrustAll implements X509TrustManager { 366 /** Single instance. */ 367 private static final DistrustAll INSTANCE = new DistrustAll(); 368 369 /** Prevent instantiation. */ 370 private DistrustAll() { 371 // Nothing to do. 372 } 373 374 @Override 375 public void checkClientTrusted(final X509Certificate[] chain, final String authType) 376 throws CertificateException { 377 throw new CertificateException(); 378 } 379 380 @Override 381 public void checkServerTrusted(final X509Certificate[] chain, final String authType) 382 throws CertificateException { 383 throw new CertificateException(); 384 } 385 386 @Override 387 public X509Certificate[] getAcceptedIssuers() { 388 return new X509Certificate[0]; 389 } 390 } 391 392 /** An X509TrustManager which trusts all certificates. */ 393 private static final class TrustAll implements X509TrustManager { 394 /** Single instance. */ 395 private static final TrustAll INSTANCE = new TrustAll(); 396 397 /** Prevent instantiation. */ 398 private TrustAll() { 399 // Nothing to do. 400 } 401 402 @Override 403 public void checkClientTrusted(final X509Certificate[] chain, final String authType) 404 throws CertificateException { 405 } 406 407 @Override 408 public void checkServerTrusted(final X509Certificate[] chain, final String authType) 409 throws CertificateException { 410 } 411 412 @Override 413 public X509Certificate[] getAcceptedIssuers() { 414 return new X509Certificate[0]; 415 } 416 } 417 418 /** 419 * Wraps the provided {@code X509TrustManager} by adding additional validation which rejects certificate chains 420 * whose subject alternative names do not match the specified host name or IP address. The check may fall back to 421 * checking a hostname in the left-most CN of the subjectDN for backwards compatibility. 422 * 423 * If the {@code hostName} is an IP address, only the {@code ipAddresses} field of the subject alternative name 424 * will be checked. Similarly if {@code hostName} is not an IP address, only the {@code dnsNames} of the subject 425 * alternative name will be checked. 426 * 427 * Host names can be matched using wild cards, for example {@code *.example.com}. 428 * 429 * If a critical subject alternative name doesn't match, verification will not fall back to checking the subjectDN 430 * and will <b>fail</b>. If a critical subject alternative name doesn't match and it contains other kinds of general 431 * names that cannot be checked verification will also <b>fail</b>. 432 * 433 * @param hostName 434 * The IP address or hostname used to connect to the LDAP server which will be matched against the 435 * subject alternative name and possibly the subjectDN as described above. 436 * @param trustManager 437 * The trust manager to be wrapped. 438 * @return The wrapped trust manager. 439 * @throws NullPointerException 440 * If {@code trustManager} or {@code hostName} was {@code null}. 441 */ 442 public static X509TrustManager checkHostName(final String hostName, 443 final X509TrustManager trustManager) { 444 Reject.ifNull(trustManager, hostName); 445 return new CheckHostName(trustManager, hostName); 446 } 447 448 /** 449 * Creates a new {@code X509TrustManager} which will use the named trust 450 * store file to determine whether to trust a certificate. It will use the 451 * default trust store format for the JVM (e.g. {@code JKS}) and will not 452 * use a password to open the trust store. 453 * 454 * @param file 455 * The trust store file name. 456 * @return A new {@code X509TrustManager} which will use the named trust 457 * store file to determine whether to trust a certificate. 458 * @throws GeneralSecurityException 459 * If the trust store could not be loaded, perhaps due to 460 * incorrect format, or missing algorithms. 461 * @throws IOException 462 * If the trust store file could not be found or could not be 463 * read. 464 * @throws NullPointerException 465 * If {@code file} was {@code null}. 466 */ 467 public static X509TrustManager checkUsingTrustStore(final String file) 468 throws GeneralSecurityException, IOException { 469 return checkUsingTrustStore(file, null, null); 470 } 471 472 /** 473 * Creates a new {@code X509TrustManager} which will use the named trust 474 * store file to determine whether to trust a certificate. It will use the 475 * provided trust store format and password. 476 * 477 * @param file 478 * The trust store file name. 479 * @param password 480 * The trust store password, which may be {@code null}. 481 * @param format 482 * The trust store format, which may be {@code null} to indicate 483 * that the default trust store format for the JVM (e.g. 484 * {@code JKS}) should be used. 485 * @return A new {@code X509TrustManager} which will use the named trust 486 * store file to determine whether to trust a certificate. 487 * @throws GeneralSecurityException 488 * If the trust store could not be loaded, perhaps due to 489 * incorrect format, or missing algorithms. 490 * @throws IOException 491 * If the trust store file could not be found or could not be 492 * read. 493 * @throws NullPointerException 494 * If {@code file} was {@code null}. 495 */ 496 public static X509TrustManager checkUsingTrustStore(final String file, final char[] password, 497 final String format) throws GeneralSecurityException, IOException { 498 Reject.ifNull(file); 499 500 final File trustStoreFile = new File(file); 501 final String trustStoreFormat = format != null ? format : KeyStore.getDefaultType(); 502 503 final KeyStore keyStore = KeyStore.getInstance(trustStoreFormat); 504 try (FileInputStream fos = new FileInputStream(trustStoreFile)) { 505 keyStore.load(fos, password); 506 } 507 508 final TrustManagerFactory tmf = 509 TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 510 tmf.init(keyStore); 511 512 for (final TrustManager tm : tmf.getTrustManagers()) { 513 if (tm instanceof X509TrustManager) { 514 return (X509TrustManager) tm; 515 } 516 } 517 throw new NoSuchAlgorithmException(); 518 } 519 520 /** 521 * Wraps the provided {@code X509TrustManager} by adding additional 522 * validation which rejects certificate chains containing certificates which 523 * have expired or are not yet valid. 524 * 525 * @param trustManager 526 * The trust manager to be wrapped. 527 * @return The wrapped trust manager. 528 * @throws NullPointerException 529 * If {@code trustManager} was {@code null}. 530 */ 531 public static X509TrustManager checkValidityDates(final X509TrustManager trustManager) { 532 Reject.ifNull(trustManager); 533 return new CheckValidityDates(trustManager); 534 } 535 536 /** 537 * Returns an {@code X509TrustManager} which does not trust any 538 * certificates. 539 * 540 * @return An {@code X509TrustManager} which does not trust any 541 * certificates. 542 */ 543 public static X509TrustManager distrustAll() { 544 return DistrustAll.INSTANCE; 545 } 546 547 /** 548 * Returns an {@code X509TrustManager} which trusts all certificates. 549 * 550 * @return An {@code X509TrustManager} which trusts all certificates. 551 */ 552 public static X509TrustManager trustAll() { 553 return TrustAll.INSTANCE; 554 } 555 556 /** Prevent instantiation. */ 557 private TrustManagers() { 558 // Nothing to do. 559 } 560}