2 * The Shibboleth License, Version 1.
4 * University Corporation for Advanced Internet Development, Inc.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are met:
11 * Redistributions of source code must retain the above copyright notice, this
12 * list of conditions and the following disclaimer.
14 * Redistributions in binary form must reproduce the above copyright notice,
15 * this list of conditions and the following disclaimer in the documentation
16 * and/or other materials provided with the distribution, if any, must include
17 * the following acknowledgment: "This product includes software developed by
18 * the University Corporation for Advanced Internet Development
19 * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
20 * may appear in the software itself, if and wherever such third-party
21 * acknowledgments normally appear.
23 * Neither the name of Shibboleth nor the names of its contributors, nor
24 * Internet2, nor the University Corporation for Advanced Internet Development,
25 * Inc., nor UCAID may be used to endorse or promote products derived from this
26 * software without specific prior written permission. For written permission,
27 * please contact shibboleth@shibboleth.org
29 * Products derived from this software may not be called Shibboleth, Internet2,
30 * UCAID, or the University Corporation for Advanced Internet Development, nor
31 * may Shibboleth appear in their name, without prior written permission of the
32 * University Corporation for Advanced Internet Development.
35 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36 * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
38 * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
39 * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
40 * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
41 * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
42 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
50 package edu.internet2.middleware.shibboleth.common;
52 import java.security.GeneralSecurityException;
53 import java.security.Key;
54 import java.security.KeyStore;
55 import java.security.PrivateKey;
56 import java.security.cert.*;
57 import java.util.ArrayList;
58 import java.util.Collection;
59 import java.util.Date;
60 import java.util.Iterator;
61 import java.util.Vector;
63 import javax.crypto.SecretKey;
65 import org.apache.log4j.Logger;
66 import org.apache.log4j.NDC;
67 import org.apache.xml.security.signature.XMLSignature;
68 import org.opensaml.*;
69 import org.w3c.dom.Document;
72 * Basic Shibboleth POST browser profile implementation with basic support for
75 * @author Scott Cantor
76 * @created April 11, 2002
78 public class ShibPOSTProfile
80 /** XML Signature algorithm to apply */
81 protected String algorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
83 /** Policy URIs to attach or check against */
84 protected ArrayList policies = new ArrayList();
86 /** Official name of issuing site */
87 protected String issuer = null;
89 /** The URL of the receiving SHIRE */
90 protected String receiver = null;
92 /** Seconds allowed to elapse from issuance of response */
93 protected int ttlSeconds = 0;
95 private static Logger log = Logger.getLogger(ShibPOSTProfile.class.getName());
98 * SHIRE-side constructor for a ShibPOSTProfile object
100 * @param policies Set of policy URIs that the implementation
102 * @param receiver URL of SHIRE
103 * @param ttlSeconds Length of time in seconds allowed to elapse
104 * from issuance of SAML response
105 * @exception SAMLException Raised if a profile implementation cannot be
106 * constructed from the supplied information
108 public ShibPOSTProfile(Collection policies, String receiver, int ttlSeconds)
111 if (policies == null || policies.size() == 0 || receiver == null || receiver.length() == 0 || ttlSeconds <= 0)
112 throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
114 this.receiver = receiver;
115 this.ttlSeconds = ttlSeconds;
116 this.policies.addAll(policies);
120 * HS-side constructor for a ShibPOSTProfile object
122 * @param policies Set of policy URIs that the implementation
124 * @param issuer "Official" name of issuing origin site
125 * @exception SAMLException Raised if a profile implementation cannot be
126 * constructed from the supplied information
128 public ShibPOSTProfile(Collection policies, String issuer)
131 if (policies == null || policies.size() == 0 || issuer == null || issuer.length() == 0)
132 throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
133 this.issuer = issuer;
134 this.policies.addAll(policies);
138 * Locates an assertion containing a "bearer" AuthenticationStatement in
139 * the response and validates the enclosing assertion with respect to the
142 * @param r The response to the accepting site
143 * @return An SSO assertion
145 * @throws SAMLException Thrown if an SSO assertion can't be found
147 public SAMLAssertion getSSOAssertion(SAMLResponse r)
150 return SAMLPOSTProfile.getSSOAssertion(r,policies);
154 * Locates a "bearer" AuthenticationStatement in the assertion and
155 * validates the statement with respect to the POST profile
157 * @param a The SSO assertion sent to the accepting site
158 * @return A "bearer" authentication statement
160 * @throws SAMLException Thrown if an SSO statement can't be found
162 public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a)
165 return SAMLPOSTProfile.getSSOStatement(a);
169 * Examines a response to determine the source site name
174 String getOriginSite(SAMLResponse r)
176 Iterator ia=r.getAssertions();
179 Iterator is=((SAMLAssertion)ia.next()).getStatements();
182 SAMLStatement s=(SAMLStatement)is.next();
183 if (s instanceof SAMLAuthenticationStatement)
184 return ((SAMLAuthenticationStatement)s).getSubject().getName().getName();
191 * Parse a Base-64 encoded buffer back into a SAML response and test its
192 * validity against the POST profile, including use of the default replay
195 * Also does trust evaluation based on the information available from the
196 * origin site mapper, in accordance with general Shibboleth processing
197 * semantics. Club-specific processing must be performed in a subclass.<P>
199 * @param buf A Base-64 encoded buffer containing a SAML
202 * @return SAML response sent by origin site
203 * @exception SAMLException Thrown if the response cannot be understood or
206 public SAMLResponse accept(byte[] buf, StringBuffer originSite)
209 // The built-in SAML functionality will do most of the basic non-crypto checks.
210 // Note that if the response only contains a status error, it gets tossed out
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 origin site
218 // with its associated data. If we can't even find a SSO statement in the response
219 // we just return the response to the caller, who will presumably notice this.
220 SAMLAssertion assertion = null;
221 SAMLAuthenticationStatement sso = null;
225 assertion = getSSOAssertion(r);
226 sso = getSSOStatement(assertion);
228 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(SAMLException.RESPONDER, "ShibPOSTProfile.accept() requires subject name qualifier");
240 originSite.setLength(0);
241 originSite.append(subject.getName().getName());
242 String handleService = assertion.getIssuer();
244 // Is this a trusted HS?
245 OriginSiteMapper mapper=Init.getMapper();
246 Iterator hsNames = mapper.getHandleServiceNames(originSite.toString());
247 boolean bFound = false;
248 while (!bFound && hsNames.hasNext())
249 if (hsNames.next().equals(handleService))
252 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
254 Key hsKey = mapper.getHandleServiceKey(handleService);
255 KeyStore ks = mapper.getTrustedRoots();
257 // Signature verification now takes place. We check the assertion and the response.
258 // Assertion signing is optional, response signing is mandatory.
262 if (assertion.isSigned())
264 log.info("verifying assertion signature");
265 verifySignature(assertion, handleService, ks, hsKey);
267 log.info("verifying response signature");
268 verifySignature(r, handleService, ks, hsKey);
278 * Used by HS to generate a signed SAML response conforming to the POST
281 * @param recipient URL of intended consumer
282 * @param name Name of subject
283 * @param nameQualifier Federates or qualifies subject name (optional)
284 * @param nameFormat URI identifying format of name
285 * @param subjectIP Client address of subject (optional)
286 * @param authMethod URI of authentication method being asserted
287 * @param authInstant Date and time of authentication being asserted
288 * @param bindings Set of SAML authorities the relying party
289 * may contact (optional)
290 * @param responseKey A secret or private key to use in response
292 * @param responseCert One or more X.509 certificates to enclose with the
293 * response (optional)
294 * @param assertionKey A secret or private key to use in assertion
295 * signature or MAC (optional)
296 * @param assertionCert One or more X.509 certificates to enclose with the
297 * assertion (optional)
298 * @return SAML response to send to accepting site
299 * @exception SAMLException Base class of exceptions that may be thrown
302 public SAMLResponse prepare(String recipient,
304 String nameQualifier,
310 Key responseKey, Collection responseCerts,
311 Key assertionKey, Collection assertionCerts
315 if (responseKey == null || (!(responseKey instanceof PrivateKey) && !(responseKey instanceof SecretKey)))
316 throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() requires a response key (private or secret)");
317 if (assertionKey != null && !(assertionKey instanceof PrivateKey) && !(assertionKey instanceof SecretKey))
318 throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an invalid type of assertion key");
320 Document doc = org.opensaml.XML.parserPool.newDocument();
322 SAMLResponse r = SAMLPOSTProfile.prepare(
335 if (assertionKey != null)
336 ((SAMLAssertion)r.getAssertions().next()).sign(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,assertionKey,assertionCerts);
337 r.sign(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,responseKey,responseCerts);
343 * Searches the replay cache for the specified assertion and inserts a
344 * newly seen assertion into the cache<P>
346 * Also performs garbage collection of the cache by deleting expired
349 * @param a The assertion to check
350 * @return true iff the assertion has not been seen before
352 public synchronized boolean checkReplayCache(SAMLAssertion a)
354 // Default implementation uses the basic replay cache implementation.
355 return SAMLPOSTProfile.checkReplayCache(a);
359 * Default signature verification algorithm uses an embedded X509
360 * certificate(s) or an explicit key to verify the signature. The certificate
361 * is examined to insure the subject CN matches the signer, and that it is
362 * signed by a trusted CA
364 * @param obj The object containing the signature
365 * @param signerName The name of the signer
366 * @param ks A keystore containing trusted root certificates
367 * @param knownKey An explicit key to use if a certificate cannot be
369 * @param simple Verify according to simple SAML signature profile?
371 * @throws SAMLException Thrown if the signature cannot be verified
373 protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
378 NDC.push("verifySignature");
382 log.error("unable to find a signature");
383 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() given an unsigned object");
386 if (knownKey != null)
388 log.info("verifying signature with known key value, ignoring signature KeyInfo");
389 obj.verify(knownKey);
393 log.info("verifying signature with embedded KeyInfo");
396 // This is pretty painful, and this is leveraging the supposedly automatic support in JDK 1.4.
397 // First we have to extract the certificates from the object.
398 Iterator certs_from_obj = obj.getX509Certificates();
399 if (!certs_from_obj.hasNext())
401 log.error("need certificates inside object to establish trust");
402 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() can't find any certificates");
405 // We assume the first one in the set is the end entity cert.
406 X509Certificate entity_cert = (X509Certificate)certs_from_obj.next();
408 // Match the CN of the entity cert with the expected signer.
409 String dname = entity_cert.getSubjectDN().getName();
410 log.debug("found entity cert with DN: " + dname);
411 String cname = "CN=" + signerName;
412 if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1))
414 log.error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: " + signerName);
415 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
418 // Prep a chain between the entity cert and the trusted roots.
419 X509CertSelector targetConstraints = new X509CertSelector();
420 targetConstraints.setCertificate(entity_cert);
421 PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
422 params.setMaxPathLength(-1);
424 Vector certbag = new Vector();
425 certbag.add(entity_cert);
426 while (certs_from_obj.hasNext())
427 certbag.add(certs_from_obj.next());
428 CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
429 CertStore store = CertStore.getInstance("Collection", ccsp);
430 params.addCertStore(store);
432 // Attempt to build a path.
433 CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
434 PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult)cpb.build(params);
436 catch (CertPathBuilderException e)
438 log.error("caught a cert path builder exception: " + e.getMessage());
439 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
441 catch (GeneralSecurityException e)
443 log.error("caught a general security exception: " + e.getMessage());
444 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);