More AA & HS unification (merged separate relying party implementations).
[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.InvalidCryptoException;
57 import org.opensaml.NoSuchProviderException;
58 import org.opensaml.ReplayCache;
59 import org.opensaml.SAMLAssertion;
60 import org.opensaml.SAMLAudienceRestrictionCondition;
61 import org.opensaml.SAMLAuthenticationStatement;
62 import org.opensaml.SAMLBrowserProfile;
63 import org.opensaml.SAMLBrowserProfileFactory;
64 import org.opensaml.SAMLConfig;
65 import org.opensaml.SAMLException;
66 import org.opensaml.SAMLNameIdentifier;
67 import org.opensaml.SAMLResponse;
68 import org.opensaml.SAMLSignedObject;
69 import org.opensaml.SAMLSubject;
70 import org.opensaml.TrustException;
71 import org.w3c.dom.Document;
72
73 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
74 import edu.internet2.middleware.shibboleth.metadata.IDPProviderRole;
75 import edu.internet2.middleware.shibboleth.metadata.MetadataException;
76 import edu.internet2.middleware.shibboleth.metadata.ProviderRole;
77 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig;
78 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderContext;
79 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig.ApplicationInfo;
80
81 // TODO: Do the cert extraction methods belong here? Probably not...
82
83 // TODO: Suggest we implement a separation layer between the SP config pieces and the input needed
84 // for this class. As long as metadata/etc. are shared, this should work.
85
86 /**
87  * Basic Shibboleth POST browser profile implementation with basic support for signing
88  * 
89  * @author Scott Cantor @created April 11, 2002
90  */
91 public class ShibBrowserProfile implements SAMLBrowserProfile {
92
93         private static Pattern  regex           = Pattern.compile(".*?CN=([^,/]+).*");
94
95         /** XML Signature algorithm to apply */
96         protected String                algorithm       = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
97
98         private static Logger   log                     = Logger.getLogger(ShibBrowserProfile.class.getName());
99
100     /** Policy URIs to attach or check against */
101     protected ArrayList     policies    = new ArrayList();
102
103     protected SAMLBrowserProfile profile = SAMLBrowserProfileFactory.getInstance(); 
104     private static ServiceProviderContext context = ServiceProviderContext.getInstance();
105
106     /*
107      * The C++ class is constructed by passing enumerations of Metadata
108      * providers, trust providers, etc from the <Application>. However,
109      * those providers can change dynamically. This version only keeps
110      * the applicationId that can be used to fetch the ApplicationInfo 
111      * object and, from it, get the collections of provider plugins.
112      * 
113      * TODO: The reason they were still dynamic in C++ was that this wrapper
114      * object was built dynamically. It's now contained within the application
115      * interface itself and so it's "scoped" within the application and shares
116      * the set of plugins from it. One reloads, the other is rebuilt.
117      */
118     private String applicationId = null;
119     
120     /**
121      * Identify the <Application> from which to get plugins.
122      * 
123      * @param applicationId 
124      */
125     public ShibBrowserProfile(String applicationId) throws NoSuchProviderException {
126         this.applicationId = applicationId;
127     }
128
129         /**
130          * Used by HS to generate a signed SAML response conforming to the POST profile
131          * <P>
132          * 
133          * @param recipient
134          *            URL of the assertion consumer
135          * @param relyingParty
136          *            the intended recipient of the response
137          * @param nameId
138          *            Name Identifier for the response
139          * @param subjectIP
140          *            Client address of subject (optional)
141          * @param authMethod
142          *            URI of authentication method being asserted
143          * @param authInstant
144          *            Date and time of authentication being asserted
145          * @param bindings
146          *            Set of SAML authorities the relying party may contact (optional)
147          * @return SAML response to send to accepting site
148          * @exception SAMLException
149          *                Base class of exceptions that may be thrown during processing
150          */
151         public SAMLResponse prepare(String recipient, RelyingParty relyingParty, SAMLNameIdentifier nameId,
152                         String subjectIP, String authMethod, Date authInstant, Collection bindings) throws SAMLException {
153
154                 Document doc = org.opensaml.XML.parserPool.newDocument();
155
156                 ArrayList audiences = new ArrayList();
157                 if (relyingParty.getProviderId() != null) {
158                         audiences.add(relyingParty.getProviderId());
159                 }
160                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
161                         audiences.add(relyingParty.getName());
162                 }
163
164                 String issuer = null;
165                 if (relyingParty.isLegacyProvider()) {
166                         
167                         log.debug("Service Provider is running Shibboleth <= 1.1.  Using old style issuer.");
168                         if (relyingParty.getIdentityProvider().getAuthNResponseSigningCredential() == null
169                                         || relyingParty.getIdentityProvider().getAuthNResponseSigningCredential().getX509Certificate() == null) {
170                                 throw new SAMLException("Cannot serve legacy style assertions without an X509 certificate");
171                         }
172                         issuer = getHostNameFromDN(relyingParty.getIdentityProvider().getAuthNResponseSigningCredential()
173                                         .getX509Certificate().getSubjectX500Principal());
174                         if (issuer == null || issuer.equals("")) {
175                                 throw new SAMLException("Error parsing certificate DN while determining legacy issuer name.");
176                         }
177
178                 } else {
179                         issuer = relyingParty.getIdentityProvider().getProviderId();
180                 }
181
182         // XXX: Inlined the old prepare method, this whole method should probably be pulled out into the IdP package.
183         // At a minimum, artifact should be integrated in.
184         SAMLResponse r = new SAMLResponse(
185                 null,
186                 recipient,
187                 Collections.singleton(
188                         new SAMLAssertion(
189                                 issuer,
190                                 new Date(),
191                                 new Date(System.currentTimeMillis() + 1000 * SAMLConfig.instance().getIntProperty("org.opensaml.clock-skew")),
192                                 Collections.singleton(
193                                         new SAMLAudienceRestrictionCondition(audiences)
194                                         ),
195                                 null,
196                                 Collections.singleton(
197                                         new SAMLAuthenticationStatement(
198                                                 new SAMLSubject(
199                                                         nameId,
200                                                         Collections.singleton(SAMLSubject.CONF_BEARER),
201                                                         null,
202                                                         null
203                                                         ),
204                                                 authMethod,
205                                                 authInstant,
206                                                 subjectIP,
207                                                 null,
208                                                 bindings
209                                                 )
210                                         )
211                                 )
212                         ),
213                 null
214                 );
215                 r.toDOM(doc);
216
217                 //Sign the assertions, if appropriate
218                 if (relyingParty.getIdentityProvider().getAuthNAssertionSigningCredential() != null
219                                 && relyingParty.getIdentityProvider().getAuthNAssertionSigningCredential().getPrivateKey() != null) {
220
221                         String assertionAlgorithm;
222                         if (relyingParty.getIdentityProvider().getAuthNAssertionSigningCredential().getCredentialType() == Credential.RSA) {
223                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
224                         } else if (relyingParty.getIdentityProvider().getAuthNAssertionSigningCredential().getCredentialType() == Credential.DSA) {
225                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
226                         } else {
227                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
228                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
229                         }
230
231                         ((SAMLAssertion) r.getAssertions().next()).sign(assertionAlgorithm, relyingParty.getIdentityProvider()
232                                         .getAuthNAssertionSigningCredential().getPrivateKey(), Arrays.asList(relyingParty.getIdentityProvider()
233                                         .getAuthNAssertionSigningCredential().getX509CertificateChain()));
234                 }
235
236                 //Sign the response, if appropriate
237                 if (relyingParty.getIdentityProvider().getAuthNResponseSigningCredential() != null
238                                 && relyingParty.getIdentityProvider().getAuthNResponseSigningCredential().getPrivateKey() != null) {
239
240                         String responseAlgorithm;
241                         if (relyingParty.getIdentityProvider().getAuthNResponseSigningCredential().getCredentialType() == Credential.RSA) {
242                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
243                         } else if (relyingParty.getIdentityProvider().getAuthNResponseSigningCredential().getCredentialType() == Credential.DSA) {
244                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
245                         } else {
246                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
247                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
248                         }
249
250                         r.sign(responseAlgorithm,
251                                         relyingParty.getIdentityProvider().getAuthNResponseSigningCredential().getPrivateKey(), Arrays
252                                                         .asList(relyingParty.getIdentityProvider().getAuthNResponseSigningCredential()
253                                                                         .getX509CertificateChain()));
254                 }
255
256                 return r;
257         }
258
259     /**
260      * Given a key from Trust associated with a HS Role from a Metadata Entity Descriptor,
261      * verify the SAML Signature.
262      * 
263      * TODO: Replace this with calls into pluggable Trust provider
264      * 
265      * @param obj           A signed SAMLObject
266      * @param signerName    The signer's ID
267      * @param ks            KeyStore [TrustProvider abstraction violation, may change]
268      * @param knownKey      Key from the Trust entry associated with the signer's Metadata
269      * @throws SAMLException
270      */
271     static void verifySignature(
272             SAMLSignedObject obj, 
273             String signerName, 
274             KeyStore ks, 
275             Key knownKey)
276         throws SAMLException {
277         try {
278             NDC.push("verifySignature");
279             
280             if (!obj.isSigned()) {
281                 log.error("unable to find a signature");
282                 throw new TrustException(SAMLException.RESPONDER,
283                 "ShibPOSTProfile.verifySignature() given an unsigned object");
284             }
285             
286             if (knownKey != null) {
287                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
288                 obj.verify(knownKey);
289                 return;
290             }
291             
292             
293             log.info("verifying signature with embedded KeyInfo");
294             obj.verify();
295             
296             // This is pretty painful, and this is leveraging the supposedly
297             // automatic support in JDK 1.4.
298             // First we have to extract the certificates from the object.
299             Iterator certs_from_obj = obj.getX509Certificates();
300             if (!certs_from_obj.hasNext()) {
301                 log.error("need certificates inside object to establish trust");
302                 throw new TrustException(SAMLException.RESPONDER,
303                 "ShibPOSTProfile.verifySignature() can't find any certificates");
304             }
305             
306             // We assume the first one in the set is the end entity cert.
307             X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
308             
309             // Match the CN of the entity cert with the expected signer.
310             String dname = entity_cert.getSubjectDN().getName();
311             log.debug("found entity cert with DN: " + dname);
312             String cname = "CN=" + signerName;
313             if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
314                 log
315                 .error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
316                         + signerName);
317                 throw new TrustException(SAMLException.RESPONDER,
318                 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
319             }
320             
321             // Prep a chain between the entity cert and the trusted roots.
322             X509CertSelector targetConstraints = new X509CertSelector();
323             targetConstraints.setCertificate(entity_cert);
324             PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
325             params.setMaxPathLength(-1);
326             
327             Vector certbag = new Vector();
328             certbag.add(entity_cert);
329             while (certs_from_obj.hasNext())
330                 certbag.add(certs_from_obj.next());
331             CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
332             CertStore store = CertStore.getInstance("Collection", ccsp);
333             params.addCertStore(store);
334             
335             // Attempt to build a path.
336             CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
337             PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
338         } catch (CertPathBuilderException e) {
339             log.error("caught a cert path builder exception: " + e.getMessage());
340             throw new TrustException(SAMLException.RESPONDER,
341                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
342         } catch (GeneralSecurityException e) {
343             log.error("caught a general security exception: " + e.getMessage());
344             throw new TrustException(SAMLException.RESPONDER,
345                     "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
346         } finally {
347             NDC.pop();
348         }
349     }
350
351     public static String getHostNameFromDN(X500Principal dn) {
352                 Matcher matches = regex.matcher(dn.getName(X500Principal.RFC2253));
353                 if (!matches.find() || matches.groupCount() > 1) {
354                         log.error("Unable to extract host name name from certificate subject DN.");
355                         return null;
356                 }
357                 return matches.group(1);
358         }
359
360     /**
361      * @see org.opensaml.SAMLBrowserProfile#setVersion(int, int)
362      */
363     public void setVersion(int major, int minor) throws SAMLException {
364         profile.setVersion(major, minor);
365     }
366
367     /**
368      * @see org.opensaml.SAMLBrowserProfile#receive(java.lang.StringBuffer, javax.servlet.http.HttpServletRequest, java.lang.String, int, org.opensaml.ReplayCache, org.opensaml.SAMLBrowserProfile.ArtifactMapper)
369      */
370     public BrowserProfileResponse receive(
371             StringBuffer issuer,
372             HttpServletRequest reqContext,
373             String recipient,
374             int supportedProfiles,
375             ReplayCache replayCache,
376             ArtifactMapper artifactMapper
377             ) throws SAMLException {
378         
379         String providerId = null;
380         issuer.setLength(0);
381         
382         // Let SAML do all the decoding and parsing
383         BrowserProfileResponse bpr = profile.receive(issuer, reqContext, providerId, supportedProfiles, replayCache, artifactMapper);
384         
385         /*
386          * Now find the Metadata for the Entity that send this assertion.
387          * From the C++, look first for issuer, then namequalifier (for 1.1 compat.)
388          */
389         EntityDescriptor entity = null;
390         String asn_issuer = bpr.assertion.getIssuer();
391         String qualifier = bpr.authnStatement.getSubject().getName().getNameQualifier();
392         ServiceProviderConfig config = context.getServiceProviderConfig();
393         ApplicationInfo appinfo = config.getApplication(applicationId);
394         
395         entity = appinfo.getEntityDescriptor(asn_issuer);
396         providerId=asn_issuer;
397         if (entity==null) {
398             providerId=qualifier;
399             entity= appinfo.getEntityDescriptor(qualifier);
400         }
401         if (entity==null) {
402             log.error("assertion issuer not found in metadata(Issuer ="+
403                     issuer+", NameQualifier="+qualifier);
404             throw new MetadataException("ShibBrowserProfile.receive() metadata lookup failed, unable to process assertion");
405         }
406         issuer.append(providerId);
407         
408         // From the Metadata, get the HS and from it the key
409         ProviderRole[] roles = entity.getRoles();
410         for (int i=0;i<roles.length;i++) {
411             ProviderRole role = roles[i];
412             if (role instanceof IDPProviderRole) {
413                 // TODO: Sync up with new SAML metadata profile (uses SAML protocol string instead of SHIB_NS)
414                 if (role.hasSupport(XML.SHIB_NS)) {
415                     ;
416                 }
417             }
418         }
419         
420         return bpr;
421     }
422 }