ef18828114ac864396f1d5e3e228fecb69be742d
[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 import java.security.GeneralSecurityException;
52 import java.security.Key;
53 import java.security.KeyStore;
54 import java.security.PrivateKey;
55 import java.security.cert.Certificate;
56 import java.security.cert.X509Certificate;
57
58 import java.util.Date;
59 import java.util.Enumeration;
60 import java.util.Iterator;
61 import javax.crypto.SecretKey;
62 import javax.xml.parsers.DocumentBuilder;
63 import javax.xml.parsers.ParserConfigurationException;
64 import org.apache.xml.security.exceptions.XMLSecurityException;
65 import org.apache.xml.security.keys.KeyInfo;
66 import org.apache.xml.security.signature.XMLSignature;
67 import org.opensaml.*;
68 import org.w3c.dom.*;
69
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 String[] policies = null;
85
86     /**  Official name of issuing site */
87     protected String issuer = null;
88
89     /**  Abstract interface into trust base */
90     protected OriginSiteMapper mapper = null;
91
92     /**  The URL of the receiving SHIRE */
93     protected String receiver = null;
94
95     /**  Seconds allowed to elapse from issuance of response */
96     protected int ttlSeconds = 0;
97
98     /**
99      *  SHIRE-side constructor for a ShibPOSTProfile object
100      *
101      * @param  policies           Array of policy URIs that the implementation
102      *      must support
103      * @param  mapper             Interface between profile and trust base
104      * @param  receiver           URL of SHIRE
105      * @param  ttlSeconds         Length of time in seconds allowed to elapse
106      *      from issuance of SAML response
107      * @exception  SAMLException  Raised if a profile implementation cannot be
108      *      constructed from the supplied information
109      */
110     public ShibPOSTProfile(String[] policies, OriginSiteMapper mapper, String receiver, int ttlSeconds)
111         throws SAMLException
112     {
113         if (policies == null || policies.length == 0 || mapper == null ||
114             receiver == null || receiver.length() == 0 || ttlSeconds <= 0)
115             throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
116
117         this.mapper = mapper;
118         this.receiver = receiver;
119         this.ttlSeconds = ttlSeconds;
120         this.policies = new String[policies.length];
121         System.arraycopy(policies, 0, this.policies, 0, policies.length);
122     }
123
124     /**
125      *  HS-side constructor for a ShibPOSTProfile object
126      *
127      * @param  policies           Array of policy URIs that the implementation
128      *      must support
129      * @param  issuer             "Official" name of issuing origin site
130      * @exception  SAMLException  Raised if a profile implementation cannot be
131      *      constructed from the supplied information
132      */
133     public ShibPOSTProfile(String[] policies, String issuer)
134         throws SAMLException
135     {
136         if (policies == null || policies.length == 0 || issuer == null || issuer.length() == 0)
137             throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
138         this.issuer = issuer;
139         this.policies = new String[policies.length];
140         System.arraycopy(policies, 0, this.policies, 0, policies.length);
141     }
142
143     /**
144      *  Locates an assertion containing a "bearer" AuthenticationStatement in
145      *  the response and validates the enclosing assertion with respect to the
146      *  POST profile
147      *
148      * @param  r          The response to the accepting site
149      * @return            An SSO assertion
150      */
151     public SAMLAssertion getSSOAssertion(SAMLResponse r)
152     {
153         return SAMLPOSTProfile.getSSOAssertion(r,policies);
154     }
155
156     /**
157      *  Locates a "bearer" AuthenticationStatement in the assertion and
158      *  validates the statement with respect to the POST profile
159      *
160      * @param  a  The SSO assertion sent to the accepting site
161      * @return    A "bearer" authentication statement
162      */
163     public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a)
164     {
165         return (a==null) ? null : 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      *
178      *
179      * @param  buf                A Base-64 encoded buffer containing a SAML
180      *      response
181      * @return                    SAML response sent by origin site
182      * @exception  SAMLException  Thrown if the response cannot be understood or
183      *      accepted
184      */
185     public SAMLResponse accept(byte[] buf)
186         throws SAMLException
187     {
188         // The built-in SAML functionality will do most of the basic non-crypto checks.
189         // Note that if the response only contains a status error, it gets tossed out
190         // as an exception.
191         SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds);
192
193         // Now we do some more non-crypto (ie. cheap) work to match up the origin site
194         // with its associated data. If we can't even find a SSO statement in the response
195         // we just return the response to the caller, who will presumably notice this.
196         SAMLAssertion assertion = getSSOAssertion(r);
197         if (assertion == null)
198             return r;
199
200         SAMLAuthenticationStatement sso = getSSOStatement(assertion);
201         if (sso == null)
202             return r;
203
204         // Check for replay.
205         if (!checkReplayCache(assertion))
206             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected a replayed SSO assertion");
207
208         // Examine the subject information.
209         SAMLSubject subject = sso.getSubject();
210         if (subject.getNameQualifier() == null)
211             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() requires subject name qualifier");
212
213         String originSite = subject.getNameQualifier();
214         String handleService = assertion.getIssuer();
215
216         // Is this a trusted HS?
217         Iterator hsNames = mapper.getHandleServiceNames(originSite);
218         boolean bFound = false;
219         while (!bFound && hsNames != null && hsNames.hasNext())
220             if (hsNames.next().equals(handleService))
221                 bFound = true;
222         if (!bFound)
223             throw new SAMLException(SAMLException.RESPONDER, "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 the response.
229         // Assertion signing is optional, response signing is mandatory.
230         if (assertion.getSignature() != null && !verifySignature(assertion, handleService, ks, hsKey))
231             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an invalid assertion signature");
232         if (!verifySignature(r, handleService, ks, hsKey))
233             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an invalid response signature");
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      *
243      *
244      * @param  recipient          URL of intended consumer
245      * @param  name               Name of subject
246      * @param  nameQualifier      Federates or qualifies subject name (optional)
247      * @param  subjectIP          Client address of subject (optional)
248      * @param  authMethod         URI of authentication method being asserted
249      * @param  authInstant        Date and time of authentication being asserted
250      * @param  bindings           Array of SAML authorities the relying party
251      *      may contact (optional)
252      * @param  responseKey        A secret or private key to use in response
253      *      signature or MAC
254      * @param  responseCert       A public key certificate to enclose with the
255      *      response (optional)
256      * @param  assertionKey       A secret or private key to use in assertion
257      *      signature or MAC (optional)
258      * @param  assertionCert      A public key certificate to enclose with the
259      *      assertion (optional)
260      * @return                    SAML response to send to accepting site
261      * @exception  SAMLException  Base class of exceptions that may be thrown
262      *      during processing
263      */
264     public SAMLResponse prepare(String recipient,
265                                 String name,
266                                 String nameQualifier,
267                                 String subjectIP,
268                                 String authMethod,
269                                 Date authInstant,
270                                 SAMLAuthorityBinding[] bindings,
271                                 Key responseKey, X509Certificate responseCert,
272                                 Key assertionKey, X509Certificate assertionCert
273                                 )
274         throws SAMLException
275     {
276         if (responseKey == null || (!(responseKey instanceof PrivateKey) && !(responseKey instanceof SecretKey)))
277             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() requires a response key (private or secret)");
278         if (assertionKey != null && !(assertionKey instanceof PrivateKey) && !(assertionKey instanceof SecretKey))
279             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an invalid type of assertion key");
280
281         DocumentBuilder builder = null;
282         try
283         {
284             builder = org.opensaml.XML.parserPool.get();
285             Document doc = builder.newDocument();
286
287             XMLSignature rsig = new XMLSignature(doc, null, algorithm);
288             XMLSignature asig = null;
289             if (assertionKey != null)
290                 asig = new XMLSignature(doc, null, algorithm);
291
292             SAMLResponse r = SAMLPOSTProfile.prepare(
293                 recipient,
294                 issuer,
295                 policies,
296                 name,
297                 nameQualifier,
298                 null,
299                 subjectIP,
300                 authMethod,
301                 authInstant,
302                 bindings,
303                 rsig,
304                 asig);
305             r.toDOM(doc);
306             if (asig != null)
307             {
308                 if (assertionCert != null)
309                     asig.addKeyInfo(assertionCert);
310                 if (assertionKey instanceof PrivateKey)
311                     asig.sign((PrivateKey)assertionKey);
312                 else
313                     asig.sign((SecretKey)assertionKey);
314             }
315             if (responseCert != null)
316                 rsig.addKeyInfo(responseCert);
317             if (responseKey instanceof PrivateKey)
318                 rsig.sign((PrivateKey)responseKey);
319             else
320                 rsig.sign((SecretKey)responseKey);
321             return r;
322         }
323         catch (ParserConfigurationException pce)
324         {
325             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() unable to obtain XML parser instance: " + pce.getMessage(), pce);
326         }
327         catch (XMLSecurityException e)
328         {
329             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an XML security problem during signature creation", e);
330         }
331         finally
332         {
333             if (builder != null)
334                 org.opensaml.XML.parserPool.put(builder);
335         }
336     }
337
338     /**
339      *  Searches the replay cache for the specified assertion and inserts a
340      *  newly seen assertion into the cache<P>
341      *
342      *  Also performs garbage collection of the cache by deleting expired
343      *  entries.
344      *
345      * @param  a            The assertion to check
346      * @return              true iff the assertion has not been seen before
347      */
348     protected synchronized boolean checkReplayCache(SAMLAssertion a)
349     {
350         // Default implementation uses the basic replay cache implementation.
351         return SAMLPOSTProfile.checkReplayCache(a);
352     }
353
354     /**
355      *  Default signature verification algorithm uses an embedded X509
356      *  certificate or an explicit key to verify the signature. The certificate
357      *  is examined to insure the subject CN matches the signer, and that it is
358      *  signed by a trusted CA
359      *
360      * @param  obj         The object containing the signature
361      * @param  signerName  The name of the signer
362      * @param  ks          A keystore containing trusted root certificates
363      * @param  knownKey    An explicit key to use if a certificate cannot be
364      *      found
365      * @return             The result of signature verification
366      */
367     protected boolean verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
368     {
369         try
370         {
371             XMLSignature sig = (obj != null) ? obj.getSignature() : null;
372             if (sig == null)
373                 return false;
374             KeyInfo ki = sig.getKeyInfo();
375             if (ks != null && ki != null)
376             {
377                 X509Certificate cert = ki.getX509Certificate();
378                 if (cert != null)
379                 {
380                     cert.checkValidity();
381                     if (!sig.checkSignatureValue(cert))
382                         return false;
383                     if (signerName != null)
384                     {
385                         String dname = cert.getSubjectDN().getName();
386                         String cname = "CN=" + signerName;
387                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1))
388                             return false;
389                     }
390
391                     String iname = cert.getIssuerDN().getName();
392                     for (Enumeration aliases = ks.aliases(); aliases.hasMoreElements(); )
393                     {
394                         String alias = (String)aliases.nextElement();
395                         if (!ks.isCertificateEntry(alias))
396                             continue;
397                         Certificate cacert = ks.getCertificate(alias);
398                         if (!(cacert instanceof X509Certificate))
399                             continue;
400                         ((X509Certificate)cacert).checkValidity();
401                         if (iname.equals(((X509Certificate)cacert).getSubjectDN().getName()))
402                         {
403                             cert.verify(cacert.getPublicKey());
404                             return true;
405                         }
406                     }
407
408                     return false;
409                 }
410             }
411             return (knownKey != null) ? sig.checkSignatureValue(knownKey) : false;
412         }
413         catch (XMLSecurityException e)
414         {
415             e.printStackTrace();
416             return false;
417         }
418         catch (GeneralSecurityException e)
419         {
420             e.printStackTrace();
421             return false;
422         }
423     }
424 }
425