4056fa44fdf7412656a3b44420eabf8acee2c6bf
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / provider / ShibbolethTrust.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package edu.internet2.middleware.shibboleth.common.provider;
18
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;
42 import java.util.Set;
43
44 import javax.security.auth.x500.X500Principal;
45
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;
61
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;
70
71 /**
72  * <code>Trust</code> implementation that does PKIX validation against key authorities included in shibboleth-specific
73  * extensions to SAML 2 metadata.
74  * 
75  * @author Walter Hoehn
76  */
77 public class ShibbolethTrust extends BasicTrust implements Trust {
78
79         private static Logger log = Logger.getLogger(ShibbolethTrust.class.getName());
80         private static final String CN_OID = "2.5.4.3";
81
82         /*
83          * @see edu.internet2.middleware.shibboleth.common.Trust#validate(java.security.cert.X509Certificate,
84          *      java.security.cert.X509Certificate[], edu.internet2.middleware.shibboleth.metadata.RoleDescriptor)
85          */
86         public boolean validate(X509Certificate certificateEE, X509Certificate[] certificateChain, RoleDescriptor descriptor) {
87
88                 return validate(certificateEE, certificateChain, descriptor, true);
89         }
90
91         /*
92          * @see edu.internet2.middleware.shibboleth.common.Trust#validate(org.opensaml.SAMLSignedObject,
93          *      edu.internet2.middleware.shibboleth.metadata.RoleDescriptor)
94          */
95         public boolean validate(SAMLSignedObject token, RoleDescriptor descriptor) {
96
97                 if (super.validate(token, descriptor)) return true;
98
99                 /* Certificates supplied with the signed object */
100                 ArrayList/* <X509Certificate> */certificates = new ArrayList/* <X509Certificate> */();
101                 X509Certificate certificateEE = null;
102
103                 /* Iterate to count the certificates, and look for the signer */
104                 Iterator icertificates;
105                 try {
106                         icertificates = token.getX509Certificates();
107                 } catch (SAMLException e1) {
108                         return false;
109                 }
110                 while (icertificates.hasNext()) {
111                         X509Certificate certificate = (X509Certificate) icertificates.next();
112                         try {
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);
119                         }
120                 }
121
122                 if (certificateEE == null) return false; // No key validates the signature
123
124                 // With a count we can now build a typed array
125                 X509Certificate[] certificateChain = new X509Certificate[certificates.size()];
126                 int i = 0;
127                 for (icertificates = certificates.iterator(); icertificates.hasNext();) {
128                         certificateChain[i++] = (X509Certificate) icertificates.next();
129                 }
130                 return validate(certificateEE, certificateChain, descriptor);
131         }
132
133         /*
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)
136          */
137         public boolean validate(X509Certificate certificateEE, X509Certificate[] certificateChain,
138                         RoleDescriptor descriptor, boolean checkName) {
139
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; }
143
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.");
147                         return false;
148                 }
149                 log.debug("Inline validation was unsuccessful.  Attmping PKIX...");
150                 // If not, try PKIX validation against the shib-custom metadata extensions
151
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
154                 if (checkName) {
155
156                         if (matchProviderId(certificateChain[0], descriptor.getEntityDescriptor().getId())) {
157                                 checkName = false;
158                         } else {
159
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.");
166                                                 continue;
167                                         }
168
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++) {
173                                                         try {
174                                                                 if (matchKeyName(certificateChain[0], keyInfo.itemKeyName(i))) {
175                                                                         checkName = false;
176                                                                         break;
177                                                                 }
178                                                         } catch (XMLSecurityException e) {
179                                                                 log.error("Problem retrieving key name from metadata: " + e);
180                                                         }
181                                                 }
182                                         }
183                                 }
184                         }
185                 }
186
187                 if (checkName) {
188                         log.error("cannot match certificate subject against acceptable key names based on the "
189                                         + "metadata entityId or KeyDescriptors");
190                         return false;
191                 }
192
193                 if (pkixValidate(certificateEE, certificateChain, descriptor.getEntityDescriptor())) { return true; }
194                 return false;
195         }
196
197         private boolean pkixValidate(X509Certificate certEE, X509Certificate[] certChain, EntityDescriptor entity) {
198
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; }
205                         }
206                 }
207
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();
211                 if (group != null) {
212                         if (pkixValidate(certEE, certChain, group)) { return true; }
213                 }
214
215                 // We've walked the entire metadata chain with no success, so fail
216                 return false;
217         }
218
219         private boolean pkixValidate(X509Certificate certEE, X509Certificate[] certChain, EntitiesDescriptor group) {
220
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; }
228                         }
229                 }
230
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; }
235                 }
236
237                 return false;
238         }
239
240         private boolean pkixValidate(X509Certificate certEE, X509Certificate[] certChain, KeyAuthority authority) {
241
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()) {
248                                 try {
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));
256                                                         }
257                                                 }
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);
262                                                                 try {
263                                                                         X509CRL crl = (X509CRL) CertificateFactory.getInstance("X.509").generateCRL(
264                                                                                         new ByteArrayInputStream(xmlCrl.getCRLBytes()));
265                                                                         if (crl.getRevokedCertificates() != null && crl.getRevokedCertificates().size() > 0) {
266                                                                                 crls.add(crl);
267                                                                         }
268                                                                 } catch (GeneralSecurityException e) {
269                                                                         log.error("Encountered an error parsing CRL from shibboleth metadata: " + e);
270                                                                 }
271                                                         }
272                                                 }
273                                         }
274
275                                 } catch (XMLSecurityException e) {
276                                         log.error("Encountered an error constructing trust list from shibboleth metadata: " + e);
277                                 }
278                         }
279                 }
280
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...");
284                         try {
285                                 CertPathValidator validator = CertPathValidator.getInstance("PKIX");
286
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();
295                                 stores.add(store);
296                                 params.setCertStores(stores);
297                                 if (crls.size() > 0) {
298                                         params.setRevocationEnabled(true);
299                                 } else {
300                                         params.setRevocationEnabled(false);
301                                 }
302                                 // System.err.println(params.toString());
303                                 CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
304                                 PKIXCertPathBuilderResult buildResult = (PKIXCertPathBuilderResult) builder.build(params);
305
306                                 PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(buildResult
307                                                 .getCertPath(), params);
308                                 log.debug("Path successfully validated.");
309                                 return true;
310
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);
315                         }
316                 }
317                 return false;
318         }
319
320         private static boolean matchKeyName(X509Certificate certificate, KeyName keyName) {
321
322                 // First, try to match DN against metadata
323                 try {
324                         if (certificate.getSubjectX500Principal().getName(X500Principal.RFC2253).equals(
325                                         new X500Principal(keyName.getKeyName()).getName(X500Principal.RFC2253))) {
326                                 log.debug("Matched against DN.");
327                                 return true;
328                         }
329                 } catch (IllegalArgumentException iae) {
330                         // squelch this runtime exception, since
331                         // this might be a valid case
332                 }
333
334                 // If that doesn't work, we try matching against
335                 // some Subject Alt Names
336                 try {
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.");
345                                                         return true;
346                                                 }
347                                         }
348                                 }
349                         }
350                 } catch (CertificateParsingException e1) {
351                         log.error("Encountered an problem trying to extract Subject Alternate "
352                                         + "Name from supplied certificate: " + e1);
353                 }
354
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.");
359                         return true;
360                 }
361
362                 return false;
363         }
364
365         private static boolean matchProviderId(X509Certificate certificate, String id) {
366
367                 // Try matching against URI Subject Alt Names
368                 try {
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.");
376                                                         return true;
377                                                 }
378                                         }
379                                 }
380                         }
381                 } catch (CertificateParsingException e1) {
382                         log.error("Encountered an problem trying to extract Subject Alternate "
383                                         + "Name from supplied certificate: " + e1);
384                 }
385
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.");
390                         return true;
391                 }
392
393                 return false;
394         }
395
396         public static String getHostNameFromDN(X500Principal dn) {
397
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
400                 try {
401                         ASN1InputStream asn1Stream = new ASN1InputStream(dn.getEncoded());
402                         DERObject parent = asn1Stream.readObject();
403
404                         if (!(parent instanceof DERSequence)) {
405                                 log.error("Unable to extract host name name from certificate subject DN: incorrect ASN.1 encoding.");
406                                 return null;
407                         }
408
409                         String cn = null;
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.");
414                                         continue;
415                                 }
416
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();
420
421                                         if (((DERSequence) grandChild).getObjectAt(0) != null
422                                                         && ((DERSequence) grandChild).getObjectAt(0).getDERObject() instanceof DERObjectIdentifier) {
423                                                 DERObjectIdentifier componentId = (DERObjectIdentifier) ((DERSequence) grandChild).getObjectAt(
424                                                                 0).getDERObject();
425
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();
431                                                         }
432                                                 }
433                                         }
434                                 }
435                         }
436                         asn1Stream.close();
437                         return cn;
438
439                 } catch (IOException e) {
440                         log.error("Unable to extract host name name from certificate subject DN: ASN.1 parsing failed: " + e);
441                         return null;
442                 }
443         }
444
445 }