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.CertificateEncodingException;
20  import java.security.cert.X509Certificate;
21  import java.util.ArrayList;
22  import java.util.List;
23  
24  import org.opensaml.ws.message.MessageContext;
25  import org.opensaml.ws.security.SecurityPolicyException;
26  import org.opensaml.xml.security.CriteriaSet;
27  import org.opensaml.xml.security.credential.Credential;
28  import org.opensaml.xml.security.credential.UsageType;
29  import org.opensaml.xml.security.criteria.EntityIDCriteria;
30  import org.opensaml.xml.security.criteria.UsageCriteria;
31  import org.opensaml.xml.security.trust.TrustEngine;
32  import org.opensaml.xml.security.x509.X509Credential;
33  import org.opensaml.xml.security.x509.X509Util;
34  import org.opensaml.xml.util.Base64;
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 org.opensaml.ws.transport.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 proceeds on
50   * that basis. If trust evaluation using this entity ID is successful, the message context's inbound transport
51   * 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)}, then rule evaluation
57   * will be attempted as described in {@link #evaluateCertificateNameDerivedPresenters(X509Credential, MessageContext)},
58   * based on the currently configured certificate name evaluation options. If this method returns a non-null certificate
59   * presenter entity ID, it will be set on the message context by calling
60   * {@link #setAuthenticatedCertificatePresenterEntityID(MessageContext, String)} The message context's inbound transport
61   * authentication state will be set to <code>true</code> via
62   * {@link org.opensaml.ws.transport.InTransport#setAuthenticated(boolean)}. Rule processing is then terminated. If the
63   * method returns null, the client certificate presenter entity ID and inbound transport authentication state will
64   * remain unmodified and rule processing continues.
65   * </p>
66   * 
67   * <p>
68   * Finally rule evaluation will proceed as described in
69   * {@link #evaluateDerivedPresenters(X509Credential, MessageContext)}. This is primarily an extension point by which
70   * subclasses may implement specific custom logic. If this method returns a non-null client certificate presenter entity
71   * ID, it will be set via {@link #setAuthenticatedCertificatePresenterEntityID(MessageContext, String)}, the message
72   * context's inbound transport authentication state will be set to <code>true</code> and rule processing is terminated.
73   * If the method returns null, the client certificate presenter entity ID and transport authentication state will remain
74   * unmodified.
75   * </p>
76   */
77  public class ClientCertAuthRule extends BaseTrustEngineRule<X509Credential> {
78  
79      /** Logger. */
80      private final Logger log = LoggerFactory.getLogger(ClientCertAuthRule.class);
81  
82      /** Options for derving client cert presenter entity ID's from an X.509 certificate. */
83      private CertificateNameOptions certNameOptions;
84  
85      /**
86       * Constructor.
87       * 
88       * @param engine Trust engine used to verify the request X509Credential
89       * @param nameOptions options for deriving certificate presenter entity ID's from an X.509 certificate
90       * 
91       */
92      public ClientCertAuthRule(TrustEngine<X509Credential> engine, CertificateNameOptions nameOptions) {
93          super(engine);
94          certNameOptions = nameOptions;
95      }
96  
97      /** {@inheritDoc} */
98      public void evaluate(MessageContext messageContext) throws SecurityPolicyException {
99  
100         Credential peerCredential = messageContext.getInboundMessageTransport().getPeerCredential();
101 
102         if (peerCredential == null) {
103             log.info("Inbound message transport did not contain a peer credential, "
104                     + "skipping client certificate authentication");
105             return;
106         }
107         if (!(peerCredential instanceof X509Credential)) {
108             log.info("Inbound message transport did not contain an X509Credential, "
109                     + "skipping client certificate authentication");
110             return;
111         }
112 
113         X509Credential requestCredential = (X509Credential) peerCredential;
114         if (log.isDebugEnabled()) {
115             try {
116                 log.debug("Attempting to authenticate inbound connection that presented the certificate:");
117                 log.debug(Base64.encodeBytes(requestCredential.getEntityCertificate().getEncoded()));
118             } catch (CertificateEncodingException e) {
119                 // do nothing
120             }
121         }
122         doEvaluate(requestCredential, messageContext);
123     }
124 
125     /**
126      * Get the currently configured certificate name options.
127      * 
128      * @return the certificate name options
129      */
130     protected CertificateNameOptions getCertificateNameOptions() {
131         return certNameOptions;
132     }
133 
134     /**
135      * Evaluate the request credential.
136      * 
137      * @param requestCredential the X509Credential derived from the request
138      * @param messageContext the message context being evaluated
139      * @throws SecurityPolicyException thrown if a certificate presenter entity ID available from the message context
140      *             and the client certificate token can not be establishd as trusted on that basis, or if there is error
141      *             during evaluation processing
142      */
143     protected void doEvaluate(X509Credential requestCredential, MessageContext messageContext)
144             throws SecurityPolicyException {
145 
146         String presenterEntityID = getCertificatePresenterEntityID(messageContext);
147 
148         if (presenterEntityID != null) {
149             log.debug("Attempting client certificate authentication using context presenter entity ID: {}",
150                     presenterEntityID);
151             if (evaluate(requestCredential, presenterEntityID, messageContext)) {
152                 log.info("Authentication via client certificate succeeded for context presenter entity ID: {}",
153                         presenterEntityID);
154                 messageContext.getInboundMessageTransport().setAuthenticated(true);
155             } else {
156                 log.error("Authentication via client certificate failed for context presenter entity ID {}",
157                         presenterEntityID);
158                 throw new SecurityPolicyException(
159                         "Client certificate authentication failed for context presenter entity ID");
160             }
161             return;
162         }
163 
164         String derivedPresenter = evaluateCertificateNameDerivedPresenters(requestCredential, messageContext);
165         if (derivedPresenter != null) {
166             log.info("Authentication via client certificate succeeded for certificate-derived presenter entity ID {}",
167                     derivedPresenter);
168             setAuthenticatedCertificatePresenterEntityID(messageContext, derivedPresenter);
169             messageContext.getInboundMessageTransport().setAuthenticated(true);
170             return;
171         }
172 
173         derivedPresenter = evaluateDerivedPresenters(requestCredential, messageContext);
174         if (derivedPresenter != null) {
175             log.info("Authentication via client certificate succeeded for derived presenter entity ID {}",
176                     derivedPresenter);
177             setAuthenticatedCertificatePresenterEntityID(messageContext, derivedPresenter);
178             messageContext.getInboundMessageTransport().setAuthenticated(true);
179             return;
180         }
181     }
182 
183     /**
184      * Get the entity ID of the presenter of the client TLS certificate, as will be used for trust evaluation purposes.
185      * 
186      * <p>
187      * The default behavior is to return the value of {@link MessageContext#getInboundMessageIssuer()}. Subclasses may
188      * override to implement different logic.
189      * </p>
190      * 
191      * @param messageContext the current message context
192      * @return the entity ID of the client TLS certificate presenter
193      */
194     protected String getCertificatePresenterEntityID(MessageContext messageContext) {
195         return messageContext.getInboundMessageIssuer();
196     }
197 
198     /**
199      * Store the sucessfully authenticated derived entity ID of the certificate presenter in the message context.
200      * 
201      * <p>
202      * The default behavior is to set the value by calling {@link MessageContext#setInboundMessageIssuer(String)}.
203      * Subclasses may override to implement different logic.
204      * </p>
205      * 
206      * @param messageContext the current message context
207      * @param entityID the successfully authenticated derived entity ID of the client TLS certificate presenter
208      */
209     protected void setAuthenticatedCertificatePresenterEntityID(MessageContext messageContext, String entityID) {
210         messageContext.setInboundMessageIssuer(entityID);
211     }
212 
213     /** {@inheritDoc} */
214     protected CriteriaSet buildCriteriaSet(String entityID, MessageContext messageContext)
215             throws SecurityPolicyException {
216 
217         CriteriaSet criteriaSet = new CriteriaSet();
218         if (!DatatypeHelper.isEmpty(entityID)) {
219             criteriaSet.add(new EntityIDCriteria(entityID));
220         }
221 
222         criteriaSet.add(new UsageCriteria(UsageType.SIGNING));
223 
224         return criteriaSet;
225     }
226 
227     /**
228      * Evaluate any candidate presenter entity ID's which may be derived from the credential or other message context
229      * information.
230      * 
231      * <p>
232      * This serves primarily as an extension point for subclasses to implement application-specific logic.
233      * </p>
234      * 
235      * <p>
236      * If multiple derived candidate entity ID's would satisfy the trust engine criteria, the choice of which one to
237      * return as the canonical presenter entity ID value is implementation-specific.
238      * </p>
239      * 
240      * @param requestCredential the X509Credential derived from the request
241      * @param messageContext the message context being evaluated
242      * @return a presenter entity ID which was successfully evaluated by the trust engine
243      * @throws SecurityPolicyException thrown if there is error during processing
244      * @deprecated Use {@link #evaluateDerivedPresenters(X509Credential,MessageContext)} instead
245      */
246     protected String evaluateDerivedIssuers(X509Credential requestCredential, MessageContext messageContext)
247             throws SecurityPolicyException {
248         return evaluateDerivedPresenters(requestCredential, messageContext);
249     }
250 
251     /**
252      * Evaluate any candidate presenter entity ID's which may be derived from the credential or other message context
253      * information.
254      * 
255      * <p>
256      * This serves primarily as an extension point for subclasses to implement application-specific logic.
257      * </p>
258      * 
259      * <p>
260      * If multiple derived candidate entity ID's would satisfy the trust engine criteria, the choice of which one to
261      * return as the canonical presenter entity ID value is implementation-specific.
262      * </p>
263      * 
264      * @param requestCredential the X509Credential derived from the request
265      * @param messageContext the message context being evaluated
266      * @return a presenter entity ID which was successfully evaluated by the trust engine
267      * @throws SecurityPolicyException thrown if there is error during processing
268      */
269     protected String evaluateDerivedPresenters(X509Credential requestCredential, MessageContext messageContext)
270             throws SecurityPolicyException {
271 
272         return null;
273     }
274 
275     /**
276      * Evaluate candidate presenter entity ID's which may be derived from the request credential's entity certificate
277      * according to the options supplied via {@link CertificateNameOptions}.
278      * 
279      * <p>
280      * Configured certificate name types are derived as candidate presenter entity ID's and processed in the following
281      * order:
282      * <ol>
283      * <li>The certificate subject DN string as serialized by the X500DNHandler obtained via
284      * {@link CertificateNameOptions#getX500DNHandler()} and using the output format indicated by
285      * {@link CertificateNameOptions#getX500SubjectDNFormat()}.</li>
286      * <li>Subject alternative names of the types configured via {@link CertificateNameOptions#getSubjectAltNames()}.
287      * Note that this is a LinkedHashSet, so the order of evaluation is the order of insertion.</li>
288      * <li>The first common name (CN) value appearing in the certificate subject DN.</li>
289      * </ol>
290      * </p>
291      * 
292      * <p>
293      * The first one of the above which is successfully evaluated by the trust engine using criteria built from
294      * {@link BaseTrustEngineRule#buildCriteriaSet(String, MessageContext)} will be returned.
295      * </p>
296      * 
297      * @param requestCredential the X509Credential derived from the request
298      * @param messageContext the message context being evaluated
299      * @return a certificate presenter entity ID which was successfully evaluated by the trust engine
300      * @throws SecurityPolicyException thrown if there is error during processing
301      * @deprecated Use {@link #evaluateCertificateNameDerivedPresenters(X509Credential,MessageContext)} instead
302      */
303     protected String evaluateCertificateNameDerivedIssuers(X509Credential requestCredential,
304             MessageContext messageContext) throws SecurityPolicyException {
305         return evaluateCertificateNameDerivedPresenters(requestCredential, messageContext);
306     }
307 
308     /**
309      * Evaluate candidate presenter entity ID's which may be derived from the request credential's entity certificate
310      * according to the options supplied via {@link CertificateNameOptions}.
311      * 
312      * <p>
313      * Configured certificate name types are derived as candidate presenter entity ID's and processed in the following
314      * order:
315      * <ol>
316      * <li>The certificate subject DN string as serialized by the X500DNHandler obtained via
317      * {@link CertificateNameOptions#getX500DNHandler()} and using the output format indicated by
318      * {@link CertificateNameOptions#getX500SubjectDNFormat()}.</li>
319      * <li>Subject alternative names of the types configured via {@link CertificateNameOptions#getSubjectAltNames()}.
320      * Note that this is a LinkedHashSet, so the order of evaluation is the order of insertion.</li>
321      * <li>The first common name (CN) value appearing in the certificate subject DN.</li>
322      * </ol>
323      * </p>
324      * 
325      * <p>
326      * The first one of the above which is successfully evaluated by the trust engine using criteria built from
327      * {@link BaseTrustEngineRule#buildCriteriaSet(String, MessageContext)} will be returned.
328      * </p>
329      * 
330      * @param requestCredential the X509Credential derived from the request
331      * @param messageContext the message context being evaluated
332      * @return a certificate presenter entity ID which was successfully evaluated by the trust engine
333      * @throws SecurityPolicyException thrown if there is error during processing
334      */
335     protected String evaluateCertificateNameDerivedPresenters(X509Credential requestCredential,
336             MessageContext messageContext) throws SecurityPolicyException {
337 
338         String candidatePresenter = null;
339 
340         if (certNameOptions.evaluateSubjectDN()) {
341             candidatePresenter = evaluateSubjectDN(requestCredential, messageContext);
342             if (candidatePresenter != null) {
343                 return candidatePresenter;
344             }
345         }
346 
347         if (!certNameOptions.getSubjectAltNames().isEmpty()) {
348             candidatePresenter = evaluateSubjectAltNames(requestCredential, messageContext);
349             if (candidatePresenter != null) {
350                 return candidatePresenter;
351             }
352         }
353 
354         if (certNameOptions.evaluateSubjectCommonName()) {
355             candidatePresenter = evaluateSubjectCommonName(requestCredential, messageContext);
356             if (candidatePresenter != null) {
357                 return candidatePresenter;
358             }
359         }
360 
361         return null;
362     }
363 
364     /**
365      * Evaluate the presenter entity ID as derived from the cert subject common name (CN).
366      * 
367      * Only the first CN value from the subject DN is evaluated.
368      * 
369      * @param requestCredential the X509Credential derived from the request
370      * @param messageContext the message context being evaluated
371      * @return a presenter entity ID which was successfully evaluated by the trust engine
372      * @throws SecurityPolicyException thrown if there is error during processing
373      */
374     protected String evaluateSubjectCommonName(X509Credential requestCredential, MessageContext messageContext)
375             throws SecurityPolicyException {
376 
377         log.debug("Evaluating client cert by deriving presenter as cert CN");
378         X509Certificate certificate = requestCredential.getEntityCertificate();
379         String candidatePresenter = getCommonName(certificate);
380         if (candidatePresenter != null) {
381             if (evaluate(requestCredential, candidatePresenter, messageContext)) {
382                 log.info("Authentication succeeded for presenter entity ID derived from CN {}", 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 types enumerated
415      * 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 }