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 }