added logging to verify
[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 org.apache.xml.security.exceptions.XMLSecurityException;
63 import org.apache.xml.security.keys.KeyInfo;
64 import org.apache.xml.security.signature.XMLSignature;
65 import org.opensaml.*;
66 import org.w3c.dom.*;
67 import org.apache.log4j.Logger;
68
69 /**
70  *  Basic Shibboleth POST browser profile implementation with basic support for
71  *  signing
72  *
73  * @author     Scott Cantor
74  * @created    April 11, 2002
75  */
76 public class ShibPOSTProfile
77 {
78     /**  XML Signature algorithm to apply */
79     protected String algorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
80
81     /**  Policy URIs to attach or check against */
82     protected String[] policies = null;
83
84     /**  Official name of issuing site */
85     protected String issuer = null;
86
87     /**  Abstract interface into trust base */
88     protected OriginSiteMapper mapper = null;
89
90     /**  The URL of the receiving SHIRE */
91     protected String receiver = null;
92
93     /**  Seconds allowed to elapse from issuance of response */
94     protected int ttlSeconds = 0;
95
96     private static Logger log = Logger.getLogger(ShibPOSTProfile.class.getName());
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         try
278         {
279             Document doc = org.opensaml.XML.parserPool.newDocument();
280
281             XMLSignature rsig = new XMLSignature(doc, null, algorithm);
282             XMLSignature asig = null;
283             if (assertionKey != null)
284                 asig = new XMLSignature(doc, null, algorithm);
285
286             SAMLResponse r = SAMLPOSTProfile.prepare(
287                 recipient,
288                 issuer,
289                 policies,
290                 name,
291                 nameQualifier,
292                 null,
293                 subjectIP,
294                 authMethod,
295                 authInstant,
296                 bindings,
297                 rsig,
298                 asig);
299             r.toDOM(doc);
300             if (asig != null)
301             {
302                 if (assertionCert != null)
303                     asig.addKeyInfo(assertionCert);
304                 if (assertionKey instanceof PrivateKey)
305                     asig.sign((PrivateKey)assertionKey);
306                 else
307                     asig.sign((SecretKey)assertionKey);
308             }
309             if (responseCert != null)
310                 rsig.addKeyInfo(responseCert);
311             if (responseKey instanceof PrivateKey)
312                 rsig.sign((PrivateKey)responseKey);
313             else
314                 rsig.sign((SecretKey)responseKey);
315             return r;
316         }
317         catch (XMLSecurityException e)
318         {
319             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an XML security problem during signature creation", e);
320         }
321     }
322
323     /**
324      *  Searches the replay cache for the specified assertion and inserts a
325      *  newly seen assertion into the cache<P>
326      *
327      *  Also performs garbage collection of the cache by deleting expired
328      *  entries.
329      *
330      * @param  a            The assertion to check
331      * @return              true iff the assertion has not been seen before
332      */
333     public synchronized boolean checkReplayCache(SAMLAssertion a)
334     {
335         // Default implementation uses the basic replay cache implementation.
336         return SAMLPOSTProfile.checkReplayCache(a);
337     }
338
339     /**
340      *  Default signature verification algorithm uses an embedded X509
341      *  certificate or an explicit key to verify the signature. The certificate
342      *  is examined to insure the subject CN matches the signer, and that it is
343      *  signed by a trusted CA
344      *
345      * @param  obj         The object containing the signature
346      * @param  signerName  The name of the signer
347      * @param  ks          A keystore containing trusted root certificates
348      * @param  knownKey    An explicit key to use if a certificate cannot be
349      *      found
350      * @return             The result of signature verification
351      */
352     protected boolean verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
353     {
354         try
355         {
356             XMLSignature sig = (obj != null) ? obj.getSignature() : null;
357             if (sig == null) {
358                 log.warn("verifySignature() unable to find a signature");
359                 return false;
360             }
361             KeyInfo ki = sig.getKeyInfo();
362             if (ks != null && ki != null)
363             {
364                 X509Certificate cert = ki.getX509Certificate();
365                 if (cert != null)
366                 {
367                     cert.checkValidity();
368                     if (!sig.checkSignatureValue(cert)) {
369                         log.warn("verifySignature() failed to verify signature using embedded certificate");
370                         return false;
371                     }
372                     if (signerName != null)
373                     {
374                         String dname = cert.getSubjectDN().getName();
375                         log.debug("verifySignature() found cert with DN: " + dname);
376                         String cname = "CN=" + signerName;
377                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
378                             log.warn("verifySignature() found a mismatch between the certificate DN and the expected signer: " + signerName);
379                             return false;
380                         }
381                     }
382
383                     String iname = cert.getIssuerDN().getName();
384                     for (Enumeration aliases = ks.aliases(); aliases.hasMoreElements(); )
385                     {
386                         String alias = (String)aliases.nextElement();
387                         if (!ks.isCertificateEntry(alias))
388                             continue;
389                         Certificate cacert = ks.getCertificate(alias);
390                         if (!(cacert instanceof X509Certificate))
391                             continue;
392                         if (iname.equals(((X509Certificate)cacert).getSubjectDN().getName()))
393                         {
394                             cert.verify(cacert.getPublicKey());
395                             ((X509Certificate)cacert).checkValidity();
396                             return true;
397                         }
398                     }
399                     log.warn("verifySignature() unable to locate the cert issuer (" + iname + ") in the CA store");
400                     return false;
401                 }
402             }
403             return (knownKey != null) ? sig.checkSignatureValue(knownKey) : false;
404         }
405         catch (XMLSecurityException e)
406         {
407             e.printStackTrace();
408             return false;
409         }
410         catch (GeneralSecurityException e)
411         {
412             e.printStackTrace();
413             return false;
414         }
415     }
416 }
417