0b66b1e5fd3e6560b389d9421cf2e212f01abfe8
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / ShibPOSTProfile.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the
6  * above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other
7  * materials provided with the distribution, if any, must include the following acknowledgment: "This product includes
8  * software developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2
9  * Project. Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2,
11  * nor the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please
13  * contact shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2,
14  * UCAID, or the University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name,
15  * without prior written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS
16  * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES,
17  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
18  * NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS
19  * WITH LICENSEE. IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED
20  * INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
23  * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24  * POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 package edu.internet2.middleware.shibboleth.common;
28
29 import java.security.GeneralSecurityException;
30 import java.security.Key;
31 import java.security.KeyStore;
32 import java.security.cert.CertPathBuilder;
33 import java.security.cert.CertPathBuilderException;
34 import java.security.cert.CertStore;
35 import java.security.cert.CollectionCertStoreParameters;
36 import java.security.cert.PKIXBuilderParameters;
37 import java.security.cert.PKIXCertPathBuilderResult;
38 import java.security.cert.X509CertSelector;
39 import java.security.cert.X509Certificate;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collection;
43 import java.util.Date;
44 import java.util.Iterator;
45 import java.util.Vector;
46 import java.util.regex.Matcher;
47 import java.util.regex.Pattern;
48
49 import org.apache.log4j.Logger;
50 import org.apache.log4j.NDC;
51 import org.apache.xml.security.signature.XMLSignature;
52 import org.opensaml.InvalidAssertionException;
53 import org.opensaml.InvalidCryptoException;
54 import org.opensaml.SAMLAssertion;
55 import org.opensaml.SAMLAuthenticationStatement;
56 import org.opensaml.SAMLException;
57 import org.opensaml.SAMLNameIdentifier;
58 import org.opensaml.SAMLPOSTProfile;
59 import org.opensaml.SAMLResponse;
60 import org.opensaml.SAMLSignedObject;
61 import org.opensaml.SAMLStatement;
62 import org.opensaml.SAMLSubject;
63 import org.opensaml.TrustException;
64 import org.w3c.dom.Document;
65
66 import edu.internet2.middleware.shibboleth.hs.HSRelyingParty;
67
68 /**
69  * Basic Shibboleth POST browser profile implementation with basic support for signing
70  * 
71  * @author Scott Cantor @created April 11, 2002
72  */
73 public class ShibPOSTProfile {
74         
75         private Pattern regex = Pattern.compile(".*CN=([^,/]+).*");
76
77         /** XML Signature algorithm to apply */
78         protected String                algorithm       = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
79
80         /** Policy URIs to attach or check against */
81         protected ArrayList             policies        = new ArrayList();
82
83         /** The URL of the receiving SHIRE */
84         protected String                receiver        = null;
85
86         /** Seconds allowed to elapse from issuance of response */
87         protected int                   ttlSeconds      = 0;
88
89         private static Logger   log                     = Logger.getLogger(ShibPOSTProfile.class.getName());
90
91         /**
92          * SHIRE-side constructor for a ShibPOSTProfile object
93          * 
94          * @param policies
95          *            Set of policy URIs that the implementation must support
96          * @param receiver
97          *            URL of SHIRE
98          * @param ttlSeconds
99          *            Length of time in seconds allowed to elapse from issuance of SAML response
100          * @exception SAMLException
101          *                Raised if a profile implementation cannot be constructed from the supplied information
102          */
103         public ShibPOSTProfile(Collection policies, String receiver, int ttlSeconds) throws SAMLException {
104                 if (policies == null || policies.size() == 0 || receiver == null || receiver.length() == 0 || ttlSeconds <= 0)
105                         throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
106
107                 this.receiver = receiver;
108                 this.ttlSeconds = ttlSeconds;
109                 this.policies.addAll(policies);
110         }
111
112         /**
113          * HS-side constructor for a ShibPOSTProfile object.
114          */
115         public ShibPOSTProfile() {}
116
117         /**
118          * Locates an assertion containing a "bearer" AuthenticationStatement in the response and validates the enclosing
119          * assertion with respect to the POST profile
120          * 
121          * @param r
122          *            The response to the accepting site
123          * @return An SSO assertion
124          * @throws SAMLException
125          *             Thrown if an SSO assertion can't be found
126          */
127         public SAMLAssertion getSSOAssertion(SAMLResponse r) throws SAMLException {
128                 return SAMLPOSTProfile.getSSOAssertion(r, policies);
129         }
130
131         /**
132          * Locates a "bearer" AuthenticationStatement in the assertion and validates the statement with respect to the POST
133          * profile
134          * 
135          * @param a
136          *            The SSO assertion sent to the accepting site
137          * @return A "bearer" authentication statement
138          * @throws SAMLException
139          *             Thrown if an SSO statement can't be found
140          */
141         public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a) throws SAMLException {
142                 return SAMLPOSTProfile.getSSOStatement(a);
143         }
144
145         /**
146          * Examines a response to determine the source site name
147          * 
148          * @param r
149          * @return
150          */
151         String getOriginSite(SAMLResponse r) {
152                 Iterator ia = r.getAssertions();
153                 while (ia.hasNext()) {
154                         Iterator is = ((SAMLAssertion) ia.next()).getStatements();
155                         while (is.hasNext()) {
156                                 SAMLStatement s = (SAMLStatement) is.next();
157                                 if (s instanceof SAMLAuthenticationStatement)
158                                         return ((SAMLAuthenticationStatement) s).getSubject().getName().getName();
159                         }
160                 }
161                 return null;
162         }
163
164         /**
165          * Parse a Base-64 encoded buffer back into a SAML response and test its validity against the POST profile,
166          * including use of the default replay cache
167          * <P>
168          * Also does trust evaluation based on the information available from the origin site mapper, in accordance with
169          * general Shibboleth processing semantics. Club-specific processing must be performed in a subclass.
170          * <P>
171          * 
172          * @param buf
173          *            A Base-64 encoded buffer containing a SAML response
174          * @param originSite
175          * @return SAML response sent by origin site
176          * @exception SAMLException
177          *                Thrown if the response cannot be understood or accepted
178          */
179         public SAMLResponse accept(byte[] buf, StringBuffer originSite) throws SAMLException {
180                 // The built-in SAML functionality will do most of the basic non-crypto checks.
181                 // Note that if the response only contains a status error, it gets
182                 // tossed out as an exception.
183                 SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds, false);
184
185                 if (originSite == null)
186                         originSite = new StringBuffer();
187
188                 // Now we do some more non-crypto (ie. cheap) work to match up the origin site
189                 // with its associated data. If we can't even find a SSO statement in
190                 // the response we just return the response to the caller, who will presumably
191                 // notice this.
192                 SAMLAssertion assertion = null;
193                 SAMLAuthenticationStatement sso = null;
194
195                 try {
196                         assertion = getSSOAssertion(r);
197                         sso = getSSOStatement(assertion);
198                 } catch (SAMLException e) {
199                         originSite.setLength(0);
200                         originSite.append(getOriginSite(r));
201                         throw e;
202                 }
203
204                 // Examine the subject information.
205                 SAMLSubject subject = sso.getSubject();
206                 if (subject.getName().getName() == null)
207                         throw new InvalidAssertionException(SAMLException.RESPONDER,
208                                         "ShibPOSTProfile.accept() requires subject name qualifier");
209
210                 originSite.setLength(0);
211                 originSite.append(subject.getName().getName());
212                 String handleService = assertion.getIssuer();
213
214                 // Is this a trusted HS?
215                 OriginSiteMapper mapper = Init.getMapper();
216                 Iterator hsNames = mapper.getHandleServiceNames(originSite.toString());
217                 boolean bFound = false;
218                 while (!bFound && hsNames.hasNext())
219                         if (hsNames.next().equals(handleService))
220                                 bFound = true;
221                 if (!bFound)
222                         throw new TrustException(SAMLException.RESPONDER,
223                                         "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
224
225                 Key hsKey = mapper.getHandleServiceKey(handleService);
226                 KeyStore ks = mapper.getTrustedRoots();
227
228                 // Signature verification now takes place. We check the assertion and
229                 // the response. Assertion signing is optional, response signing is mandatory.
230                 try {
231                         NDC.push("accept");
232                         if (assertion.isSigned()) {
233                                 log.info("verifying assertion signature");
234                                 verifySignature(assertion, handleService, ks, hsKey);
235                         }
236                         log.info("verifying response signature");
237                         verifySignature(r, handleService, ks, hsKey);
238                 } finally {
239                         NDC.pop();
240                 }
241                 return r;
242         }
243
244         /**
245          * Used by HS to generate a signed SAML response conforming to the POST profile
246          * <P>
247          * 
248          * @param recipient
249          *            URL of the assertion consumer
250          * @param relyingParty
251          *            the intended recipient of the response
252          * @param nameId
253          *            Name Identifier for the response
254          * @param subjectIP
255          *            Client address of subject (optional)
256          * @param authMethod
257          *            URI of authentication method being asserted
258          * @param authInstant
259          *            Date and time of authentication being asserted
260          * @param bindings
261          *            Set of SAML authorities the relying party may contact (optional)
262          * @return SAML response to send to accepting site
263          * @exception SAMLException
264          *                Base class of exceptions that may be thrown during processing
265          */
266         public SAMLResponse prepare(String recipient, HSRelyingParty relyingParty, SAMLNameIdentifier nameId,
267                         String subjectIP, String authMethod, Date authInstant, Collection bindings) throws SAMLException {
268
269                 Document doc = org.opensaml.XML.parserPool.newDocument();
270
271                 ArrayList audiences = new ArrayList();
272                 if (relyingParty.getProviderId() != null) {
273                         audiences.add(relyingParty.getProviderId());
274                 }
275                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
276                         audiences.add(relyingParty.getName());
277                 }
278
279                 String issuer = null;
280                 if (relyingParty.isLegacyProvider()) {
281                         log.debug("Service Provider is running Shibboleth <= 1.1.  Using old style issuer.");
282
283                         if (relyingParty.getIdentityProvider().getResponseSigningCredential() == null
284                                         || relyingParty.getIdentityProvider().getResponseSigningCredential().getX509Certificate() == null) {
285                                 throw new SAMLException("Cannot serve legacy style assertions without an X509 certificate");
286                         }
287
288                         Matcher matches = regex.matcher(relyingParty.getIdentityProvider().getResponseSigningCredential()
289                                         .getX509Certificate().getSubjectDN().getName());
290                         if (!matches.find() || matches.group(1).equals("")) {
291                                 throw new SAMLException("Error parsing certificate DN while determining legacy issuer name.");
292
293                         }
294                         issuer = matches.group(1);
295
296                 } else {
297                         issuer = relyingParty.getIdentityProvider().getProviderId();
298                 }
299
300                 SAMLResponse r = SAMLPOSTProfile.prepare(recipient, issuer, audiences, nameId, subjectIP, authMethod,
301                                 authInstant, bindings);
302                 r.toDOM(doc);
303
304                 //Sign the assertions, if appropriate
305                 if (relyingParty.getIdentityProvider().getAssertionSigningCredential() != null
306                                 && relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey() != null) {
307
308                         String assertionAlgorithm;
309                         if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.RSA) {
310                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
311                         } else if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.DSA) {
312                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
313                         } else {
314                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
315                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
316                         }
317
318                         ((SAMLAssertion) r.getAssertions().next()).sign(assertionAlgorithm, relyingParty.getIdentityProvider()
319                                         .getAssertionSigningCredential().getPrivateKey(), Arrays.asList(relyingParty.getIdentityProvider()
320                                         .getAssertionSigningCredential().getX509CertificateChain()));
321                 }
322
323                 //Sign the response, if appropriate
324                 if (relyingParty.getIdentityProvider().getResponseSigningCredential() != null
325                                 && relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey() != null) {
326
327                         String responseAlgorithm;
328                         if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.RSA) {
329                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
330                         } else if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.DSA) {
331                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
332                         } else {
333                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
334                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
335                         }
336
337                         r.sign(responseAlgorithm,
338                                         relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey(), Arrays
339                                                         .asList(relyingParty.getIdentityProvider().getResponseSigningCredential()
340                                                                         .getX509CertificateChain()));
341                 }
342
343                 return r;
344         }
345
346         /**
347          * Searches the replay cache for the specified assertion and inserts a newly seen assertion into the cache
348          * <P>
349          * Also performs garbage collection of the cache by deleting expired entries.
350          * 
351          * @param a
352          *            The assertion to check
353          * @return true iff the assertion has not been seen before
354          */
355         public synchronized boolean checkReplayCache(SAMLAssertion a) {
356                 // Default implementation uses the basic replay cache implementation.
357                 return SAMLPOSTProfile.checkReplayCache(a);
358         }
359
360         /**
361          * Default signature verification algorithm uses an embedded X509 certificate(s) or an explicit key to verify the
362          * signature. The certificate is examined to insure the subject CN matches the signer, and that it is signed by a
363          * trusted CA
364          * 
365          * @param obj
366          *            The object containing the signature
367          * @param signerName
368          *            The name of the signer
369          * @param ks
370          *            A keystore containing trusted root certificates
371          * @param knownKey
372          *            An explicit key to use if a certificate cannot be found
373          * @param simple
374          *            Verify according to simple SAML signature profile?
375          * @throws SAMLException
376          *             Thrown if the signature cannot be verified
377          */
378         protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
379                         throws SAMLException {
380                 try {
381                         NDC.push("verifySignature");
382
383                         if (!obj.isSigned()) {
384                                 log.error("unable to find a signature");
385                                 throw new TrustException(SAMLException.RESPONDER,
386                                                 "ShibPOSTProfile.verifySignature() given an unsigned object");
387                         }
388
389                         if (knownKey != null) {
390                                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
391                                 obj.verify(knownKey);
392                                 return;
393                         }
394
395                         log.info("verifying signature with embedded KeyInfo");
396                         obj.verify();
397
398                         // This is pretty painful, and this is leveraging the supposedly
399                         // automatic support in JDK 1.4.
400                         // First we have to extract the certificates from the object.
401                         Iterator certs_from_obj = obj.getX509Certificates();
402                         if (!certs_from_obj.hasNext()) {
403                                 log.error("need certificates inside object to establish trust");
404                                 throw new TrustException(SAMLException.RESPONDER,
405                                                 "ShibPOSTProfile.verifySignature() can't find any certificates");
406                         }
407
408                         // We assume the first one in the set is the end entity cert.
409                         X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
410
411                         // Match the CN of the entity cert with the expected signer.
412                         String dname = entity_cert.getSubjectDN().getName();
413                         log.debug("found entity cert with DN: " + dname);
414                         String cname = "CN=" + signerName;
415                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
416                                 log
417                                                 .error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
418                                                                 + signerName);
419                                 throw new TrustException(SAMLException.RESPONDER,
420                                                 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
421                         }
422
423                         // Prep a chain between the entity cert and the trusted roots.
424                         X509CertSelector targetConstraints = new X509CertSelector();
425                         targetConstraints.setCertificate(entity_cert);
426                         PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
427                         params.setMaxPathLength(-1);
428
429                         Vector certbag = new Vector();
430                         certbag.add(entity_cert);
431                         while (certs_from_obj.hasNext())
432                                 certbag.add(certs_from_obj.next());
433                         CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
434                         CertStore store = CertStore.getInstance("Collection", ccsp);
435                         params.addCertStore(store);
436
437                         // Attempt to build a path.
438                         CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
439                         PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
440                 } catch (CertPathBuilderException e) {
441                         log.error("caught a cert path builder exception: " + e.getMessage());
442                         throw new TrustException(SAMLException.RESPONDER,
443                                         "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
444                 } catch (GeneralSecurityException e) {
445                         log.error("caught a general security exception: " + e.getMessage());
446                         throw new TrustException(SAMLException.RESPONDER,
447                                         "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
448                 } finally {
449                         NDC.pop();
450                 }
451         }
452 }