More work converting to multi-federation HS.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / ShibPOSTProfile.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation
3  * for Advanced Internet Development, Inc. All rights reserved
4  * 
5  * 
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  * 
9  * Redistributions of source code must retain the above copyright notice, this
10  * list of conditions and the following disclaimer.
11  * 
12  * Redistributions in binary form must reproduce the above copyright notice,
13  * this list of conditions and the following disclaimer in the documentation
14  * and/or other materials provided with the distribution, if any, must include
15  * the following acknowledgment: "This product includes software developed by
16  * the University Corporation for Advanced Internet Development
17  * <http://www.ucaid.edu> Internet2 Project. Alternately, this acknowledegement
18  * may appear in the software itself, if and wherever such third-party
19  * acknowledgments normally appear.
20  * 
21  * Neither the name of Shibboleth nor the names of its contributors, nor
22  * Internet2, nor the University Corporation for Advanced Internet Development,
23  * Inc., nor UCAID may be used to endorse or promote products derived from this
24  * software without specific prior written permission. For written permission,
25  * please contact shibboleth@shibboleth.org
26  * 
27  * Products derived from this software may not be called Shibboleth, Internet2,
28  * UCAID, or the University Corporation for Advanced Internet Development, nor
29  * may Shibboleth appear in their name, without prior written permission of the
30  * University Corporation for Advanced Internet Development.
31  * 
32  * 
33  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
34  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
36  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
37  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
38  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
39  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY
40  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
41  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
42  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
45  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46  */
47
48 package edu.internet2.middleware.shibboleth.common;
49
50 import java.security.GeneralSecurityException;
51 import java.security.Key;
52 import java.security.KeyStore;
53 import java.security.cert.CertPathBuilder;
54 import java.security.cert.CertPathBuilderException;
55 import java.security.cert.CertStore;
56 import java.security.cert.CollectionCertStoreParameters;
57 import java.security.cert.PKIXBuilderParameters;
58 import java.security.cert.PKIXCertPathBuilderResult;
59 import java.security.cert.X509CertSelector;
60 import java.security.cert.X509Certificate;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.Collection;
64 import java.util.Date;
65 import java.util.Iterator;
66 import java.util.Vector;
67
68 import org.apache.log4j.Logger;
69 import org.apache.log4j.NDC;
70 import org.apache.xml.security.signature.XMLSignature;
71 import org.opensaml.InvalidAssertionException;
72 import org.opensaml.InvalidCryptoException;
73 import org.opensaml.SAMLAssertion;
74 import org.opensaml.SAMLAuthenticationStatement;
75 import org.opensaml.SAMLException;
76 import org.opensaml.SAMLNameIdentifier;
77 import org.opensaml.SAMLPOSTProfile;
78 import org.opensaml.SAMLResponse;
79 import org.opensaml.SAMLSignedObject;
80 import org.opensaml.SAMLStatement;
81 import org.opensaml.SAMLSubject;
82 import org.opensaml.TrustException;
83 import org.w3c.dom.Document;
84
85 /**
86  * Basic Shibboleth POST browser profile implementation with basic support for
87  * signing
88  * 
89  * @author Scott Cantor @created April 11, 2002
90  */
91 public class ShibPOSTProfile {
92         /** XML Signature algorithm to apply */
93         protected String algorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
94
95         /** Policy URIs to attach or check against */
96         protected ArrayList policies = new ArrayList();
97
98         /** Official name of issuing site */
99         protected String issuer = null;
100
101         /** The URL of the receiving SHIRE */
102         protected String receiver = null;
103
104         /** Seconds allowed to elapse from issuance of response */
105         protected int ttlSeconds = 0;
106
107         private static Logger log = Logger.getLogger(ShibPOSTProfile.class.getName());
108
109         /**
110          * SHIRE-side constructor for a ShibPOSTProfile object
111          * 
112          * @param policies
113          *            Set of policy URIs that the implementation must support
114          * @param receiver
115          *            URL of SHIRE
116          * @param ttlSeconds
117          *            Length of time in seconds allowed to elapse from issuance of
118          *            SAML response
119          * @exception SAMLException
120          *                Raised if a profile implementation cannot be constructed
121          *                from the supplied information
122          */
123         public ShibPOSTProfile(Collection policies, String receiver, int ttlSeconds) throws SAMLException {
124                 if (policies == null || policies.size() == 0 || receiver == null || receiver.length() == 0 || ttlSeconds <= 0)
125                         throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
126
127                 this.receiver = receiver;
128                 this.ttlSeconds = ttlSeconds;
129                 this.policies.addAll(policies);
130         }
131         /**
132          * HS-side constructor for a ShibPOSTProfile object.
133          *  
134          */
135         public ShibPOSTProfile() {
136         }
137
138         /**
139          * Locates an assertion containing a "bearer" AuthenticationStatement in
140          * the response and validates the enclosing assertion with respect to the
141          * POST profile
142          * 
143          * @param r
144          *            The response to the accepting site
145          * @return An SSO assertion
146          * 
147          * @throws SAMLException
148          *             Thrown if an SSO assertion can't be found
149          */
150         public SAMLAssertion getSSOAssertion(SAMLResponse r) throws SAMLException {
151                 return SAMLPOSTProfile.getSSOAssertion(r, policies);
152         }
153
154         /**
155          * Locates a "bearer" AuthenticationStatement in the assertion and
156          * validates the statement with respect to the POST profile
157          * 
158          * @param a
159          *            The SSO assertion sent to the accepting site
160          * @return A "bearer" authentication statement
161          * 
162          * @throws SAMLException
163          *             Thrown if an SSO statement can't be found
164          */
165         public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a) throws SAMLException {
166                 return SAMLPOSTProfile.getSSOStatement(a);
167         }
168
169         /**
170          * Examines a response to determine the source site name
171          * 
172          * @param r
173          * @return
174          */
175         String getOriginSite(SAMLResponse r) {
176                 Iterator ia = r.getAssertions();
177                 while (ia.hasNext()) {
178                         Iterator is = ((SAMLAssertion) ia.next()).getStatements();
179                         while (is.hasNext()) {
180                                 SAMLStatement s = (SAMLStatement) is.next();
181                                 if (s instanceof SAMLAuthenticationStatement)
182                                         return ((SAMLAuthenticationStatement) s).getSubject().getName().getName();
183                         }
184                 }
185                 return null;
186         }
187
188         /**
189          * Parse a Base-64 encoded buffer back into a SAML response and test its
190          * validity against the POST profile, including use of the default replay
191          * cache
192          * <P>
193          * 
194          * Also does trust evaluation based on the information available from the
195          * origin site mapper, in accordance with general Shibboleth processing
196          * semantics. Club-specific processing must be performed in a subclass.
197          * <P>
198          * 
199          * @param buf
200          *            A Base-64 encoded buffer containing a SAML response
201          * @param originSite
202          * @return SAML response sent by origin site
203          * @exception SAMLException
204          *                Thrown if the response cannot be understood or accepted
205          */
206         public SAMLResponse accept(byte[] buf, StringBuffer originSite) throws SAMLException {
207                 // The built-in SAML functionality will do most of the basic non-crypto
208                 // checks.
209                 // Note that if the response only contains a status error, it gets
210                 // tossed out
211                 // as an exception.
212                 SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds, false);
213
214                 if (originSite == null)
215                         originSite = new StringBuffer();
216
217                 // Now we do some more non-crypto (ie. cheap) work to match up the
218                 // origin site
219                 // with its associated data. If we can't even find a SSO statement in
220                 // the response
221                 // we just return the response to the caller, who will presumably
222                 // notice this.
223                 SAMLAssertion assertion = null;
224                 SAMLAuthenticationStatement sso = null;
225
226                 try {
227                         assertion = getSSOAssertion(r);
228                         sso = getSSOStatement(assertion);
229                 } catch (SAMLException e) {
230                         originSite.setLength(0);
231                         originSite.append(getOriginSite(r));
232                         throw e;
233                 }
234
235                 // Examine the subject information.
236                 SAMLSubject subject = sso.getSubject();
237                 if (subject.getName().getName() == null)
238                         throw new InvalidAssertionException(
239                                 SAMLException.RESPONDER,
240                                 "ShibPOSTProfile.accept() requires subject name qualifier");
241
242                 originSite.setLength(0);
243                 originSite.append(subject.getName().getName());
244                 String handleService = assertion.getIssuer();
245
246                 // Is this a trusted HS?
247                 OriginSiteMapper mapper = Init.getMapper();
248                 Iterator hsNames = mapper.getHandleServiceNames(originSite.toString());
249                 boolean bFound = false;
250                 while (!bFound && hsNames.hasNext())
251                         if (hsNames.next().equals(handleService))
252                                 bFound = true;
253                 if (!bFound)
254                         throw new TrustException(
255                                 SAMLException.RESPONDER,
256                                 "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
257
258                 Key hsKey = mapper.getHandleServiceKey(handleService);
259                 KeyStore ks = mapper.getTrustedRoots();
260
261                 // Signature verification now takes place. We check the assertion and
262                 // the response.
263                 // Assertion signing is optional, response signing is mandatory.
264                 try {
265                         NDC.push("accept");
266                         if (assertion.isSigned()) {
267                                 log.info("verifying assertion signature");
268                                 verifySignature(assertion, handleService, ks, hsKey);
269                         }
270                         log.info("verifying response signature");
271                         verifySignature(r, handleService, ks, hsKey);
272                 } finally {
273                         NDC.pop();
274                 }
275                 return r;
276         }
277
278         /**
279          * Used by HS to generate a signed SAML response conforming to the POST
280          * profile
281          * <P>
282          * 
283          * @param recipient
284          *            URL of the assertion consumer
285          * @param relyingParty
286          *            the intended recipient of the response
287          * @param nameId
288          *            Name Identifier for the response
289          * @param subjectIP
290          *            Client address of subject (optional)
291          * @param authMethod
292          *            URI of authentication method being asserted
293          * @param authInstant
294          *            Date and time of authentication being asserted
295          * @param bindings
296          *            Set of SAML authorities the relying party may contact
297          *            (optional)
298          * @return SAML response to send to accepting site
299          * @exception SAMLException
300          *                Base class of exceptions that may be thrown during
301          *                processing
302          */
303         public SAMLResponse prepare(
304                 String recipient,
305                 RelyingParty relyingParty,
306                 SAMLNameIdentifier nameId,
307                 String subjectIP,
308                 String authMethod,
309                 Date authInstant,
310                 Collection bindings)
311                 throws SAMLException {
312
313                 if (relyingParty.getIdentityProvider().getResponseSigningCredential() == null
314                         || relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey() == null) {
315                         throw new InvalidCryptoException(
316                                 SAMLException.RESPONDER,
317                                 "ShibPOSTProfile.prepare() requires a response key.");
318                 }
319
320                 String responseAlgorithm;
321                 if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.RSA) {
322                         responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
323                 } else if (
324                         relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.DSA) {
325                         responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
326                 } else {
327                         throw new InvalidCryptoException(
328                                 SAMLException.RESPONDER,
329                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
330                 }
331
332                 Document doc = org.opensaml.XML.parserPool.newDocument();
333
334                 ArrayList audiences = new ArrayList();
335                 audiences.add(relyingParty.getProviderId());
336                 if (!relyingParty.getProviderId().equals(relyingParty.getName()))
337                         audiences.add(relyingParty.getName());
338
339                 String issuer;
340                 if (relyingParty.isLegacyProvider()) {
341                         //TODO must resolve this
342                         issuer = "fooIssuer";
343                 } else {
344                         issuer = relyingParty.getProviderId();
345                 }
346
347                 SAMLResponse r =
348                         SAMLPOSTProfile.prepare(recipient, issuer, audiences, nameId, subjectIP, authMethod, authInstant, bindings);
349                 r.toDOM(doc);
350
351                 if (relyingParty.getIdentityProvider().getAssertionSigningCredential() != null
352                         && relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey() != null) {
353
354                         String assertionAlgorithm;
355                         if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType()
356                                 == Credential.RSA) {
357                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
358                         } else if (
359                                 relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType()
360                                         == Credential.DSA) {
361                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
362                         } else {
363                                 throw new InvalidCryptoException(
364                                         SAMLException.RESPONDER,
365                                         "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
366                         }
367
368                         ((SAMLAssertion) r.getAssertions().next()).sign(
369                                 assertionAlgorithm,
370                                 relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey(),
371                                 Arrays.asList(
372                                         relyingParty.getIdentityProvider().getAssertionSigningCredential().getX509CertificateChain()));
373                 }
374
375                 r.sign(
376                         responseAlgorithm,
377                         relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey(),
378                         Arrays.asList(relyingParty.getIdentityProvider().getResponseSigningCredential().getX509CertificateChain()));
379
380                 return r;
381         }
382
383         /**
384          * Searches the replay cache for the specified assertion and inserts a
385          * newly seen assertion into the cache
386          * <P>
387          * 
388          * Also performs garbage collection of the cache by deleting expired
389          * entries.
390          * 
391          * @param a
392          *            The assertion to check
393          * @return true iff the assertion has not been seen before
394          */
395         public synchronized boolean checkReplayCache(SAMLAssertion a) {
396                 // Default implementation uses the basic replay cache implementation.
397                 return SAMLPOSTProfile.checkReplayCache(a);
398         }
399
400         /**
401          * Default signature verification algorithm uses an embedded X509
402          * certificate(s) or an explicit key to verify the signature. The
403          * certificate is examined to insure the subject CN matches the signer, and
404          * that it is signed by a trusted CA
405          * 
406          * @param obj
407          *            The object containing the signature
408          * @param signerName
409          *            The name of the signer
410          * @param ks
411          *            A keystore containing trusted root certificates
412          * @param knownKey
413          *            An explicit key to use if a certificate cannot be found
414          * @param simple
415          *            Verify according to simple SAML signature profile?
416          * 
417          * @throws SAMLException
418          *             Thrown if the signature cannot be verified
419          */
420         protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
421                 throws SAMLException {
422                 try {
423                         NDC.push("verifySignature");
424
425                         if (!obj.isSigned()) {
426                                 log.error("unable to find a signature");
427                                 throw new TrustException(
428                                         SAMLException.RESPONDER,
429                                         "ShibPOSTProfile.verifySignature() given an unsigned object");
430                         }
431
432                         if (knownKey != null) {
433                                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
434                                 obj.verify(knownKey);
435                                 return;
436                         }
437
438                         log.info("verifying signature with embedded KeyInfo");
439                         obj.verify();
440
441                         // This is pretty painful, and this is leveraging the supposedly
442                         // automatic support in JDK 1.4.
443                         // First we have to extract the certificates from the object.
444                         Iterator certs_from_obj = obj.getX509Certificates();
445                         if (!certs_from_obj.hasNext()) {
446                                 log.error("need certificates inside object to establish trust");
447                                 throw new TrustException(
448                                         SAMLException.RESPONDER,
449                                         "ShibPOSTProfile.verifySignature() can't find any certificates");
450                         }
451
452                         // We assume the first one in the set is the end entity cert.
453                         X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
454
455                         // Match the CN of the entity cert with the expected signer.
456                         String dname = entity_cert.getSubjectDN().getName();
457                         log.debug("found entity cert with DN: " + dname);
458                         String cname = "CN=" + signerName;
459                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
460                                 log.error(
461                                         "verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
462                                                 + signerName);
463                                 throw new TrustException(
464                                         SAMLException.RESPONDER,
465                                         "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
466                         }
467
468                         // Prep a chain between the entity cert and the trusted roots.
469                         X509CertSelector targetConstraints = new X509CertSelector();
470                         targetConstraints.setCertificate(entity_cert);
471                         PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
472                         params.setMaxPathLength(-1);
473
474                         Vector certbag = new Vector();
475                         certbag.add(entity_cert);
476                         while (certs_from_obj.hasNext())
477                                 certbag.add(certs_from_obj.next());
478                         CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
479                         CertStore store = CertStore.getInstance("Collection", ccsp);
480                         params.addCertStore(store);
481
482                         // Attempt to build a path.
483                         CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
484                         PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
485                 } catch (CertPathBuilderException e) {
486                         log.error("caught a cert path builder exception: " + e.getMessage());
487                         throw new TrustException(
488                                 SAMLException.RESPONDER,
489                                 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
490                                 e);
491                 } catch (GeneralSecurityException e) {
492                         log.error("caught a general security exception: " + e.getMessage());
493                         throw new TrustException(
494                                 SAMLException.RESPONDER,
495                                 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
496                                 e);
497                 } finally {
498                         NDC.pop();
499                 }
500         }
501 }