9420087c20bbc97076475eaa59eeb4788ddc438b
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / ShibPOSTProfile.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.Date;
44 import java.util.Iterator;
45 import java.util.Vector;
46 import java.util.regex.Matcher;
47 import java.util.regex.Pattern;
48
49 import javax.security.auth.x500.X500Principal;
50
51 import org.apache.log4j.Logger;
52 import org.apache.log4j.NDC;
53 import org.apache.xml.security.signature.XMLSignature;
54 import org.opensaml.InvalidAssertionException;
55 import org.opensaml.InvalidCryptoException;
56 import org.opensaml.SAMLAssertion;
57 import org.opensaml.SAMLAuthenticationStatement;
58 import org.opensaml.SAMLException;
59 import org.opensaml.SAMLNameIdentifier;
60 import org.opensaml.SAMLPOSTProfile;
61 import org.opensaml.SAMLResponse;
62 import org.opensaml.SAMLSignedObject;
63 import org.opensaml.SAMLStatement;
64 import org.opensaml.SAMLSubject;
65 import org.opensaml.TrustException;
66 import org.w3c.dom.Document;
67
68 import edu.internet2.middleware.shibboleth.hs.HSRelyingParty;
69
70 /**
71  * Basic Shibboleth POST browser profile implementation with basic support for signing
72  * 
73  * @author Scott Cantor @created April 11, 2002
74  */
75 public class ShibPOSTProfile {
76
77         private static Pattern  regex           = Pattern.compile(".*CN=([^,/]+).*");
78
79         /** XML Signature algorithm to apply */
80         protected String                algorithm       = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
81
82         /** Policy URIs to attach or check against */
83         protected ArrayList             policies        = new ArrayList();
84
85         /** The URL of the receiving SHIRE */
86         protected String                receiver        = null;
87
88         /** Seconds allowed to elapse from issuance of response */
89         protected int                   ttlSeconds      = 0;
90
91         private static Logger   log                     = Logger.getLogger(ShibPOSTProfile.class.getName());
92
93         /**
94          * SHIRE-side constructor for a ShibPOSTProfile object
95          * 
96          * @param policies
97          *            Set of policy URIs that the implementation must support
98          * @param receiver
99          *            URL of SHIRE
100          * @param ttlSeconds
101          *            Length of time in seconds allowed to elapse from issuance of SAML response
102          * @exception SAMLException
103          *                Raised if a profile implementation cannot be constructed from the supplied information
104          */
105         public ShibPOSTProfile(Collection policies, String receiver, int ttlSeconds) throws SAMLException {
106                 if (policies == null || policies.size() == 0 || receiver == null || receiver.length() == 0 || ttlSeconds <= 0)
107                         throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
108
109                 this.receiver = receiver;
110                 this.ttlSeconds = ttlSeconds;
111                 this.policies.addAll(policies);
112         }
113
114         /**
115          * HS-side constructor for a ShibPOSTProfile object.
116          */
117         public ShibPOSTProfile() {}
118
119         /**
120          * Locates an assertion containing a "bearer" AuthenticationStatement in the response and validates the enclosing
121          * assertion with respect to the POST profile
122          * 
123          * @param r
124          *            The response to the accepting site
125          * @return An SSO assertion
126          * @throws SAMLException
127          *             Thrown if an SSO assertion can't be found
128          */
129         public SAMLAssertion getSSOAssertion(SAMLResponse r) throws SAMLException {
130                 return SAMLPOSTProfile.getSSOAssertion(r, policies);
131         }
132
133         /**
134          * Locates a "bearer" AuthenticationStatement in the assertion and validates the statement with respect to the POST
135          * profile
136          * 
137          * @param a
138          *            The SSO assertion sent to the accepting site
139          * @return A "bearer" authentication statement
140          * @throws SAMLException
141          *             Thrown if an SSO statement can't be found
142          */
143         public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a) throws SAMLException {
144                 return SAMLPOSTProfile.getSSOStatement(a);
145         }
146
147         /**
148          * Examines a response to determine the source site name
149          * 
150          * @param r
151          * @return
152          */
153         String getOriginSite(SAMLResponse r) {
154                 Iterator ia = r.getAssertions();
155                 while (ia.hasNext()) {
156                         Iterator is = ((SAMLAssertion) ia.next()).getStatements();
157                         while (is.hasNext()) {
158                                 SAMLStatement s = (SAMLStatement) is.next();
159                                 if (s instanceof SAMLAuthenticationStatement)
160                                         return ((SAMLAuthenticationStatement) s).getSubject().getName().getName();
161                         }
162                 }
163                 return null;
164         }
165
166         /**
167          * Parse a Base-64 encoded buffer back into a SAML response and test its validity against the POST profile,
168          * including use of the default replay cache
169          * <P>
170          * Also does trust evaluation based on the information available from the origin site mapper, in accordance with
171          * general Shibboleth processing semantics. Club-specific processing must be performed in a subclass.
172          * <P>
173          * 
174          * @param buf
175          *            A Base-64 encoded buffer containing a SAML response
176          * @param originSite
177          * @return SAML response sent by origin site
178          * @exception SAMLException
179          *                Thrown if the response cannot be understood or accepted
180          */
181         public SAMLResponse accept(byte[] buf, StringBuffer originSite) throws SAMLException {
182                 // The built-in SAML functionality will do most of the basic non-crypto checks.
183                 // Note that if the response only contains a status error, it gets
184                 // tossed out as an exception.
185                 SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds, false);
186
187                 if (originSite == null)
188                         originSite = new StringBuffer();
189
190                 // Now we do some more non-crypto (ie. cheap) work to match up the origin site
191                 // with its associated data. If we can't even find a SSO statement in
192                 // the response we just return the response to the caller, who will presumably
193                 // notice this.
194                 SAMLAssertion assertion = null;
195                 SAMLAuthenticationStatement sso = null;
196
197                 try {
198                         assertion = getSSOAssertion(r);
199                         sso = getSSOStatement(assertion);
200                 } catch (SAMLException e) {
201                         originSite.setLength(0);
202                         originSite.append(getOriginSite(r));
203                         throw e;
204                 }
205
206                 // Examine the subject information.
207                 SAMLSubject subject = sso.getSubject();
208                 if (subject.getName().getName() == null)
209                         throw new InvalidAssertionException(SAMLException.RESPONDER,
210                                         "ShibPOSTProfile.accept() requires subject name qualifier");
211
212                 originSite.setLength(0);
213                 originSite.append(subject.getName().getName());
214                 String handleService = assertion.getIssuer();
215
216                 // Is this a trusted HS?
217                 OriginSiteMapper mapper = Init.getMapper();
218                 Iterator hsNames = mapper.getHandleServiceNames(originSite.toString());
219                 boolean bFound = false;
220                 while (!bFound && hsNames.hasNext())
221                         if (hsNames.next().equals(handleService))
222                                 bFound = true;
223                 if (!bFound)
224                         throw new TrustException(SAMLException.RESPONDER,
225                                         "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
226
227                 Key hsKey = mapper.getHandleServiceKey(handleService);
228                 KeyStore ks = mapper.getTrustedRoots();
229
230                 // Signature verification now takes place. We check the assertion and
231                 // the response. Assertion signing is optional, response signing is mandatory.
232                 try {
233                         NDC.push("accept");
234                         if (assertion.isSigned()) {
235                                 log.info("verifying assertion signature");
236                                 verifySignature(assertion, handleService, ks, hsKey);
237                         }
238                         log.info("verifying response signature");
239                         verifySignature(r, handleService, ks, hsKey);
240                 } finally {
241                         NDC.pop();
242                 }
243                 return r;
244         }
245
246         /**
247          * Used by HS to generate a signed SAML response conforming to the POST profile
248          * <P>
249          * 
250          * @param recipient
251          *            URL of the assertion consumer
252          * @param relyingParty
253          *            the intended recipient of the response
254          * @param nameId
255          *            Name Identifier for the response
256          * @param subjectIP
257          *            Client address of subject (optional)
258          * @param authMethod
259          *            URI of authentication method being asserted
260          * @param authInstant
261          *            Date and time of authentication being asserted
262          * @param bindings
263          *            Set of SAML authorities the relying party may contact (optional)
264          * @return SAML response to send to accepting site
265          * @exception SAMLException
266          *                Base class of exceptions that may be thrown during processing
267          */
268         public SAMLResponse prepare(String recipient, HSRelyingParty relyingParty, SAMLNameIdentifier nameId,
269                         String subjectIP, String authMethod, Date authInstant, Collection bindings) throws SAMLException {
270
271                 Document doc = org.opensaml.XML.parserPool.newDocument();
272
273                 ArrayList audiences = new ArrayList();
274                 if (relyingParty.getProviderId() != null) {
275                         audiences.add(relyingParty.getProviderId());
276                 }
277                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
278                         audiences.add(relyingParty.getName());
279                 }
280
281                 String issuer = null;
282                 if (relyingParty.isLegacyProvider()) {
283                         
284                         log.debug("Service Provider is running Shibboleth <= 1.1.  Using old style issuer.");
285                         if (relyingParty.getIdentityProvider().getResponseSigningCredential() == null
286                                         || relyingParty.getIdentityProvider().getResponseSigningCredential().getX509Certificate() == null) {
287                                 throw new SAMLException("Cannot serve legacy style assertions without an X509 certificate");
288                         }
289                         issuer = getHostNameFromDN(relyingParty.getIdentityProvider().getResponseSigningCredential()
290                                         .getX509Certificate().getSubjectX500Principal());
291                         if (issuer == null || issuer.equals("")) {
292                                 throw new SAMLException("Error parsing certificate DN while determining legacy issuer name.");
293                         }
294
295                 } else {
296                         issuer = relyingParty.getIdentityProvider().getProviderId();
297                 }
298
299                 SAMLResponse r = SAMLPOSTProfile.prepare(recipient, issuer, audiences, nameId, subjectIP, authMethod,
300                                 authInstant, bindings);
301                 r.toDOM(doc);
302
303                 //Sign the assertions, if appropriate
304                 if (relyingParty.getIdentityProvider().getAssertionSigningCredential() != null
305                                 && relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey() != null) {
306
307                         String assertionAlgorithm;
308                         if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.RSA) {
309                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
310                         } else if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.DSA) {
311                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
312                         } else {
313                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
314                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
315                         }
316
317                         ((SAMLAssertion) r.getAssertions().next()).sign(assertionAlgorithm, relyingParty.getIdentityProvider()
318                                         .getAssertionSigningCredential().getPrivateKey(), Arrays.asList(relyingParty.getIdentityProvider()
319                                         .getAssertionSigningCredential().getX509CertificateChain()));
320                 }
321
322                 //Sign the response, if appropriate
323                 if (relyingParty.getIdentityProvider().getResponseSigningCredential() != null
324                                 && relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey() != null) {
325
326                         String responseAlgorithm;
327                         if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.RSA) {
328                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
329                         } else if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.DSA) {
330                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
331                         } else {
332                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
333                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
334                         }
335
336                         r.sign(responseAlgorithm,
337                                         relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey(), Arrays
338                                                         .asList(relyingParty.getIdentityProvider().getResponseSigningCredential()
339                                                                         .getX509CertificateChain()));
340                 }
341
342                 return r;
343         }
344
345         /**
346          * Searches the replay cache for the specified assertion and inserts a newly seen assertion into the cache
347          * <P>
348          * Also performs garbage collection of the cache by deleting expired entries.
349          * 
350          * @param a
351          *            The assertion to check
352          * @return true iff the assertion has not been seen before
353          */
354         public synchronized boolean checkReplayCache(SAMLAssertion a) {
355                 // Default implementation uses the basic replay cache implementation.
356                 return SAMLPOSTProfile.checkReplayCache(a);
357         }
358
359         /**
360          * Default signature verification algorithm uses an embedded X509 certificate(s) or an explicit key to verify the
361          * signature. The certificate is examined to insure the subject CN matches the signer, and that it is signed by a
362          * trusted CA
363          * 
364          * @param obj
365          *            The object containing the signature
366          * @param signerName
367          *            The name of the signer
368          * @param ks
369          *            A keystore containing trusted root certificates
370          * @param knownKey
371          *            An explicit key to use if a certificate cannot be found
372          * @param simple
373          *            Verify according to simple SAML signature profile?
374          * @throws SAMLException
375          *             Thrown if the signature cannot be verified
376          */
377         protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
378                         throws SAMLException {
379                 try {
380                         NDC.push("verifySignature");
381
382                         if (!obj.isSigned()) {
383                                 log.error("unable to find a signature");
384                                 throw new TrustException(SAMLException.RESPONDER,
385                                                 "ShibPOSTProfile.verifySignature() given an unsigned object");
386                         }
387
388                         if (knownKey != null) {
389                                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
390                                 obj.verify(knownKey);
391                                 return;
392                         }
393
394                         log.info("verifying signature with embedded KeyInfo");
395                         obj.verify();
396
397                         // This is pretty painful, and this is leveraging the supposedly
398                         // automatic support in JDK 1.4.
399                         // First we have to extract the certificates from the object.
400                         Iterator certs_from_obj = obj.getX509Certificates();
401                         if (!certs_from_obj.hasNext()) {
402                                 log.error("need certificates inside object to establish trust");
403                                 throw new TrustException(SAMLException.RESPONDER,
404                                                 "ShibPOSTProfile.verifySignature() can't find any certificates");
405                         }
406
407                         // We assume the first one in the set is the end entity cert.
408                         X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
409
410                         // Match the CN of the entity cert with the expected signer.
411                         String dname = entity_cert.getSubjectDN().getName();
412                         log.debug("found entity cert with DN: " + dname);
413                         String cname = "CN=" + signerName;
414                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
415                                 log
416                                                 .error("verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
417                                                                 + signerName);
418                                 throw new TrustException(SAMLException.RESPONDER,
419                                                 "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
420                         }
421
422                         // Prep a chain between the entity cert and the trusted roots.
423                         X509CertSelector targetConstraints = new X509CertSelector();
424                         targetConstraints.setCertificate(entity_cert);
425                         PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
426                         params.setMaxPathLength(-1);
427
428                         Vector certbag = new Vector();
429                         certbag.add(entity_cert);
430                         while (certs_from_obj.hasNext())
431                                 certbag.add(certs_from_obj.next());
432                         CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
433                         CertStore store = CertStore.getInstance("Collection", ccsp);
434                         params.addCertStore(store);
435
436                         // Attempt to build a path.
437                         CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
438                         PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
439                 } catch (CertPathBuilderException e) {
440                         log.error("caught a cert path builder exception: " + e.getMessage());
441                         throw new TrustException(SAMLException.RESPONDER,
442                                         "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
443                 } catch (GeneralSecurityException e) {
444                         log.error("caught a general security exception: " + e.getMessage());
445                         throw new TrustException(SAMLException.RESPONDER,
446                                         "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path", e);
447                 } finally {
448                         NDC.pop();
449                 }
450         }
451
452         public static String getHostNameFromDN(X500Principal dn) {
453                 Matcher matches = regex.matcher(dn.getName(X500Principal.RFC2253));
454                 if (!matches.find() || matches.groupCount() > 1) {
455                         log.error("Unable to extract host name name from certificate subject DN.");
456                         return null;
457                 }
458                 return matches.group(1);
459         }
460 }