blob: 3601805a71a93a50adb3c67d505e4e7e1e70c5df [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2004-2007 Sun Microsystems, Inc. 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. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package sun.security.ssl;
27
28import java.lang.ref.*;
29import java.util.*;
30import static java.util.Locale.ENGLISH;
31import java.util.concurrent.atomic.AtomicLong;
32import java.net.Socket;
33
34import java.security.*;
35import java.security.KeyStore.*;
36import java.security.cert.*;
37import java.security.cert.Certificate;
38
39import 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 */
58final 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}