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