2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package edu.internet2.middleware.shibboleth.common.provider;
19 import java.io.ByteArrayInputStream;
20 import java.io.IOException;
21 import java.security.GeneralSecurityException;
22 import java.security.cert.CertPathBuilder;
23 import java.security.cert.CertPathValidator;
24 import java.security.cert.CertPathValidatorException;
25 import java.security.cert.CertStore;
26 import java.security.cert.CertificateFactory;
27 import java.security.cert.CertificateParsingException;
28 import java.security.cert.CollectionCertStoreParameters;
29 import java.security.cert.PKIXBuilderParameters;
30 import java.security.cert.PKIXCertPathBuilderResult;
31 import java.security.cert.PKIXCertPathValidatorResult;
32 import java.security.cert.TrustAnchor;
33 import java.security.cert.X509CRL;
34 import java.security.cert.X509CertSelector;
35 import java.security.cert.X509Certificate;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.HashSet;
40 import java.util.Iterator;
41 import java.util.List;
44 import javax.security.auth.x500.X500Principal;
46 import org.apache.log4j.Logger;
47 import org.apache.xml.security.exceptions.XMLSecurityException;
48 import org.apache.xml.security.keys.KeyInfo;
49 import org.apache.xml.security.keys.content.KeyName;
50 import org.apache.xml.security.keys.content.X509Data;
51 import org.apache.xml.security.keys.content.x509.XMLX509CRL;
52 import org.apache.xml.security.keys.content.x509.XMLX509Certificate;
53 import org.bouncycastle.asn1.ASN1InputStream;
54 import org.bouncycastle.asn1.DERObject;
55 import org.bouncycastle.asn1.DERObjectIdentifier;
56 import org.bouncycastle.asn1.DERSequence;
57 import org.bouncycastle.asn1.DERSet;
58 import org.bouncycastle.asn1.DERString;
59 import org.opensaml.SAMLException;
60 import org.opensaml.SAMLSignedObject;
62 import edu.internet2.middleware.shibboleth.common.Trust;
63 import edu.internet2.middleware.shibboleth.metadata.EntitiesDescriptor;
64 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
65 import edu.internet2.middleware.shibboleth.metadata.ExtendedEntitiesDescriptor;
66 import edu.internet2.middleware.shibboleth.metadata.ExtendedEntityDescriptor;
67 import edu.internet2.middleware.shibboleth.metadata.KeyAuthority;
68 import edu.internet2.middleware.shibboleth.metadata.KeyDescriptor;
69 import edu.internet2.middleware.shibboleth.metadata.RoleDescriptor;
72 * <code>Trust</code> implementation that does PKIX validation against key authorities included in shibboleth-specific
73 * extensions to SAML 2 metadata.
75 * @author Walter Hoehn
77 public class ShibbolethTrust extends BasicTrust implements Trust {
79 private static Logger log = Logger.getLogger(ShibbolethTrust.class.getName());
80 private static final String CN_OID = "2.5.4.3";
83 * @see edu.internet2.middleware.shibboleth.common.Trust#validate(java.security.cert.X509Certificate,
84 * java.security.cert.X509Certificate[], edu.internet2.middleware.shibboleth.metadata.RoleDescriptor)
86 public boolean validate(X509Certificate certificateEE, X509Certificate[] certificateChain, RoleDescriptor descriptor) {
88 return validate(certificateEE, certificateChain, descriptor, true);
92 * @see edu.internet2.middleware.shibboleth.common.Trust#validate(org.opensaml.SAMLSignedObject,
93 * edu.internet2.middleware.shibboleth.metadata.RoleDescriptor)
95 public boolean validate(SAMLSignedObject token, RoleDescriptor descriptor) {
97 if (super.validate(token, descriptor)) return true;
99 /* Certificates supplied with the signed object */
100 ArrayList/* <X509Certificate> */certificates = new ArrayList/* <X509Certificate> */();
101 X509Certificate certificateEE = null;
103 /* Iterate to count the certificates, and look for the signer */
104 Iterator icertificates;
106 icertificates = token.getX509Certificates();
107 } catch (SAMLException e1) {
110 while (icertificates.hasNext()) {
111 X509Certificate certificate = (X509Certificate) icertificates.next();
113 token.verify(certificate);
114 // This is the certificate that signed the object
115 certificateEE = certificate;
116 certificates.add(certificate);
117 } catch (SAMLException e) {
118 certificates.add(certificate);
122 if (certificateEE == null) return false; // No key validates the signature
124 // With a count we can now build a typed array
125 X509Certificate[] certificateChain = new X509Certificate[certificates.size()];
127 for (icertificates = certificates.iterator(); icertificates.hasNext();) {
128 certificateChain[i++] = (X509Certificate) icertificates.next();
130 return validate(certificateEE, certificateChain, descriptor);
134 * @see edu.internet2.middleware.shibboleth.common.Trust#validate(java.security.cert.X509Certificate,
135 * java.security.cert.X509Certificate[], edu.internet2.middleware.shibboleth.metadata.RoleDescriptor, boolean)
137 public boolean validate(X509Certificate certificateEE, X509Certificate[] certificateChain,
138 RoleDescriptor descriptor, boolean checkName) {
140 // If we can successfully validate with an inline key, that's fine
141 boolean defaultValidation = super.validate(certificateEE, certificateChain, descriptor, checkName);
142 if (defaultValidation == true) { return true; }
144 // Make sure we have the data we need
145 if (descriptor == null || certificateEE == null) {
146 log.error("Appropriate data was not supplied for trust evaluation.");
149 log.debug("Inline validation was unsuccessful. Attmping PKIX...");
150 // If not, try PKIX validation against the shib-custom metadata extensions
152 // First, we want to see if we can match a keyName from the metadata against the cert
153 // Iterator through all the keys in the metadata
156 if (matchProviderId(certificateChain[0], descriptor.getEntityDescriptor().getId())) {
160 Iterator keyDescriptors = descriptor.getKeyDescriptors();
161 while (checkName && keyDescriptors.hasNext()) {
162 // Look for a key descriptor with the right usage bits
163 KeyDescriptor keyDescriptor = (KeyDescriptor) keyDescriptors.next();
164 if (keyDescriptor.getUse() == KeyDescriptor.ENCRYPTION) {
165 log.debug("Skipping key descriptor with inappropriate usage indicator.");
169 // We found one, see if we can match the metadata's keyName against the cert
170 KeyInfo keyInfo = keyDescriptor.getKeyInfo();
171 if (keyInfo.containsKeyName()) {
172 for (int i = 0; i < keyInfo.lengthKeyName(); i++) {
174 if (matchKeyName(certificateChain[0], keyInfo.itemKeyName(i))) {
178 } catch (XMLSecurityException e) {
179 log.error("Problem retrieving key name from metadata: " + e);
188 log.error("cannot match certificate subject against acceptable key names based on the "
189 + "metadata entityId or KeyDescriptors");
193 if (pkixValidate(certificateEE, certificateChain, descriptor.getEntityDescriptor())) { return true; }
197 private boolean pkixValidate(X509Certificate certEE, X509Certificate[] certChain, EntityDescriptor entity) {
199 if (entity instanceof ExtendedEntityDescriptor) {
200 Iterator keyAuthorities = ((ExtendedEntityDescriptor) entity).getKeyAuthorities();
201 // if we have any key authorities, construct a flat list of trust anchors representing each and attempt to
202 // validate against them in turn
203 while (keyAuthorities.hasNext()) {
204 if (pkixValidate(certEE, certChain, (KeyAuthority) keyAuthorities.next())) { return true; }
208 // We couldn't do path validation based on metadata attached to the entity, we now need to walk up the chain of
209 // nested entities and attempt to validate at each group level
210 EntitiesDescriptor group = entity.getEntitiesDescriptor();
212 if (pkixValidate(certEE, certChain, group)) { return true; }
215 // We've walked the entire metadata chain with no success, so fail
219 private boolean pkixValidate(X509Certificate certEE, X509Certificate[] certChain, EntitiesDescriptor group) {
221 log.debug("Attemping to validate against parent group.");
222 if (group instanceof ExtendedEntitiesDescriptor) {
223 Iterator keyAuthorities = ((ExtendedEntitiesDescriptor) group).getKeyAuthorities();
224 // if we have any key authorities, construct a flat list of trust anchors representing each and attempt to
225 // validate against them in turn
226 while (keyAuthorities.hasNext()) {
227 if (pkixValidate(certEE, certChain, (KeyAuthority) keyAuthorities.next())) { return true; }
231 // If not, attempt to walk up the chain for validation
232 EntitiesDescriptor parent = group.getEntitiesDescriptor();
233 if (parent != null) {
234 if (pkixValidate(certEE, certChain, parent)) { return true; }
240 private boolean pkixValidate(X509Certificate certEE, X509Certificate[] certChain, KeyAuthority authority) {
242 Set anchors = new HashSet();
243 Set crls = new HashSet();
244 Iterator keyInfos = authority.getKeyInfos();
245 while (keyInfos.hasNext()) {
246 KeyInfo keyInfo = (KeyInfo) keyInfos.next();
247 if (keyInfo.containsX509Data()) {
249 // Add all certificates in the authority as trust anchors
250 for (int i = 0; i < keyInfo.lengthX509Data(); i++) {
251 X509Data data = keyInfo.itemX509Data(i);
252 if (data.containsCertificate()) {
253 for (int j = 0; j < data.lengthCertificate(); j++) {
254 XMLX509Certificate xmlCert = data.itemCertificate(j);
255 anchors.add(new TrustAnchor(xmlCert.getX509Certificate(), null));
258 // Compile all CRLs in the authority
259 if (data.containsCRL()) {
260 for (int j = 0; j < data.lengthCRL(); j++) {
261 XMLX509CRL xmlCrl = data.itemCRL(j);
263 X509CRL crl = (X509CRL) CertificateFactory.getInstance("X.509").generateCRL(
264 new ByteArrayInputStream(xmlCrl.getCRLBytes()));
265 if (crl.getRevokedCertificates() != null && crl.getRevokedCertificates().size() > 0) {
268 } catch (GeneralSecurityException e) {
269 log.error("Encountered an error parsing CRL from shibboleth metadata: " + e);
275 } catch (XMLSecurityException e) {
276 log.error("Encountered an error constructing trust list from shibboleth metadata: " + e);
281 // alright, if we were able to create a trust list, attempt a pkix validation against the list
282 if (anchors.size() > 0) {
283 log.debug("Constructed a trust list from key authority. Attempting path validation...");
285 CertPathValidator validator = CertPathValidator.getInstance("PKIX");
287 X509CertSelector selector = new X509CertSelector();
288 selector.setCertificate(certEE);
289 PKIXBuilderParameters params = new PKIXBuilderParameters(anchors, selector);
290 params.setMaxPathLength(authority.getVerifyDepth());
291 List storeMaterial = new ArrayList(crls);
292 storeMaterial.addAll(Arrays.asList(certChain));
293 CertStore store = CertStore.getInstance("Collection", new CollectionCertStoreParameters(storeMaterial));
294 List stores = new ArrayList();
296 params.setCertStores(stores);
297 if (crls.size() > 0) {
298 params.setRevocationEnabled(true);
300 params.setRevocationEnabled(false);
302 // System.err.println(params.toString());
303 CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
304 PKIXCertPathBuilderResult buildResult = (PKIXCertPathBuilderResult) builder.build(params);
306 PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(buildResult
307 .getCertPath(), params);
308 log.debug("Path successfully validated.");
311 } catch (CertPathValidatorException e) {
312 log.debug("Path failed to validate: " + e);
313 } catch (GeneralSecurityException e) {
314 log.error("Encountered an error during validation: " + e);
320 private static boolean matchKeyName(X509Certificate certificate, KeyName keyName) {
322 // First, try to match DN against metadata
324 if (certificate.getSubjectX500Principal().getName(X500Principal.RFC2253).equals(
325 new X500Principal(keyName.getKeyName()).getName(X500Principal.RFC2253))) {
326 log.debug("Matched against DN.");
329 } catch (IllegalArgumentException iae) {
330 // squelch this runtime exception, since
331 // this might be a valid case
334 // If that doesn't work, we try matching against
335 // some Subject Alt Names
337 Collection altNames = certificate.getSubjectAlternativeNames();
338 if (altNames != null) {
339 for (Iterator nameIterator = altNames.iterator(); nameIterator.hasNext();) {
340 List altName = (List) nameIterator.next();
341 if (altName.get(0).equals(new Integer(2)) || altName.get(0).equals(new Integer(6))) {
342 // 2 is DNS, 6 is URI
343 if (altName.get(0).equals(keyName.getKeyName())) {
344 log.debug("Matched against SubjectAltName.");
350 } catch (CertificateParsingException e1) {
351 log.error("Encountered an problem trying to extract Subject Alternate "
352 + "Name from supplied certificate: " + e1);
355 // If that doesn't work, try to match using
356 // SSL-style hostname matching
357 if (getHostNameFromDN(certificate.getSubjectX500Principal()).equals(keyName.getKeyName())) {
358 log.debug("Matched against hostname.");
365 private static boolean matchProviderId(X509Certificate certificate, String id) {
367 // Try matching against URI Subject Alt Names
369 Collection altNames = certificate.getSubjectAlternativeNames();
370 if (altNames != null) {
371 for (Iterator nameIterator = altNames.iterator(); nameIterator.hasNext();) {
372 List altName = (List) nameIterator.next();
373 if (altName.get(0).equals(new Integer(6))) { // 6 is URI
374 if (altName.get(0).equals(id)) {
375 log.debug("Entity ID matched against SubjectAltName.");
381 } catch (CertificateParsingException e1) {
382 log.error("Encountered an problem trying to extract Subject Alternate "
383 + "Name from supplied certificate: " + e1);
386 // If that doesn't work, try to match using
387 // SSL-style hostname matching
388 if (getHostNameFromDN(certificate.getSubjectX500Principal()).equals(id)) {
389 log.debug("Entity ID matched against hostname.");
396 public static String getHostNameFromDN(X500Principal dn) {
398 // Parse the ASN.1 representation of the dn and grab the last CN component that we find
399 // We used to do this with the dn string, but the JDK's default parsing caused problems with some DNs
401 ASN1InputStream asn1Stream = new ASN1InputStream(dn.getEncoded());
402 DERObject parent = asn1Stream.readObject();
404 if (!(parent instanceof DERSequence)) {
405 log.error("Unable to extract host name name from certificate subject DN: incorrect ASN.1 encoding.");
410 for (int i = 0; i < ((DERSequence) parent).size(); i++) {
411 DERObject dnComponent = ((DERSequence) parent).getObjectAt(i).getDERObject();
412 if (!(dnComponent instanceof DERSet)) {
413 log.debug("No DN components.");
417 // Each DN component is a set
418 for (int j = 0; j < ((DERSet) dnComponent).size(); j++) {
419 DERObject grandChild = ((DERSet) dnComponent).getObjectAt(j).getDERObject();
421 if (((DERSequence) grandChild).getObjectAt(0) != null
422 && ((DERSequence) grandChild).getObjectAt(0).getDERObject() instanceof DERObjectIdentifier) {
423 DERObjectIdentifier componentId = (DERObjectIdentifier) ((DERSequence) grandChild).getObjectAt(
426 if (CN_OID.equals(componentId.getId())) {
427 // OK, this dn component is actually a cn attribute
428 if (((DERSequence) grandChild).getObjectAt(1) != null
429 && ((DERSequence) grandChild).getObjectAt(1).getDERObject() instanceof DERString) {
430 cn = ((DERString) ((DERSequence) grandChild).getObjectAt(1).getDERObject()).getString();
439 } catch (IOException e) {
440 log.error("Unable to extract host name name from certificate subject DN: ASN.1 parsing failed: " + e);