Patches for opensaml profile/binding changes.
[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.Arrays;
42 import java.util.Collection;
43 import java.util.Collections;
44 import java.util.Date;
45 import java.util.Iterator;
46 import java.util.Vector;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49
50 import javax.security.auth.x500.X500Principal;
51 import javax.servlet.http.HttpServletRequest;
52
53 import org.apache.log4j.Logger;
54 import org.apache.log4j.NDC;
55 import org.apache.xml.security.signature.XMLSignature;
56 import org.opensaml.*;
57 import org.w3c.dom.Document;
58
59 import edu.internet2.middleware.shibboleth.hs.HSRelyingParty;
60 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
61 import edu.internet2.middleware.shibboleth.metadata.IDPProviderRole;
62 import edu.internet2.middleware.shibboleth.metadata.MetadataException;
63 import edu.internet2.middleware.shibboleth.metadata.ProviderRole;
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          * Used by HS to generate a signed SAML response conforming to the POST profile
118          * <P>
119          * 
120          * @param recipient
121          *            URL of the assertion consumer
122          * @param relyingParty
123          *            the intended recipient of the response
124          * @param nameId
125          *            Name Identifier for the response
126          * @param subjectIP
127          *            Client address of subject (optional)
128          * @param authMethod
129          *            URI of authentication method being asserted
130          * @param authInstant
131          *            Date and time of authentication being asserted
132          * @param bindings
133          *            Set of SAML authorities the relying party may contact (optional)
134          * @return SAML response to send to accepting site
135          * @exception SAMLException
136          *                Base class of exceptions that may be thrown during processing
137          */
138         public SAMLResponse prepare(String recipient, HSRelyingParty relyingParty, SAMLNameIdentifier nameId,
139                         String subjectIP, String authMethod, Date authInstant, Collection bindings) throws SAMLException {
140
141                 Document doc = org.opensaml.XML.parserPool.newDocument();
142
143                 ArrayList audiences = new ArrayList();
144                 if (relyingParty.getProviderId() != null) {
145                         audiences.add(relyingParty.getProviderId());
146                 }
147                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
148                         audiences.add(relyingParty.getName());
149                 }
150
151                 String issuer = null;
152                 if (relyingParty.isLegacyProvider()) {
153                         
154                         log.debug("Service Provider is running Shibboleth <= 1.1.  Using old style issuer.");
155                         if (relyingParty.getIdentityProvider().getResponseSigningCredential() == null
156                                         || relyingParty.getIdentityProvider().getResponseSigningCredential().getX509Certificate() == null) {
157                                 throw new SAMLException("Cannot serve legacy style assertions without an X509 certificate");
158                         }
159                         issuer = getHostNameFromDN(relyingParty.getIdentityProvider().getResponseSigningCredential()
160                                         .getX509Certificate().getSubjectX500Principal());
161                         if (issuer == null || issuer.equals("")) {
162                                 throw new SAMLException("Error parsing certificate DN while determining legacy issuer name.");
163                         }
164
165                 } else {
166                         issuer = relyingParty.getIdentityProvider().getProviderId();
167                 }
168
169         // XXX: Inlined the old prepare method, this whole method should probably be pulled out into the IdP package.
170         // At a minimum, artifact should be integrated in.
171         SAMLResponse r = new SAMLResponse(
172                 null,
173                 recipient,
174                 Collections.singleton(
175                         new SAMLAssertion(
176                                 issuer,
177                                 new Date(),
178                                 new Date(System.currentTimeMillis() + 1000 * SAMLConfig.instance().getIntProperty("org.opensaml.clock-skew")),
179                                 Collections.singleton(
180                                         new SAMLAudienceRestrictionCondition(audiences)
181                                         ),
182                                 null,
183                                 Collections.singleton(
184                                         new SAMLAuthenticationStatement(
185                                                 new SAMLSubject(
186                                                         nameId,
187                                                         Collections.singleton(SAMLSubject.CONF_BEARER),
188                                                         null,
189                                                         null
190                                                         ),
191                                                 authMethod,
192                                                 authInstant,
193                                                 subjectIP,
194                                                 null,
195                                                 bindings
196                                                 )
197                                         )
198                                 )
199                         ),
200                 null
201                 );
202                 r.toDOM(doc);
203
204                 //Sign the assertions, if appropriate
205                 if (relyingParty.getIdentityProvider().getAssertionSigningCredential() != null
206                                 && relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey() != null) {
207
208                         String assertionAlgorithm;
209                         if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.RSA) {
210                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
211                         } else if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.DSA) {
212                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
213                         } else {
214                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
215                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
216                         }
217
218                         ((SAMLAssertion) r.getAssertions().next()).sign(assertionAlgorithm, relyingParty.getIdentityProvider()
219                                         .getAssertionSigningCredential().getPrivateKey(), Arrays.asList(relyingParty.getIdentityProvider()
220                                         .getAssertionSigningCredential().getX509CertificateChain()));
221                 }
222
223                 //Sign the response, if appropriate
224                 if (relyingParty.getIdentityProvider().getResponseSigningCredential() != null
225                                 && relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey() != null) {
226
227                         String responseAlgorithm;
228                         if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.RSA) {
229                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
230                         } else if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.DSA) {
231                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
232                         } else {
233                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
234                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
235                         }
236
237                         r.sign(responseAlgorithm,
238                                         relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey(), Arrays
239                                                         .asList(relyingParty.getIdentityProvider().getResponseSigningCredential()
240                                                                         .getX509CertificateChain()));
241                 }
242
243                 return r;
244         }
245
246     /**
247      * Given a key from Trust associated with a HS Role from a Metadata Entity Descriptor,
248      * verify the SAML Signature.
249      * 
250      * TODO: Replace this with calls into pluggable Trust provider
251      * 
252      * @param obj           A signed SAMLObject
253      * @param signerName    The signer's ID
254      * @param ks            KeyStore [TrustProvider abstraction violation, may change]
255      * @param knownKey      Key from the Trust entry associated with the signer's Metadata
256      * @throws SAMLException
257      */
258     static void verifySignature(
259             SAMLSignedObject obj, 
260             String signerName, 
261             KeyStore ks, 
262             Key knownKey)
263         throws SAMLException {
264         try {
265             NDC.push("verifySignature");
266             
267             if (!obj.isSigned()) {
268                 log.error("unable to find a signature");
269                 throw new TrustException(SAMLException.RESPONDER,
270                 "ShibPOSTProfile.verifySignature() given an unsigned object");
271             }
272             
273             if (knownKey != null) {
274                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
275                 obj.verify(knownKey);
276                 return;
277             }
278             
279             
280             log.info("verifying signature with embedded KeyInfo");
281             obj.verify();
282             
283             // This is pretty painful, and this is leveraging the supposedly
284             // automatic support in JDK 1.4.
285             // First we have to extract the certificates from the object.
286             Iterator certs_from_obj = obj.getX509Certificates();
287             if (!certs_from_obj.hasNext()) {
288                 log.error("need certificates inside object to establish trust");
289                 throw new TrustException(SAMLException.RESPONDER,
290                 "ShibPOSTProfile.verifySignature() can't find any certificates");
291             }
292             
293             // We assume the first one in the set is the end entity cert.
294             X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
295             
296             // Match the CN of the entity cert with the expected signer.
297             String dname = entity_cert.getSubjectDN().getName();
298             log.debug("found entity cert with DN: " + dname);
299             String cname = "CN=" + signerName;
300             if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
301                 log
302                 .error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
303                         + signerName);
304                 throw new TrustException(SAMLException.RESPONDER,
305                 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
306             }
307             
308             // Prep a chain between the entity cert and the trusted roots.
309             X509CertSelector targetConstraints = new X509CertSelector();
310             targetConstraints.setCertificate(entity_cert);
311             PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
312             params.setMaxPathLength(-1);
313             
314             Vector certbag = new Vector();
315             certbag.add(entity_cert);
316             while (certs_from_obj.hasNext())
317                 certbag.add(certs_from_obj.next());
318             CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
319             CertStore store = CertStore.getInstance("Collection", ccsp);
320             params.addCertStore(store);
321             
322             // Attempt to build a path.
323             CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
324             PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
325         } catch (CertPathBuilderException e) {
326             log.error("caught a cert path builder exception: " + e.getMessage());
327             throw new TrustException(SAMLException.RESPONDER,
328                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
329         } catch (GeneralSecurityException e) {
330             log.error("caught a general security exception: " + e.getMessage());
331             throw new TrustException(SAMLException.RESPONDER,
332                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
333         } finally {
334             NDC.pop();
335         }
336     }
337
338     public static String getHostNameFromDN(X500Principal dn) {
339                 Matcher matches = regex.matcher(dn.getName(X500Principal.RFC2253));
340                 if (!matches.find() || matches.groupCount() > 1) {
341                         log.error("Unable to extract host name name from certificate subject DN.");
342                         return null;
343                 }
344                 return matches.group(1);
345         }
346
347     /**
348      * @see org.opensaml.SAMLBrowserProfile#setVersion(int, int)
349      */
350     public void setVersion(int major, int minor) throws SAMLException {
351         profile.setVersion(major, minor);
352     }
353
354     /**
355      * @see org.opensaml.SAMLBrowserProfile#receive(java.lang.StringBuffer, javax.servlet.http.HttpServletRequest, java.lang.String, int, org.opensaml.ReplayCache, org.opensaml.SAMLBrowserProfile.ArtifactMapper)
356      */
357     public BrowserProfileResponse receive(
358             StringBuffer issuer,
359             HttpServletRequest reqContext,
360             String recipient,
361             int supportedProfiles,
362             ReplayCache replayCache,
363             ArtifactMapper artifactMapper
364             ) throws SAMLException {
365         
366         String providerId = null;
367         issuer.setLength(0);
368         
369         // Let SAML do all the decoding and parsing
370         BrowserProfileResponse bpr = profile.receive(issuer, reqContext, providerId, supportedProfiles, replayCache, artifactMapper);
371         
372         /*
373          * Now find the Metadata for the Entity that send this assertion.
374          * From the C++, look first for issuer, then namequalifier (for 1.1 compat.)
375          */
376         EntityDescriptor entity = null;
377         String asn_issuer = bpr.assertion.getIssuer();
378         String qualifier = bpr.authnStatement.getSubject().getName().getNameQualifier();
379         ServiceProviderConfig config = context.getServiceProviderConfig();
380         ApplicationInfo appinfo = config.getApplication(applicationId);
381         
382         entity = appinfo.getEntityDescriptor(asn_issuer);
383         providerId=asn_issuer;
384         if (entity==null) {
385             providerId=qualifier;
386             entity= appinfo.getEntityDescriptor(qualifier);
387         }
388         if (entity==null) {
389             log.error("assertion issuer not found in metadata(Issuer ="+
390                     issuer+", NameQualifier="+qualifier);
391             throw new MetadataException("ShibBrowserProfile.receive() metadata lookup failed, unable to process assertion");
392         }
393         issuer.append(providerId);
394         
395         // From the Metadata, get the HS and from it the key
396         ProviderRole[] roles = entity.getRoles();
397         for (int i=0;i<roles.length;i++) {
398             ProviderRole role = roles[i];
399             if (role instanceof IDPProviderRole) {
400                 // TODO: Sync up with new SAML metadata profile (uses SAML protocol string instead of SHIB_NS)
401                 if (role.hasSupport(XML.SHIB_NS)) {
402                     ;
403                 }
404             }
405         }
406         
407         return bpr;
408     }
409 }