00ce93002e3f2a6b6e956ab2cc80db1356ba2eed
[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          * @param responseCredential
299          *            Credential to use for signing the SAML Response
300          * @param assertaionCredential
301          *            Credential to use for signing any SAML Assertions contained
302          *            in the Response
303          * @return SAML response to send to accepting site
304          * @exception SAMLException
305          *                Base class of exceptions that may be thrown during
306          *                processing
307          */
308         public SAMLResponse prepare(
309                 String recipient,
310                 RelyingParty relyingParty,
311                 SAMLNameIdentifier nameId,
312                 String subjectIP,
313                 String authMethod,
314                 Date authInstant,
315                 Collection bindings,
316                 Credential responseCredential,
317                 Credential assertionCredential)
318                 throws SAMLException {
319
320                 if (responseCredential == null || responseCredential.getPrivateKey() == null) {
321                         throw new InvalidCryptoException(
322                                 SAMLException.RESPONDER,
323                                 "ShibPOSTProfile.prepare() requires a response key.");
324                 }
325                 
326                 String responseAlgorithm;
327                 if (responseCredential.getCredentialType() == Credential.RSA) {
328                         responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
329                 } else if (responseCredential.getCredentialType() == Credential.DSA) {
330                         responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
331                 } else {
332                         throw new InvalidCryptoException(
333                                 SAMLException.RESPONDER,
334                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
335                 }
336
337                 Document doc = org.opensaml.XML.parserPool.newDocument();
338
339                 ArrayList audiences = new ArrayList();
340                 audiences.add(relyingParty.getProviderId());
341                 audiences.add(relyingParty.getName());
342
343                 SAMLResponse r =
344                         SAMLPOSTProfile.prepare(
345                                 recipient,
346                                 relyingParty.getConfigProperty("edu.internet2.middleware.shibboleth.hs.HandleServlet.issuer"),
347                                 audiences,
348                                 nameId,
349                                 subjectIP,
350                                 authMethod,
351                                 authInstant,
352                                 bindings);
353                 r.toDOM(doc);
354
355                 if (assertionCredential != null & assertionCredential.getPrivateKey() != null) {
356
357                         String assertionAlgorithm;
358                         if (assertionCredential.getCredentialType() == Credential.RSA) {
359                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
360                         } else if (assertionCredential.getCredentialType() == 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                                 assertionCredential.getPrivateKey(),
371                                 Arrays.asList(assertionCredential.getX509CertificateChain()));
372                 }
373
374                 r.sign(
375                         responseAlgorithm,
376                         responseCredential.getPrivateKey(),
377                         Arrays.asList(responseCredential.getX509CertificateChain()));
378
379                 return r;
380         }
381
382         /**
383          * Searches the replay cache for the specified assertion and inserts a
384          * newly seen assertion into the cache
385          * <P>
386          * 
387          * Also performs garbage collection of the cache by deleting expired
388          * entries.
389          * 
390          * @param a
391          *            The assertion to check
392          * @return true iff the assertion has not been seen before
393          */
394         public synchronized boolean checkReplayCache(SAMLAssertion a) {
395                 // Default implementation uses the basic replay cache implementation.
396                 return SAMLPOSTProfile.checkReplayCache(a);
397         }
398
399         /**
400          * Default signature verification algorithm uses an embedded X509
401          * certificate(s) or an explicit key to verify the signature. The
402          * certificate is examined to insure the subject CN matches the signer, and
403          * that it is signed by a trusted CA
404          * 
405          * @param obj
406          *            The object containing the signature
407          * @param signerName
408          *            The name of the signer
409          * @param ks
410          *            A keystore containing trusted root certificates
411          * @param knownKey
412          *            An explicit key to use if a certificate cannot be found
413          * @param simple
414          *            Verify according to simple SAML signature profile?
415          * 
416          * @throws SAMLException
417          *             Thrown if the signature cannot be verified
418          */
419         protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
420                 throws SAMLException {
421                 try {
422                         NDC.push("verifySignature");
423
424                         if (!obj.isSigned()) {
425                                 log.error("unable to find a signature");
426                                 throw new TrustException(
427                                         SAMLException.RESPONDER,
428                                         "ShibPOSTProfile.verifySignature() given an unsigned object");
429                         }
430
431                         if (knownKey != null) {
432                                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
433                                 obj.verify(knownKey);
434                                 return;
435                         }
436
437                         log.info("verifying signature with embedded KeyInfo");
438                         obj.verify();
439
440                         // This is pretty painful, and this is leveraging the supposedly
441                         // automatic support in JDK 1.4.
442                         // First we have to extract the certificates from the object.
443                         Iterator certs_from_obj = obj.getX509Certificates();
444                         if (!certs_from_obj.hasNext()) {
445                                 log.error("need certificates inside object to establish trust");
446                                 throw new TrustException(
447                                         SAMLException.RESPONDER,
448                                         "ShibPOSTProfile.verifySignature() can't find any certificates");
449                         }
450
451                         // We assume the first one in the set is the end entity cert.
452                         X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
453
454                         // Match the CN of the entity cert with the expected signer.
455                         String dname = entity_cert.getSubjectDN().getName();
456                         log.debug("found entity cert with DN: " + dname);
457                         String cname = "CN=" + signerName;
458                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
459                                 log.error(
460                                         "verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
461                                                 + signerName);
462                                 throw new TrustException(
463                                         SAMLException.RESPONDER,
464                                         "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
465                         }
466
467                         // Prep a chain between the entity cert and the trusted roots.
468                         X509CertSelector targetConstraints = new X509CertSelector();
469                         targetConstraints.setCertificate(entity_cert);
470                         PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
471                         params.setMaxPathLength(-1);
472
473                         Vector certbag = new Vector();
474                         certbag.add(entity_cert);
475                         while (certs_from_obj.hasNext())
476                                 certbag.add(certs_from_obj.next());
477                         CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
478                         CertStore store = CertStore.getInstance("Collection", ccsp);
479                         params.addCertStore(store);
480
481                         // Attempt to build a path.
482                         CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
483                         PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
484                 } catch (CertPathBuilderException e) {
485                         log.error("caught a cert path builder exception: " + e.getMessage());
486                         throw new TrustException(
487                                 SAMLException.RESPONDER,
488                                 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
489                                 e);
490                 } catch (GeneralSecurityException e) {
491                         log.error("caught a general security exception: " + e.getMessage());
492                         throw new TrustException(
493                                 SAMLException.RESPONDER,
494                                 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
495                                 e);
496                 } finally {
497                         NDC.pop();
498                 }
499         }
500 }