2 * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation
3 * for Advanced Internet Development, Inc. All rights reserved
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
9 * Redistributions of source code must retain the above copyright notice, this
10 * list of conditions and the following disclaimer.
12 * Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution, if any, must include
15 * the following acknowledgment: "This product includes software developed by
16 * the University Corporation for Advanced Internet Development
17 * <http://www.ucaid.edu> Internet2 Project. Alternately, this acknowledegement
18 * may appear in the software itself, if and wherever such third-party
19 * acknowledgments normally appear.
21 * Neither the name of Shibboleth nor the names of its contributors, nor
22 * Internet2, nor the University Corporation for Advanced Internet Development,
23 * Inc., nor UCAID may be used to endorse or promote products derived from this
24 * software without specific prior written permission. For written permission,
25 * please contact shibboleth@shibboleth.org
27 * Products derived from this software may not be called Shibboleth, Internet2,
28 * UCAID, or the University Corporation for Advanced Internet Development, nor
29 * may Shibboleth appear in their name, without prior written permission of the
30 * University Corporation for Advanced Internet Development.
33 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
34 * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
36 * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
37 * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
38 * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
39 * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY
40 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
41 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
42 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
45 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48 package edu.internet2.middleware.shibboleth.common;
50 import java.security.GeneralSecurityException;
51 import java.security.Key;
52 import java.security.KeyStore;
53 import java.security.cert.CertPathBuilder;
54 import java.security.cert.CertPathBuilderException;
55 import java.security.cert.CertStore;
56 import java.security.cert.CollectionCertStoreParameters;
57 import java.security.cert.PKIXBuilderParameters;
58 import java.security.cert.PKIXCertPathBuilderResult;
59 import java.security.cert.X509CertSelector;
60 import java.security.cert.X509Certificate;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.Collection;
64 import java.util.Date;
65 import java.util.Iterator;
66 import java.util.Vector;
68 import org.apache.log4j.Logger;
69 import org.apache.log4j.NDC;
70 import org.apache.xml.security.signature.XMLSignature;
71 import org.opensaml.InvalidAssertionException;
72 import org.opensaml.InvalidCryptoException;
73 import org.opensaml.SAMLAssertion;
74 import org.opensaml.SAMLAuthenticationStatement;
75 import org.opensaml.SAMLException;
76 import org.opensaml.SAMLNameIdentifier;
77 import org.opensaml.SAMLPOSTProfile;
78 import org.opensaml.SAMLResponse;
79 import org.opensaml.SAMLSignedObject;
80 import org.opensaml.SAMLStatement;
81 import org.opensaml.SAMLSubject;
82 import org.opensaml.TrustException;
83 import org.w3c.dom.Document;
86 * Basic Shibboleth POST browser profile implementation with basic support for
89 * @author Scott Cantor @created April 11, 2002
91 public class ShibPOSTProfile {
92 /** XML Signature algorithm to apply */
93 protected String algorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
95 /** Policy URIs to attach or check against */
96 protected ArrayList policies = new ArrayList();
98 /** Official name of issuing site */
99 protected String issuer = null;
101 /** The URL of the receiving SHIRE */
102 protected String receiver = null;
104 /** Seconds allowed to elapse from issuance of response */
105 protected int ttlSeconds = 0;
107 private static Logger log = Logger.getLogger(ShibPOSTProfile.class.getName());
110 * SHIRE-side constructor for a ShibPOSTProfile object
113 * Set of policy URIs that the implementation must support
117 * Length of time in seconds allowed to elapse from issuance of
119 * @exception SAMLException
120 * Raised if a profile implementation cannot be constructed
121 * from the supplied information
123 public ShibPOSTProfile(Collection policies, String receiver, int ttlSeconds) throws SAMLException {
124 if (policies == null || policies.size() == 0 || receiver == null || receiver.length() == 0 || ttlSeconds <= 0)
125 throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
127 this.receiver = receiver;
128 this.ttlSeconds = ttlSeconds;
129 this.policies.addAll(policies);
132 * HS-side constructor for a ShibPOSTProfile object.
135 public ShibPOSTProfile() {
139 * Locates an assertion containing a "bearer" AuthenticationStatement in
140 * the response and validates the enclosing assertion with respect to the
144 * The response to the accepting site
145 * @return An SSO assertion
147 * @throws SAMLException
148 * Thrown if an SSO assertion can't be found
150 public SAMLAssertion getSSOAssertion(SAMLResponse r) throws SAMLException {
151 return SAMLPOSTProfile.getSSOAssertion(r, policies);
155 * Locates a "bearer" AuthenticationStatement in the assertion and
156 * validates the statement with respect to the POST profile
159 * The SSO assertion sent to the accepting site
160 * @return A "bearer" authentication statement
162 * @throws SAMLException
163 * Thrown if an SSO statement can't be found
165 public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a) throws SAMLException {
166 return SAMLPOSTProfile.getSSOStatement(a);
170 * Examines a response to determine the source site name
175 String getOriginSite(SAMLResponse r) {
176 Iterator ia = r.getAssertions();
177 while (ia.hasNext()) {
178 Iterator is = ((SAMLAssertion) ia.next()).getStatements();
179 while (is.hasNext()) {
180 SAMLStatement s = (SAMLStatement) is.next();
181 if (s instanceof SAMLAuthenticationStatement)
182 return ((SAMLAuthenticationStatement) s).getSubject().getName().getName();
189 * Parse a Base-64 encoded buffer back into a SAML response and test its
190 * validity against the POST profile, including use of the default replay
194 * Also does trust evaluation based on the information available from the
195 * origin site mapper, in accordance with general Shibboleth processing
196 * semantics. Club-specific processing must be performed in a subclass.
200 * A Base-64 encoded buffer containing a SAML response
202 * @return SAML response sent by origin site
203 * @exception SAMLException
204 * Thrown if the response cannot be understood or accepted
206 public SAMLResponse accept(byte[] buf, StringBuffer originSite) throws SAMLException {
207 // The built-in SAML functionality will do most of the basic non-crypto
209 // Note that if the response only contains a status error, it gets
212 SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds, false);
214 if (originSite == null)
215 originSite = new StringBuffer();
217 // Now we do some more non-crypto (ie. cheap) work to match up the
219 // with its associated data. If we can't even find a SSO statement in
221 // we just return the response to the caller, who will presumably
223 SAMLAssertion assertion = null;
224 SAMLAuthenticationStatement sso = null;
227 assertion = getSSOAssertion(r);
228 sso = getSSOStatement(assertion);
229 } catch (SAMLException e) {
230 originSite.setLength(0);
231 originSite.append(getOriginSite(r));
235 // Examine the subject information.
236 SAMLSubject subject = sso.getSubject();
237 if (subject.getName().getName() == null)
238 throw new InvalidAssertionException(
239 SAMLException.RESPONDER,
240 "ShibPOSTProfile.accept() requires subject name qualifier");
242 originSite.setLength(0);
243 originSite.append(subject.getName().getName());
244 String handleService = assertion.getIssuer();
246 // Is this a trusted HS?
247 OriginSiteMapper mapper = Init.getMapper();
248 Iterator hsNames = mapper.getHandleServiceNames(originSite.toString());
249 boolean bFound = false;
250 while (!bFound && hsNames.hasNext())
251 if (hsNames.next().equals(handleService))
254 throw new TrustException(
255 SAMLException.RESPONDER,
256 "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
258 Key hsKey = mapper.getHandleServiceKey(handleService);
259 KeyStore ks = mapper.getTrustedRoots();
261 // Signature verification now takes place. We check the assertion and
263 // Assertion signing is optional, response signing is mandatory.
266 if (assertion.isSigned()) {
267 log.info("verifying assertion signature");
268 verifySignature(assertion, handleService, ks, hsKey);
270 log.info("verifying response signature");
271 verifySignature(r, handleService, ks, hsKey);
279 * Used by HS to generate a signed SAML response conforming to the POST
284 * URL of the assertion consumer
285 * @param relyingParty
286 * the intended recipient of the response
288 * Name Identifier for the response
290 * Client address of subject (optional)
292 * URI of authentication method being asserted
294 * Date and time of authentication being asserted
296 * Set of SAML authorities the relying party may contact
298 * @return SAML response to send to accepting site
299 * @exception SAMLException
300 * Base class of exceptions that may be thrown during
303 public SAMLResponse prepare(
305 RelyingParty relyingParty,
306 SAMLNameIdentifier nameId,
311 throws SAMLException {
313 if (relyingParty.getIdentityProvider().getResponseSigningCredential() == null
314 || relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey() == null) {
315 throw new InvalidCryptoException(
316 SAMLException.RESPONDER,
317 "ShibPOSTProfile.prepare() requires a response key.");
320 String responseAlgorithm;
321 if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.RSA) {
322 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
324 relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.DSA) {
325 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
327 throw new InvalidCryptoException(
328 SAMLException.RESPONDER,
329 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
332 Document doc = org.opensaml.XML.parserPool.newDocument();
334 ArrayList audiences = new ArrayList();
335 audiences.add(relyingParty.getProviderId());
336 if (!relyingParty.getProviderId().equals(relyingParty.getName()))
337 audiences.add(relyingParty.getName());
340 if (relyingParty.isLegacyProvider()) {
341 //TODO must resolve this
342 issuer = "fooIssuer";
344 issuer = relyingParty.getProviderId();
348 SAMLPOSTProfile.prepare(recipient, issuer, audiences, nameId, subjectIP, authMethod, authInstant, bindings);
351 if (relyingParty.getIdentityProvider().getAssertionSigningCredential() != null
352 && relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey() != null) {
354 String assertionAlgorithm;
355 if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType()
357 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
359 relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType()
361 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
363 throw new InvalidCryptoException(
364 SAMLException.RESPONDER,
365 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
368 ((SAMLAssertion) r.getAssertions().next()).sign(
370 relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey(),
372 relyingParty.getIdentityProvider().getAssertionSigningCredential().getX509CertificateChain()));
377 relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey(),
378 Arrays.asList(relyingParty.getIdentityProvider().getResponseSigningCredential().getX509CertificateChain()));
384 * Searches the replay cache for the specified assertion and inserts a
385 * newly seen assertion into the cache
388 * Also performs garbage collection of the cache by deleting expired
392 * The assertion to check
393 * @return true iff the assertion has not been seen before
395 public synchronized boolean checkReplayCache(SAMLAssertion a) {
396 // Default implementation uses the basic replay cache implementation.
397 return SAMLPOSTProfile.checkReplayCache(a);
401 * Default signature verification algorithm uses an embedded X509
402 * certificate(s) or an explicit key to verify the signature. The
403 * certificate is examined to insure the subject CN matches the signer, and
404 * that it is signed by a trusted CA
407 * The object containing the signature
409 * The name of the signer
411 * A keystore containing trusted root certificates
413 * An explicit key to use if a certificate cannot be found
415 * Verify according to simple SAML signature profile?
417 * @throws SAMLException
418 * Thrown if the signature cannot be verified
420 protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
421 throws SAMLException {
423 NDC.push("verifySignature");
425 if (!obj.isSigned()) {
426 log.error("unable to find a signature");
427 throw new TrustException(
428 SAMLException.RESPONDER,
429 "ShibPOSTProfile.verifySignature() given an unsigned object");
432 if (knownKey != null) {
433 log.info("verifying signature with known key value, ignoring signature KeyInfo");
434 obj.verify(knownKey);
438 log.info("verifying signature with embedded KeyInfo");
441 // This is pretty painful, and this is leveraging the supposedly
442 // automatic support in JDK 1.4.
443 // First we have to extract the certificates from the object.
444 Iterator certs_from_obj = obj.getX509Certificates();
445 if (!certs_from_obj.hasNext()) {
446 log.error("need certificates inside object to establish trust");
447 throw new TrustException(
448 SAMLException.RESPONDER,
449 "ShibPOSTProfile.verifySignature() can't find any certificates");
452 // We assume the first one in the set is the end entity cert.
453 X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
455 // Match the CN of the entity cert with the expected signer.
456 String dname = entity_cert.getSubjectDN().getName();
457 log.debug("found entity cert with DN: " + dname);
458 String cname = "CN=" + signerName;
459 if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
461 "verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
463 throw new TrustException(
464 SAMLException.RESPONDER,
465 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
468 // Prep a chain between the entity cert and the trusted roots.
469 X509CertSelector targetConstraints = new X509CertSelector();
470 targetConstraints.setCertificate(entity_cert);
471 PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
472 params.setMaxPathLength(-1);
474 Vector certbag = new Vector();
475 certbag.add(entity_cert);
476 while (certs_from_obj.hasNext())
477 certbag.add(certs_from_obj.next());
478 CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
479 CertStore store = CertStore.getInstance("Collection", ccsp);
480 params.addCertStore(store);
482 // Attempt to build a path.
483 CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
484 PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
485 } catch (CertPathBuilderException e) {
486 log.error("caught a cert path builder exception: " + e.getMessage());
487 throw new TrustException(
488 SAMLException.RESPONDER,
489 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
491 } catch (GeneralSecurityException e) {
492 log.error("caught a general security exception: " + e.getMessage());
493 throw new TrustException(
494 SAMLException.RESPONDER,
495 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",