Added dual version support.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / ShibBrowserProfile.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the
6  * above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other
7  * materials provided with the distribution, if any, must include the following acknowledgment: "This product includes
8  * software developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2
9  * Project. Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2,
11  * nor the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please
13  * contact shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2,
14  * UCAID, or the University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name,
15  * without prior written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS
16  * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES,
17  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
18  * NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS
19  * WITH LICENSEE. IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED
20  * INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
23  * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24  * POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 package edu.internet2.middleware.shibboleth.common;
28
29 import java.security.GeneralSecurityException;
30 import java.security.Key;
31 import java.security.KeyStore;
32 import java.security.cert.CertPathBuilder;
33 import java.security.cert.CertPathBuilderException;
34 import java.security.cert.CertStore;
35 import java.security.cert.CollectionCertStoreParameters;
36 import java.security.cert.PKIXBuilderParameters;
37 import java.security.cert.PKIXCertPathBuilderResult;
38 import java.security.cert.X509CertSelector;
39 import java.security.cert.X509Certificate;
40 import java.util.ArrayList;
41 import java.util.Iterator;
42 import java.util.Vector;
43
44 import javax.servlet.http.HttpServletRequest;
45
46 import org.apache.log4j.Logger;
47 import org.apache.log4j.NDC;
48 import org.apache.xml.security.signature.XMLSignature;
49 import org.opensaml.NoSuchProviderException;
50 import org.opensaml.ReplayCache;
51 import org.opensaml.SAMLBrowserProfile;
52 import org.opensaml.SAMLBrowserProfileFactory;
53 import org.opensaml.SAMLException;
54 import org.opensaml.SAMLSignedObject;
55 import org.opensaml.TrustException;
56
57 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
58 import edu.internet2.middleware.shibboleth.metadata.MetadataException;
59 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig;
60 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderContext;
61 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig.ApplicationInfo;
62
63 // TODO: Suggest we implement a separation layer between the SP config pieces and the input needed
64 // for this class. As long as metadata/etc. are shared, this should work.
65
66 /**
67  * Basic Shibboleth POST browser profile implementation with basic support for signing
68  * 
69  * @author Scott Cantor @created April 11, 2002
70  */
71 public class ShibBrowserProfile implements SAMLBrowserProfile {
72
73
74
75         /** XML Signature algorithm to apply */
76         protected String                algorithm       = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
77
78         private static Logger   log                     = Logger.getLogger(ShibBrowserProfile.class.getName());
79
80     /** Policy URIs to attach or check against */
81     protected ArrayList     policies    = new ArrayList();
82
83     protected SAMLBrowserProfile profile = SAMLBrowserProfileFactory.getInstance(); 
84     private static ServiceProviderContext context = ServiceProviderContext.getInstance();
85
86     /*
87      * The C++ class is constructed by passing enumerations of Metadata
88      * providers, trust providers, etc from the <Application>. However,
89      * those providers can change dynamically. This version only keeps
90      * the applicationId that can be used to fetch the ApplicationInfo 
91      * object and, from it, get the collections of provider plugins.
92      * 
93      * TODO: The reason they were still dynamic in C++ was that this wrapper
94      * object was built dynamically. It's now contained within the application
95      * interface itself and so it's "scoped" within the application and shares
96      * the set of plugins from it. One reloads, the other is rebuilt.
97      */
98     private String applicationId = null;
99     
100     /**
101      * Identify the <Application> from which to get plugins.
102      * 
103      * @param applicationId 
104      */
105     public ShibBrowserProfile(String applicationId) throws NoSuchProviderException {
106         this.applicationId = applicationId;
107     }
108
109         /**
110      * Given a key from Trust associated with a HS Role from a Metadata Entity Descriptor,
111      * verify the SAML Signature.
112      * 
113      * TODO: Replace this with calls into pluggable Trust provider
114      * 
115      * @param obj           A signed SAMLObject
116      * @param signerName    The signer's ID
117      * @param ks            KeyStore [TrustProvider abstraction violation, may change]
118      * @param knownKey      Key from the Trust entry associated with the signer's Metadata
119      * @throws SAMLException
120      */
121     static void verifySignature(
122             SAMLSignedObject obj, 
123             String signerName, 
124             KeyStore ks, 
125             Key knownKey)
126         throws SAMLException {
127         try {
128             NDC.push("verifySignature");
129             
130             if (!obj.isSigned()) {
131                 log.error("unable to find a signature");
132                 throw new TrustException(SAMLException.RESPONDER,
133                 "ShibPOSTProfile.verifySignature() given an unsigned object");
134             }
135             
136             if (knownKey != null) {
137                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
138                 obj.verify(knownKey);
139                 return;
140             }
141             
142             
143             log.info("verifying signature with embedded KeyInfo");
144             obj.verify();
145             
146             // This is pretty painful, and this is leveraging the supposedly
147             // automatic support in JDK 1.4.
148             // First we have to extract the certificates from the object.
149             Iterator certs_from_obj = obj.getX509Certificates();
150             if (!certs_from_obj.hasNext()) {
151                 log.error("need certificates inside object to establish trust");
152                 throw new TrustException(SAMLException.RESPONDER,
153                 "ShibPOSTProfile.verifySignature() can't find any certificates");
154             }
155             
156             // We assume the first one in the set is the end entity cert.
157             X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
158             
159             // Match the CN of the entity cert with the expected signer.
160             String dname = entity_cert.getSubjectDN().getName();
161             log.debug("found entity cert with DN: " + dname);
162             String cname = "CN=" + signerName;
163             if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
164                 log
165                 .error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
166                         + signerName);
167                 throw new TrustException(SAMLException.RESPONDER,
168                 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
169             }
170             
171             // Prep a chain between the entity cert and the trusted roots.
172             X509CertSelector targetConstraints = new X509CertSelector();
173             targetConstraints.setCertificate(entity_cert);
174             PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
175             params.setMaxPathLength(-1);
176             
177             Vector certbag = new Vector();
178             certbag.add(entity_cert);
179             while (certs_from_obj.hasNext())
180                 certbag.add(certs_from_obj.next());
181             CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
182             CertStore store = CertStore.getInstance("Collection", ccsp);
183             params.addCertStore(store);
184             
185             // Attempt to build a path.
186             CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
187             PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
188         } catch (CertPathBuilderException e) {
189             log.error("caught a cert path builder exception: " + e.getMessage());
190             throw new TrustException(SAMLException.RESPONDER,
191                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
192         } catch (GeneralSecurityException e) {
193             log.error("caught a general security exception: " + e.getMessage());
194             throw new TrustException(SAMLException.RESPONDER,
195                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
196         } finally {
197             NDC.pop();
198         }
199     }
200
201
202
203     /**
204      * @see org.opensaml.SAMLBrowserProfile#receive(java.lang.StringBuffer, javax.servlet.http.HttpServletRequest, java.lang.String, int, org.opensaml.ReplayCache, org.opensaml.SAMLBrowserProfile.ArtifactMapper, int)
205      */
206     public BrowserProfileResponse receive(
207             StringBuffer issuer,
208             HttpServletRequest reqContext,
209             String recipient,
210             int supportedProfiles,
211             ReplayCache replayCache,
212             ArtifactMapper artifactMapper,
213             int minorVersion
214             ) throws SAMLException {
215         
216         String providerId = null;
217         issuer.setLength(0);
218         
219         // Let SAML do all the decoding and parsing
220         BrowserProfileResponse bpr = profile.receive(issuer, reqContext, recipient, supportedProfiles, replayCache, artifactMapper, minorVersion);
221         
222         /*
223          * Now find the Metadata for the Entity that send this assertion.
224          * From the C++, look first for issuer, then namequalifier (for 1.1 compat.)
225          */
226         EntityDescriptor entity = null;
227         String asn_issuer = bpr.assertion.getIssuer();
228         String qualifier = bpr.authnStatement.getSubject().getNameIdentifier().getNameQualifier();
229         ServiceProviderConfig config = context.getServiceProviderConfig();
230         ApplicationInfo appinfo = config.getApplication(applicationId);
231         
232         entity = appinfo.lookup(asn_issuer);
233         providerId=asn_issuer;
234         if (entity==null) {
235             providerId=qualifier;
236             entity= appinfo.lookup(qualifier);
237         }
238         if (entity==null) {
239             log.error("assertion issuer not found in metadata(Issuer ="+
240                     issuer+", NameQualifier="+qualifier);
241             throw new MetadataException("ShibBrowserProfile.receive() metadata lookup failed, unable to process assertion");
242         }
243         issuer.append(providerId);
244         
245         // TODO: finish profile extension using metadata/trust interfaces
246         
247         return bpr;
248     }
249 }