Updated OpenSAML library.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / ShibPOSTProfile.java
1 package edu.internet2.middleware.shibboleth.common;
2 import java.security.GeneralSecurityException;
3 import java.security.Key;
4 import java.security.KeyStore;
5 import java.security.PrivateKey;
6 import java.security.cert.Certificate;
7 import java.security.cert.X509Certificate;
8
9 import java.util.Date;
10 import java.util.Enumeration;
11 import java.util.Iterator;
12 import javax.crypto.SecretKey;
13 import javax.xml.parsers.DocumentBuilder;
14 import javax.xml.parsers.ParserConfigurationException;
15 import org.apache.xml.security.exceptions.XMLSecurityException;
16 import org.apache.xml.security.keys.KeyInfo;
17 import org.apache.xml.security.signature.XMLSignature;
18 import org.opensaml.*;
19 import org.w3c.dom.*;
20
21
22 /**
23  *  Basic Shibboleth POST browser profile implementation with basic support for
24  *  signing
25  *
26  * @author     Scott Cantor
27  * @created    April 11, 2002
28  */
29 public class ShibPOSTProfile
30 {
31     /**  XML Signature algorithm to apply */
32     protected String algorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
33
34     /**  Policy URIs to attach or check against */
35     protected String[] policies = null;
36
37     /**  Official name of issuing site */
38     protected String issuer = null;
39
40     /**  Abstract interface into trust base */
41     protected OriginSiteMapper mapper = null;
42
43     /**  The URL of the receiving SHIRE */
44     protected String receiver = null;
45
46     /**  Seconds allowed to elapse from issuance of response */
47     protected int ttlSeconds = 0;
48
49     /**
50      *  SHIRE-side constructor for a ShibPOSTProfile object
51      *
52      * @param  policies           Array of policy URIs that the implementation
53      *      must support
54      * @param  mapper             Interface between profile and trust base
55      * @param  receiver           URL of SHIRE
56      * @param  ttlSeconds         Length of time in seconds allowed to elapse
57      *      from issuance of SAML response
58      * @exception  SAMLException  Raised if a profile implementation cannot be
59      *      constructed from the supplied information
60      */
61     public ShibPOSTProfile(String[] policies, OriginSiteMapper mapper, String receiver, int ttlSeconds)
62         throws SAMLException
63     {
64         if (policies == null || policies.length == 0 || mapper == null ||
65             receiver == null || receiver.length() == 0 || ttlSeconds <= 0)
66             throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
67
68         this.mapper = mapper;
69         this.receiver = receiver;
70         this.ttlSeconds = ttlSeconds;
71         this.policies = new String[policies.length];
72         System.arraycopy(policies, 0, this.policies, 0, policies.length);
73     }
74
75     /**
76      *  HS-side constructor for a ShibPOSTProfile object
77      *
78      * @param  policies           Array of policy URIs that the implementation
79      *      must support
80      * @param  issuer             "Official" name of issuing origin site
81      * @exception  SAMLException  Raised if a profile implementation cannot be
82      *      constructed from the supplied information
83      */
84     public ShibPOSTProfile(String[] policies, String issuer)
85         throws SAMLException
86     {
87         if (policies == null || policies.length == 0 || issuer == null || issuer.length() == 0)
88             throw new SAMLException(SAMLException.REQUESTER, "ShibPOSTProfile() found a null or invalid argument");
89         this.issuer = issuer;
90         this.policies = new String[policies.length];
91         System.arraycopy(policies, 0, this.policies, 0, policies.length);
92     }
93
94     /**
95      *  Locates the first AuthenticationStatement in the response and validates
96      *  the statement and the enclosing assertion with respect to the POST
97      *  profile
98      *
99      * @param  r                  The response to the accepting site
100      * @return                    An authentication statement
101      * @exception  SAMLException  Base class of exceptions that may be thrown
102      *      during processing
103      */
104     public SAMLAuthenticationStatement getSSOStatement(SAMLResponse r)
105         throws SAMLException
106     {
107         return SAMLPOSTProfile.getSSOStatement(r, policies);
108     }
109
110     /**
111      *  Parse a Base-64 encoded buffer back into a SAML response and test its
112      *  validity against the POST profile, including use of the default replay
113      *  cache<P>
114      *
115      *  Also does trust evaluation based on the information available from the
116      *  origin site mapper, in accordance with general Shibboleth processing
117      *  semantics. Club-specific processing must be performed in a subclass.<P>
118      *
119      *
120      *
121      * @param  buf                A Base-64 encoded buffer containing a SAML
122      *      response
123      * @return                    SAML response sent by origin site
124      * @exception  SAMLException  Thrown if the response cannot be understood or
125      *      accepted
126      */
127     public SAMLResponse accept(byte[] buf)
128         throws SAMLException
129     {
130         // The built-in SAML functionality will do most of the basic non-crypto checks.
131         // Note that if the response only contains a status error, it gets tossed out
132         // as an exception.
133         SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds);
134
135         // Now we do some more non-crypto (ie. cheap) work to match up the origin site
136         // with its associated data. If we can't even find a SSO statement in the response
137         // we just return the response to the caller, who will presumably notice this.
138         SAMLAuthenticationStatement sso = SAMLPOSTProfile.getSSOStatement(r, policies);
139         if (sso == null)
140             return r;
141
142         // Kind of clunky, we need to get the assertion containing the SSO statement,
143         // currently in a brute force way...
144         SAMLAssertion assertion = null;
145         SAMLAssertion[] assertions = r.getAssertions();
146         for (int i = 0; assertion == null && i < assertions.length; i++)
147         {
148             SAMLStatement[] states = assertions[i].getStatements();
149             for (int j = 0; j < states.length; j++)
150             {
151                 if (states[j] == sso)
152                 {
153                     assertion = assertions[i];
154                     break;
155                 }
156             }
157         }
158
159         // Check for replay.
160         if (!checkReplayCache(assertion.getAssertionID(), new Date(assertion.getNotOnOrAfter().getTime() + 300000)))
161             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected a replayed SSO assertion");
162
163         // Examine the subject information.
164         SAMLSubject subject = sso.getSubject();
165         if (subject.getNameQualifier() == null)
166             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() requires subject name qualifier");
167
168         String originSite = subject.getNameQualifier();
169         String handleService = assertion.getIssuer();
170
171         // Is this a trusted HS?
172         Iterator hsNames = mapper.getHandleServiceNames(originSite);
173         boolean bFound = false;
174         while (!bFound && hsNames != null && hsNames.hasNext())
175             if (hsNames.next().equals(handleService))
176                 bFound = true;
177         if (!bFound)
178             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
179
180         Key hsKey = mapper.getHandleServiceKey(handleService);
181         KeyStore ks = mapper.getTrustedRoots();
182
183         // Signature verification now takes place. We check the assertion and the response.
184         // Assertion signing is optional, response signing is mandatory.
185         if (assertion.getSignature() != null && !verifySignature(assertion, handleService, ks, hsKey))
186             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an invalid assertion signature");
187         if (!verifySignature(r, handleService, ks, hsKey))
188             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an invalid response signature");
189
190         return r;
191     }
192
193     /**
194      *  Used by HS to generate a signed SAML response conforming to the POST
195      *  profile<P>
196      *
197      *
198      *
199      * @param  recipient          URL of intended consumer
200      * @param  name               Name of subject
201      * @param  nameQualifier      Federates or qualifies subject name (optional)
202      * @param  subjectIP          Client address of subject (optional)
203      * @param  authMethod         URI of authentication method being asserted
204      * @param  authInstant        Date and time of authentication being asserted
205      * @param  bindings           Array of SAML authorities the relying party
206      *      may contact (optional)
207      * @param  responseKey        A secret or private key to use in response
208      *      signature or MAC
209      * @param  responseCert       A public key certificate to enclose with the
210      *      response (optional)
211      * @param  assertionKey       A secret or private key to use in assertion
212      *      signature or MAC (optional)
213      * @param  assertionCert      A public key certificate to enclose with the
214      *      assertion (optional)
215      * @return                    SAML response to send to accepting site
216      * @exception  SAMLException  Base class of exceptions that may be thrown
217      *      during processing
218      */
219     public SAMLResponse prepare(String recipient,
220                                 String name,
221                                 String nameQualifier,
222                                 String subjectIP,
223                                 String authMethod,
224                                 Date authInstant,
225                                 SAMLAuthorityBinding[] bindings,
226                                 Key responseKey, X509Certificate responseCert,
227                                 Key assertionKey, X509Certificate assertionCert
228                                 )
229         throws SAMLException
230     {
231         if (responseKey == null || (!(responseKey instanceof PrivateKey) && !(responseKey instanceof SecretKey)))
232             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() requires a response key (private or secret)");
233         if (assertionKey != null && !(assertionKey instanceof PrivateKey) && !(assertionKey instanceof SecretKey))
234             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an invalid type of assertion key");
235
236         DocumentBuilder builder = null;
237         try
238         {
239             builder = org.opensaml.XML.parserPool.get();
240             Document doc = builder.newDocument();
241
242             XMLSignature rsig = new XMLSignature(doc, null, algorithm);
243             XMLSignature asig = null;
244             if (assertionKey != null)
245                 asig = new XMLSignature(doc, null, algorithm);
246
247             SAMLResponse r = SAMLPOSTProfile.prepare(
248                 recipient,
249                 issuer,
250                 policies,
251                 name,
252                 nameQualifier,
253                 null,
254                 subjectIP,
255                 authMethod,
256                 authInstant,
257                 bindings,
258                 rsig,
259                 asig);
260             r.toDOM(doc);
261             if (asig != null)
262             {
263                 if (assertionCert != null)
264                     asig.addKeyInfo(assertionCert);
265                 if (assertionKey instanceof PrivateKey)
266                     asig.sign((PrivateKey)assertionKey);
267                 else
268                     asig.sign((SecretKey)assertionKey);
269             }
270             if (responseCert != null)
271                 rsig.addKeyInfo(responseCert);
272             if (responseKey instanceof PrivateKey)
273                 rsig.sign((PrivateKey)responseKey);
274             else
275                 rsig.sign((SecretKey)responseKey);
276             return r;
277         }
278         catch (ParserConfigurationException pce)
279         {
280             throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() unable to obtain XML parser instance: " + pce.getMessage(), pce);
281         }
282         catch (XMLSecurityException e)
283         {
284             throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an XML security problem during signature creation", e);
285         }
286         finally
287         {
288             if (builder != null)
289                 org.opensaml.XML.parserPool.put(builder);
290         }
291     }
292
293     /**
294      *  Searches the replay cache for the specified assertion ID and inserts a
295      *  newly seen ID into the cache<P>
296      *
297      *  Also performs garbage collection of the cache by deleting expired
298      *  entries.
299      *
300      * @param  expires      The datetime at which the specified assertion ID can
301      *      be flushed
302      * @param  assertionID  Description of Parameter
303      * @return              true iff the assertion has not been seen before
304      */
305     protected synchronized boolean checkReplayCache(String assertionID, Date expires)
306     {
307         // Default implementation uses the basic replay cache implementation.
308         return SAMLPOSTProfile.checkReplayCache(assertionID, expires);
309     }
310
311     /**
312      *  Default signature verification algorithm uses an embedded X509
313      *  certificate or an explicit key to verify the signature. The certificate
314      *  is examined to insure the subject CN matches the signer, and that it is
315      *  signed by a trusted CA
316      *
317      * @param  obj         The object containing the signature
318      * @param  signerName  The name of the signer
319      * @param  ks          A keystore containing trusted root certificates
320      * @param  knownKey    An explicit key to use if a certificate cannot be
321      *      found
322      * @return             The result of signature verification
323      */
324     protected boolean verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
325     {
326         try
327         {
328             XMLSignature sig = (obj != null) ? obj.getSignature() : null;
329             if (sig == null)
330                 return false;
331             KeyInfo ki = sig.getKeyInfo();
332             if (ks != null && ki != null)
333             {
334                 X509Certificate cert = ki.getX509Certificate();
335                 if (cert != null)
336                 {
337                     cert.checkValidity();
338                     if (!sig.checkSignatureValue(cert))
339                         return false;
340                     if (signerName != null)
341                     {
342                         String dname = cert.getSubjectDN().getName();
343                         String cname = "CN=" + signerName;
344                         if (!dname.equalsIgnoreCase(cname) && !dname.regionMatches(true, 0, cname + ',', 0, cname.length() + 1))
345                             return false;
346                     }
347
348                     String iname = cert.getIssuerDN().getName();
349                     for (Enumeration aliases = ks.aliases(); aliases.hasMoreElements(); )
350                     {
351                         String alias = (String)aliases.nextElement();
352                         if (!ks.isCertificateEntry(alias))
353                             continue;
354                         Certificate cacert = ks.getCertificate(alias);
355                         if (!(cacert instanceof X509Certificate))
356                             continue;
357                         ((X509Certificate)cacert).checkValidity();
358                         if (iname.equals(((X509Certificate)cacert).getSubjectDN().getName()))
359                         {
360                             cert.verify(cacert.getPublicKey());
361                             return true;
362                         }
363                     }
364
365                     return false;
366                 }
367             }
368             return (knownKey != null) ? sig.checkSignatureValue(knownKey) : false;
369         }
370         catch (XMLSecurityException e)
371         {
372             return false;
373         }
374         catch (GeneralSecurityException e)
375         {
376             return false;
377         }
378     }
379 }
380