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