1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package edu.internet2.middleware.shibboleth.common.security;
18
19 import java.lang.ref.SoftReference;
20 import java.security.cert.CRLException;
21 import java.security.cert.CertificateException;
22 import java.security.cert.X509CRL;
23 import java.security.cert.X509Certificate;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.concurrent.locks.Lock;
32 import java.util.concurrent.locks.ReadWriteLock;
33 import java.util.concurrent.locks.ReentrantReadWriteLock;
34
35 import javax.xml.namespace.QName;
36
37 import org.opensaml.saml2.common.Extensions;
38 import org.opensaml.saml2.metadata.EntitiesDescriptor;
39 import org.opensaml.saml2.metadata.EntityDescriptor;
40 import org.opensaml.saml2.metadata.KeyDescriptor;
41 import org.opensaml.saml2.metadata.RoleDescriptor;
42 import org.opensaml.saml2.metadata.provider.MetadataProvider;
43 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
44 import org.opensaml.saml2.metadata.provider.ObservableMetadataProvider;
45 import org.opensaml.security.MetadataCriteria;
46 import org.opensaml.xml.XMLObject;
47 import org.opensaml.xml.security.CriteriaSet;
48 import org.opensaml.xml.security.SecurityException;
49 import org.opensaml.xml.security.credential.UsageType;
50 import org.opensaml.xml.security.criteria.EntityIDCriteria;
51 import org.opensaml.xml.security.criteria.UsageCriteria;
52 import org.opensaml.xml.security.keyinfo.KeyInfoHelper;
53 import org.opensaml.xml.security.x509.BasicPKIXValidationInformation;
54 import org.opensaml.xml.security.x509.PKIXValidationInformation;
55 import org.opensaml.xml.security.x509.PKIXValidationInformationResolver;
56 import org.opensaml.xml.signature.KeyInfo;
57 import org.opensaml.xml.signature.KeyName;
58 import org.opensaml.xml.util.DatatypeHelper;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import edu.internet2.middleware.shibboleth.common.xmlobject.ShibbolethMetadataKeyAuthority;
63
64
65
66
67
68
69
70
71
72
73 public class MetadataPKIXValidationInformationResolver implements PKIXValidationInformationResolver {
74
75
76 public static final int KEY_AUTHORITY_VERIFY_DEPTH_DEFAULT = 1;
77
78
79 private final Logger log = LoggerFactory.getLogger(MetadataPKIXValidationInformationResolver.class);
80
81
82 private MetadataProvider metadata;
83
84
85 private Map<MetadataCacheKey, SoftReference<List<PKIXValidationInformation>>> entityPKIXCache;
86
87
88 private Map<Extensions, SoftReference<List<PKIXValidationInformation>>> extensionsCache;
89
90
91 private Map<MetadataCacheKey, SoftReference<Set<String>>> entityNamesCache;
92
93
94 private ReadWriteLock rwlock;
95
96
97
98
99
100
101
102
103 public MetadataPKIXValidationInformationResolver(MetadataProvider metadataProvider) {
104 super();
105 if (metadataProvider == null) {
106 throw new IllegalArgumentException("Metadata provider may not be null");
107 }
108 metadata = metadataProvider;
109
110 entityPKIXCache = new HashMap<MetadataCacheKey, SoftReference<List<PKIXValidationInformation>>>();
111 extensionsCache = new HashMap<Extensions, SoftReference<List<PKIXValidationInformation>>>();
112 entityNamesCache = new HashMap<MetadataCacheKey, SoftReference<Set<String>>>();
113
114 rwlock = new ReentrantReadWriteLock();
115
116 if (metadata instanceof ObservableMetadataProvider) {
117 ObservableMetadataProvider observable = (ObservableMetadataProvider) metadataProvider;
118 observable.getObservers().add(new MetadataProviderObserver());
119 }
120
121 }
122
123
124 public PKIXValidationInformation resolveSingle(CriteriaSet criteriaSet) throws SecurityException {
125 Iterable<PKIXValidationInformation> pkixInfo = resolve(criteriaSet);
126 if (pkixInfo.iterator().hasNext()) {
127 return pkixInfo.iterator().next();
128 } else {
129 return null;
130 }
131 }
132
133
134 public Iterable<PKIXValidationInformation> resolve(CriteriaSet criteriaSet) throws SecurityException {
135
136 checkCriteriaRequirements(criteriaSet);
137
138 String entityID = criteriaSet.get(EntityIDCriteria.class).getEntityID();
139 MetadataCriteria mdCriteria = criteriaSet.get(MetadataCriteria.class);
140 QName role = mdCriteria.getRole();
141 String protocol = mdCriteria.getProtocol();
142 UsageCriteria usageCriteria = criteriaSet.get(UsageCriteria.class);
143 UsageType usage = null;
144 if (usageCriteria != null) {
145 usage = usageCriteria.getUsage();
146 } else {
147 usage = UsageType.UNSPECIFIED;
148 }
149
150
151 log.debug("Forcing on-demand metadata provider refresh if necessary");
152 try {
153 metadata.getMetadata();
154 } catch (MetadataProviderException e) {
155
156 }
157
158 MetadataCacheKey cacheKey = new MetadataCacheKey(entityID, role, protocol, usage);
159 List<PKIXValidationInformation> pkixInfoSet = retrievePKIXInfoFromCache(cacheKey);
160
161 if (pkixInfoSet == null) {
162 pkixInfoSet = retrievePKIXInfoFromMetadata(entityID, role, protocol, usage);
163 cachePKIXInfo(cacheKey, pkixInfoSet);
164 }
165
166 return pkixInfoSet;
167 }
168
169
170 public Set<String> resolveTrustedNames(CriteriaSet criteriaSet) throws SecurityException,
171 UnsupportedOperationException {
172
173 checkCriteriaRequirements(criteriaSet);
174
175 String entityID = criteriaSet.get(EntityIDCriteria.class).getEntityID();
176 MetadataCriteria mdCriteria = criteriaSet.get(MetadataCriteria.class);
177 QName role = mdCriteria.getRole();
178 String protocol = mdCriteria.getProtocol();
179 UsageCriteria usageCriteria = criteriaSet.get(UsageCriteria.class);
180 UsageType usage = null;
181 if (usageCriteria != null) {
182 usage = usageCriteria.getUsage();
183 } else {
184 usage = UsageType.UNSPECIFIED;
185 }
186
187
188 log.debug("Forcing on-demand metadata provider refresh if necessary");
189 try {
190 metadata.getMetadata();
191 } catch (MetadataProviderException e) {
192
193 }
194
195 MetadataCacheKey cacheKey = new MetadataCacheKey(entityID, role, protocol, usage);
196 Set<String> trustedNames = retrieveTrustedNamesFromCache(cacheKey);
197
198 if (trustedNames == null) {
199 trustedNames = retrieveTrustedNamesFromMetadata(entityID, role, protocol, usage);
200 cacheTrustedNames(cacheKey, trustedNames);
201 }
202
203 return trustedNames;
204 }
205
206
207 public boolean supportsTrustedNameResolution() {
208 return true;
209 }
210
211
212
213
214
215
216 protected ReadWriteLock getReadWriteLock() {
217 return rwlock;
218 }
219
220
221
222
223
224
225 protected void checkCriteriaRequirements(CriteriaSet criteriaSet) {
226 EntityIDCriteria entityCriteria = criteriaSet.get(EntityIDCriteria.class);
227 MetadataCriteria mdCriteria = criteriaSet.get(MetadataCriteria.class);
228 if (entityCriteria == null) {
229 throw new IllegalArgumentException("Entity criteria must be supplied");
230 }
231 if (mdCriteria == null) {
232 throw new IllegalArgumentException("SAML metadata criteria must be supplied");
233 }
234 if (DatatypeHelper.isEmpty(entityCriteria.getEntityID())) {
235 throw new IllegalArgumentException("Entity ID criteria value must be supplied");
236 }
237 if (mdCriteria.getRole() == null) {
238 throw new IllegalArgumentException("Metadata role criteria value must be supplied");
239 }
240 }
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255 protected List<PKIXValidationInformation> retrievePKIXInfoFromMetadata(String entityID, QName role,
256 String protocol, UsageType usage) throws SecurityException {
257
258 log.debug("Attempting to retrieve PKIX validation info from metadata for entity: {}", entityID);
259 List<PKIXValidationInformation> pkixInfoSet = new ArrayList<PKIXValidationInformation>();
260
261 List<RoleDescriptor> roleDescriptors = getRoleDescriptors(entityID, role, protocol);
262 if(roleDescriptors == null || roleDescriptors.isEmpty()){
263 return pkixInfoSet;
264 }
265
266 for (RoleDescriptor roleDescriptor : roleDescriptors) {
267 List<PKIXValidationInformation> roleInfo = resolvePKIXInfo(roleDescriptor);
268 if (roleInfo != null && !roleInfo.isEmpty()) {
269 pkixInfoSet.addAll(roleInfo);
270 }
271 }
272
273 return pkixInfoSet;
274 }
275
276
277
278
279
280
281
282
283
284
285 protected List<PKIXValidationInformation> resolvePKIXInfo(RoleDescriptor roleDescriptor)
286 throws SecurityException {
287
288 List<PKIXValidationInformation> pkixInfoSet = new ArrayList<PKIXValidationInformation>();
289
290 XMLObject current = roleDescriptor.getParent();
291 while (current != null) {
292 if (current instanceof EntityDescriptor) {
293 pkixInfoSet.addAll(resolvePKIXInfo(((EntityDescriptor) current).getExtensions()));
294 } else if (current instanceof EntitiesDescriptor) {
295 pkixInfoSet.addAll(resolvePKIXInfo(((EntitiesDescriptor) current).getExtensions()));
296 }
297 current = current.getParent();
298 }
299 return pkixInfoSet;
300 }
301
302
303
304
305
306
307
308
309
310 protected List<PKIXValidationInformation> resolvePKIXInfo(Extensions extensions) throws SecurityException {
311 if (extensions == null) {
312 return Collections.emptyList();
313 }
314
315 List<PKIXValidationInformation> pkixInfoSet = retrieveExtensionsInfoFromCache(extensions);
316 if (pkixInfoSet != null) {
317 return pkixInfoSet;
318 }
319
320 if (log.isDebugEnabled()) {
321 String parentName = getExtensionsParentName(extensions);
322 if (parentName != null) {
323 if (extensions.getParent() instanceof EntityDescriptor) {
324 log.debug("Resolving PKIX validation info for Extensions "
325 + "with EntityDescriptor parent: {}", parentName);
326 } else if (extensions.getParent() instanceof EntitiesDescriptor) {
327 log.debug("Resolving PKIX validation info for Extensions "
328 + "with EntitiesDescriptor parent: {}", parentName);
329 }
330 } else {
331 log.debug("Resolving PKIX validation info for Extensions "
332 + "with unidentified parent");
333 }
334 }
335
336 pkixInfoSet = new ArrayList<PKIXValidationInformation>();
337
338 List<XMLObject> authorities =
339 extensions.getUnknownXMLObjects(ShibbolethMetadataKeyAuthority.DEFAULT_ELEMENT_NAME);
340 if (authorities == null || authorities.isEmpty()) {
341 return pkixInfoSet;
342 }
343
344 for (XMLObject xmlObj : authorities) {
345 PKIXValidationInformation authoritySet = resolvePKIXInfo((ShibbolethMetadataKeyAuthority) xmlObj);
346 if (authoritySet != null) {
347 pkixInfoSet.add(authoritySet);
348 }
349 }
350 cacheExtensionsInfo(extensions, pkixInfoSet);
351 return pkixInfoSet;
352 }
353
354
355
356
357
358
359
360
361
362 protected PKIXValidationInformation resolvePKIXInfo(ShibbolethMetadataKeyAuthority keyAuthority)
363 throws SecurityException {
364
365 List<X509Certificate> certs = new ArrayList<X509Certificate>();
366 List<X509CRL> crls = new ArrayList<X509CRL>();
367 Integer depth = keyAuthority.getVerifyDepth();
368 if (depth == null) {
369 depth = KEY_AUTHORITY_VERIFY_DEPTH_DEFAULT;
370 }
371
372 List<KeyInfo> keyInfos = keyAuthority.getKeyInfos();
373 if (keyInfos == null || keyInfos.isEmpty()) {
374 return null;
375 }
376
377 for (KeyInfo keyInfo : keyInfos) {
378 certs.addAll(getX509Certificates(keyInfo));
379 crls.addAll(getX509CRLs(keyInfo));
380 }
381
382
383 if (certs.isEmpty() && crls.isEmpty()) {
384 return null;
385 }
386
387 return new BasicPKIXValidationInformation(certs, crls, depth);
388 }
389
390
391
392
393
394
395
396
397 protected List<X509Certificate> getX509Certificates(KeyInfo keyInfo) throws SecurityException {
398 try {
399 return KeyInfoHelper.getCertificates(keyInfo);
400 } catch (CertificateException e) {
401 throw new SecurityException("Error extracting certificates from KeyAuthority KeyInfo", e);
402 }
403
404 }
405
406
407
408
409
410
411
412
413 protected List<X509CRL> getX509CRLs(KeyInfo keyInfo) throws SecurityException {
414 try {
415 return KeyInfoHelper.getCRLs(keyInfo);
416 } catch (CRLException e) {
417 throw new SecurityException("Error extracting CRL's from KeyAuthority KeyInfo", e);
418 }
419
420 }
421
422
423
424
425
426
427
428
429
430
431
432
433
434 protected Set<String> retrieveTrustedNamesFromMetadata(String entityID, QName role, String protocol,
435 UsageType usage) throws SecurityException {
436
437 log.debug("Attempting to retrieve trusted names for PKIX validation from metadata for entity: {}", entityID);
438 Set<String> trustedNames = new HashSet<String>();
439
440 List<RoleDescriptor> roleDescriptors = getRoleDescriptors(entityID, role, protocol);
441 if(roleDescriptors == null || roleDescriptors.isEmpty()){
442 return trustedNames;
443 }
444
445 for (RoleDescriptor roleDescriptor : roleDescriptors) {
446 List<KeyDescriptor> keyDescriptors = roleDescriptor.getKeyDescriptors();
447 if(keyDescriptors == null || keyDescriptors.isEmpty()){
448 return trustedNames;
449 }
450 for (KeyDescriptor keyDescriptor : keyDescriptors) {
451 UsageType mdUsage = keyDescriptor.getUse();
452 if (mdUsage == null) {
453 mdUsage = UsageType.UNSPECIFIED;
454 }
455 if (matchUsage(mdUsage, usage)) {
456 if (keyDescriptor.getKeyInfo() != null) {
457 trustedNames.addAll(getTrustedNames(keyDescriptor.getKeyInfo()));
458 }
459 }
460 }
461
462 }
463
464 return trustedNames;
465 }
466
467
468
469
470
471
472
473 protected Set<String> getTrustedNames(KeyInfo keyInfo) {
474
475 Set<String> names = new HashSet<String>();
476 names.addAll(KeyInfoHelper.getKeyNames(keyInfo));
477 return names;
478 }
479
480
481
482
483
484
485
486
487 protected boolean matchUsage(UsageType metadataUsage, UsageType criteriaUsage) {
488 if (metadataUsage == UsageType.UNSPECIFIED || criteriaUsage == UsageType.UNSPECIFIED) {
489 return true;
490 }
491 return metadataUsage == criteriaUsage;
492 }
493
494
495
496
497
498
499
500
501
502
503 protected List<RoleDescriptor> getRoleDescriptors(String entityID, QName role, String protocol)
504 throws SecurityException {
505 try {
506 if (DatatypeHelper.isEmpty(protocol)) {
507 return metadata.getRole(entityID, role);
508 } else {
509 RoleDescriptor roleDescriptor = metadata.getRole(entityID, role, protocol);
510 if (roleDescriptor == null) {
511 return null;
512 }
513 List<RoleDescriptor> roles = new ArrayList<RoleDescriptor>();
514 roles.add(roleDescriptor);
515 return roles;
516 }
517 } catch (MetadataProviderException e) {
518 log.error("Unable to read metadata from provider", e);
519 throw new SecurityException("Unable to read metadata provider", e);
520 }
521 }
522
523
524
525
526
527
528
529 protected List<PKIXValidationInformation> retrievePKIXInfoFromCache(MetadataCacheKey cacheKey) {
530 log.debug("Attempting to retrieve PKIX validation info from cache using index: {}", cacheKey);
531 Lock readLock = getReadWriteLock().readLock();
532 readLock.lock();
533 log.debug("Read lock over cache acquired");
534 try {
535 if (entityPKIXCache.containsKey(cacheKey)) {
536 SoftReference<List<PKIXValidationInformation>> reference = entityPKIXCache.get(cacheKey);
537 if (reference.get() != null) {
538 log.debug("Retrieved PKIX validation info from cache using index: {}", cacheKey);
539 return reference.get();
540 }
541 }
542 } finally {
543 readLock.unlock();
544 log.debug("Read lock over cache released");
545 }
546
547 log.debug("Unable to retrieve PKIX validation info from cache using index: {}", cacheKey);
548 return null;
549 }
550
551
552
553
554
555
556
557 protected List<PKIXValidationInformation> retrieveExtensionsInfoFromCache(Extensions extensions) {
558 if (log.isDebugEnabled()) {
559 String parentName = getExtensionsParentName(extensions);
560 if (parentName != null) {
561 if (extensions.getParent() instanceof EntityDescriptor) {
562 log.debug("Attempting to retrieve PKIX validation info from cache for Extensions "
563 + "with EntityDescriptor parent: {}", parentName);
564 } else if (extensions.getParent() instanceof EntitiesDescriptor) {
565 log.debug("Attempting to retrieve PKIX validation info from cache for Extensions "
566 + "with EntitiesDescriptor parent: {}", parentName);
567 }
568 } else {
569 log.debug("Attempting to retrieve PKIX validation info from cache for Extensions "
570 + "with unidentified parent");
571 }
572 }
573
574 Lock readLock = getReadWriteLock().readLock();
575 readLock.lock();
576 log.debug("Read lock over cache acquired");
577 try {
578 if (extensionsCache.containsKey(extensions)) {
579 SoftReference<List<PKIXValidationInformation>> reference = extensionsCache.get(extensions);
580 if (reference.get() != null) {
581 log.debug("Retrieved PKIX validation info from cache using index: {}", extensions);
582 return reference.get();
583 }
584 }
585 } finally {
586 readLock.unlock();
587 log.debug("Read lock over cache released");
588 }
589 log.debug("Unable to retrieve PKIX validation info from cache using index: {}", extensions);
590 return null;
591 }
592
593
594
595
596
597
598
599 protected Set<String> retrieveTrustedNamesFromCache(MetadataCacheKey cacheKey) {
600 log.debug("Attempting to retrieve trusted names from cache using index: {}", cacheKey);
601 Lock readLock = getReadWriteLock().readLock();
602 readLock.lock();
603 log.debug("Read lock over cache acquired");
604 try {
605 if (entityNamesCache.containsKey(cacheKey)) {
606 SoftReference<Set<String>> reference = entityNamesCache.get(cacheKey);
607 if (reference.get() != null) {
608 log.debug("Retrieved trusted names from cache using index: {}", cacheKey);
609 return reference.get();
610 }
611 }
612 } finally {
613 readLock.unlock();
614 log.debug("Read lock over cache released");
615 }
616
617 log.debug("Unable to retrieve trusted names from cache using index: {}", cacheKey);
618 return null;
619 }
620
621
622
623
624
625
626
627 protected void cachePKIXInfo(MetadataCacheKey cacheKey, List<PKIXValidationInformation> pkixInfo) {
628 Lock writeLock = getReadWriteLock().writeLock();
629 writeLock.lock();
630 log.debug("Write lock over cache acquired");
631 try {
632 entityPKIXCache.put(cacheKey, new SoftReference<List<PKIXValidationInformation>>(pkixInfo));
633 log.debug("Added new PKIX info to entity cache with key: {}", cacheKey);
634 } finally {
635 writeLock.unlock();
636 log.debug("Write lock over cache released");
637 }
638 }
639
640
641
642
643
644
645
646 protected void cacheExtensionsInfo(Extensions extensions, List<PKIXValidationInformation> pkixInfo) {
647 Lock writeLock = getReadWriteLock().writeLock();
648 writeLock.lock();
649 log.debug("Write lock over cache acquired");
650 try {
651 extensionsCache.put(extensions, new SoftReference<List<PKIXValidationInformation>>(pkixInfo));
652 if (log.isDebugEnabled()) {
653 log.debug("Added new PKIX info to cache for Extensions with parent: {}",
654 getExtensionsParentName(extensions));
655 }
656 } finally {
657 writeLock.unlock();
658 log.debug("Write lock over cache released");
659 }
660 }
661
662
663
664
665
666
667
668 protected void cacheTrustedNames(MetadataCacheKey cacheKey, Set<String> names) {
669 Lock writeLock = getReadWriteLock().writeLock();
670 writeLock.lock();
671 log.debug("Write lock over cache acquired");
672 try {
673 entityNamesCache.put(cacheKey, new SoftReference<Set<String>>(names));
674 log.debug("Added new PKIX info to entity cache with key: {}", cacheKey);
675 } finally {
676 writeLock.unlock();
677 log.debug("Write lock over cache released");
678 }
679 }
680
681
682
683
684
685
686
687
688
689
690
691 protected String getExtensionsParentName(Extensions extensions) {
692 XMLObject parent = extensions.getParent();
693 if (parent == null) {
694 return null;
695 }
696 if (parent instanceof EntityDescriptor) {
697 return ((EntityDescriptor) parent).getEntityID();
698 } else if (extensions.getParent() instanceof EntitiesDescriptor) {
699 return ((EntitiesDescriptor) parent).getName();
700 }
701 return null;
702 }
703
704
705
706
707 protected class MetadataCacheKey {
708
709
710 private String id;
711
712
713 private QName role;
714
715
716 private String protocol;
717
718
719 private UsageType usage;
720
721
722
723
724
725
726
727
728
729 protected MetadataCacheKey(String entityID, QName entityRole, String entityProtocol, UsageType entityUsage) {
730 if (entityID == null) {
731 throw new IllegalArgumentException("Entity ID may not be null");
732 }
733 if (entityRole == null) {
734 throw new IllegalArgumentException("Entity role may not be null");
735 }
736 if (entityUsage == null) {
737 throw new IllegalArgumentException("Usage may not be null");
738 }
739 id = entityID;
740 role = entityRole;
741 protocol = entityProtocol;
742 usage = entityUsage;
743 }
744
745
746 public boolean equals(Object obj) {
747 if (obj == this) {
748 return true;
749 }
750 if (!(obj instanceof MetadataCacheKey)) {
751 return false;
752 }
753 MetadataCacheKey other = (MetadataCacheKey) obj;
754 if (!this.id.equals(other.id) || !this.role.equals(other.role) || this.usage != other.usage) {
755 return false;
756 }
757 if (this.protocol == null) {
758 if (other.protocol != null) {
759 return false;
760 }
761 } else {
762 if (!this.protocol.equals(other.protocol)) {
763 return false;
764 }
765 }
766 return true;
767 }
768
769
770 public int hashCode() {
771 int result = 17;
772 result = 37 * result + id.hashCode();
773 result = 37 * result + role.hashCode();
774 if (protocol != null) {
775 result = 37 * result + protocol.hashCode();
776 }
777 result = 37 * result + usage.hashCode();
778 return result;
779 }
780
781
782 public String toString() {
783 return String.format("[%s,%s,%s,%s]", id, role, protocol, usage);
784 }
785
786 }
787
788
789
790
791 protected class MetadataProviderObserver implements ObservableMetadataProvider.Observer {
792
793
794 public void onEvent(MetadataProvider provider) {
795 Lock writeLock = getReadWriteLock().writeLock();
796 writeLock.lock();
797 log.debug("Write lock over cache acquired");
798 try {
799 entityPKIXCache.clear();
800 extensionsCache.clear();
801 entityNamesCache.clear();
802 log.info("PKIX validation info cache cleared");
803 } finally {
804 writeLock.unlock();
805 log.debug("Write lock over cache released");
806 }
807 }
808 }
809
810 }