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