af1dc3af072dbf984748fb4046582ba1bb84914a
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / ShibPOSTProfile.java
1 /* 
2  * The Shibboleth License, Version 1. 
3  * Copyright (c) 2002 
4  * University Corporation for Advanced Internet Development, Inc. 
5  * All rights reserved
6  * 
7  * 
8  * Redistribution and use in source and binary forms, with or without 
9  * modification, are permitted provided that the following conditions are met:
10  * 
11  * Redistributions of source code must retain the above copyright notice, this 
12  * list of conditions and the following disclaimer.
13  * 
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.
22  * 
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
28  * 
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.
33  * 
34  * 
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.
48  */
49
50 package edu.internet2.middleware.shibboleth.common;
51
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;
62
63 import javax.crypto.SecretKey;
64
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;
70
71 /**
72  *  Basic Shibboleth POST browser profile implementation with basic support for
73  *  signing
74  *
75  * @author     Scott Cantor
76  * @created    April 11, 2002
77  */
78 public class ShibPOSTProfile
79 {
80     /**  XML Signature algorithm to apply */
81     protected String algorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
82
83     /**  Policy URIs to attach or check against */
84     protected ArrayList policies = new ArrayList();
85
86     /**  Official name of issuing site */
87     protected String issuer = null;
88
89     /**  The URL of the receiving SHIRE */
90     protected String receiver = null;
91
92     /**  Seconds allowed to elapse from issuance of response */
93     protected int ttlSeconds = 0;
94
95     private static Logger log = Logger.getLogger(ShibPOSTProfile.class.getName());
96
97     /**
98      *  SHIRE-side constructor for a ShibPOSTProfile object
99      *
100      * @param  policies           Set of policy URIs that the implementation
101      *      must support
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
107      */
108     public ShibPOSTProfile(Collection policies, String receiver, int ttlSeconds)
109         throws SAMLException
110     {
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");
113
114         this.receiver = receiver;
115         this.ttlSeconds = ttlSeconds;
116         this.policies.addAll(policies);
117     }
118
119     /**
120      *  HS-side constructor for a ShibPOSTProfile object
121      *
122      * @param  policies           Set of policy URIs that the implementation
123      *      must support
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
127      */
128     public ShibPOSTProfile(Collection policies, String issuer)
129         throws SAMLException
130     {
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);
135     }
136
137     /**
138      *  Locates an assertion containing a "bearer" AuthenticationStatement in
139      *  the response and validates the enclosing assertion with respect to the
140      *  POST profile
141      *
142      * @param  r          The response to the accepting site
143      * @return            An SSO assertion
144      * 
145      * @throws SAMLException    Thrown if an SSO assertion can't be found
146      */
147     public SAMLAssertion getSSOAssertion(SAMLResponse r)
148         throws SAMLException
149     {
150         return SAMLPOSTProfile.getSSOAssertion(r,policies);
151     }
152
153     /**
154      *  Locates a "bearer" AuthenticationStatement in the assertion and
155      *  validates the statement with respect to the POST profile
156      *
157      * @param  a  The SSO assertion sent to the accepting site
158      * @return    A "bearer" authentication statement
159      *
160      * @throws SAMLException    Thrown if an SSO statement can't be found
161      */
162     public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a)
163         throws SAMLException
164     {
165         return SAMLPOSTProfile.getSSOStatement(a);
166     }
167
168     /**
169      *  Examines a response to determine the source site name
170      *  
171      * @param r     
172      * @return
173      */
174     String getOriginSite(SAMLResponse r)
175     {
176         Iterator ia=r.getAssertions();
177         while (ia.hasNext())
178         {
179             Iterator is=((SAMLAssertion)ia.next()).getStatements();
180             while (is.hasNext())
181             {
182                 SAMLStatement s=(SAMLStatement)is.next();
183                 if (s instanceof SAMLAuthenticationStatement)
184                     return ((SAMLAuthenticationStatement)s).getSubject().getName().getName();
185             }
186         }
187         return null;
188     }
189
190     /**
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
193      *  cache<P>
194      *
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>
198      *
199      * @param  buf                A Base-64 encoded buffer containing a SAML
200      *      response
201      * @param  originSite         
202      * @return                    SAML response sent by origin site
203      * @exception  SAMLException  Thrown if the response cannot be understood or
204      *      accepted
205      */
206     public SAMLResponse accept(byte[] buf, StringBuffer originSite)
207         throws SAMLException
208     {
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
211         // as an exception.
212         SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds, false);
213
214         if (originSite == null)
215             originSite = new StringBuffer();
216
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;
222
223         try
224         {
225             assertion = getSSOAssertion(r);
226             sso = getSSOStatement(assertion);
227         }
228         catch (SAMLException e)
229         {
230             originSite.setLength(0);
231             originSite.append(getOriginSite(r));
232             throw e;
233         }
234
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");
239
240         originSite.setLength(0);
241         originSite.append(subject.getName().getName());
242         String handleService = assertion.getIssuer();
243
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))
250                 bFound = true;
251         if (!bFound)
252             throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
253
254         Key hsKey = mapper.getHandleServiceKey(handleService);
255         KeyStore ks = mapper.getTrustedRoots();
256
257         // Signature verification now takes place. We check the assertion and the response.
258         // Assertion signing is optional, response signing is mandatory.
259         try
260         {
261             NDC.push("accept");
262             if (assertion.isSigned())
263             {
264                 log.info("verifying assertion signature");
265                 verifySignature(assertion, handleService, ks, hsKey);
266             }
267             log.info("verifying response signature");
268             verifySignature(r, handleService, ks, hsKey);
269         }
270         finally
271         {
272             NDC.pop();
273         }
274         return r;
275     }
276
277     /**
278      *  Used by HS to generate a signed SAML response conforming to the POST
279      *  profile<P>
280      *
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
291      *      signature or MAC
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
300      *      during processing
301      */
302     public SAMLResponse prepare(String recipient,
303                                 String name,
304                                 String nameQualifier,
305                                 String nameFormat,
306                                 String subjectIP,
307                                 String authMethod,
308                                 Date authInstant,
309                                 Collection bindings,
310                                 Key responseKey, Collection responseCerts,
311                                 Key assertionKey, Collection assertionCerts
312                                 )
313         throws SAMLException
314     {
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");
319
320         Document doc = org.opensaml.XML.parserPool.newDocument();
321
322         SAMLResponse r = SAMLPOSTProfile.prepare(
323             recipient,
324             issuer,
325             policies,
326             name,
327             nameQualifier,
328             nameFormat,
329             subjectIP,
330             authMethod,
331             authInstant,
332             bindings);
333         r.toDOM(doc);
334
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);
338         
339         return r;
340     }
341
342     /**
343      *  Searches the replay cache for the specified assertion and inserts a
344      *  newly seen assertion into the cache<P>
345      *
346      *  Also performs garbage collection of the cache by deleting expired
347      *  entries.
348      *
349      * @param  a            The assertion to check
350      * @return              true iff the assertion has not been seen before
351      */
352     public synchronized boolean checkReplayCache(SAMLAssertion a)
353     {
354         // Default implementation uses the basic replay cache implementation.
355         return SAMLPOSTProfile.checkReplayCache(a);
356     }
357
358     /**
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
363      *
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
368      *      found
369      * @param  simple      Verify according to simple SAML signature profile?
370      * 
371      * @throws SAMLException    Thrown if the signature cannot be verified
372      */
373     protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
374         throws SAMLException
375     {
376         try
377         {
378             NDC.push("verifySignature");
379             
380             if (!obj.isSigned())
381             {
382                 log.error("unable to find a signature");
383                 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() given an unsigned object");
384             }
385             
386             if (knownKey != null)
387             {
388                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
389                 obj.verify(knownKey);
390                 return;
391             }
392             
393             log.info("verifying signature with embedded KeyInfo");
394             obj.verify();
395             
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())
400             {
401                 log.error("need certificates inside object to establish trust");
402                 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() can't find any certificates");
403             }
404             
405             // We assume the first one in the set is the end entity cert.
406             X509Certificate entity_cert = (X509Certificate)certs_from_obj.next();
407
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))
413             {
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");
416             }
417             
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);
423             
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);
431             
432             // Attempt to build a path.
433             CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
434             PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult)cpb.build(params);
435         }
436         catch (CertPathBuilderException e)
437         {
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);
440         }
441         catch (GeneralSecurityException e)
442         {
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);
445         }
446         finally
447         {
448             NDC.pop();
449         }
450     }
451 }
452