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 }