2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
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;
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;
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;
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;
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
59 * @author Scott Cantor (cantor.2@osu.edu)
60 * @author Walter Hoehn
62 public class SAML2PersistentID extends BaseAttributeDefinition implements AttributeDefinitionPlugIn {
64 private static Logger log = Logger.getLogger(SAML2PersistentID.class.getName());
65 protected byte salt[];
66 protected String localPersistentId = null;
69 * Constructor for SAML2PersistentID. Creates a PlugIn based on configuration information presented in a DOM
72 public SAML2PersistentID(Element e) throws ResolutionPlugInException {
75 localPersistentId = e.getAttributeNS(null, "sourceName");
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.");
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.");
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.");
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();
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");
108 if (ksPath == null || ksPath.length() == 0 || keyAlias == null || keyAlias.length() == 0 || ksPass == null
109 || ksPass.length() == 0 || keyPass == null || keyPass.length() == 0) {
111 log.error("Missing <Salt> keyStore attributes from attribute definition configuration.");
112 throw new ResolutionPlugInException("Failed to initialize Attribute Definition PlugIn.");
116 KeyStore keyStore = KeyStore.getInstance("JCEKS");
118 keyStore.load(new ShibResource(ksPath, this.getClass()).getInputStream(), ksPass.toCharArray());
119 SecretKey secret = (SecretKey) keyStore.getKey(keyAlias, keyPass.toCharArray());
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.");
126 this.salt = secret.getEncoded();
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. ");
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)
161 public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
162 Dependencies depends) throws ResolutionPlugInException {
164 log.debug("Resolving attribute: (" + getId() + ")");
166 if (requester == null || requester.equals("")) {
167 log.debug("Could not create persistent ID for unauthenticated requester.");
168 attribute.setResolved();
172 if (responder == null || responder.equals("")) {
173 log.error("Could not create persistent ID for null responder.");
174 attribute.setResolved();
178 String localId = null;
180 // Resolve the correct local persistent identifier.
181 if (!attributeDependencyIds.isEmpty()) {
182 ResolverAttribute dep = depends.getAttributeResolution((String) attributeDependencyIds.iterator().next());
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.");
194 log.error("An attribute dependency of attribute (" + getId()
195 + ") returned no values, expecting one.");
199 log.error("An attribute dependency of attribute (" + getId()
200 + ") was not included in the dependency chain.");
203 } else if (!connectorDependencyIds.isEmpty()) {
204 Attributes attrs = depends.getConnectorResolution((String) connectorDependencyIds.iterator().next());
206 Attribute attr = attrs.get(localPersistentId);
208 if (attr.size() != 1) {
209 log.error("An attribute dependency of attribute (" + getId() + ") returned " + attr.size()
210 + " values, expecting only one.");
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);
222 log.error("A connector dependency of attribute (" + getId() + ") did not return any attributes.");
226 localId = principal.getName();
229 if (localId == null || localId.equals("")) {
230 log.error("Specified source data not supplied from dependencies. Unable to create persistent ID.");
231 attribute.setResolved();
235 standardProcessing(attribute);
237 // Hash the data together to produce the persistent ID.
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)));
246 // SAML2 persistent NameId format
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.setAttributeNS(org.opensaml.XML.XMLNS_NS, "xmlns", XML.SAML2ASSERT_NS);
253 nameIDNode.setAttributeNS(null, "Format", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
254 nameIDNode.setAttributeNS(null, "NameQualifier", responder);
255 nameIDNode.setAttributeNS(null, "SPNameQualifier", requester);
256 nameIDNode.appendChild(placeHolder.createTextNode(result.replaceAll(System
257 .getProperty("line.separator"), "")));
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.");
267 } catch (NoSuchAlgorithmException e) {
268 log.error("Unable to load SHA-1 hash algorithm.");
273 private boolean usingDefaultSecret() {
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);
282 class DOMValueHandler implements ValueHandler {
284 public void toDOM(Element valueElement, Object value, Document document) {
286 valueElement.appendChild(document.importNode((Node) value, true));
289 public Iterator getValues(Collection internalValues) {
291 return new SerializationIterator(internalValues.iterator());
294 public boolean equals(Object object) {
296 if (object instanceof DOMValueHandler) { return true; }
300 private class SerializationIterator implements Iterator {
304 SerializationIterator(Iterator i) {
309 public boolean hasNext() {
311 return wrapped.hasNext();
314 public Object next() {
316 return Parser.serialize(((Element) wrapped.next()));
319 public void remove() {