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.ws.security.provider;
18  
19  import java.security.cert.X509Certificate;
20  import java.util.ArrayList;
21  import java.util.List;
22  
23  import org.opensaml.ws.message.MessageContext;
24  import org.opensaml.ws.security.SecurityPolicyException;
25  import org.opensaml.ws.transport.InTransport;
26  import org.opensaml.ws.transport.Transport;
27  import org.opensaml.xml.security.CriteriaSet;
28  import org.opensaml.xml.security.credential.Credential;
29  import org.opensaml.xml.security.credential.UsageType;
30  import org.opensaml.xml.security.criteria.EntityIDCriteria;
31  import org.opensaml.xml.security.criteria.UsageCriteria;
32  import org.opensaml.xml.security.trust.TrustEngine;
33  import org.opensaml.xml.security.x509.X509Credential;
34  import org.opensaml.xml.security.x509.X509Util;
35  import org.opensaml.xml.util.DatatypeHelper;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * Policy rule that checks if the client cert used to authenticate the request is valid and trusted.
41   * 
42   * <p>
43   * This rule is only evaluated if the message context contains a peer {@link X509Credential} as returned from the
44   * inbound message context's inbound message transport {@link Transport#getPeerCredential()}.
45   * </p>
46   * 
47   * <p>
48   * The entity ID used to perform trust evaluation of the X509 credential is first retrieved via
49   * {@link #getCertificatePresenterEntityID(MessageContext)}. If this value is non-null, trust evaluation 
50   * proceeds on that basis.  If trust evaluation using this entity ID is successful, the message context's inbound
51   * transport authentication state will be set to <code>true</code> and processing is terminated. If unsuccessful, a
52   * {@link SecurityPolicyException} is thrown.
53   * </p>
54   * 
55   * <p>
56   * If a non-null value was available from {@link #getCertificatePresenterEntityID(MessageContext)},
57   * then rule evaluation will be attempted as described in
58   * {@link #evaluateCertificateNameDerivedPresenters(X509Credential, MessageContext)}, based on the currently configured
59   * certificate name evaluation options. If this method returns a non-null certificate presenter entity ID,
60   * it will be set on the message context by calling
61   * {@link #setAuthenticatedCertificatePresenterEntityID(MessageContext, String)}
62   * The message context's inbound transport authentication state will be set to <code>true</code> via
63   * {@link InTransport#setAuthenticated(boolean)}.
64   * Rule processing is then terminated. If the method returns null, the client certificate presenter entity ID
65   * and inbound transport authentication state will remain unmodified and rule processing continues.
66   * </p>
67   * 
68   * <p>
69   * Finally rule evaluation will proceed as described in
70   * {@link #evaluateDerivedPresenters(X509Credential, MessageContext)}.
71   * This is primarily an extension point by which subclasses may implement specific custom logic. If this method returns
72   * a non-null client certificate presenter entity ID, it will be set via 
73   * {@link #setAuthenticatedCertificatePresenterEntityID(MessageContext, String)}, the message
74   * context's inbound transport authentication state will be set to <code>true</code> and rule processing is
75   * terminated. If the method returns null, the client certificate presenter entity ID and transport authentication
76   * state will remain unmodified.
77   * </p>
78   */
79  public class ClientCertAuthRule extends BaseTrustEngineRule<X509Credential> {
80  
81      /** Logger. */
82      private final Logger log = LoggerFactory.getLogger(ClientCertAuthRule.class);
83  
84      /** Options for derving client cert presenter entity ID's from an X.509 certificate. */
85      private CertificateNameOptions certNameOptions;
86  
87      /**
88       * Constructor.
89       * 
90       * @param engine Trust engine used to verify the request X509Credential
91       * @param nameOptions options for deriving certificate presenter entity ID's from an X.509 certificate
92       * 
93       */
94      public ClientCertAuthRule(TrustEngine<X509Credential> engine, CertificateNameOptions nameOptions) {
95          super(engine);
96          certNameOptions = nameOptions;
97      }
98  
99      /** {@inheritDoc} */
100     public void evaluate(MessageContext messageContext) throws SecurityPolicyException {
101 
102         Credential peerCredential = messageContext.getInboundMessageTransport().getPeerCredential();
103 
104         if (peerCredential == null) {
105             log.info("Inbound message transport did not contain a peer credential, "
106                     + "skipping client certificate authentication");
107             return;
108         }
109         if (!(peerCredential instanceof X509Credential)) {
110             log.info("Inbound message transport did not contain an X509Credential, "
111                     + "skipping client certificate authentication");
112             return;
113         }
114 
115         X509Credential requestCredential = (X509Credential) peerCredential;
116 
117         doEvaluate(requestCredential, messageContext);
118     }
119 
120     /**
121      * Get the currently configured certificate name options.
122      * 
123      * @return the certificate name options
124      */
125     protected CertificateNameOptions getCertificateNameOptions() {
126         return certNameOptions;
127     }
128 
129     /**
130      * Evaluate the request credential.
131      * 
132      * @param requestCredential the X509Credential derived from the request
133      * @param messageContext the message context being evaluated
134      * @throws SecurityPolicyException thrown if a certificate presenter entity ID available from the message context
135      *              and the client certificate token can not be establishd as trusted on that basis,
136      *              or if there is error during evaluation processing
137      */
138     protected void doEvaluate(X509Credential requestCredential, MessageContext messageContext)
139             throws SecurityPolicyException {
140 
141         String presenterEntityID = getCertificatePresenterEntityID(messageContext);
142 
143         if (presenterEntityID != null) {
144             log.debug("Attempting client certificate authentication using context presenter entity ID: {}",
145                     presenterEntityID);
146             if (evaluate(requestCredential, presenterEntityID, messageContext)) {
147                 log.info("Authentication via client certificate succeeded for context presenter entity ID: {}",
148                         presenterEntityID);
149                 messageContext.getInboundMessageTransport().setAuthenticated(true);
150             } else {
151                 log.error("Authentication via client certificate failed for context presenter entity ID {}",
152                         presenterEntityID);
153                 throw new SecurityPolicyException(
154                         "Client certificate authentication failed for context presenter entity ID");
155             }
156             return;
157         }
158 
159         String derivedPresenter = evaluateCertificateNameDerivedPresenters(requestCredential, messageContext);
160         if (derivedPresenter != null) {
161             log.info("Authentication via client certificate succeeded for certificate-derived presenter entity ID {}",
162                     derivedPresenter);
163             setAuthenticatedCertificatePresenterEntityID(messageContext, derivedPresenter);
164             messageContext.getInboundMessageTransport().setAuthenticated(true);
165             return;
166         }
167 
168         derivedPresenter = evaluateDerivedPresenters(requestCredential, messageContext);
169         if (derivedPresenter != null) {
170             log.info("Authentication via client certificate succeeded for derived presenter entity ID {}",
171                     derivedPresenter);
172             setAuthenticatedCertificatePresenterEntityID(messageContext, derivedPresenter);
173             messageContext.getInboundMessageTransport().setAuthenticated(true);
174             return;
175         }
176     }
177 
178     /**
179      * Get the entity ID of the presenter of the client TLS certificate, as will be used
180      * for trust evaluation purposes.
181      * 
182      * <p>The default behavior is to return the value of
183      * {@link MessageContext#getInboundMessageIssuer()}.  Subclasses may override to implement
184      * different logic.
185      * </p>
186      * 
187      * @param messageContext the current message context
188      * @return the entity ID of the client TLS certificate presenter
189      */
190     protected String getCertificatePresenterEntityID(MessageContext messageContext) {
191         return messageContext.getInboundMessageIssuer();
192     }
193     
194     /**
195      * Store the sucessfully authenticated derived entity ID of the certificate presenter
196      * in the message context.
197      * 
198      * <p>The default behavior is to set the value by calling
199      * {@link MessageContext#setInboundMessageIssuer(String)}. Subclasses may override to implement
200      * different logic.
201      * </p>
202      * 
203      * @param messageContext the current message context
204      * @param entityID the successfully authenticated derived entity ID of the client TLS
205      *              certificate presenter
206      */
207     protected void setAuthenticatedCertificatePresenterEntityID(MessageContext messageContext,
208             String entityID) {
209         messageContext.setInboundMessageIssuer(entityID);
210     }
211 
212     /** {@inheritDoc} */
213     protected CriteriaSet buildCriteriaSet(String entityID, MessageContext messageContext)
214             throws SecurityPolicyException {
215 
216         CriteriaSet criteriaSet = new CriteriaSet();
217         if (!DatatypeHelper.isEmpty(entityID)) {
218             criteriaSet.add(new EntityIDCriteria(entityID));
219         }
220 
221         criteriaSet.add(new UsageCriteria(UsageType.SIGNING));
222 
223         return criteriaSet;
224     }
225 
226     /**
227      * Evaluate any candidate presenter entity ID's which may be derived from the credential or other message context
228      * information.
229      * 
230      * <p>
231      * This serves primarily as an extension point for subclasses to implement application-specific logic.
232      * </p>
233      * 
234      * <p>
235      * If multiple derived candidate entity ID's would satisfy the trust engine criteria, the choice of which one to
236      * return as the canonical presenter entity ID value is implementation-specific.
237      * </p>
238      * 
239      * @param requestCredential the X509Credential derived from the request
240      * @param messageContext the message context being evaluated
241      * @return a presenter entity ID which was successfully evaluated by the trust engine
242      * @throws SecurityPolicyException thrown if there is error during processing
243      * @deprecated Use {@link #evaluateDerivedPresenters(X509Credential,MessageContext)} instead
244      */
245     protected String evaluateDerivedIssuers(X509Credential requestCredential, MessageContext messageContext)
246             throws SecurityPolicyException {
247                 return evaluateDerivedPresenters(requestCredential, messageContext);
248             }
249 
250     /**
251      * Evaluate any candidate presenter entity ID's which may be derived from the credential or other message context
252      * information.
253      * 
254      * <p>
255      * This serves primarily as an extension point for subclasses to implement application-specific logic.
256      * </p>
257      * 
258      * <p>
259      * If multiple derived candidate entity ID's would satisfy the trust engine criteria, the choice of which one to
260      * return as the canonical presenter entity ID value is implementation-specific.
261      * </p>
262      * 
263      * @param requestCredential the X509Credential derived from the request
264      * @param messageContext the message context being evaluated
265      * @return a presenter entity ID which was successfully evaluated by the trust engine
266      * @throws SecurityPolicyException thrown if there is error during processing
267      */
268     protected String evaluateDerivedPresenters(X509Credential requestCredential, MessageContext messageContext)
269             throws SecurityPolicyException {
270 
271         return null;
272     }
273 
274     /**
275      * Evaluate candidate presenter entity ID's which may be derived from the request credential's entity certificate
276      * according to the options supplied via {@link CertificateNameOptions}.
277      * 
278      * <p>
279      * Configured certificate name types are derived as candidate presenter entity ID's and processed
280      * in the following order:
281      * <ol>
282      * <li>The certificate subject DN string as serialized by the X500DNHandler obtained via
283      * {@link CertificateNameOptions#getX500DNHandler()} and using the output format indicated by
284      * {@link CertificateNameOptions#getX500SubjectDNFormat()}.</li>
285      * <li>Subject alternative names of the types configured via {@link CertificateNameOptions#getSubjectAltNames()}.
286      * Note that this is a LinkedHashSet, so the order of evaluation is the order of insertion.</li>
287      * <li>The first common name (CN) value appearing in the certificate subject DN.</li>
288      * </ol>
289      * </p>
290      * 
291      * <p>
292      * The first one of the above which is successfully evaluated by the trust engine using criteria built from
293      * {@link BaseTrustEngineRule#buildCriteriaSet(String, MessageContext)} will be returned.
294      * </p>
295      * 
296      * @param requestCredential the X509Credential derived from the request
297      * @param messageContext the message context being evaluated
298      * @return a certificate presenter entity ID which was successfully evaluated by the trust engine
299      * @throws SecurityPolicyException thrown if there is error during processing
300      * @deprecated Use {@link #evaluateCertificateNameDerivedPresenters(X509Credential,MessageContext)} instead
301      */
302     protected String evaluateCertificateNameDerivedIssuers(X509Credential requestCredential,
303             MessageContext messageContext) throws SecurityPolicyException {
304                 return evaluateCertificateNameDerivedPresenters(requestCredential, messageContext);
305             }
306 
307     /**
308      * Evaluate candidate presenter entity ID's which may be derived from the request credential's entity certificate
309      * according to the options supplied via {@link CertificateNameOptions}.
310      * 
311      * <p>
312      * Configured certificate name types are derived as candidate presenter entity ID's and processed
313      * in the following order:
314      * <ol>
315      * <li>The certificate subject DN string as serialized by the X500DNHandler obtained via
316      * {@link CertificateNameOptions#getX500DNHandler()} and using the output format indicated by
317      * {@link CertificateNameOptions#getX500SubjectDNFormat()}.</li>
318      * <li>Subject alternative names of the types configured via {@link CertificateNameOptions#getSubjectAltNames()}.
319      * Note that this is a LinkedHashSet, so the order of evaluation is the order of insertion.</li>
320      * <li>The first common name (CN) value appearing in the certificate subject DN.</li>
321      * </ol>
322      * </p>
323      * 
324      * <p>
325      * The first one of the above which is successfully evaluated by the trust engine using criteria built from
326      * {@link BaseTrustEngineRule#buildCriteriaSet(String, MessageContext)} will be returned.
327      * </p>
328      * 
329      * @param requestCredential the X509Credential derived from the request
330      * @param messageContext the message context being evaluated
331      * @return a certificate presenter entity ID which was successfully evaluated by the trust engine
332      * @throws SecurityPolicyException thrown if there is error during processing
333      */
334     protected String evaluateCertificateNameDerivedPresenters(X509Credential requestCredential,
335             MessageContext messageContext) throws SecurityPolicyException {
336 
337         String candidatePresenter = null;
338 
339         if (certNameOptions.evaluateSubjectDN()) {
340             candidatePresenter = evaluateSubjectDN(requestCredential, messageContext);
341             if (candidatePresenter != null) {
342                 return candidatePresenter;
343             }
344         }
345 
346         if (!certNameOptions.getSubjectAltNames().isEmpty()) {
347             candidatePresenter = evaluateSubjectAltNames(requestCredential, messageContext);
348             if (candidatePresenter != null) {
349                 return candidatePresenter;
350             }
351         }
352 
353         if (certNameOptions.evaluateSubjectCommonName()) {
354             candidatePresenter = evaluateSubjectCommonName(requestCredential, messageContext);
355             if (candidatePresenter != null) {
356                 return candidatePresenter;
357             }
358         }
359 
360         return null;
361     }
362 
363     /**
364      * Evaluate the presenter entity ID as derived from the cert subject common name (CN).
365      * 
366      * Only the first CN value from the subject DN is evaluated.
367      * 
368      * @param requestCredential the X509Credential derived from the request
369      * @param messageContext the message context being evaluated
370      * @return a presenter entity ID which was successfully evaluated by the trust engine
371      * @throws SecurityPolicyException thrown if there is error during processing
372      */
373     protected String evaluateSubjectCommonName(X509Credential requestCredential, MessageContext messageContext)
374             throws SecurityPolicyException {
375 
376         log.debug("Evaluating client cert by deriving presenter as cert CN");
377         X509Certificate certificate = requestCredential.getEntityCertificate();
378         String candidatePresenter = getCommonName(certificate);
379         if (candidatePresenter != null) {
380             if (evaluate(requestCredential, candidatePresenter, messageContext)) {
381                 log.info("Authentication succeeded for presenter entity ID derived from CN {}",
382                         candidatePresenter);
383                 return candidatePresenter;
384             }
385         }
386         return null;
387     }
388 
389     /**
390      * Evaluate the presenter entity ID as derived from the cert subject DN.
391      * 
392      * @param requestCredential the X509Credential derived from the request
393      * @param messageContext the message context being evaluated
394      * @return a presenter entity ID which was successfully evaluated by the trust engine
395      * @throws SecurityPolicyException thrown if there is error during processing
396      */
397     protected String evaluateSubjectDN(X509Credential requestCredential, MessageContext messageContext)
398             throws SecurityPolicyException {
399 
400         log.debug("Evaluating client cert by deriving presenter as cert subject DN");
401         X509Certificate certificate = requestCredential.getEntityCertificate();
402         String candidatePresenter = getSubjectName(certificate);
403         if (candidatePresenter != null) {
404             if (evaluate(requestCredential, candidatePresenter, messageContext)) {
405                 log.info("Authentication succeeded for presenter entity ID derived from subject DN {}",
406                         candidatePresenter);
407                 return candidatePresenter;
408             }
409         }
410         return null;
411     }
412 
413     /**
414      * Evaluate the presenter entity ID as derived from the cert subject alternative names specified by
415      * types enumerated in {@link CertificateNameOptions#getSubjectAltNames()}.
416      * 
417      * @param requestCredential the X509Credential derived from the request
418      * @param messageContext the message context being evaluated
419      * @return a presenter entity ID which was successfully evaluated by the trust engine
420      * @throws SecurityPolicyException thrown if there is error during processing
421      */
422     protected String evaluateSubjectAltNames(X509Credential requestCredential, MessageContext messageContext)
423             throws SecurityPolicyException {
424 
425         log.debug("Evaluating client cert by deriving presenter from subject alt names");
426         X509Certificate certificate = requestCredential.getEntityCertificate();
427         for (Integer altNameType : certNameOptions.getSubjectAltNames()) {
428             log.debug("Evaluating alt names of type: {}", altNameType.toString());
429             List<String> altNames = getAltNames(certificate, altNameType);
430             for (String altName : altNames) {
431                 if (evaluate(requestCredential, altName, messageContext)) {
432                     log.info("Authentication succeeded for presenter entity ID derived from subject alt name {}",
433                             altName);
434                     return altName;
435                 }
436             }
437         }
438         return null;
439     }
440 
441     /**
442      * Get the first common name (CN) value from the subject DN of the specified certificate.
443      * 
444      * @param cert the certificate being processed
445      * @return the first CN value, or null if there are none
446      */
447     protected String getCommonName(X509Certificate cert) {
448         List<String> names = X509Util.getCommonNames(cert.getSubjectX500Principal());
449         if (names != null && !names.isEmpty()) {
450             String name = names.get(0);
451             log.debug("Extracted common name from certificate: {}", name);
452             return name;
453         }
454         return null;
455     }
456 
457     /**
458      * Get subject name from a certificate, using the currently configured X500DNHandler and subject DN output format.
459      * 
460      * @param cert the certificate being processed
461      * @return the subject name
462      */
463     protected String getSubjectName(X509Certificate cert) {
464         if (cert == null) {
465             return null;
466         }
467         String name = null;
468         if (!DatatypeHelper.isEmpty(certNameOptions.getX500SubjectDNFormat())) {
469             name = certNameOptions.getX500DNHandler().getName(cert.getSubjectX500Principal(),
470                     certNameOptions.getX500SubjectDNFormat());
471         } else {
472             name = certNameOptions.getX500DNHandler().getName(cert.getSubjectX500Principal());
473         }
474         log.debug("Extracted subject name from certificate: {}", name);
475         return name;
476     }
477 
478     /**
479      * Get the list of subject alt name values from the certificate which are of the specified alt name type.
480      * 
481      * @param cert the certificate from which to extract alt names
482      * @param altNameType the type of alt name to extract
483      * 
484      * @return the list of certificate subject alt names
485      */
486     protected List<String> getAltNames(X509Certificate cert, Integer altNameType) {
487         log.debug("Extracting alt names from certificate of type: {}", altNameType.toString());
488         Integer[] nameTypes = new Integer[] { altNameType };
489         List altNames = X509Util.getAltNames(cert, nameTypes);
490         List<String> names = new ArrayList<String>();
491         for (Object altNameValue : altNames) {
492             if (!(altNameValue instanceof String)) {
493                 log.debug("Skipping non-String certificate alt name value");
494             } else {
495                 names.add((String) altNameValue);
496             }
497         }
498         log.debug("Extracted alt names from certificate: {}", names.toString());
499         return names;
500     }
501 
502 }