Convert ShibPOSTProfile to use new NameIdentifier and Credential classes.
[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         /**
133          * HS-side constructor for a ShibPOSTProfile object
134          * 
135          * @param policies
136          *            Set of policy URIs that the implementation must support
137          * @param issuer
138          *            "Official" name of issuing origin site
139          * @exception SAMLException
140          *                Raised if a profile implementation cannot be constructed
141          *                from the supplied information
142          */
143         public ShibPOSTProfile(Collection policies, String issuer) throws SAMLException {
144                 if (policies == null || policies.size() == 0 || issuer == null || issuer.length() == 0)
145                         throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
146                 this.issuer = issuer;
147                 this.policies.addAll(policies);
148         }
149
150         /**
151          * Locates an assertion containing a "bearer" AuthenticationStatement in
152          * the response and validates the enclosing assertion with respect to the
153          * POST profile
154          * 
155          * @param r
156          *            The response to the accepting site
157          * @return An SSO assertion
158          * 
159          * @throws SAMLException
160          *             Thrown if an SSO assertion can't be found
161          */
162         public SAMLAssertion getSSOAssertion(SAMLResponse r) throws SAMLException {
163                 return SAMLPOSTProfile.getSSOAssertion(r, policies);
164         }
165
166         /**
167          * Locates a "bearer" AuthenticationStatement in the assertion and
168          * validates the statement with respect to the POST profile
169          * 
170          * @param a
171          *            The SSO assertion sent to the accepting site
172          * @return A "bearer" authentication statement
173          * 
174          * @throws SAMLException
175          *             Thrown if an SSO statement can't be found
176          */
177         public SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a) throws SAMLException {
178                 return SAMLPOSTProfile.getSSOStatement(a);
179         }
180
181         /**
182          * Examines a response to determine the source site name
183          * 
184          * @param r
185          * @return
186          */
187         String getOriginSite(SAMLResponse r) {
188                 Iterator ia = r.getAssertions();
189                 while (ia.hasNext()) {
190                         Iterator is = ((SAMLAssertion) ia.next()).getStatements();
191                         while (is.hasNext()) {
192                                 SAMLStatement s = (SAMLStatement) is.next();
193                                 if (s instanceof SAMLAuthenticationStatement)
194                                         return ((SAMLAuthenticationStatement) s).getSubject().getName().getName();
195                         }
196                 }
197                 return null;
198         }
199
200         /**
201          * Parse a Base-64 encoded buffer back into a SAML response and test its
202          * validity against the POST profile, including use of the default replay
203          * cache
204          * <P>
205          * 
206          * Also does trust evaluation based on the information available from the
207          * origin site mapper, in accordance with general Shibboleth processing
208          * semantics. Club-specific processing must be performed in a subclass.
209          * <P>
210          * 
211          * @param buf
212          *            A Base-64 encoded buffer containing a SAML response
213          * @param originSite
214          * @return SAML response sent by origin site
215          * @exception SAMLException
216          *                Thrown if the response cannot be understood or accepted
217          */
218         public SAMLResponse accept(byte[] buf, StringBuffer originSite) throws SAMLException {
219                 // The built-in SAML functionality will do most of the basic non-crypto
220                 // checks.
221                 // Note that if the response only contains a status error, it gets
222                 // tossed out
223                 // as an exception.
224                 SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds, false);
225
226                 if (originSite == null)
227                         originSite = new StringBuffer();
228
229                 // Now we do some more non-crypto (ie. cheap) work to match up the
230                 // origin site
231                 // with its associated data. If we can't even find a SSO statement in
232                 // the response
233                 // we just return the response to the caller, who will presumably
234                 // notice this.
235                 SAMLAssertion assertion = null;
236                 SAMLAuthenticationStatement sso = null;
237
238                 try {
239                         assertion = getSSOAssertion(r);
240                         sso = getSSOStatement(assertion);
241                 } catch (SAMLException e) {
242                         originSite.setLength(0);
243                         originSite.append(getOriginSite(r));
244                         throw e;
245                 }
246
247                 // Examine the subject information.
248                 SAMLSubject subject = sso.getSubject();
249                 if (subject.getName().getName() == null)
250                         throw new InvalidAssertionException(
251                                 SAMLException.RESPONDER,
252                                 "ShibPOSTProfile.accept() requires subject name qualifier");
253
254                 originSite.setLength(0);
255                 originSite.append(subject.getName().getName());
256                 String handleService = assertion.getIssuer();
257
258                 // Is this a trusted HS?
259                 OriginSiteMapper mapper = Init.getMapper();
260                 Iterator hsNames = mapper.getHandleServiceNames(originSite.toString());
261                 boolean bFound = false;
262                 while (!bFound && hsNames.hasNext())
263                         if (hsNames.next().equals(handleService))
264                                 bFound = true;
265                 if (!bFound)
266                         throw new TrustException(
267                                 SAMLException.RESPONDER,
268                                 "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
269
270                 Key hsKey = mapper.getHandleServiceKey(handleService);
271                 KeyStore ks = mapper.getTrustedRoots();
272
273                 // Signature verification now takes place. We check the assertion and
274                 // the response.
275                 // Assertion signing is optional, response signing is mandatory.
276                 try {
277                         NDC.push("accept");
278                         if (assertion.isSigned()) {
279                                 log.info("verifying assertion signature");
280                                 verifySignature(assertion, handleService, ks, hsKey);
281                         }
282                         log.info("verifying response signature");
283                         verifySignature(r, handleService, ks, hsKey);
284                 } finally {
285                         NDC.pop();
286                 }
287                 return r;
288         }
289
290         /**
291          * Used by HS to generate a signed SAML response conforming to the POST
292          * profile
293          * <P>
294          * 
295          * @param recipient
296          *            URL of intended consumer
297          * @param nameId
298          *            Name Identifier for the response
299          * @param subjectIP
300          *            Client address of subject (optional)
301          * @param authMethod
302          *            URI of authentication method being asserted
303          * @param authInstant
304          *            Date and time of authentication being asserted
305          * @param bindings
306          *            Set of SAML authorities the relying party may contact
307          *            (optional)
308          * @param responseCredential
309          *            Credential to use for signing the SAML Response
310          * @param assertaionCredential
311          *            Credential to use for signing any SAML Assertions contained
312          *            in the Response
313          * @return SAML response to send to accepting site
314          * @exception SAMLException
315          *                Base class of exceptions that may be thrown during
316          *                processing
317          */
318         public SAMLResponse prepare(
319                 String recipient,
320                 SAMLNameIdentifier nameId,
321                 String subjectIP,
322                 String authMethod,
323                 Date authInstant,
324                 Collection bindings,
325                 Credential responseCredential,
326                 Credential assertionCredential)
327                 throws SAMLException {
328
329                 //TODO This typing is only a strawman... will definitely need to revist as
330                 // we support additional types
331                 if ((responseCredential != null && responseCredential.getCredentialType() != Credential.X509)
332                         || (assertionCredential != null && assertionCredential.getCredentialType() != Credential.X509)) {
333
334                 }
335
336                 if (responseCredential == null || responseCredential.getPrivateKey() == null)
337                         throw new InvalidCryptoException(
338                                 SAMLException.RESPONDER,
339                                 "ShibPOSTProfile.prepare() requires a response key.");
340
341                 Document doc = org.opensaml.XML.parserPool.newDocument();
342
343                 SAMLResponse r =
344                         SAMLPOSTProfile.prepare(recipient, issuer, policies, nameId, subjectIP, authMethod, authInstant, bindings);
345                 r.toDOM(doc);
346
347                 if (assertionCredential != null & assertionCredential.getPrivateKey() != null)
348                         ((SAMLAssertion) r.getAssertions().next()).sign(
349                                 XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,
350                                 assertionCredential.getPrivateKey(),
351                                 Arrays.asList(assertionCredential.getX509CertificateChain()));
352
353                 r.sign(
354                         XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,
355                         responseCredential.getPrivateKey(),
356                         Arrays.asList(responseCredential.getX509CertificateChain()));
357
358                 return r;
359         }
360
361         /**
362          * Searches the replay cache for the specified assertion and inserts a
363          * newly seen assertion into the cache
364          * <P>
365          * 
366          * Also performs garbage collection of the cache by deleting expired
367          * entries.
368          * 
369          * @param a
370          *            The assertion to check
371          * @return true iff the assertion has not been seen before
372          */
373         public synchronized boolean checkReplayCache(SAMLAssertion a) {
374                 // Default implementation uses the basic replay cache implementation.
375                 return SAMLPOSTProfile.checkReplayCache(a);
376         }
377
378         /**
379          * Default signature verification algorithm uses an embedded X509
380          * certificate(s) or an explicit key to verify the signature. The
381          * certificate is examined to insure the subject CN matches the signer, and
382          * that it is signed by a trusted CA
383          * 
384          * @param obj
385          *            The object containing the signature
386          * @param signerName
387          *            The name of the signer
388          * @param ks
389          *            A keystore containing trusted root certificates
390          * @param knownKey
391          *            An explicit key to use if a certificate cannot be found
392          * @param simple
393          *            Verify according to simple SAML signature profile?
394          * 
395          * @throws SAMLException
396          *             Thrown if the signature cannot be verified
397          */
398         protected void verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
399                 throws SAMLException {
400                 try {
401                         NDC.push("verifySignature");
402
403                         if (!obj.isSigned()) {
404                                 log.error("unable to find a signature");
405                                 throw new TrustException(
406                                         SAMLException.RESPONDER,
407                                         "ShibPOSTProfile.verifySignature() given an unsigned object");
408                         }
409
410                         if (knownKey != null) {
411                                 log.info("verifying signature with known key value, ignoring signature KeyInfo");
412                                 obj.verify(knownKey);
413                                 return;
414                         }
415
416                         log.info("verifying signature with embedded KeyInfo");
417                         obj.verify();
418
419                         // This is pretty painful, and this is leveraging the supposedly
420                         // automatic support in JDK 1.4.
421                         // First we have to extract the certificates from the object.
422                         Iterator certs_from_obj = obj.getX509Certificates();
423                         if (!certs_from_obj.hasNext()) {
424                                 log.error("need certificates inside object to establish trust");
425                                 throw new TrustException(
426                                         SAMLException.RESPONDER,
427                                         "ShibPOSTProfile.verifySignature() can't find any certificates");
428                         }
429
430                         // We assume the first one in the set is the end entity cert.
431                         X509Certificate entity_cert = (X509Certificate) certs_from_obj.next();
432
433                         // Match the CN of the entity cert with the expected signer.
434                         String dname = entity_cert.getSubjectDN().getName();
435                         log.debug("found entity cert with DN: " + dname);
436                         String cname = "CN=" + signerName;
437                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1)) {
438                                 log.error(
439                                         "verifySignature() found a mismatch between the entity certificate's DN and the expected signer: "
440                                                 + signerName);
441                                 throw new TrustException(
442                                         SAMLException.RESPONDER,
443                                         "ShibPOSTProfile.verifySignature() found mismatch between entity certificate and expected signer");
444                         }
445
446                         // Prep a chain between the entity cert and the trusted roots.
447                         X509CertSelector targetConstraints = new X509CertSelector();
448                         targetConstraints.setCertificate(entity_cert);
449                         PKIXBuilderParameters params = new PKIXBuilderParameters(ks, targetConstraints);
450                         params.setMaxPathLength(-1);
451
452                         Vector certbag = new Vector();
453                         certbag.add(entity_cert);
454                         while (certs_from_obj.hasNext())
455                                 certbag.add(certs_from_obj.next());
456                         CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certbag);
457                         CertStore store = CertStore.getInstance("Collection", ccsp);
458                         params.addCertStore(store);
459
460                         // Attempt to build a path.
461                         CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
462                         PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) cpb.build(params);
463                 } catch (CertPathBuilderException e) {
464                         log.error("caught a cert path builder exception: " + e.getMessage());
465                         throw new TrustException(
466                                 SAMLException.RESPONDER,
467                                 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
468                                 e);
469                 } catch (GeneralSecurityException e) {
470                         log.error("caught a general security exception: " + e.getMessage());
471                         throw new TrustException(
472                                 SAMLException.RESPONDER,
473                                 "ShibPOSTProfile.verifySignature() unable to build a PKIX certificate path",
474                                 e);
475                 } finally {
476                         NDC.pop();
477                 }
478         }
479 }