1 /* 2 * Copyright (c) 2004, 2007, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.security.ssl; 27 28 import java.lang.ref.*; 29 import java.util.*; 30 import static java.util.Locale.ENGLISH; 31 import java.util.concurrent.atomic.AtomicLong; 32 import java.net.Socket; 33 34 import java.security.*; 35 import java.security.KeyStore.*; 36 import java.security.cert.*; 37 import java.security.cert.Certificate; 38 39 import javax.net.ssl.*; 40 41 /** 42 * The new X509 key manager implementation. The main differences to the 43 * old SunX509 key manager are: 44 * . it is based around the KeyStore.Builder API. This allows it to use 45 * other forms of KeyStore protection or password input (e.g. a 46 * CallbackHandler) or to have keys within one KeyStore protected by 47 * different keys. 48 * . it can use multiple KeyStores at the same time. 49 * . it is explicitly designed to accomodate KeyStores that change over 50 * the lifetime of the process. 51 * . it makes an effort to choose the key that matches best, i.e. one that 52 * is not expired and has the appropriate certificate extensions. 53 * 54 * Note that this code is not explicitly performance optimzied yet. 55 * 56 * @author Andreas Sterbenz 57 */ 58 final class X509KeyManagerImpl extends X509ExtendedKeyManager 59 implements X509KeyManager { 60 61 private static final Debug debug = Debug.getInstance("ssl"); 62 63 private final static boolean useDebug = 64 (debug != null) && Debug.isOn("keymanager"); 65 66 // for unit testing only, set via privileged reflection 67 private static Date verificationDate; 68 69 // list of the builders 70 private final List<Builder> builders; 71 72 // counter to generate unique ids for the aliases 73 private final AtomicLong uidCounter; 74 75 // cached entries 76 private final Map<String,Reference<PrivateKeyEntry>> entryCacheMap; 77 78 X509KeyManagerImpl(Builder builder) { 79 this(Collections.singletonList(builder)); 80 } 81 82 X509KeyManagerImpl(List<Builder> builders) { 83 this.builders = builders; 84 uidCounter = new AtomicLong(); 85 entryCacheMap = Collections.synchronizedMap 86 (new SizedMap<String,Reference<PrivateKeyEntry>>()); 87 } 88 89 // LinkedHashMap with a max size of 10 90 // see LinkedHashMap JavaDocs 91 private static class SizedMap<K,V> extends LinkedHashMap<K,V> { 92 @Override protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { 93 return size() > 10; 94 } 95 } 96 97 // 98 // public methods 99 // 100 101 public X509Certificate[] getCertificateChain(String alias) { 102 PrivateKeyEntry entry = getEntry(alias); 103 return entry == null ? null : 104 (X509Certificate[])entry.getCertificateChain(); 105 } 106 107 public PrivateKey getPrivateKey(String alias) { 108 PrivateKeyEntry entry = getEntry(alias); 109 return entry == null ? null : entry.getPrivateKey(); 110 } 111 112 public String chooseClientAlias(String[] keyTypes, Principal[] issuers, 113 Socket socket) { 114 return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT); 115 } 116 117 public String chooseEngineClientAlias(String[] keyTypes, 118 Principal[] issuers, SSLEngine engine) { 119 return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT); 120 } 121 122 public String chooseServerAlias(String keyType, 123 Principal[] issuers, Socket socket) { 124 return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER); 125 } 126 127 public String chooseEngineServerAlias(String keyType, 128 Principal[] issuers, SSLEngine engine) { 129 return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER); 130 } 131 132 public String[] getClientAliases(String keyType, Principal[] issuers) { 133 return getAliases(keyType, issuers, CheckType.CLIENT); 134 } 135 136 public String[] getServerAliases(String keyType, Principal[] issuers) { 137 return getAliases(keyType, issuers, CheckType.SERVER); 138 } 139 140 // 141 // implementation private methods 142 // 143 144 // we construct the alias we return to JSSE as seen in the code below 145 // a unique id is included to allow us to reliably cache entries 146 // between the calls to getCertificateChain() and getPrivateKey() 147 // even if tokens are inserted or removed 148 private String makeAlias(EntryStatus entry) { 149 return uidCounter.incrementAndGet() + "." + entry.builderIndex + "." 150 + entry.alias; 151 } 152 153 private PrivateKeyEntry getEntry(String alias) { 154 // if the alias is null, return immediately 155 if (alias == null) { 156 return null; 157 } 158 159 // try to get the entry from cache 160 Reference<PrivateKeyEntry> ref = entryCacheMap.get(alias); 161 PrivateKeyEntry entry = (ref != null) ? ref.get() : null; 162 if (entry != null) { 163 return entry; 164 } 165 166 // parse the alias 167 int firstDot = alias.indexOf('.'); 168 int secondDot = alias.indexOf('.', firstDot + 1); 169 if ((firstDot == -1) || (secondDot == firstDot)) { 170 // invalid alias 171 return null; 172 } 173 try { 174 int builderIndex = Integer.parseInt 175 (alias.substring(firstDot + 1, secondDot)); 176 String keyStoreAlias = alias.substring(secondDot + 1); 177 Builder builder = builders.get(builderIndex); 178 KeyStore ks = builder.getKeyStore(); 179 Entry newEntry = ks.getEntry 180 (keyStoreAlias, builder.getProtectionParameter(alias)); 181 if (newEntry instanceof PrivateKeyEntry == false) { 182 // unexpected type of entry 183 return null; 184 } 185 entry = (PrivateKeyEntry)newEntry; 186 entryCacheMap.put(alias, new SoftReference(entry)); 187 return entry; 188 } catch (Exception e) { 189 // ignore 190 return null; 191 } 192 } 193 194 // Class to help verify that the public key algorithm (and optionally 195 // the signature algorithm) of a certificate matches what we need. 196 private static class KeyType { 197 198 final String keyAlgorithm; 199 final String sigKeyAlgorithm; 200 201 KeyType(String algorithm) { 202 int k = algorithm.indexOf("_"); 203 if (k == -1) { 204 keyAlgorithm = algorithm; 205 sigKeyAlgorithm = null; 206 } else { 207 keyAlgorithm = algorithm.substring(0, k); 208 sigKeyAlgorithm = algorithm.substring(k + 1); 209 } 210 } 211 212 boolean matches(Certificate[] chain) { 213 if (!chain[0].getPublicKey().getAlgorithm().equals(keyAlgorithm)) { 214 return false; 215 } 216 if (sigKeyAlgorithm == null) { 217 return true; 218 } 219 if (chain.length > 1) { 220 // if possible, check the public key in the issuer cert 221 return sigKeyAlgorithm.equals(chain[1].getPublicKey().getAlgorithm()); 222 } else { 223 // Check the signature algorithm of the certificate itself. 224 // Look for the "withRSA" in "SHA1withRSA", etc. 225 X509Certificate issuer = (X509Certificate)chain[0]; 226 String sigAlgName = issuer.getSigAlgName().toUpperCase(ENGLISH); 227 String pattern = "WITH" + sigKeyAlgorithm.toUpperCase(ENGLISH); 228 return sigAlgName.contains(pattern); 229 } 230 } 231 } 232 233 private static List<KeyType> getKeyTypes(String ... keyTypes) { 234 if ((keyTypes == null) || (keyTypes.length == 0) || (keyTypes[0] == null)) { 235 return null; 236 } 237 List<KeyType> list = new ArrayList<KeyType>(keyTypes.length); 238 for (String keyType : keyTypes) { 239 list.add(new KeyType(keyType)); 240 } 241 return list; 242 } 243 244 /* 245 * Return the best alias that fits the given parameters. 246 * The algorithm we use is: 247 * . scan through all the aliases in all builders in order 248 * . as soon as we find a perfect match, return 249 * (i.e. a match with a cert that has appropriate key usage 250 * and is not expired). 251 * . if we do not find a perfect match, keep looping and remember 252 * the imperfect matches 253 * . at the end, sort the imperfect matches. we prefer expired certs 254 * with appropriate key usage to certs with the wrong key usage. 255 * return the first one of them. 256 */ 257 private String chooseAlias(List<KeyType> keyTypeList, 258 Principal[] issuers, CheckType checkType) { 259 if (keyTypeList == null || keyTypeList.size() == 0) { 260 return null; 261 } 262 263 Set<Principal> issuerSet = getIssuerSet(issuers); 264 List<EntryStatus> allResults = null; 265 for (int i = 0, n = builders.size(); i < n; i++) { 266 try { 267 List<EntryStatus> results = 268 getAliases(i, keyTypeList, issuerSet, false, checkType); 269 if (results != null) { 270 // the results will either be a single perfect match 271 // or 1 or more imperfect matches 272 // if it's a perfect match, return immediately 273 EntryStatus status = results.get(0); 274 if (status.checkResult == CheckResult.OK) { 275 if (useDebug) { 276 debug.println("KeyMgr: choosing key: " + status); 277 } 278 return makeAlias(status); 279 } 280 if (allResults == null) { 281 allResults = new ArrayList<EntryStatus>(); 282 } 283 allResults.addAll(results); 284 } 285 } catch (Exception e) { 286 // ignore 287 } 288 } 289 if (allResults == null) { 290 if (useDebug) { 291 debug.println("KeyMgr: no matching key found"); 292 } 293 return null; 294 } 295 Collections.sort(allResults); 296 if (useDebug) { 297 debug.println("KeyMgr: no good matching key found, " 298 + "returning best match out of:"); 299 debug.println(allResults.toString()); 300 } 301 return makeAlias(allResults.get(0)); 302 } 303 304 /* 305 * Return all aliases that (approximately) fit the parameters. 306 * These are perfect matches plus imperfect matches (expired certificates 307 * and certificates with the wrong extensions). 308 * The perfect matches will be first in the array. 309 */ 310 public String[] getAliases(String keyType, Principal[] issuers, 311 CheckType checkType) { 312 if (keyType == null) { 313 return null; 314 } 315 316 Set<Principal> issuerSet = getIssuerSet(issuers); 317 List<KeyType> keyTypeList = getKeyTypes(keyType); 318 List<EntryStatus> allResults = null; 319 for (int i = 0, n = builders.size(); i < n; i++) { 320 try { 321 List<EntryStatus> results = 322 getAliases(i, keyTypeList, issuerSet, true, checkType); 323 if (results != null) { 324 if (allResults == null) { 325 allResults = new ArrayList<EntryStatus>(); 326 } 327 allResults.addAll(results); 328 } 329 } catch (Exception e) { 330 // ignore 331 } 332 } 333 if (allResults == null || allResults.size() == 0) { 334 if (useDebug) { 335 debug.println("KeyMgr: no matching alias found"); 336 } 337 return null; 338 } 339 Collections.sort(allResults); 340 if (useDebug) { 341 debug.println("KeyMgr: getting aliases: " + allResults); 342 } 343 return toAliases(allResults); 344 } 345 346 // turn candidate entries into unique aliases we can return to JSSE 347 private String[] toAliases(List<EntryStatus> results) { 348 String[] s = new String[results.size()]; 349 int i = 0; 350 for (EntryStatus result : results) { 351 s[i++] = makeAlias(result); 352 } 353 return s; 354 } 355 356 // make a Set out of the array 357 private Set<Principal> getIssuerSet(Principal[] issuers) { 358 if ((issuers != null) && (issuers.length != 0)) { 359 return new HashSet<Principal>(Arrays.asList(issuers)); 360 } else { 361 return null; 362 } 363 } 364 365 // a candidate match 366 // identifies the entry by builder and alias 367 // and includes the result of the certificate check 368 private static class EntryStatus implements Comparable<EntryStatus> { 369 370 final int builderIndex; 371 final int keyIndex; 372 final String alias; 373 final CheckResult checkResult; 374 375 EntryStatus(int builderIndex, int keyIndex, String alias, 376 Certificate[] chain, CheckResult checkResult) { 377 this.builderIndex = builderIndex; 378 this.keyIndex = keyIndex; 379 this.alias = alias; 380 this.checkResult = checkResult; 381 } 382 383 public int compareTo(EntryStatus other) { 384 int result = this.checkResult.compareTo(other.checkResult); 385 return (result == 0) ? (this.keyIndex - other.keyIndex) : result; 386 } 387 388 public String toString() { 389 String s = alias + " (verified: " + checkResult + ")"; 390 if (builderIndex == 0) { 391 return s; 392 } else { 393 return "Builder #" + builderIndex + ", alias: " + s; 394 } 395 } 396 } 397 398 // enum for the type of certificate check we want to perform 399 // (client or server) 400 // also includes the check code itself 401 private static enum CheckType { 402 403 // enum constant for "no check" (currently not used) 404 NONE(Collections.<String>emptySet()), 405 406 // enum constant for "tls client" check 407 // valid EKU for TLS client: any, tls_client 408 CLIENT(new HashSet<String>(Arrays.asList(new String[] { 409 "2.5.29.37.0", "1.3.6.1.5.5.7.3.2" }))), 410 411 // enum constant for "tls server" check 412 // valid EKU for TLS server: any, tls_server, ns_sgc, ms_sgc 413 SERVER(new HashSet<String>(Arrays.asList(new String[] { 414 "2.5.29.37.0", "1.3.6.1.5.5.7.3.1", "2.16.840.1.113730.4.1", 415 "1.3.6.1.4.1.311.10.3.3" }))); 416 417 // set of valid EKU values for this type 418 final Set<String> validEku; 419 420 CheckType(Set<String> validEku) { 421 this.validEku = validEku; 422 } 423 424 private static boolean getBit(boolean[] keyUsage, int bit) { 425 return (bit < keyUsage.length) && keyUsage[bit]; 426 } 427 428 // check if this certificate is appropriate for this type of use 429 // first check extensions, if they match, check expiration 430 // note: we may want to move this code into the sun.security.validator 431 // package 432 CheckResult check(X509Certificate cert, Date date) { 433 if (this == NONE) { 434 return CheckResult.OK; 435 } 436 437 // check extensions 438 try { 439 // check extended key usage 440 List<String> certEku = cert.getExtendedKeyUsage(); 441 if ((certEku != null) && Collections.disjoint(validEku, certEku)) { 442 // if extension present and it does not contain any of 443 // the valid EKU OIDs, return extension_mismatch 444 return CheckResult.EXTENSION_MISMATCH; 445 } 446 447 // check key usage 448 boolean[] ku = cert.getKeyUsage(); 449 if (ku != null) { 450 String algorithm = cert.getPublicKey().getAlgorithm(); 451 boolean kuSignature = getBit(ku, 0); 452 if (algorithm.equals("RSA")) { 453 // require either signature bit 454 // or if server also allow key encipherment bit 455 if (kuSignature == false) { 456 if ((this == CLIENT) || (getBit(ku, 2) == false)) { 457 return CheckResult.EXTENSION_MISMATCH; 458 } 459 } 460 } else if (algorithm.equals("DSA")) { 461 // require signature bit 462 if (kuSignature == false) { 463 return CheckResult.EXTENSION_MISMATCH; 464 } 465 } else if (algorithm.equals("DH")) { 466 // require keyagreement bit 467 if (getBit(ku, 4) == false) { 468 return CheckResult.EXTENSION_MISMATCH; 469 } 470 } else if (algorithm.equals("EC")) { 471 // require signature bit 472 if (kuSignature == false) { 473 return CheckResult.EXTENSION_MISMATCH; 474 } 475 // For servers, also require key agreement. 476 // This is not totally accurate as the keyAgreement bit 477 // is only necessary for static ECDH key exchange and 478 // not ephemeral ECDH. We leave it in for now until 479 // there are signs that this check causes problems 480 // for real world EC certificates. 481 if ((this == SERVER) && (getBit(ku, 4) == false)) { 482 return CheckResult.EXTENSION_MISMATCH; 483 } 484 } 485 } 486 } catch (CertificateException e) { 487 // extensions unparseable, return failure 488 return CheckResult.EXTENSION_MISMATCH; 489 } 490 491 try { 492 cert.checkValidity(date); 493 return CheckResult.OK; 494 } catch (CertificateException e) { 495 return CheckResult.EXPIRED; 496 } 497 } 498 } 499 500 // enum for the result of the extension check 501 // NOTE: the order of the constants is important as they are used 502 // for sorting, i.e. OK is best, followed by EXPIRED and EXTENSION_MISMATCH 503 private static enum CheckResult { 504 OK, // ok or not checked 505 EXPIRED, // extensions valid but cert expired 506 EXTENSION_MISMATCH, // extensions invalid (expiration not checked) 507 } 508 509 /* 510 * Return a List of all candidate matches in the specified builder 511 * that fit the parameters. 512 * We exclude entries in the KeyStore if they are not: 513 * . private key entries 514 * . the certificates are not X509 certificates 515 * . the algorithm of the key in the EE cert doesn't match one of keyTypes 516 * . none of the certs is issued by a Principal in issuerSet 517 * Using those entries would not be possible or they would almost 518 * certainly be rejected by the peer. 519 * 520 * In addition to those checks, we also check the extensions in the EE 521 * cert and its expiration. Even if there is a mismatch, we include 522 * such certificates because they technically work and might be accepted 523 * by the peer. This leads to more graceful failure and better error 524 * messages if the cert expires from one day to the next. 525 * 526 * The return values are: 527 * . null, if there are no matching entries at all 528 * . if 'findAll' is 'false' and there is a perfect match, a List 529 * with a single element (early return) 530 * . if 'findAll' is 'false' and there is NO perfect match, a List 531 * with all the imperfect matches (expired, wrong extensions) 532 * . if 'findAll' is 'true', a List with all perfect and imperfect 533 * matches 534 */ 535 private List<EntryStatus> getAliases(int builderIndex, 536 List<KeyType> keyTypes, Set<Principal> issuerSet, 537 boolean findAll, CheckType checkType) throws Exception { 538 Builder builder = builders.get(builderIndex); 539 KeyStore ks = builder.getKeyStore(); 540 List<EntryStatus> results = null; 541 Date date = verificationDate; 542 boolean preferred = false; 543 for (Enumeration<String> e = ks.aliases(); e.hasMoreElements(); ) { 544 String alias = e.nextElement(); 545 // check if it is a key entry (private key or secret key) 546 if (ks.isKeyEntry(alias) == false) { 547 continue; 548 } 549 550 Certificate[] chain = ks.getCertificateChain(alias); 551 if ((chain == null) || (chain.length == 0)) { 552 // must be secret key entry, ignore 553 continue; 554 } 555 // check keytype 556 int keyIndex = -1; 557 int j = 0; 558 for (KeyType keyType : keyTypes) { 559 if (keyType.matches(chain)) { 560 keyIndex = j; 561 break; 562 } 563 j++; 564 } 565 if (keyIndex == -1) { 566 if (useDebug) { 567 debug.println("Ignoring alias " + alias 568 + ": key algorithm does not match"); 569 } 570 continue; 571 } 572 // check issuers 573 if (issuerSet != null) { 574 boolean found = false; 575 for (Certificate cert : chain) { 576 if (cert instanceof X509Certificate == false) { 577 // not an X509Certificate, ignore this entry 578 break; 579 } 580 X509Certificate xcert = (X509Certificate)cert; 581 if (issuerSet.contains(xcert.getIssuerX500Principal())) { 582 found = true; 583 break; 584 } 585 } 586 if (found == false) { 587 if (useDebug) { 588 debug.println("Ignoring alias " + alias 589 + ": issuers do not match"); 590 } 591 continue; 592 } 593 } 594 if (date == null) { 595 date = new Date(); 596 } 597 CheckResult checkResult = 598 checkType.check((X509Certificate)chain[0], date); 599 EntryStatus status = 600 new EntryStatus(builderIndex, keyIndex, 601 alias, chain, checkResult); 602 if (!preferred && checkResult == CheckResult.OK && keyIndex == 0) { 603 preferred = true; 604 } 605 if (preferred && (findAll == false)) { 606 // if we have a good match and do not need all matches, 607 // return immediately 608 return Collections.singletonList(status); 609 } else { 610 if (results == null) { 611 results = new ArrayList<EntryStatus>(); 612 } 613 results.add(status); 614 } 615 } 616 return results; 617 } 618 619 }