Push SAML Attribute namespace configuration into the resolver. (Needed for proper...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / provider / SAML2PersistentID.java
1 /*
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.
24  */
25
26 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
27
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;
39
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;
46
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;
53
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;
62
63 /**
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
66  * Namde Identifiers.
67  * 
68  * @author Scott Cantor (cantor.2@osu.edu)
69  * @author Walter Hoehn
70  */
71 public class SAML2PersistentID extends BaseAttributeDefinition implements AttributeDefinitionPlugIn {
72
73         private static Logger log = Logger.getLogger(SAML2PersistentID.class.getName());
74         protected byte salt[];
75         protected String localPersistentId = null;
76
77         /**
78          * Constructor for SAML2PersistentID. Creates a PlugIn based on configuration information presented in a DOM
79          * Element.
80          */
81         public SAML2PersistentID(Element e) throws ResolutionPlugInException {
82
83                 super(e);
84                 localPersistentId = e.getAttributeNS(null, "sourceName");
85
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.");
91                         }
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.");
98                 }
99
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.");
105                 }
106
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();
111                 else {
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");
116
117                         if (ksPath == null || ksPath.length() == 0 || keyAlias == null || keyAlias.length() == 0 || ksPass == null
118                                         || ksPass.length() == 0 || keyPass == null || keyPass.length() == 0) {
119
120                                 log.error("Missing <Salt> keyStore attributes from attribute definition configuration.");
121                                 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
122                         }
123
124                         try {
125                                 KeyStore keyStore = KeyStore.getInstance("JCEKS");
126
127                                 keyStore.load(new ShibResource(ksPath, this.getClass()).getInputStream(), ksPass.toCharArray());
128                                 SecretKey secret = (SecretKey) keyStore.getKey(keyAlias, keyPass.toCharArray());
129
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.");
134                                 }
135                                 this.salt = secret.getEncoded();
136
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. ");
162                         }
163                 }
164         }
165
166         /**
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)
169          */
170         public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
171                         Dependencies depends) throws ResolutionPlugInException {
172
173                 log.debug("Resolving attribute: (" + getId() + ")");
174
175                 if (requester == null || requester.equals("")) {
176                         log.debug("Could not create persistent ID for unauthenticated requester.");
177                         attribute.setResolved();
178                         return;
179                 }
180
181                 if (responder == null || responder.equals("")) {
182                         log.error("Could not create persistent ID for null responder.");
183                         attribute.setResolved();
184                         return;
185                 }
186
187                 String localId = null;
188
189                 // Resolve the correct local persistent identifier.
190                 if (!attributeDependencyIds.isEmpty()) {
191                         ResolverAttribute dep = depends.getAttributeResolution((String) attributeDependencyIds.iterator().next());
192                         if (dep != null) {
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.");
200                                                 return;
201                                         }
202                                 } else {
203                                         log.error("An attribute dependency of attribute (" + getId()
204                                                         + ") returned no values, expecting one.");
205                                         return;
206                                 }
207                         } else {
208                                 log.error("An attribute dependency of attribute (" + getId()
209                                                 + ") was not included in the dependency chain.");
210                                 return;
211                         }
212                 } else if (!connectorDependencyIds.isEmpty()) {
213                         Attributes attrs = depends.getConnectorResolution((String) connectorDependencyIds.iterator().next());
214                         if (attrs != null) {
215                                 Attribute attr = attrs.get(localPersistentId);
216                                 if (attr != null) {
217                                         if (attr.size() != 1) {
218                                                 log.error("An attribute dependency of attribute (" + getId() + ") returned " + attr.size()
219                                                                 + " values, expecting only one.");
220                                         } else {
221                                                 try {
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);
226                                                         return;
227                                                 }
228                                         }
229                                 }
230                         } else {
231                                 log.error("A connector dependency of attribute (" + getId() + ") did not return any attributes.");
232                                 return;
233                         }
234                 } else {
235                         localId = principal.getName();
236                 }
237
238                 if (localId == null || localId.equals("")) {
239                         log.error("Specified source data not supplied from dependencies.  Unable to create persistent ID.");
240                         attribute.setResolved();
241                         return;
242                 }
243
244                 standardProcessing(attribute);
245
246                 // Hash the data together to produce the persistent ID.
247                 try {
248                         MessageDigest md = MessageDigest.getInstance("SHA");
249                         md.update(requester.getBytes());
250                         md.update((byte) '!');
251                         md.update(localId.getBytes());
252                         md.update((byte) '!');
253                         String result = new String(Base64.encode(md.digest(salt)));
254
255                         // SAML2 persistent NameId format
256                         try {
257                                 DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
258                                 docFactory.setNamespaceAware(true);
259                                 Document placeHolder = docFactory.newDocumentBuilder().newDocument();
260                                 Element nameIDNode = placeHolder.createElementNS(XML.SAML2ASSERT_NS, "NameID");
261                                 nameIDNode.setAttribute("xmlns", XML.SAML2ASSERT_NS);
262                                 nameIDNode.setAttribute("Format", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
263                                 nameIDNode.setAttribute("NameQualifier", responder);
264                                 nameIDNode.setAttribute("SPNameQualifier", requester);
265                                 nameIDNode.appendChild(placeHolder.createTextNode(result.replaceAll(System
266                                                 .getProperty("line.separator"), "")));
267
268                                 attribute.addValue(nameIDNode);
269                                 attribute.registerValueHandler(new DOMValueHandler());
270                                 attribute.setResolved();
271                         } catch (ParserConfigurationException pce) {
272                                 log.error("Unable to create DOM for persistent ID: " + pce);
273                                 throw new ResolutionPlugInException("Error generating persistent ID.");
274                         }
275
276                 } catch (NoSuchAlgorithmException e) {
277                         log.error("Unable to load SHA-1 hash algorithm.");
278                         return;
279                 }
280         }
281
282         private boolean usingDefaultSecret() {
283
284                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
285                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
286                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
287                                 (byte) 0x61, (byte) 0xEF};
288                 return Arrays.equals(defaultKey, salt);
289         }
290
291         class DOMValueHandler implements ValueHandler {
292
293                 public void toDOM(Element valueElement, Object value, Document document) {
294
295                         valueElement.appendChild(document.importNode((Node) value, true));
296                 }
297
298                 public Iterator getValues(Collection internalValues) {
299
300                         return new SerializationIterator(internalValues.iterator());
301                 }
302
303                 public boolean equals(Object object) {
304
305                         if (object instanceof DOMValueHandler) { return true; }
306                         return false;
307                 }
308
309                 private class SerializationIterator implements Iterator {
310
311                         Iterator wrapped;
312
313                         SerializationIterator(Iterator i) {
314
315                                 wrapped = i;
316                         }
317
318                         public boolean hasNext() {
319
320                                 return wrapped.hasNext();
321                         }
322
323                         public Object next() {
324
325                                 return Parser.serialize(((Element) wrapped.next()));
326                         }
327
328                         public void remove() {
329
330                                 wrapped.remove();
331                         }
332
333                 }
334         }
335 }