Added format URI.
[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      *  Parse a Base-64 encoded buffer back into a SAML response and test its
170      *  validity against the POST profile, including use of the default replay
171      *  cache<P>
172      *
173      *  Also does trust evaluation based on the information available from the
174      *  origin site mapper, in accordance with general Shibboleth processing
175      *  semantics. Club-specific processing must be performed in a subclass.<P>
176      *
177      * @param  buf                A Base-64 encoded buffer containing a SAML
178      *      response
179      * @return                    SAML response sent by origin site
180      * @exception  SAMLException  Thrown if the response cannot be understood or
181      *      accepted
182      */
183     public SAMLResponse accept(byte[] buf)
184         throws SAMLException
185     {
186         // The built-in SAML functionality will do most of the basic non-crypto checks.
187         // Note that if the response only contains a status error, it gets tossed out
188         // as an exception.
189         SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds);
190
191         // Now we do some more non-crypto (ie. cheap) work to match up the origin site
192         // with its associated data. If we can't even find a SSO statement in the response
193         // we just return the response to the caller, who will presumably notice this.
194         SAMLAssertion assertion = getSSOAssertion(r);
195         SAMLAuthenticationStatement sso = getSSOStatement(assertion);
196
197         // Examine the subject information.
198         SAMLSubject subject = sso.getSubject();
199         if (subject.getNameQualifier() == null)
200             throw new InvalidAssertionException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() requires subject name qualifier");
201
202         String originSite = subject.getNameQualifier();
203         String handleService = assertion.getIssuer();
204
205         // Is this a trusted HS?
206         OriginSiteMapper mapper=Init.getMapper();
207         Iterator hsNames = mapper.getHandleServiceNames(originSite);
208         boolean bFound = false;
209         while (!bFound && hsNames.hasNext())
210             if (hsNames.next().equals(handleService))
211                 bFound = true;
212         if (!bFound)
213             throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
214
215         Key hsKey = mapper.getHandleServiceKey(handleService);
216         KeyStore ks = mapper.getTrustedRoots();
217
218         // Signature verification now takes place. We check the assertion and the response.
219         // Assertion signing is optional, response signing is mandatory.
220         try
221         {
222             NDC.push("accept");
223             if (assertion.isSigned())
224             {
225                 log.info("verifying assertion signature");
226                 verifySignature(assertion, handleService, ks, hsKey, false);
227             }
228             log.info("verifying response signature");
229             verifySignature(r, handleService, ks, hsKey, true);
230         }
231         finally
232         {
233             NDC.pop();
234         }
235         return r;
236     }
237
238     /**
239      *  Used by HS to generate a signed SAML response conforming to the POST
240      *  profile<P>
241      *
242      * @param  recipient          URL of intended consumer
243      * @param  name               Name of subject
244      * @param  nameQualifier      Federates or qualifies subject name (optional)
245      * @param  subjectIP          Client address of subject (optional)
246      * @param  authMethod         URI of authentication method being asserted
247      * @param  authInstant        Date and time of authentication being asserted
248      * @param  bindings           Set of SAML authorities the relying party
249      *      may contact (optional)
250      * @param  responseKey        A secret or private key to use in response
251      *      signature or MAC
252      * @param  responseCert       One or more X.509 certificates to enclose with the
253      *      response (optional)
254      * @param  assertionKey       A secret or private key to use in assertion
255      *      signature or MAC (optional)
256      * @param  assertionCert      One or more X.509 certificates to enclose with the
257      *      assertion (optional)
258      * @return                    SAML response to send to accepting site
259      * @exception  SAMLException  Base class of exceptions that may be thrown
260      *      during processing
261      */
262     public SAMLResponse prepare(String recipient,
263                                 String name,
264                                 String nameQualifier,
265                                 String subjectIP,
266                                 String authMethod,
267                                 Date authInstant,
268                                 Collection bindings,
269                                 Key responseKey, Collection responseCerts,
270                                 Key assertionKey, Collection assertionCerts
271                                 )
272         throws SAMLException
273     {
274         if (responseKey == null || (!(responseKey instanceof PrivateKey) && !(responseKey instanceof SecretKey)))
275             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() requires a response key (private or secret)");
276         if (assertionKey != null && !(assertionKey instanceof PrivateKey) && !(assertionKey instanceof SecretKey))
277             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an invalid type of assertion key");
278
279         Document doc = org.opensaml.XML.parserPool.newDocument();
280
281         SAMLResponse r = SAMLPOSTProfile.prepare(
282             recipient,
283             issuer,
284             policies,
285             name,
286             nameQualifier,
287             Constants.SHIB_NAMEID_FORMAT_URI,
288             subjectIP,
289             authMethod,
290             authInstant,
291             bindings);
292         r.toDOM(doc);
293
294         if (assertionKey != null)
295             ((SAMLAssertion)r.getAssertions().next()).sign(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,assertionKey,assertionCerts,false);
296         r.sign(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,responseKey,responseCerts,true);
297         
298         return r;
299     }
300
301     /**
302      *  Searches the replay cache for the specified assertion and inserts a
303      *  newly seen assertion into the cache<P>
304      *
305      *  Also performs garbage collection of the cache by deleting expired
306      *  entries.
307      *
308      * @param  a            The assertion to check
309      * @return              true iff the assertion has not been seen before
310      */
311     public synchronized boolean checkReplayCache(SAMLAssertion a)
312     {
313         // Default implementation uses the basic replay cache implementation.
314         return SAMLPOSTProfile.checkReplayCache(a);
315     }
316
317     /**
318      *  Default signature verification algorithm uses an embedded X509
319      *  certificate(s) or an explicit key to verify the signature. The certificate
320      *  is examined to insure the subject CN matches the signer, and that it is
321      *  signed by a trusted CA
322      *
323      * @param  obj         The object containing the signature
324      * @param  signerName  The name of the signer
325      * @param  ks          A keystore containing trusted root certificates
326      * @param  knownKey    An explicit key to use if a certificate cannot be
327      *      found
328      * @param  simple      Verify according to simple SAML signature profile?
329      * 
330      * @throws SAMLException    Thrown if the signature cannot be verified
331      */
332     protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey, boolean simple)
333         throws SAMLException
334     {
335         try
336         {
337             NDC.push("verifySignature");
338             
339             if (!obj.isSigned())
340             {
341                 log.error("unable to find a signature");
342                 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() given an unsigned object");
343             }
344             
345             if (knownKey != null)
346             {
347                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
348                 obj.verify(knownKey,simple);
349                 return;
350             }
351             
352             log.info("verifying signature with embedded KeyInfo");
353             obj.verify(simple);
354             
355             // This is pretty painful, and this is leveraging the supposedly automatic support in JDK 1.4.
356             // First we have to extract the certificates from the object.
357             Iterator certs_from_obj = obj.getX509Certificates();
358             if (!certs_from_obj.hasNext())
359             {
360                 log.error("need certificates inside object to establish trust");
361                 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() can't find any certificates");
362             }
363             
364             // We assume the first one in the set is the end entity cert.
365             X509Certificate entity_cert = (X509Certificate)certs_from_obj.next();
366
367             // Match the CN of the entity cert with the expected signer.
368             String dname = entity_cert.getSubjectDN().getName();
369             log.debug("found entity cert with DN: " + dname);
370             String cname = "CN=" + signerName;
371             if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1))
372             {
373                 log.error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: " + signerName);
374                 throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
375             }
376             
377             // Prep a chain between the entity cert and the trusted roots.
378             X509CertSelector targetConstraints = new X509CertSelector();
379             targetConstraints.setCertificate(entity_cert);
380             PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
381             params.setMaxPathLength(-1);
382             
383             Vector certbag = new Vector();
384             certbag.add(entity_cert);
385             while (certs_from_obj.hasNext())
386                 certbag.add(certs_from_obj.next());
387             CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
388             CertStore store = CertStore.getInstance("Collection", ccsp);
389             params.addCertStore(store);
390             
391             // Attempt to build a path.
392             CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
393             PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult)cpb.build(params);
394         }
395         catch (CertPathBuilderException e)
396         {
397             log.error("caught a cert path builder exception: " + e.getMessage());
398             throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
399         }
400         catch (GeneralSecurityException e)
401         {
402             log.error("caught a general security exception: " + e.getMessage());
403             throw new TrustException(SAMLException.RESPONDER, "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
404         }
405         finally
406         {
407             NDC.pop();
408         }
409     }
410 }
411