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;
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.*;
23 * Basic Shibboleth POST browser profile implementation with basic support for
26 * @author Scott Cantor
27 * @created April 11, 2002
29 public class ShibPOSTProfile
31 /** XML Signature algorithm to apply */
32 protected String algorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
34 /** Policy URIs to attach or check against */
35 protected String[] policies = null;
37 /** Official name of issuing site */
38 protected String issuer = null;
40 /** Abstract interface into trust base */
41 protected OriginSiteMapper mapper = null;
43 /** The URL of the receiving SHIRE */
44 protected String receiver = null;
46 /** Seconds allowed to elapse from issuance of response */
47 protected int ttlSeconds = 0;
50 * SHIRE-side constructor for a ShibPOSTProfile object
52 * @param policies Array of policy URIs that the implementation
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
61 public ShibPOSTProfile(String[] policies, OriginSiteMapper mapper, String receiver, int ttlSeconds)
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");
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);
76 * HS-side constructor for a ShibPOSTProfile object
78 * @param policies Array of policy URIs that the implementation
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
84 public ShibPOSTProfile(String[] policies, String issuer)
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");
90 this.policies = new String[policies.length];
91 System.arraycopy(policies, 0, this.policies, 0, policies.length);
95 * Locates the first AuthenticationStatement in the response and validates
96 * the statement and the enclosing assertion with respect to the POST
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
104 public SAMLAuthenticationStatement getSSOStatement(SAMLResponse r)
107 return SAMLPOSTProfile.getSSOStatement(r, policies);
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
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>
121 * @param buf A Base-64 encoded buffer containing a SAML
123 * @return SAML response sent by origin site
124 * @exception SAMLException Thrown if the response cannot be understood or
127 public SAMLResponse accept(byte[] buf)
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
133 SAMLResponse r = SAMLPOSTProfile.accept(buf, receiver, ttlSeconds);
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);
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++)
148 SAMLStatement[] states = assertions[i].getStatements();
149 for (int j = 0; j < states.length; j++)
151 if (states[j] == sso)
153 assertion = assertions[i];
160 if (!checkReplayCache(assertion.getAssertionID(), new Date(assertion.getNotOnOrAfter().getTime() + 300000)))
161 throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected a replayed SSO assertion");
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");
168 String originSite = subject.getNameQualifier();
169 String handleService = assertion.getIssuer();
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))
178 throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.accept() detected an untrusted HS for the origin site");
180 Key hsKey = mapper.getHandleServiceKey(handleService);
181 KeyStore ks = mapper.getTrustedRoots();
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");
194 * Used by HS to generate a signed SAML response conforming to the POST
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
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
219 public SAMLResponse prepare(String recipient,
221 String nameQualifier,
225 SAMLAuthorityBinding[] bindings,
226 Key responseKey, X509Certificate responseCert,
227 Key assertionKey, X509Certificate assertionCert
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");
236 DocumentBuilder builder = null;
239 builder = org.opensaml.XML.parserPool.get();
240 Document doc = builder.newDocument();
242 XMLSignature rsig = new XMLSignature(doc, null, algorithm);
243 XMLSignature asig = null;
244 if (assertionKey != null)
245 asig = new XMLSignature(doc, null, algorithm);
247 SAMLResponse r = SAMLPOSTProfile.prepare(
263 if (assertionCert != null)
264 asig.addKeyInfo(assertionCert);
265 if (assertionKey instanceof PrivateKey)
266 asig.sign((PrivateKey)assertionKey);
268 asig.sign((SecretKey)assertionKey);
270 if (responseCert != null)
271 rsig.addKeyInfo(responseCert);
272 if (responseKey instanceof PrivateKey)
273 rsig.sign((PrivateKey)responseKey);
275 rsig.sign((SecretKey)responseKey);
278 catch (ParserConfigurationException pce)
280 throw new SAMLException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() unable to obtain XML parser instance: " + pce.getMessage(), pce);
282 catch (XMLSecurityException e)
284 throw new InvalidCryptoException(SAMLException.RESPONDER, "ShibPOSTProfile.prepare() detected an XML security problem during signature creation", e);
289 org.opensaml.XML.parserPool.put(builder);
294 * Searches the replay cache for the specified assertion ID and inserts a
295 * newly seen ID into the cache<P>
297 * Also performs garbage collection of the cache by deleting expired
300 * @param expires The datetime at which the specified assertion ID can
302 * @param assertionID Description of Parameter
303 * @return true iff the assertion has not been seen before
305 protected synchronized boolean checkReplayCache(String assertionID, Date expires)
307 // Default implementation uses the basic replay cache implementation.
308 return SAMLPOSTProfile.checkReplayCache(assertionID, expires);
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
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
322 * @return The result of signature verification
324 protected boolean verifySignature(SAMLSignedObject obj, String signerName, KeyStore ks, Key knownKey)
328 XMLSignature sig = (obj != null) ? obj.getSignature() : null;
331 KeyInfo ki = sig.getKeyInfo();
332 if (ks != null && ki != null)
334 X509Certificate cert = ki.getX509Certificate();
337 cert.checkValidity();
338 if (!sig.checkSignatureValue(cert))
340 if (signerName != null)
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))
348 String iname = cert.getIssuerDN().getName();
349 for (Enumeration aliases = ks.aliases(); aliases.hasMoreElements(); )
351 String alias = (String)aliases.nextElement();
352 if (!ks.isCertificateEntry(alias))
354 Certificate cacert = ks.getCertificate(alias);
355 if (!(cacert instanceof X509Certificate))
357 ((X509Certificate)cacert).checkValidity();
358 if (iname.equals(((X509Certificate)cacert).getSubjectDN().getName()))
360 cert.verify(cacert.getPublicKey());
368 return (knownKey != null) ? sig.checkSignatureValue(knownKey) : false;
370 catch (XMLSecurityException e)
374 catch (GeneralSecurityException e)