fdd01a43dc1f22dbb20f32e97fdcf135913b2d0b
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / provider / SAML2PersistentID.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
18
19 import java.io.IOException;
20 import java.security.KeyStore;
21 import java.security.KeyStoreException;
22 import java.security.MessageDigest;
23 import java.security.NoSuchAlgorithmException;
24 import java.security.Principal;
25 import java.security.UnrecoverableKeyException;
26 import java.security.cert.CertificateException;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Iterator;
30
31 import javax.crypto.SecretKey;
32 import javax.naming.NamingException;
33 import javax.naming.directory.Attribute;
34 import javax.naming.directory.Attributes;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.parsers.ParserConfigurationException;
37
38 import org.apache.log4j.Logger;
39 import org.bouncycastle.util.encoders.Base64;
40 import org.w3c.dom.Document;
41 import org.w3c.dom.Element;
42 import org.w3c.dom.Node;
43 import org.w3c.dom.NodeList;
44
45 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn;
46 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
47 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
48 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
49 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
50 import edu.internet2.middleware.shibboleth.common.ShibResource;
51 import edu.internet2.middleware.shibboleth.common.XML;
52 import edu.internet2.middleware.shibboleth.xml.Parser;
53
54 /**
55  * <code>AttributeDefinition</code> implementation that provides a persistent, but pseudonymous, identifier for
56  * principals by hashing the principal name, requester, and a fixed secret salt. Forward compatible with SAML2 persisten
57  * Namde Identifiers.
58  * 
59  * @author Scott Cantor (cantor.2@osu.edu)
60  * @author Walter Hoehn
61  */
62 public class SAML2PersistentID extends BaseAttributeDefinition implements AttributeDefinitionPlugIn {
63
64         private static Logger log = Logger.getLogger(SAML2PersistentID.class.getName());
65         protected byte salt[];
66         protected String localPersistentId = null;
67
68         /**
69          * Constructor for SAML2PersistentID. Creates a PlugIn based on configuration information presented in a DOM
70          * Element.
71          */
72         public SAML2PersistentID(Element e) throws ResolutionPlugInException {
73
74                 super(e);
75                 localPersistentId = e.getAttributeNS(null, "sourceName");
76
77                 // Make sure we understand how to resolve the local persistent ID for the principal.
78                 if (localPersistentId != null && localPersistentId.length() > 0) {
79                         if (connectorDependencyIds.size() != 1 || !attributeDependencyIds.isEmpty()) {
80                                 log.error("Can't specify the sourceName attribute without a single connector dependency.");
81                                 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
82                         }
83                 } else if (!connectorDependencyIds.isEmpty()) {
84                         log.error("Can't specify a connector dependency without supplying the sourceName attribute.");
85                         throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
86                 } else if (attributeDependencyIds.size() > 1) {
87                         log.error("Can't specify more than one attribute dependency, this is ambiguous.");
88                         throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
89                 }
90
91                 // Salt can be either embedded in the element or pulled out of a keystore.
92                 NodeList salts = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Salt");
93                 if (salts == null || salts.getLength() != 1) {
94                         log.error("Missing <Salt> from attribute definition configuration.");
95                         throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
96                 }
97
98                 Element salt = (Element) salts.item(0);
99                 Node child = salt.getFirstChild();
100                 if (child != null && child.getNodeType() == Node.TEXT_NODE && child.getNodeValue() != null
101                                 && child.getNodeValue().length() >= 16) this.salt = child.getNodeValue().getBytes();
102                 else {
103                         String ksPath = salt.getAttributeNS(null, "keyStorePath");
104                         String keyAlias = salt.getAttributeNS(null, "keyStoreKeyAlias");
105                         String ksPass = salt.getAttributeNS(null, "keyStorePassword");
106                         String keyPass = salt.getAttributeNS(null, "keyStoreKeyPassword");
107
108                         if (ksPath == null || ksPath.length() == 0 || keyAlias == null || keyAlias.length() == 0 || ksPass == null
109                                         || ksPass.length() == 0 || keyPass == null || keyPass.length() == 0) {
110
111                                 log.error("Missing <Salt> keyStore attributes from attribute definition configuration.");
112                                 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
113                         }
114
115                         try {
116                                 KeyStore keyStore = KeyStore.getInstance("JCEKS");
117
118                                 keyStore.load(new ShibResource(ksPath, this.getClass()).getInputStream(), ksPass.toCharArray());
119                                 SecretKey secret = (SecretKey) keyStore.getKey(keyAlias, keyPass.toCharArray());
120
121                                 if (usingDefaultSecret()) {
122                                         log.warn("You are running the SAML2PersistentID PlugIn with the default "
123                                                         + "secret key as a salt.  This is UNSAFE!  Please change "
124                                                         + "this configuration and restart the IdP.");
125                                 }
126                                 this.salt = secret.getEncoded();
127
128                         } catch (KeyStoreException ex) {
129                                 log.error("An error occurred while loading the java keystore.  Unable to initialize "
130                                                 + "Attribute Definition PlugIn: " + ex);
131                                 throw new ResolutionPlugInException(
132                                                 "An error occurred while loading the java keystore.  Unable to initialize Attribute Definition PlugIn.");
133                         } catch (CertificateException ex) {
134                                 log.error("The java keystore contained corrupted data.  Unable to initialize "
135                                                 + "Attribute Definition PlugIn: " + ex);
136                                 throw new ResolutionPlugInException(
137                                                 "The java keystore contained corrupted data.  Unable to initialize Attribute Definition PlugIn.");
138                         } catch (NoSuchAlgorithmException ex) {
139                                 log.error("Appropriate JCE provider not found in the java environment. Unable to initialize "
140                                                 + "Attribute Definition PlugIn: " + ex);
141                                 throw new ResolutionPlugInException(
142                                                 "Appropriate JCE provider not found in the java environment. Unable to initialize Attribute Definition PlugIn.");
143                         } catch (IOException ex) {
144                                 log.error("An error accessing while loading the java keystore.  Unable to initialize "
145                                                 + "Attribute Definition PlugIn: " + ex);
146                                 throw new ResolutionPlugInException(
147                                                 "An error occurred while accessing the java keystore.  Unable to initialize Attribute Definition PlugIn.");
148                         } catch (UnrecoverableKeyException ex) {
149                                 log.error("Secret could not be loaded from the java keystore.  Verify that the alias and "
150                                                 + "password are correct: " + ex);
151                                 throw new ResolutionPlugInException(
152                                                 "Secret could not be loaded from the java keystore.  Verify that the alias and password are correct. ");
153                         }
154                 }
155         }
156
157         /**
158          * @see edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn#resolve(edu.internet2.middleware.shibboleth.aa.attrresolv.ArpAttribute,
159          *      java.security.Principal, java.lang.String, edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies)
160          */
161         public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
162                         Dependencies depends) throws ResolutionPlugInException {
163
164                 log.debug("Resolving attribute: (" + getId() + ")");
165
166                 if (requester == null || requester.equals("")) {
167                         log.debug("Could not create persistent ID for unauthenticated requester.");
168                         attribute.setResolved();
169                         return;
170                 }
171
172                 if (responder == null || responder.equals("")) {
173                         log.error("Could not create persistent ID for null responder.");
174                         attribute.setResolved();
175                         return;
176                 }
177
178                 String localId = null;
179
180                 // Resolve the correct local persistent identifier.
181                 if (!attributeDependencyIds.isEmpty()) {
182                         ResolverAttribute dep = depends.getAttributeResolution((String) attributeDependencyIds.iterator().next());
183                         if (dep != null) {
184                                 Iterator vals = dep.getValues();
185                                 if (vals.hasNext()) {
186                                         log.debug("Found persistent ID value for attribute (" + getId() + ").");
187                                         localId = (String) vals.next();
188                                         if (vals.hasNext()) {
189                                                 log.error("An attribute dependency of attribute (" + getId()
190                                                                 + ") returned multiple values, expecting only one.");
191                                                 return;
192                                         }
193                                 } else {
194                                         log.error("An attribute dependency of attribute (" + getId()
195                                                         + ") returned no values, expecting one.");
196                                         return;
197                                 }
198                         } else {
199                                 log.error("An attribute dependency of attribute (" + getId()
200                                                 + ") was not included in the dependency chain.");
201                                 return;
202                         }
203                 } else if (!connectorDependencyIds.isEmpty()) {
204                         Attributes attrs = depends.getConnectorResolution((String) connectorDependencyIds.iterator().next());
205                         if (attrs != null) {
206                                 Attribute attr = attrs.get(localPersistentId);
207                                 if (attr != null) {
208                                         if (attr.size() != 1) {
209                                                 log.error("An attribute dependency of attribute (" + getId() + ") returned " + attr.size()
210                                                                 + " values, expecting only one.");
211                                         } else {
212                                                 try {
213                                                         localId = (String) attr.get();
214                                                         log.debug("Found persistent ID value for attribute (" + getId() + ").");
215                                                 } catch (NamingException e) {
216                                                         log.error("A connector dependency of attribute (" + getId() + ") threw an exception: " + e);
217                                                         return;
218                                                 }
219                                         }
220                                 }
221                         } else {
222                                 log.error("A connector dependency of attribute (" + getId() + ") did not return any attributes.");
223                                 return;
224                         }
225                 } else {
226                         localId = principal.getName();
227                 }
228
229                 if (localId == null || localId.equals("")) {
230                         log.error("Specified source data not supplied from dependencies.  Unable to create persistent ID.");
231                         attribute.setResolved();
232                         return;
233                 }
234
235                 standardProcessing(attribute);
236
237                 // Hash the data together to produce the persistent ID.
238                 try {
239                         MessageDigest md = MessageDigest.getInstance("SHA");
240                         md.update(requester.getBytes());
241                         md.update((byte) '!');
242                         md.update(localId.getBytes());
243                         md.update((byte) '!');
244                         String result = new String(Base64.encode(md.digest(salt)));
245
246                         // SAML2 persistent NameId format
247                         try {
248                                 DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
249                                 docFactory.setNamespaceAware(true);
250                                 Document placeHolder = docFactory.newDocumentBuilder().newDocument();
251                                 Element nameIDNode = placeHolder.createElementNS(XML.SAML2ASSERT_NS, "NameID");
252                                 nameIDNode.setAttribute("xmlns", XML.SAML2ASSERT_NS);
253                                 nameIDNode.setAttribute("Format", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
254                                 nameIDNode.setAttribute("NameQualifier", responder);
255                                 nameIDNode.setAttribute("SPNameQualifier", requester);
256                                 nameIDNode.appendChild(placeHolder.createTextNode(result.replaceAll(System
257                                                 .getProperty("line.separator"), "")));
258
259                                 attribute.addValue(nameIDNode);
260                                 attribute.registerValueHandler(new DOMValueHandler());
261                                 attribute.setResolved();
262                         } catch (ParserConfigurationException pce) {
263                                 log.error("Unable to create DOM for persistent ID: " + pce);
264                                 throw new ResolutionPlugInException("Error generating persistent ID.");
265                         }
266
267                 } catch (NoSuchAlgorithmException e) {
268                         log.error("Unable to load SHA-1 hash algorithm.");
269                         return;
270                 }
271         }
272
273         private boolean usingDefaultSecret() {
274
275                 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
276                                 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
277                                 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
278                                 (byte) 0x61, (byte) 0xEF};
279                 return Arrays.equals(defaultKey, salt);
280         }
281
282         class DOMValueHandler implements ValueHandler {
283
284                 public void toDOM(Element valueElement, Object value, Document document) {
285
286                         valueElement.appendChild(document.importNode((Node) value, true));
287                 }
288
289                 public Iterator getValues(Collection internalValues) {
290
291                         return new SerializationIterator(internalValues.iterator());
292                 }
293
294                 public boolean equals(Object object) {
295
296                         if (object instanceof DOMValueHandler) { return true; }
297                         return false;
298                 }
299
300                 private class SerializationIterator implements Iterator {
301
302                         Iterator wrapped;
303
304                         SerializationIterator(Iterator i) {
305
306                                 wrapped = i;
307                         }
308
309                         public boolean hasNext() {
310
311                                 return wrapped.hasNext();
312                         }
313
314                         public Object next() {
315
316                                 return Parser.serialize(((Element) wrapped.next()));
317                         }
318
319                         public void remove() {
320
321                                 wrapped.remove();
322                         }
323
324                 }
325         }
326 }