Exposed replay cache to SHIRE
[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         // Examine the subject information.
205         SAMLSubject subject = sso.getSubject();
206         if (subject.getNameQualifier() == null)
207             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() requires subject name qualifier");
208
209         String originSite = subject.getNameQualifier();
210         String handleService = assertion.getIssuer();
211
212         // Is this a trusted HS?
213         Iterator hsNames = mapper.getHandleServiceNames(originSite);
214         boolean bFound = false;
215         while (!bFound && hsNames != null && hsNames.hasNext())
216             if (hsNames.next().equals(handleService))
217                 bFound = true;
218         if (!bFound)
219             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
220
221         Key hsKey = mapper.getHandleServiceKey(handleService);
222         KeyStore ks = mapper.getTrustedRoots();
223
224         // Signature verification now takes place. We check the assertion and the response.
225         // Assertion signing is optional, response signing is mandatory.
226         if (assertion.getSignature() != null && !verifySignature(assertion, handleService, ks, hsKey))
227             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an invalid assertion signature");
228         if (!verifySignature(r, handleService, ks, hsKey))
229             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an invalid response signature");
230
231         return r;
232     }
233
234     /**
235      *  Used by HS to generate a signed SAML response conforming to the POST
236      *  profile<P>
237      *
238      *
239      *
240      * @param  recipient          URL of intended consumer
241      * @param  name               Name of subject
242      * @param  nameQualifier      Federates or qualifies subject name (optional)
243      * @param  subjectIP          Client address of subject (optional)
244      * @param  authMethod         URI of authentication method being asserted
245      * @param  authInstant        Date and time of authentication being asserted
246      * @param  bindings           Array of SAML authorities the relying party
247      *      may contact (optional)
248      * @param  responseKey        A secret or private key to use in response
249      *      signature or MAC
250      * @param  responseCert       A public key certificate to enclose with the
251      *      response (optional)
252      * @param  assertionKey       A secret or private key to use in assertion
253      *      signature or MAC (optional)
254      * @param  assertionCert      A public key certificate to enclose with the
255      *      assertion (optional)
256      * @return                    SAML response to send to accepting site
257      * @exception  SAMLException  Base class of exceptions that may be thrown
258      *      during processing
259      */
260     public SAMLResponse prepare(String recipient,
261                                 String name,
262                                 String nameQualifier,
263                                 String subjectIP,
264                                 String authMethod,
265                                 Date authInstant,
266                                 SAMLAuthorityBinding[] bindings,
267                                 Key responseKey, X509Certificate responseCert,
268                                 Key assertionKey, X509Certificate assertionCert
269                                 )
270         throws SAMLException
271     {
272         if (responseKey == null || (!(responseKey instanceof PrivateKey) && !(responseKey instanceof SecretKey)))
273             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() requires a response key (private or secret)");
274         if (assertionKey != null && !(assertionKey instanceof PrivateKey) && !(assertionKey instanceof SecretKey))
275             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an invalid type of assertion key");
276
277         DocumentBuilder builder = null;
278         try
279         {
280             builder = org.opensaml.XML.parserPool.get();
281             Document doc = builder.newDocument();
282
283             XMLSignature rsig = new XMLSignature(doc, null, algorithm);
284             XMLSignature asig = null;
285             if (assertionKey != null)
286                 asig = new XMLSignature(doc, null, algorithm);
287
288             SAMLResponse r = SAMLPOSTProfile.prepare(
289                 recipient,
290                 issuer,
291                 policies,
292                 name,
293                 nameQualifier,
294                 null,
295                 subjectIP,
296                 authMethod,
297                 authInstant,
298                 bindings,
299                 rsig,
300                 asig);
301             r.toDOM(doc);
302             if (asig != null)
303             {
304                 if (assertionCert != null)
305                     asig.addKeyInfo(assertionCert);
306                 if (assertionKey instanceof PrivateKey)
307                     asig.sign((PrivateKey)assertionKey);
308                 else
309                     asig.sign((SecretKey)assertionKey);
310             }
311             if (responseCert != null)
312                 rsig.addKeyInfo(responseCert);
313             if (responseKey instanceof PrivateKey)
314                 rsig.sign((PrivateKey)responseKey);
315             else
316                 rsig.sign((SecretKey)responseKey);
317             return r;
318         }
319         catch (ParserConfigurationException pce)
320         {
321             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() unable to obtain XML parser instance: " + pce.getMessage(), pce);
322         }
323         catch (XMLSecurityException e)
324         {
325             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an XML security problem during signature creation", e);
326         }
327         finally
328         {
329             if (builder != null)
330                 org.opensaml.XML.parserPool.put(builder);
331         }
332     }
333
334     /**
335      *  Searches the replay cache for the specified assertion and inserts a
336      *  newly seen assertion into the cache<P>
337      *
338      *  Also performs garbage collection of the cache by deleting expired
339      *  entries.
340      *
341      * @param  a            The assertion to check
342      * @return              true iff the assertion has not been seen before
343      */
344     public synchronized boolean checkReplayCache(SAMLAssertion a)
345     {
346         // Default implementation uses the basic replay cache implementation.
347         return SAMLPOSTProfile.checkReplayCache(a);
348     }
349
350     /**
351      *  Default signature verification algorithm uses an embedded X509
352      *  certificate or an explicit key to verify the signature. The certificate
353      *  is examined to insure the subject CN matches the signer, and that it is
354      *  signed by a trusted CA
355      *
356      * @param  obj         The object containing the signature
357      * @param  signerName  The name of the signer
358      * @param  ks          A keystore containing trusted root certificates
359      * @param  knownKey    An explicit key to use if a certificate cannot be
360      *      found
361      * @return             The result of signature verification
362      */
363     protected boolean verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
364     {
365         try
366         {
367             XMLSignature sig = (obj != null) ? obj.getSignature() : null;
368             if (sig == null)
369                 return false;
370             KeyInfo ki = sig.getKeyInfo();
371             if (ks != null && ki != null)
372             {
373                 X509Certificate cert = ki.getX509Certificate();
374                 if (cert != null)
375                 {
376                     cert.checkValidity();
377                     if (!sig.checkSignatureValue(cert))
378                         return false;
379                     if (signerName != null)
380                     {
381                         String dname = cert.getSubjectDN().getName();
382                         String cname = "CN=" + signerName;
383                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1))
384                             return false;
385                     }
386
387                     String iname = cert.getIssuerDN().getName();
388                     for (Enumeration aliases = ks.aliases(); aliases.hasMoreElements(); )
389                     {
390                         String alias = (String)aliases.nextElement();
391                         if (!ks.isCertificateEntry(alias))
392                             continue;
393                         Certificate cacert = ks.getCertificate(alias);
394                         if (!(cacert instanceof X509Certificate))
395                             continue;
396                         ((X509Certificate)cacert).checkValidity();
397                         if (iname.equals(((X509Certificate)cacert).getSubjectDN().getName()))
398                         {
399                             cert.verify(cacert.getPublicKey());
400                             return true;
401                         }
402                     }
403
404                     return false;
405                 }
406             }
407             return (knownKey != null) ? sig.checkSignatureValue(knownKey) : false;
408         }
409         catch (XMLSecurityException e)
410         {
411             e.printStackTrace();
412             return false;
413         }
414         catch (GeneralSecurityException e)
415         {
416             e.printStackTrace();
417             return false;
418         }
419     }
420 }
421