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