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