View Javadoc

1   /*
2    * Copyright [2006] [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 org.opensaml.xml.security.x509;
18  
19  import java.security.GeneralSecurityException;
20  import java.security.cert.CRL;
21  import java.security.cert.CertPathBuilder;
22  import java.security.cert.CertPathBuilderException;
23  import java.security.cert.CertStore;
24  import java.security.cert.CertStoreException;
25  import java.security.cert.Certificate;
26  import java.security.cert.CollectionCertStoreParameters;
27  import java.security.cert.PKIXBuilderParameters;
28  import java.security.cert.PKIXCertPathBuilderResult;
29  import java.security.cert.TrustAnchor;
30  import java.security.cert.X509CRL;
31  import java.security.cert.X509CertSelector;
32  import java.security.cert.X509Certificate;
33  import java.util.ArrayList;
34  import java.util.Collection;
35  import java.util.Date;
36  import java.util.HashSet;
37  import java.util.List;
38  import java.util.Set;
39  
40  import org.opensaml.xml.security.SecurityException;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  /**
45   * An implementation of {@link PKIXTrustEvaluator} that is based on the Java CertPath API.
46   */
47  public class CertPathPKIXTrustEvaluator implements PKIXTrustEvaluator {
48  
49      /** Class logger. */
50      private final Logger log = LoggerFactory.getLogger(CertPathPKIXTrustEvaluator.class);
51      
52      /** Responsible for parsing and serializing X.500 names to/from {@link X500Principal} instances. */
53      private X500DNHandler x500DNHandler;
54      
55      /** Options influencing processing behavior. */
56      private PKIXValidationOptions options;
57  
58      /** Constructor. */
59      public CertPathPKIXTrustEvaluator() {
60          options = new PKIXValidationOptions();
61          x500DNHandler = new InternalX500DNHandler();
62      }
63      
64      /**
65       * Constructor.
66       * 
67       * @param newOptions PKIX validation options
68       */
69      public CertPathPKIXTrustEvaluator(PKIXValidationOptions newOptions) {
70          if (newOptions == null) {
71              throw new IllegalArgumentException("PKIXValidationOptions may not be null");
72          }
73          options = newOptions;
74          x500DNHandler = new InternalX500DNHandler();
75      }
76      
77      /** {@inheritDoc} */
78      public PKIXValidationOptions getPKIXValidationOptions() {
79          return options;
80      }
81  
82      /**
83       * Set the desired PKIX validation options set.
84       * 
85       * @param newOptions the new set of options
86       */
87      public void setPKIXValidationOptions(PKIXValidationOptions newOptions) {
88          if (newOptions == null) {
89              throw new IllegalArgumentException("PKIXValidationOptions may not be null");
90          }
91          options = newOptions;
92      }
93  
94      /**
95       * Get the handler which process X.500 distinguished names.
96       * 
97       * Defaults to {@link InternalX500DNHandler}.
98       * 
99       * @return returns the X500DNHandler instance
100      */
101     public X500DNHandler getX500DNHandler() {
102         return x500DNHandler;
103     }
104 
105     /**
106      * Set the handler which process X.500 distinguished names.
107      * 
108      * Defaults to {@link InternalX500DNHandler}.
109      * 
110      * @param handler the new X500DNHandler instance
111      */
112     public void setX500DNHandler(X500DNHandler handler) {
113         if (handler == null) {
114             throw new IllegalArgumentException("X500DNHandler may not be null");
115         }
116         x500DNHandler = handler;
117     }
118 
119     /** {@inheritDoc} */
120     public boolean validate(PKIXValidationInformation validationInfo, X509Credential untrustedCredential)
121             throws SecurityException {
122         
123         if (log.isDebugEnabled()) {
124             log.debug("Attempting PKIX path validation on untrusted credential: {}",
125                     X509Util.getIdentifiersToken(untrustedCredential, x500DNHandler));
126         }        
127         
128         try {
129             PKIXBuilderParameters params = getPKIXBuilderParameters(validationInfo, untrustedCredential);
130 
131             log.trace("Building certificate validation path");
132 
133             CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
134             PKIXCertPathBuilderResult buildResult = (PKIXCertPathBuilderResult) builder.build(params);
135             if (log.isDebugEnabled()) {
136                 logCertPathDebug(buildResult, untrustedCredential.getEntityCertificate());
137                 log.debug("PKIX validation succeeded for untrusted credential: {}",
138                         X509Util.getIdentifiersToken(untrustedCredential, x500DNHandler));
139             }            
140             return true;
141 
142         } catch (CertPathBuilderException e) {
143             if (log.isTraceEnabled()) {
144                 log.trace("PKIX path construction failed for untrusted credential: " 
145                         + X509Util.getIdentifiersToken(untrustedCredential, x500DNHandler), e);
146             } else {
147                 log.error("PKIX path construction failed for untrusted credential: " 
148                         + X509Util.getIdentifiersToken(untrustedCredential, x500DNHandler) + ": " + e.getMessage());
149             }
150             return false;
151         } catch (GeneralSecurityException e) {
152             log.error("PKIX validation failure", e);
153             throw new SecurityException("PKIX validation failure", e);
154         }
155     }
156 
157     /**
158      * Creates the set of PKIX builder parameters to use when building the cert path builder.
159      * 
160      * @param validationInfo PKIX validation information
161      * @param untrustedCredential credential to be validated
162      * 
163      * @return PKIX builder params
164      * 
165      * @throws GeneralSecurityException thrown if the parameters can not be created
166      */
167     protected PKIXBuilderParameters getPKIXBuilderParameters(PKIXValidationInformation validationInfo,
168             X509Credential untrustedCredential) throws GeneralSecurityException {
169         Set<TrustAnchor> trustAnchors = getTrustAnchors(validationInfo);
170         if (trustAnchors == null || trustAnchors.isEmpty()) {
171             throw new GeneralSecurityException(
172                     "Unable to validate X509 certificate, no trust anchors found in the PKIX validation information");
173         }
174 
175         X509CertSelector selector = new X509CertSelector();
176         selector.setCertificate(untrustedCredential.getEntityCertificate());
177 
178         log.trace("Adding trust anchors to PKIX validator parameters");
179         PKIXBuilderParameters params = new PKIXBuilderParameters(trustAnchors, selector);
180 
181         Integer effectiveVerifyDepth = getEffectiveVerificationDepth(validationInfo);
182         log.trace("Setting max verification depth to: {} ", effectiveVerifyDepth);
183         params.setMaxPathLength(effectiveVerifyDepth);
184 
185         CertStore certStore = buildCertStore(validationInfo, untrustedCredential);
186         params.addCertStore(certStore);
187 
188         boolean isForceRevocationEnabled = false;
189         boolean forcedRevocation = false;
190         if (options instanceof CertPathPKIXValidationOptions) {
191            CertPathPKIXValidationOptions certpathOptions = (CertPathPKIXValidationOptions) options;
192            isForceRevocationEnabled = certpathOptions.isForceRevocationEnabled();
193            forcedRevocation = certpathOptions.isRevocationEnabled();
194         }
195         
196         if (isForceRevocationEnabled) {
197             log.trace("PKIXBuilderParameters#setRevocationEnabled is being forced to: {}", forcedRevocation);
198             params.setRevocationEnabled(forcedRevocation);
199         } else {
200             if (storeContainsCRLs(certStore)) {
201                 log.trace("At least one CRL was present in cert store, enabling revocation checking");
202                 params.setRevocationEnabled(true);
203             } else {
204                 log.trace("No CRLs present in cert store, disabling revocation checking");
205                 params.setRevocationEnabled(false);
206             }
207         }
208 
209         return params;
210     }
211 
212     /**
213      * Determine whether there are any CRL's in the {@link CertStore} that is to be used.
214      * 
215      * @param certStore the cert store that will be used for validation
216      * @return true if the store contains at least 1 CRL instance, false otherwise
217      */
218     protected boolean storeContainsCRLs(CertStore certStore) {
219         Collection<? extends CRL> crls = null;
220         try {
221             //Save some cycles and memory: Collection cert store allows null as specifier to return all.
222             //crls = certStore.getCRLs( new X509CRLSelector() );
223             crls = certStore.getCRLs(null);
224         } catch (CertStoreException e) {
225             log.error("Error examining cert store for CRL's, treating as if no CRL's present", e);
226             return false;
227         }
228         if (crls != null && !crls.isEmpty()) {
229             return true;
230         }
231         return false;
232     }
233 
234     /**
235      * Get the effective maximum path depth to use when constructing PKIX cert path builder parameters.
236      * 
237      * @param validationInfo PKIX validation information
238      * @return the effective max verification depth to use
239      */
240     protected Integer getEffectiveVerificationDepth(PKIXValidationInformation validationInfo) {
241         Integer effectiveVerifyDepth = validationInfo.getVerificationDepth();
242         if (effectiveVerifyDepth == null) {
243             effectiveVerifyDepth = options.getDefaultVerificationDepth();
244         }
245         return effectiveVerifyDepth;
246     }
247 
248     /**
249      * Creates the collection of trust anchors to use during validation.
250      * 
251      * @param validationInfo PKIX validation information
252      * 
253      * @return trust anchors to use during validation
254      */
255     protected Set<TrustAnchor> getTrustAnchors(PKIXValidationInformation validationInfo) {
256         Collection<X509Certificate> validationCertificates = validationInfo.getCertificates();
257 
258         log.trace("Constructing trust anchors for PKIX validation");
259         Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
260         for (X509Certificate cert : validationCertificates) {
261             trustAnchors.add(buildTrustAnchor(cert));
262         }
263 
264         if (log.isTraceEnabled()) {
265             for (TrustAnchor anchor : trustAnchors) {
266                 log.trace("TrustAnchor: {}", anchor.toString());
267             }
268         }
269 
270         return trustAnchors;
271     }
272 
273     /**
274      * Build a trust anchor from the given X509 certificate.
275      * 
276      * This could for example be extended by subclasses to add custom name constraints, if desired.
277      * 
278      * @param cert the certificate which serves as the trust anchor
279      * @return the newly constructed TrustAnchor
280      */
281     protected TrustAnchor buildTrustAnchor(X509Certificate cert) {
282         return new TrustAnchor(cert, null);
283     }
284 
285     /**
286      * Creates the certificate store that will be used during validation.
287      * 
288      * @param validationInfo PKIX validation information
289      * @param untrustedCredential credential to be validated
290      * 
291      * @return certificate store used during validation
292      * 
293      * @throws GeneralSecurityException thrown if the certificate store can not be created from the cert and CRL
294      *             material
295      */
296     protected CertStore buildCertStore(PKIXValidationInformation validationInfo, X509Credential untrustedCredential)
297             throws GeneralSecurityException {
298 
299         log.trace("Creating cert store to use during path validation");
300 
301         log.trace("Adding entity certificate chain to cert store");
302         List<Object> storeMaterial = new ArrayList<Object>(untrustedCredential.getEntityCertificateChain());
303         if (log.isTraceEnabled()) {
304             for (X509Certificate cert : untrustedCredential.getEntityCertificateChain()) {
305                 log.trace(String.format("Added X509Certificate from entity cert chain to cert store "
306                         + "with subject name '%s' issued by '%s' with serial number '%s'",
307                         x500DNHandler.getName(cert.getSubjectX500Principal()),
308                         x500DNHandler.getName(cert.getIssuerX500Principal()),
309                         cert.getSerialNumber().toString()));
310             }
311         }
312         
313         Date now = new Date();
314         
315         if (validationInfo.getCRLs() != null && !validationInfo.getCRLs().isEmpty()) {
316             log.trace("Processing CRL's from PKIX info set");
317             addCRLsToStoreMaterial(storeMaterial, validationInfo.getCRLs(), now);
318         }        
319         
320         if (untrustedCredential.getCRLs() != null && !untrustedCredential.getCRLs().isEmpty() 
321                 && options.isProcessCredentialCRLs()) {
322             log.trace("Processing CRL's from untrusted credential");
323             addCRLsToStoreMaterial(storeMaterial, untrustedCredential.getCRLs(), now);
324         }        
325         
326         return CertStore.getInstance("Collection", new CollectionCertStoreParameters(storeMaterial));
327     }
328     
329     /**
330      * Add CRL's from the specified collection to the list of certs and CRL's being collected
331      * for the CertStore.
332      * 
333      * @param storeMaterial list of certs and CRL's to be updated.
334      * @param crls collection of CRL's to be processed
335      * @param now current date/time
336      */
337     protected void addCRLsToStoreMaterial(List<Object> storeMaterial, Collection<X509CRL> crls, Date now) {
338         for (X509CRL crl : crls) {
339             boolean isEmpty = crl.getRevokedCertificates() == null || crl.getRevokedCertificates().isEmpty();
340             boolean isExpired = crl.getNextUpdate().before(now);
341             if (!isEmpty || options.isProcessEmptyCRLs()) {
342                 if (!isExpired || options.isProcessExpiredCRLs()) {
343                     storeMaterial.add(crl);
344                     if (log.isTraceEnabled()) {
345                         log.trace("Added X509CRL to cert store from issuer {} dated {}",
346                                 x500DNHandler.getName(crl.getIssuerX500Principal()), crl.getThisUpdate());
347                         if (isEmpty) {
348                             log.trace("X509CRL added to cert store from issuer {} dated {} was empty",
349                                     x500DNHandler.getName(crl.getIssuerX500Principal()), crl.getThisUpdate());
350                         }
351                     }
352                     if (isExpired) {
353                         log.warn("Using X509CRL from issuer {} with a nextUpdate in the past: {}",
354                                 x500DNHandler.getName(crl.getIssuerX500Principal()), crl.getNextUpdate());
355                     }
356                 } else {
357                     if (log.isTraceEnabled()) {
358                         log.trace("Expired X509CRL not added to cert store, from issuer {} nextUpdate {}",
359                                 x500DNHandler.getName(crl.getIssuerX500Principal()), crl.getNextUpdate());
360                     }
361                 }
362             } else {
363                 if (log.isTraceEnabled()) {
364                     log.trace("Empty X509CRL not added to cert store, from issuer {} dated {}",
365                             x500DNHandler.getName(crl.getIssuerX500Principal()), crl.getThisUpdate());
366                 }
367             }
368         }
369     }
370 
371     /**
372      * Log information from the constructed cert path at level debug.
373      * 
374      * @param buildResult the PKIX cert path builder result containing the cert path and trust anchor
375      * @param targetCert the cert untrusted certificate that was being evaluated
376      */
377     private void logCertPathDebug(PKIXCertPathBuilderResult buildResult, X509Certificate targetCert) {
378         log.debug("Built valid PKIX cert path");
379         log.debug("Target certificate: {}", x500DNHandler.getName(targetCert.getSubjectX500Principal()));
380         for (Certificate cert : buildResult.getCertPath().getCertificates()) {
381             log.debug("CertPath certificate: {}", x500DNHandler.getName(((X509Certificate) cert)
382                     .getSubjectX500Principal()));
383         }
384         TrustAnchor ta = buildResult.getTrustAnchor();
385         if (ta.getTrustedCert() != null) {
386             log.debug("TrustAnchor: {}", x500DNHandler.getName(ta.getTrustedCert().getSubjectX500Principal()));
387         } else if (ta.getCA() != null) {
388             log.debug("TrustAnchor: {}", x500DNHandler.getName(ta.getCA()));
389         } else {
390             log.debug("TrustAnchor: {}", ta.getCAName());
391         }
392     }
393 
394 }