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 * @param responseCredential
299 * Credential to use for signing the SAML Response
300 * @param assertaionCredential
301 * Credential to use for signing any SAML Assertions contained
303 * @return SAML response to send to accepting site
304 * @exception SAMLException
305 * Base class of exceptions that may be thrown during
308 public SAMLResponse prepare(
310 RelyingParty relyingParty,
311 SAMLNameIdentifier nameId,
316 Credential responseCredential,
317 Credential assertionCredential)
318 throws SAMLException {
320 if (responseCredential == null || responseCredential.getPrivateKey() == null) {
321 throw new InvalidCryptoException(
322 SAMLException.RESPONDER,
323 "ShibPOSTProfile.prepare() requires a response key.");
326 String responseAlgorithm;
327 if (responseCredential.getCredentialType() == Credential.RSA) {
328 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
329 } else if (responseCredential.getCredentialType() == Credential.DSA) {
330 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
332 throw new InvalidCryptoException(
333 SAMLException.RESPONDER,
334 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
337 Document doc = org.opensaml.XML.parserPool.newDocument();
339 ArrayList audiences = new ArrayList();
340 audiences.add(relyingParty.getProviderId());
341 audiences.add(relyingParty.getName());
344 SAMLPOSTProfile.prepare(
346 relyingParty.getConfigProperty("edu.internet2.middleware.shibboleth.hs.HandleServlet.issuer"),
355 if (assertionCredential != null & assertionCredential.getPrivateKey() != null) {
357 String assertionAlgorithm;
358 if (assertionCredential.getCredentialType() == Credential.RSA) {
359 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
360 } else if (assertionCredential.getCredentialType() == Credential.DSA) {
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 assertionCredential.getPrivateKey(),
371 Arrays.asList(assertionCredential.getX509CertificateChain()));
376 responseCredential.getPrivateKey(),
377 Arrays.asList(responseCredential.getX509CertificateChain()));
383 * Searches the replay cache for the specified assertion and inserts a
384 * newly seen assertion into the cache
387 * Also performs garbage collection of the cache by deleting expired
391 * The assertion to check
392 * @return true iff the assertion has not been seen before
394 public synchronized boolean checkReplayCache(SAMLAssertion a) {
395 // Default implementation uses the basic replay cache implementation.
396 return SAMLPOSTProfile.checkReplayCache(a);
400 * Default signature verification algorithm uses an embedded X509
401 * certificate(s) or an explicit key to verify the signature. The
402 * certificate is examined to insure the subject CN matches the signer, and
403 * that it is signed by a trusted CA
406 * The object containing the signature
408 * The name of the signer
410 * A keystore containing trusted root certificates
412 * An explicit key to use if a certificate cannot be found
414 * Verify according to simple SAML signature profile?
416 * @throws SAMLException
417 * Thrown if the signature cannot be verified
419 protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
420 throws SAMLException {
422 NDC.push("verifySignature");
424 if (!obj.isSigned()) {
425 log.error("unable to find a signature");
426 throw new TrustException(
427 SAMLException.RESPONDER,
428 "ShibPOSTProfile.verifySignature() given an unsigned object");
431 if (knownKey != null) {
432 log.info("verifying signature with known key value, ignoring signature KeyInfo");
433 obj.verify(knownKey);
437 log.info("verifying signature with embedded KeyInfo");
440 // This is pretty painful, and this is leveraging the supposedly
441 // automatic support in JDK 1.4.
442 // First we have to extract the certificates from the object.
443 Iterator certs_from_obj = obj.getX509Certificates();
444 if (!certs_from_obj.hasNext()) {
445 log.error("need certificates inside object to establish trust");
446 throw new TrustException(
447 SAMLException.RESPONDER,
448 "ShibPOSTProfile.verifySignature() can't find any certificates");
451 // We assume the first one in the set is the end entity cert.
452 X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
454 // Match the CN of the entity cert with the expected signer.
455 String dname = entity_cert.getSubjectDN().getName();
456 log.debug("found entity cert with DN: " + dname);
457 String cname = "CN=" + signerName;
458 if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
460 "verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
462 throw new TrustException(
463 SAMLException.RESPONDER,
464 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
467 // Prep a chain between the entity cert and the trusted roots.
468 X509CertSelector targetConstraints = new X509CertSelector();
469 targetConstraints.setCertificate(entity_cert);
470 PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
471 params.setMaxPathLength(-1);
473 Vector certbag = new Vector();
474 certbag.add(entity_cert);
475 while (certs_from_obj.hasNext())
476 certbag.add(certs_from_obj.next());
477 CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
478 CertStore store = CertStore.getInstance("Collection", ccsp);
479 params.addCertStore(store);
481 // Attempt to build a path.
482 CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
483 PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
484 } catch (CertPathBuilderException e) {
485 log.error("caught a cert path builder exception: " + e.getMessage());
486 throw new TrustException(
487 SAMLException.RESPONDER,
488 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
490 } catch (GeneralSecurityException e) {
491 log.error("caught a general security exception: " + e.getMessage());
492 throw new TrustException(
493 SAMLException.RESPONDER,
494 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",