Implemented new persistent id attribute format in accordance with "SAML eduPerson...
[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.Iterator;
38
39 import javax.crypto.SecretKey;
40 import javax.naming.NamingException;
41 import javax.naming.directory.Attribute;
42 import javax.naming.directory.Attributes;
43
44 import org.apache.log4j.Logger;
45 import org.bouncycastle.util.encoders.Base64;
46 import org.w3c.dom.Element;
47 import org.w3c.dom.Node;
48 import org.w3c.dom.NodeList;
49
50 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn;
51 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
52 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
53 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
54 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
55 import edu.internet2.middleware.shibboleth.common.ShibResource;
56
57 /**
58  * <code>AttributeDefinition</code> implementation that provides a persistent, but pseudonymous, identifier for
59  * principals by hashing the principal name, requester, and a fixed secret salt. Forward compatible with SAML2 persisten
60  * Namde Identifiers.
61  * 
62  * @author Scott Cantor (cantor.2@osu.edu)
63  * @author Walter Hoehn
64  */
65 public class SAML2PersistentID extends BaseAttributeDefinition implements AttributeDefinitionPlugIn {
66
67         private static Logger log = Logger.getLogger(SAML2PersistentID.class.getName());
68         protected byte salt[];
69         protected String localPersistentId = null;
70
71         /**
72          * Constructor for SAML2PersistentID. Creates a PlugIn based on configuration information presented in a DOM
73          * Element.
74          */
75         public SAML2PersistentID(Element e) throws ResolutionPlugInException {
76
77                 super(e);
78                 localPersistentId = e.getAttributeNS(null, "sourceName");
79
80                 // Make sure we understand how to resolve the local persistent ID for the principal.
81                 if (localPersistentId != null && localPersistentId.length() > 0) {
82                         if (connectorDependencyIds.size() != 1 || !attributeDependencyIds.isEmpty()) {
83                                 log.error("Can't specify the sourceName attribute without a single connector dependency.");
84                                 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
85                         }
86                 } else if (!connectorDependencyIds.isEmpty()) {
87                         log.error("Can't specify a connector dependency without supplying the sourceName attribute.");
88                         throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
89                 } else if (attributeDependencyIds.size() > 1) {
90                         log.error("Can't specify more than one attribute dependency, this is ambiguous.");
91                         throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
92                 }
93
94                 // Salt can be either embedded in the element or pulled out of a keystore.
95                 NodeList salts = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Salt");
96                 if (salts == null || salts.getLength() != 1) {
97                         log.error("Missing <Salt> from attribute definition configuration.");
98                         throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
99                 }
100
101                 Element salt = (Element) salts.item(0);
102                 Node child = salt.getFirstChild();
103                 if (child != null && child.getNodeType() == Node.TEXT_NODE && child.getNodeValue() != null
104                                 && child.getNodeValue().length() >= 16) this.salt = child.getNodeValue().getBytes();
105                 else {
106                         String ksPath = salt.getAttributeNS(null, "keyStorePath");
107                         String keyAlias = salt.getAttributeNS(null, "keyStoreKeyAlias");
108                         String ksPass = salt.getAttributeNS(null, "keyStorePassword");
109                         String keyPass = salt.getAttributeNS(null, "keyStoreKeyPassword");
110
111                         if (ksPath == null || ksPath.length() == 0 || keyAlias == null || keyAlias.length() == 0 || ksPass == null
112                                         || ksPass.length() == 0 || keyPass == null || keyPass.length() == 0) {
113
114                                 log.error("Missing <Salt> keyStore attributes from attribute definition configuration.");
115                                 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
116                         }
117
118                         try {
119                                 KeyStore keyStore = KeyStore.getInstance("JCEKS");
120
121                                 keyStore.load(new ShibResource(ksPath, this.getClass()).getInputStream(), ksPass.toCharArray());
122                                 SecretKey secret = (SecretKey) keyStore.getKey(keyAlias, keyPass.toCharArray());
123
124                                 if (usingDefaultSecret()) {
125                                         log.warn("You are running the SAML2PersistentID PlugIn with the default "
126                                                         + "secret key as a salt.  This is UNSAFE!  Please change "
127                                                         + "this configuration and restart the IdP.");
128                                 }
129                                 this.salt = secret.getEncoded();
130
131                         } catch (KeyStoreException ex) {
132                                 log.error("An error occurred while loading the java keystore.  Unable to initialize "
133                                                 + "Attribute Definition PlugIn: " + ex);
134                                 throw new ResolutionPlugInException(
135                                                 "An error occurred while loading the java keystore.  Unable to initialize Attribute Definition PlugIn.");
136                         } catch (CertificateException ex) {
137                                 log.error("The java keystore contained corrupted data.  Unable to initialize "
138                                                 + "Attribute Definition PlugIn: " + ex);
139                                 throw new ResolutionPlugInException(
140                                                 "The java keystore contained corrupted data.  Unable to initialize Attribute Definition PlugIn.");
141                         } catch (NoSuchAlgorithmException ex) {
142                                 log.error("Appropriate JCE provider not found in the java environment. Unable to initialize "
143                                                 + "Attribute Definition PlugIn: " + ex);
144                                 throw new ResolutionPlugInException(
145                                                 "Appropriate JCE provider not found in the java environment. Unable to initialize Attribute Definition PlugIn.");
146                         } catch (IOException ex) {
147                                 log.error("An error accessing while loading the java keystore.  Unable to initialize "
148                                                 + "Attribute Definition PlugIn: " + ex);
149                                 throw new ResolutionPlugInException(
150                                                 "An error occurred while accessing the java keystore.  Unable to initialize Attribute Definition PlugIn.");
151                         } catch (UnrecoverableKeyException ex) {
152                                 log.error("Secret could not be loaded from the java keystore.  Verify that the alias and "
153                                                 + "password are correct: " + ex);
154                                 throw new ResolutionPlugInException(
155                                                 "Secret could not be loaded from the java keystore.  Verify that the alias and password are correct. ");
156                         }
157                 }
158         }
159
160         /**
161          * @see edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn#resolve(edu.internet2.middleware.shibboleth.aa.attrresolv.ArpAttribute,
162          *      java.security.Principal, java.lang.String, edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies)
163          */
164         public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
165                         Dependencies depends) throws ResolutionPlugInException {
166
167                 log.debug("Resolving attribute: (" + getId() + ")");
168
169                 if (requester == null || requester.equals("")) {
170                         log.debug("Could not create persistent ID for unauthenticated requester.");
171                         attribute.setResolved();
172                         return;
173                 }
174
175                 if (responder == null || responder.equals("")) {
176                         log.error("Could not create persistent ID for null responder.");
177                         attribute.setResolved();
178                         return;
179                 }
180
181                 String localId = null;
182
183                 // Resolve the correct local persistent identifier.
184                 if (!attributeDependencyIds.isEmpty()) {
185                         ResolverAttribute dep = depends.getAttributeResolution((String) attributeDependencyIds.iterator().next());
186                         if (dep != null) {
187                                 Iterator vals = dep.getValues();
188                                 if (vals.hasNext()) {
189                                         log.debug("Found persistent ID value for attribute (" + getId() + ").");
190                                         localId = (String) vals.next();
191                                         if (vals.hasNext()) {
192                                                 log.error("An attribute dependency of attribute (" + getId()
193                                                                 + ") returned multiple values, expecting only one.");
194                                                 return;
195                                         }
196                                 } else {
197                                         log.error("An attribute dependency of attribute (" + getId()
198                                                         + ") returned no values, expecting one.");
199                                         return;
200                                 }
201                         } else {
202                                 log.error("An attribute dependency of attribute (" + getId()
203                                                 + ") was not included in the dependency chain.");
204                                 return;
205                         }
206                 } else if (!connectorDependencyIds.isEmpty()) {
207                         Attributes attrs = depends.getConnectorResolution((String) connectorDependencyIds.iterator().next());
208                         if (attrs != null) {
209                                 Attribute attr = attrs.get(localPersistentId);
210                                 if (attr != null) {
211                                         if (attr.size() != 1) {
212                                                 log.error("An attribute dependency of attribute (" + getId() + ") returned " + attr.size()
213                                                                 + " values, expecting only one.");
214                                         } else {
215                                                 try {
216                                                         localId = (String) attr.get();
217                                                         log.debug("Found persistent ID value for attribute (" + getId() + ").");
218                                                 } catch (NamingException e) {
219                                                         log.error("A connector dependency of attribute (" + getId() + ") threw an exception: " + e);
220                                                         return;
221                                                 }
222                                         }
223                                 }
224                         } else {
225                                 log.error("A connector dependency of attribute (" + getId() + ") did not return any attributes.");
226                                 return;
227                         }
228                 } else {
229                         localId = principal.getName();
230                 }
231
232                 if (localId == null || localId.equals("")) {
233                         log.error("Specified source data not supplied from dependencies.  Unable to create persistent ID.");
234                         attribute.setResolved();
235                         return;
236                 }
237
238                 if (lifeTime != -1) {
239                         attribute.setLifetime(lifeTime);
240                 }
241
242                 // Hash the data together to produce the persistent ID.
243                 try {
244                         MessageDigest md = MessageDigest.getInstance("SHA");
245                         md.update(requester.getBytes());
246                         md.update((byte) '!');
247                         md.update(localId.getBytes());
248                         md.update((byte) '!');
249                         String result = new String(Base64.encode(md.digest(salt)));
250
251                         // :-/ String-ified SAML2 persistent NameId format
252                         StringBuffer id = new StringBuffer();
253                         id.append("<saml2:NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\" NameQualifier=\"");
254                         id.append(responder);
255                         id.append("\" SPNameQualifier=\"");
256                         id.append(requester);
257                         id.append("\" >");
258                         id.append(result.replaceAll(System.getProperty("line.separator"), ""));
259                         id.append("</saml2:NameID>");
260
261                         attribute.addValue(id.toString());
262                         attribute.setResolved();
263
264                 } catch (NoSuchAlgorithmException e) {
265                         log.error("Unable to load SHA-1 hash algorithm.");
266                         return;
267                 }
268         }
269
270         private boolean usingDefaultSecret() {
271
272                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
273                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
274                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
275                                 (byte) 0x61, (byte) 0xEF};
276                 return Arrays.equals(defaultKey, salt);
277         }
278 }