2 * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3 * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4 * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5 * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6 * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7 * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8 * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu>Internet2 Project.
9 * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10 * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11 * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12 * products derived from this software without specific prior written permission. For written permission, please contact
13 * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14 * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15 * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16 * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18 * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19 * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
28 import java.io.IOException;
29 import java.security.KeyStore;
30 import java.security.KeyStoreException;
31 import java.security.MessageDigest;
32 import java.security.NoSuchAlgorithmException;
33 import java.security.Principal;
34 import java.security.UnrecoverableKeyException;
35 import java.security.cert.CertificateException;
36 import java.util.Arrays;
37 import java.util.Collection;
38 import java.util.Iterator;
40 import javax.crypto.SecretKey;
41 import javax.naming.NamingException;
42 import javax.naming.directory.Attribute;
43 import javax.naming.directory.Attributes;
44 import javax.xml.parsers.DocumentBuilderFactory;
45 import javax.xml.parsers.ParserConfigurationException;
47 import org.apache.log4j.Logger;
48 import org.bouncycastle.util.encoders.Base64;
49 import org.w3c.dom.Document;
50 import org.w3c.dom.Element;
51 import org.w3c.dom.Node;
52 import org.w3c.dom.NodeList;
54 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn;
55 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
56 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
57 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
58 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
59 import edu.internet2.middleware.shibboleth.common.ShibResource;
60 import edu.internet2.middleware.shibboleth.common.XML;
61 import edu.internet2.middleware.shibboleth.xml.Parser;
64 * <code>AttributeDefinition</code> implementation that provides a persistent, but pseudonymous, identifier for
65 * principals by hashing the principal name, requester, and a fixed secret salt. Forward compatible with SAML2 persisten
68 * @author Scott Cantor (cantor.2@osu.edu)
69 * @author Walter Hoehn
71 public class SAML2PersistentID extends BaseAttributeDefinition implements AttributeDefinitionPlugIn {
73 private static Logger log = Logger.getLogger(SAML2PersistentID.class.getName());
74 protected byte salt[];
75 protected String localPersistentId = null;
78 * Constructor for SAML2PersistentID. Creates a PlugIn based on configuration information presented in a DOM
81 public SAML2PersistentID(Element e) throws ResolutionPlugInException {
84 localPersistentId = e.getAttributeNS(null, "sourceName");
86 // Make sure we understand how to resolve the local persistent ID for the principal.
87 if (localPersistentId != null && localPersistentId.length() > 0) {
88 if (connectorDependencyIds.size() != 1 || !attributeDependencyIds.isEmpty()) {
89 log.error("Can't specify the sourceName attribute without a single connector dependency.");
90 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
92 } else if (!connectorDependencyIds.isEmpty()) {
93 log.error("Can't specify a connector dependency without supplying the sourceName attribute.");
94 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
95 } else if (attributeDependencyIds.size() > 1) {
96 log.error("Can't specify more than one attribute dependency, this is ambiguous.");
97 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
100 // Salt can be either embedded in the element or pulled out of a keystore.
101 NodeList salts = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Salt");
102 if (salts == null || salts.getLength() != 1) {
103 log.error("Missing <Salt> from attribute definition configuration.");
104 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
107 Element salt = (Element) salts.item(0);
108 Node child = salt.getFirstChild();
109 if (child != null && child.getNodeType() == Node.TEXT_NODE && child.getNodeValue() != null
110 && child.getNodeValue().length() >= 16) this.salt = child.getNodeValue().getBytes();
112 String ksPath = salt.getAttributeNS(null, "keyStorePath");
113 String keyAlias = salt.getAttributeNS(null, "keyStoreKeyAlias");
114 String ksPass = salt.getAttributeNS(null, "keyStorePassword");
115 String keyPass = salt.getAttributeNS(null, "keyStoreKeyPassword");
117 if (ksPath == null || ksPath.length() == 0 || keyAlias == null || keyAlias.length() == 0 || ksPass == null
118 || ksPass.length() == 0 || keyPass == null || keyPass.length() == 0) {
120 log.error("Missing <Salt> keyStore attributes from attribute definition configuration.");
121 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
125 KeyStore keyStore = KeyStore.getInstance("JCEKS");
127 keyStore.load(new ShibResource(ksPath, this.getClass()).getInputStream(), ksPass.toCharArray());
128 SecretKey secret = (SecretKey) keyStore.getKey(keyAlias, keyPass.toCharArray());
130 if (usingDefaultSecret()) {
131 log.warn("You are running the SAML2PersistentID PlugIn with the default "
132 + "secret key as a salt. This is UNSAFE! Please change "
133 + "this configuration and restart the IdP.");
135 this.salt = secret.getEncoded();
137 } catch (KeyStoreException ex) {
138 log.error("An error occurred while loading the java keystore. Unable to initialize "
139 + "Attribute Definition PlugIn: " + ex);
140 throw new ResolutionPlugInException(
141 "An error occurred while loading the java keystore. Unable to initialize Attribute Definition PlugIn.");
142 } catch (CertificateException ex) {
143 log.error("The java keystore contained corrupted data. Unable to initialize "
144 + "Attribute Definition PlugIn: " + ex);
145 throw new ResolutionPlugInException(
146 "The java keystore contained corrupted data. Unable to initialize Attribute Definition PlugIn.");
147 } catch (NoSuchAlgorithmException ex) {
148 log.error("Appropriate JCE provider not found in the java environment. Unable to initialize "
149 + "Attribute Definition PlugIn: " + ex);
150 throw new ResolutionPlugInException(
151 "Appropriate JCE provider not found in the java environment. Unable to initialize Attribute Definition PlugIn.");
152 } catch (IOException ex) {
153 log.error("An error accessing while loading the java keystore. Unable to initialize "
154 + "Attribute Definition PlugIn: " + ex);
155 throw new ResolutionPlugInException(
156 "An error occurred while accessing the java keystore. Unable to initialize Attribute Definition PlugIn.");
157 } catch (UnrecoverableKeyException ex) {
158 log.error("Secret could not be loaded from the java keystore. Verify that the alias and "
159 + "password are correct: " + ex);
160 throw new ResolutionPlugInException(
161 "Secret could not be loaded from the java keystore. Verify that the alias and password are correct. ");
167 * @see edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn#resolve(edu.internet2.middleware.shibboleth.aa.attrresolv.ArpAttribute,
168 * java.security.Principal, java.lang.String, edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies)
170 public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
171 Dependencies depends) throws ResolutionPlugInException {
173 log.debug("Resolving attribute: (" + getId() + ")");
175 if (requester == null || requester.equals("")) {
176 log.debug("Could not create persistent ID for unauthenticated requester.");
177 attribute.setResolved();
181 if (responder == null || responder.equals("")) {
182 log.error("Could not create persistent ID for null responder.");
183 attribute.setResolved();
187 String localId = null;
189 // Resolve the correct local persistent identifier.
190 if (!attributeDependencyIds.isEmpty()) {
191 ResolverAttribute dep = depends.getAttributeResolution((String) attributeDependencyIds.iterator().next());
193 Iterator vals = dep.getValues();
194 if (vals.hasNext()) {
195 log.debug("Found persistent ID value for attribute (" + getId() + ").");
196 localId = (String) vals.next();
197 if (vals.hasNext()) {
198 log.error("An attribute dependency of attribute (" + getId()
199 + ") returned multiple values, expecting only one.");
203 log.error("An attribute dependency of attribute (" + getId()
204 + ") returned no values, expecting one.");
208 log.error("An attribute dependency of attribute (" + getId()
209 + ") was not included in the dependency chain.");
212 } else if (!connectorDependencyIds.isEmpty()) {
213 Attributes attrs = depends.getConnectorResolution((String) connectorDependencyIds.iterator().next());
215 Attribute attr = attrs.get(localPersistentId);
217 if (attr.size() != 1) {
218 log.error("An attribute dependency of attribute (" + getId() + ") returned " + attr.size()
219 + " values, expecting only one.");
222 localId = (String) attr.get();
223 log.debug("Found persistent ID value for attribute (" + getId() + ").");
224 } catch (NamingException e) {
225 log.error("A connector dependency of attribute (" + getId() + ") threw an exception: " + e);
231 log.error("A connector dependency of attribute (" + getId() + ") did not return any attributes.");
235 localId = principal.getName();
238 if (localId == null || localId.equals("")) {
239 log.error("Specified source data not supplied from dependencies. Unable to create persistent ID.");
240 attribute.setResolved();
244 if (lifeTime != -1) {
245 attribute.setLifetime(lifeTime);
248 // Hash the data together to produce the persistent ID.
250 MessageDigest md = MessageDigest.getInstance("SHA");
251 md.update(requester.getBytes());
252 md.update((byte) '!');
253 md.update(localId.getBytes());
254 md.update((byte) '!');
255 String result = new String(Base64.encode(md.digest(salt)));
257 // SAML2 persistent NameId format
259 DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
260 docFactory.setNamespaceAware(true);
261 Document placeHolder = docFactory.newDocumentBuilder().newDocument();
262 Element nameIDNode = placeHolder.createElementNS(XML.SAML2ASSERT_NS, "NameID");
263 nameIDNode.setAttribute("xmlns", XML.SAML2ASSERT_NS);
264 nameIDNode.setAttribute("Format", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
265 nameIDNode.setAttribute("NameQualifier", responder);
266 nameIDNode.setAttribute("SPNameQualifier", requester);
267 nameIDNode.appendChild(placeHolder.createTextNode(result.replaceAll(System
268 .getProperty("line.separator"), "")));
270 attribute.addValue(nameIDNode);
271 attribute.registerValueHandler(new DOMValueHandler());
272 attribute.setResolved();
273 } catch (ParserConfigurationException pce) {
274 log.error("Unable to create DOM for persistent ID: " + pce);
275 throw new ResolutionPlugInException("Error generating persistent ID.");
278 } catch (NoSuchAlgorithmException e) {
279 log.error("Unable to load SHA-1 hash algorithm.");
284 private boolean usingDefaultSecret() {
286 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
287 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
288 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
289 (byte) 0x61, (byte) 0xEF};
290 return Arrays.equals(defaultKey, salt);
293 class DOMValueHandler implements ValueHandler {
295 public void toDOM(Element valueElement, Object value, Document document) {
297 valueElement.appendChild(document.importNode((Node) value, true));
300 public Iterator getValues(Collection internalValues) {
302 return new SerializationIterator(internalValues.iterator());
305 public boolean equals(Object object) {
307 if (object instanceof DOMValueHandler) { return true; }
311 private class SerializationIterator implements Iterator {
315 SerializationIterator(Iterator i) {
320 public boolean hasNext() {
322 return wrapped.hasNext();
325 public Object next() {
327 return Parser.serialize(((Element) wrapped.next()));
330 public void remove() {