6ca659c8dc9238339b5c45869ada64ef7a1141ad
[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 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45
46 import javax.security.auth.x500.X500Principal;
47 import javax.servlet.http.HttpServletRequest;
48
49 import org.apache.log4j.Logger;
50 import org.apache.log4j.NDC;
51 import org.apache.xml.security.signature.XMLSignature;
52 import org.opensaml.NoSuchProviderException;
53 import org.opensaml.ReplayCache;
54 import org.opensaml.SAMLBrowserProfile;
55 import org.opensaml.SAMLBrowserProfileFactory;
56 import org.opensaml.SAMLException;
57 import org.opensaml.SAMLSignedObject;
58 import org.opensaml.TrustException;
59
60 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
61 import edu.internet2.middleware.shibboleth.metadata.IDPSSODescriptor;
62 import edu.internet2.middleware.shibboleth.metadata.MetadataException;
63 import edu.internet2.middleware.shibboleth.metadata.RoleDescriptor;
64 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig;
65 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderContext;
66 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig.ApplicationInfo;
67
68 // TODO: Do the cert extraction methods belong here? Probably not...
69
70 // TODO: Suggest we implement a separation layer between the SP config pieces and the input needed
71 // for this class. As long as metadata/etc. are shared, this should work.
72
73 /**
74  * Basic Shibboleth POST browser profile implementation with basic support for signing
75  * 
76  * @author Scott Cantor @created April 11, 2002
77  */
78 public class ShibBrowserProfile implements SAMLBrowserProfile {
79
80         private static Pattern  regex           = Pattern.compile(".*?CN=([^,/]+).*");
81
82         /** XML Signature algorithm to apply */
83         protected String                algorithm       = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
84
85         private static Logger   log                     = Logger.getLogger(ShibBrowserProfile.class.getName());
86
87     /** Policy URIs to attach or check against */
88     protected ArrayList     policies    = new ArrayList();
89
90     protected SAMLBrowserProfile profile = SAMLBrowserProfileFactory.getInstance(); 
91     private static ServiceProviderContext context = ServiceProviderContext.getInstance();
92
93     /*
94      * The C++ class is constructed by passing enumerations of Metadata
95      * providers, trust providers, etc from the <Application>. However,
96      * those providers can change dynamically. This version only keeps
97      * the applicationId that can be used to fetch the ApplicationInfo 
98      * object and, from it, get the collections of provider plugins.
99      * 
100      * TODO: The reason they were still dynamic in C++ was that this wrapper
101      * object was built dynamically. It's now contained within the application
102      * interface itself and so it's "scoped" within the application and shares
103      * the set of plugins from it. One reloads, the other is rebuilt.
104      */
105     private String applicationId = null;
106     
107     /**
108      * Identify the <Application> from which to get plugins.
109      * 
110      * @param applicationId 
111      */
112     public ShibBrowserProfile(String applicationId) throws NoSuchProviderException {
113         this.applicationId = applicationId;
114     }
115
116         /**
117      * Given a key from Trust associated with a HS Role from a Metadata Entity Descriptor,
118      * verify the SAML Signature.
119      * 
120      * TODO: Replace this with calls into pluggable Trust provider
121      * 
122      * @param obj           A signed SAMLObject
123      * @param signerName    The signer's ID
124      * @param ks            KeyStore [TrustProvider abstraction violation, may change]
125      * @param knownKey      Key from the Trust entry associated with the signer's Metadata
126      * @throws SAMLException
127      */
128     static void verifySignature(
129             SAMLSignedObject obj, 
130             String signerName, 
131             KeyStore ks, 
132             Key knownKey)
133         throws SAMLException {
134         try {
135             NDC.push("verifySignature");
136             
137             if (!obj.isSigned()) {
138                 log.error("unable to find a signature");
139                 throw new TrustException(SAMLException.RESPONDER,
140                 "ShibPOSTProfile.verifySignature() given an unsigned object");
141             }
142             
143             if (knownKey != null) {
144                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
145                 obj.verify(knownKey);
146                 return;
147             }
148             
149             
150             log.info("verifying signature with embedded KeyInfo");
151             obj.verify();
152             
153             // This is pretty painful, and this is leveraging the supposedly
154             // automatic support in JDK 1.4.
155             // First we have to extract the certificates from the object.
156             Iterator certs_from_obj = obj.getX509Certificates();
157             if (!certs_from_obj.hasNext()) {
158                 log.error("need certificates inside object to establish trust");
159                 throw new TrustException(SAMLException.RESPONDER,
160                 "ShibPOSTProfile.verifySignature() can't find any certificates");
161             }
162             
163             // We assume the first one in the set is the end entity cert.
164             X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
165             
166             // Match the CN of the entity cert with the expected signer.
167             String dname = entity_cert.getSubjectDN().getName();
168             log.debug("found entity cert with DN: " + dname);
169             String cname = "CN=" + signerName;
170             if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
171                 log
172                 .error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
173                         + signerName);
174                 throw new TrustException(SAMLException.RESPONDER,
175                 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
176             }
177             
178             // Prep a chain between the entity cert and the trusted roots.
179             X509CertSelector targetConstraints = new X509CertSelector();
180             targetConstraints.setCertificate(entity_cert);
181             PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
182             params.setMaxPathLength(-1);
183             
184             Vector certbag = new Vector();
185             certbag.add(entity_cert);
186             while (certs_from_obj.hasNext())
187                 certbag.add(certs_from_obj.next());
188             CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
189             CertStore store = CertStore.getInstance("Collection", ccsp);
190             params.addCertStore(store);
191             
192             // Attempt to build a path.
193             CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
194             PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
195         } catch (CertPathBuilderException e) {
196             log.error("caught a cert path builder exception: " + e.getMessage());
197             throw new TrustException(SAMLException.RESPONDER,
198                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
199         } catch (GeneralSecurityException e) {
200             log.error("caught a general security exception: " + e.getMessage());
201             throw new TrustException(SAMLException.RESPONDER,
202                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
203         } finally {
204             NDC.pop();
205         }
206     }
207
208     public static String getHostNameFromDN(X500Principal dn) {
209                 Matcher matches = regex.matcher(dn.getName(X500Principal.RFC2253));
210                 if (!matches.find() || matches.groupCount() > 1) {
211                         log.error("Unable to extract host name name from certificate subject DN.");
212                         return null;
213                 }
214                 return matches.group(1);
215         }
216
217     /**
218      * @see org.opensaml.SAMLBrowserProfile#setVersion(int, int)
219      */
220     public void setVersion(int major, int minor) throws SAMLException {
221         profile.setVersion(major, minor);
222     }
223
224     /**
225      * @see org.opensaml.SAMLBrowserProfile#receive(java.lang.StringBuffer, javax.servlet.http.HttpServletRequest, java.lang.String, int, org.opensaml.ReplayCache, org.opensaml.SAMLBrowserProfile.ArtifactMapper)
226      */
227     public BrowserProfileResponse receive(
228             StringBuffer issuer,
229             HttpServletRequest reqContext,
230             String recipient,
231             int supportedProfiles,
232             ReplayCache replayCache,
233             ArtifactMapper artifactMapper
234             ) throws SAMLException {
235         
236         String providerId = null;
237         issuer.setLength(0);
238         
239         // Let SAML do all the decoding and parsing
240         BrowserProfileResponse bpr = profile.receive(issuer, reqContext, providerId, supportedProfiles, replayCache, artifactMapper);
241         
242         /*
243          * Now find the Metadata for the Entity that send this assertion.
244          * From the C++, look first for issuer, then namequalifier (for 1.1 compat.)
245          */
246         EntityDescriptor entity = null;
247         String asn_issuer = bpr.assertion.getIssuer();
248         String qualifier = bpr.authnStatement.getSubject().getName().getNameQualifier();
249         ServiceProviderConfig config = context.getServiceProviderConfig();
250         ApplicationInfo appinfo = config.getApplication(applicationId);
251         
252         entity = appinfo.lookup(asn_issuer);
253         providerId=asn_issuer;
254         if (entity==null) {
255             providerId=qualifier;
256             entity= appinfo.lookup(qualifier);
257         }
258         if (entity==null) {
259             log.error("assertion issuer not found in metadata(Issuer ="+
260                     issuer+", NameQualifier="+qualifier);
261             throw new MetadataException("ShibBrowserProfile.receive() metadata lookup failed, unable to process assertion");
262         }
263         issuer.append(providerId);
264         
265         // TODO: finish profile extension using metadata/trust interfaces
266         
267         return bpr;
268     }
269 }