A little more work on the credentials loader.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / Credentials.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved
4  * 
5  * 
6  * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
7  * following conditions are met:
8  * 
9  * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
10  * disclaimer.
11  * 
12  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
13  * disclaimer in the documentation and/or other materials provided with the distribution, if any, must include the
14  * following acknowledgment: "This product includes software developed by the University Corporation for Advanced
15  * Internet Development <http://www.ucaid.edu> Internet2 Project. Alternately, this acknowledegement may appear in the
16  * software itself, if and wherever such third-party acknowledgments normally appear.
17  * 
18  * Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor the University Corporation for
19  * Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote products derived from this software
20  * without specific prior written permission. For written permission, please contact shibboleth@shibboleth.org
21  * 
22  * Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the University Corporation
23  * for Advanced Internet Development, nor may Shibboleth appear in their name, without prior written permission of the
24  * University Corporation for Advanced Internet Development.
25  * 
26  * 
27  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR
28  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
29  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE,
30  * ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
31  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
32  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
33  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36  */
37
38 package edu.internet2.middleware.shibboleth.common;
39
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.security.KeyFactory;
43 import java.security.KeyStore;
44 import java.security.KeyStoreException;
45 import java.security.NoSuchAlgorithmException;
46 import java.security.PrivateKey;
47 import java.security.UnrecoverableKeyException;
48 import java.util.Collection;
49 import java.util.Hashtable;
50 import java.util.Iterator;
51
52 import java.security.cert.Certificate;
53 import java.security.cert.CertificateException;
54 import java.security.cert.CertificateFactory;
55 import java.security.cert.X509Certificate;
56 import java.security.spec.PKCS8EncodedKeySpec;
57
58 import org.apache.log4j.Logger;
59 import org.w3c.dom.Element;
60 import org.w3c.dom.Node;
61 import org.w3c.dom.NodeList;
62
63 /**
64  * @author Walter Hoehn
65  *  
66  */
67 public class Credentials {
68
69         public static final String credentialsNamespace = "urn:mace:shibboleth:credentials";
70
71         private static Logger log = Logger.getLogger(Credentials.class.getName());
72         private Hashtable data = new Hashtable();
73
74         public Credentials(Element e) {
75
76                 //TODO talk to Scott about the possibility of changing "resolver" to "loader", to avoid confusion with the AR
77
78                 if (!e.getTagName().equals("Credentials")) {
79                         throw new IllegalArgumentException();
80                 }
81
82                 NodeList resolverNodes = e.getChildNodes();
83                 if (resolverNodes.getLength() <= 0) {
84                         log.error("Credentials configuration inclues no Credential Resolver definitions.");
85                         throw new IllegalArgumentException("Cannot load credentials.");
86                 }
87
88                 for (int i = 0; resolverNodes.getLength() > i; i++) {
89                         if (resolverNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
90                                 try {
91
92                                         String credentialId = ((Element) resolverNodes.item(i)).getAttribute("id");
93                                         if (credentialId == null || credentialId.equals("")) {
94                                                 log.error("Found credential that was not labeled with a unique \"id\" attribute. Skipping.");
95                                         }
96
97                                         if (data.containsKey(credentialId)) {
98                                                 log.error("Duplicate credential id (" + credentialId + ") found. Skipping");
99                                         }
100
101                                         log.info("Found credential (" + credentialId + "). Loading...");
102                                         data.put(credentialId, CredentialFactory.loadCredential((Element) resolverNodes.item(i)));
103
104                                 } catch (CredentialFactoryException cfe) {
105                                         log.error("Could not load credential, skipping: " + cfe.getMessage());
106                                 } catch (ClassCastException cce) {
107                                         log.error("Problem realizing credential configuration" + cce.getMessage());
108                                 }
109                         }
110                 }
111         }
112
113         public boolean containsCredential(String identifier) {
114                 return data.containsKey(identifier);
115         }
116
117         public Credential getCredential(String identifier) {
118                 return (Credential) data.get(identifier);
119         }
120
121         static class CredentialFactory {
122
123                 private static Logger log = Logger.getLogger(CredentialFactory.class.getName());
124
125                 public static Credential loadCredential(Element e) throws CredentialFactoryException {
126                         if (e.getTagName().equals("KeyInfo")) {
127                                 return new KeyInfoCredentialResolver().loadCredential(e);
128                         }
129
130                         if (e.getTagName().equals("FileCredResolver")) {
131                                 return new FileCredentialResolver().loadCredential(e);
132                         }
133
134                         if (e.getTagName().equals("KeyStoreResolver")) {
135                                 return new KeystoreCredentialResolver().loadCredential(e);
136                         }
137
138                         if (e.getTagName().equals("CustomCredResolver")) {
139                                 return new CustomCredentialResolver().loadCredential(e);
140                         }
141
142                         log.error("Unrecognized Credential Resolver type: " + e.getTagName());
143                         throw new CredentialFactoryException("Failed to load credential.");
144                 }
145
146         }
147
148 }
149
150 class KeyInfoCredentialResolver implements CredentialResolver {
151         private static Logger log = Logger.getLogger(KeyInfoCredentialResolver.class.getName());
152         KeyInfoCredentialResolver() throws CredentialFactoryException {
153                 log.error("Credential Resolver (KeyInfoCredentialResolver) not implemented");
154                 throw new CredentialFactoryException("Failed to load credential.");
155         }
156
157         public Credential loadCredential(Element e) {
158                 return null;
159         }
160 }
161
162 class FileCredentialResolver implements CredentialResolver {
163         private static Logger log = Logger.getLogger(FileCredentialResolver.class.getName());
164
165         public Credential loadCredential(Element e) throws CredentialFactoryException {
166
167                 if (!e.getTagName().equals("FileCredResolver")) {
168                         log.error("Invalid Credential Resolver configuration: expected <FileCredResolver> .");
169                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
170                 }
171
172                 String id = e.getAttribute("id");
173                 if (id == null || id.equals("")) {
174                         log.error("Credential Resolvers require specification of the attribute \"id\".");
175                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
176                 }
177
178                 String certFormat = getCertFormat(e);
179                 String certPath = getCertPath(e);
180                 String keyFormat = "DER";
181                 String keyPath = "/conf/test.pemkey";
182                 log.debug("Certificate Format: (" + certFormat + ").");
183                 log.debug("Certificate Path: (" + certPath + ").");
184                 
185                 //TODO provider optional
186                 //TODO other kinds of certs?
187                 try {
188                 CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
189                 Collection chain = certFactory.generateCertificates(new ShibResource(certPath, this.getClass()).getInputStream());
190                 if (chain.isEmpty()) {
191                         log.error("File did not contain any valid certificates.");
192                         throw new CredentialFactoryException("File did not contain any valid certificates.");
193                 }
194                 Iterator iterator = chain.iterator();
195                 while (iterator.hasNext()) {
196                         System.err.println(((X509Certificate)iterator.next()).getSubjectDN());
197                 }
198                 //TODO remove this
199                 }catch (Exception p) {
200                         System.err.println(p);
201                         p.printStackTrace();
202                 }
203                 
204                 try {
205                         
206                         //TODO provider??
207                         //TODO other algorithms
208                                         KeyFactory keyFactory = KeyFactory.getInstance("RSA");
209                                         InputStream keyStream = new ShibResource(certPath, this.getClass()).getInputStream();
210                                         byte[] inputBuffer = new byte[8];
211                                         int i;
212                                         ByteContainer inputBytes = new ByteContainer(400);
213                                         do {
214                                                 i = keyStream.read(inputBuffer);
215                                                 for (int j = 0; j < i; j++) {
216                                                         inputBytes.append(inputBuffer[j]);
217                                                 }
218                                         } while (i > -1);
219
220 //TODO other encodings?
221                                         PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(inputBytes.toByteArray());
222                                         PrivateKey key = keyFactory.generatePrivate(keySpec);
223
224                                 } catch (Exception p) {
225                                         log.error("Problem reading private key: " + p.getMessage());
226                                         //throw new ExtKeyToolException("Problem reading private key.  Keys should be DER encoded pkcs8 or DER encoded native format.");
227                                 }
228                 
229
230                 return new Credential(null, null);
231         }
232         
233         private String getCertFormat(Element e) throws CredentialFactoryException {
234
235                 NodeList certificateElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Certificate");
236                 if (certificateElements.getLength() < 1) {
237                         log.error("Certificate not specified.");
238                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate> specification.");
239                 }
240                 if (certificateElements.getLength() > 1) {
241                         log.error("Multiple Certificate path specifications, using first.");
242                 }
243                 
244                 String format = ((Element)certificateElements.item(0)).getAttribute("id");
245                 if (format == null || format.equals("")) {
246                         log.debug("No format specified for certificate, using default (PEM) format.");
247                         format = "PEM";
248                 }
249                 
250                 if ((!format.equals("PEM")) && (!format.equals("DER"))) {
251                         log.error("File credential resolver only supports the (DER) and (PEM) formats.");
252                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
253                 }
254                 
255                 return format;
256         }
257
258         private String getCertPath(Element e) throws CredentialFactoryException {
259
260                 NodeList certificateElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Certificate");
261                 if (certificateElements.getLength() < 1) {
262                         log.error("Certificate not specified.");
263                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate> specification.");
264                 }
265                 if (certificateElements.getLength() > 1) {
266                         log.error("Multiple Certificate path specifications, using first.");
267                 }
268                 
269                 NodeList pathElements = ((Element) certificateElements.item(0)).getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
270                 if (pathElements.getLength() < 1) {
271                         log.error("Certificate path not specified.");
272                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate><Path/></Certificate> specification.");
273                 }
274                 if (pathElements.getLength() > 1) {
275                         log.error("Multiple Certificate path specifications, using first.");
276                 }
277                 Node tnode = pathElements.item(0).getFirstChild();
278                 String path = null;
279                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
280                         path = tnode.getNodeValue();
281                 }
282                 if (path == null || path.equals("")) {
283                         log.error("Certificate path not specified.");
284                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate><Path/></Certificate> specification.");
285                 }
286                 return path;
287         }
288         
289         
290         
291         /**
292          * Auto-enlarging container for bytes.
293          */
294
295         // Sure makes you wish bytes were first class objects.
296
297         private class ByteContainer {
298
299                 private byte[] buffer;
300                 private int cushion;
301                 private int currentSize = 0;
302
303                 private ByteContainer(int cushion) {
304                         buffer = new byte[cushion];
305                         this.cushion = cushion;
306                 }
307
308                 private void grow() {
309                         log.debug("Growing ByteContainer.");
310                         int newSize = currentSize + cushion;
311                         byte[] b = new byte[newSize];
312                         int toCopy = Math.min(currentSize, newSize);
313                         int i;
314                         for (i = 0; i < toCopy; i++) {
315                                 b[i] = buffer[i];
316                         }
317                         buffer = b;
318                 }
319
320                 /** 
321                  * Returns an array of the bytes in the container. <p>
322                  */
323
324                 private byte[] toByteArray() {
325                         byte[] b = new byte[currentSize];
326                         for (int i = 0; i < currentSize; i++) {
327                                 b[i] = buffer[i];
328                         }
329                         return b;
330                 }
331
332                 /** 
333                  * Add one byte to the end of the container.
334                  */
335
336                 private void append(byte b) {
337                         if (currentSize == buffer.length) {
338                                 grow();
339                         }
340                         buffer[currentSize] = b;
341                         currentSize++;
342                 }
343
344         }
345         
346         
347         
348 }
349
350 class KeystoreCredentialResolver implements CredentialResolver {
351
352         private static Logger log = Logger.getLogger(KeystoreCredentialResolver.class.getName());
353
354         public Credential loadCredential(Element e) throws CredentialFactoryException {
355
356                 if (!e.getTagName().equals("KeyStoreResolver")) {
357                         log.error("Invalid Credential Resolver configuration: expected <KeyStoreResolver> .");
358                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
359                 }
360
361                 String id = e.getAttribute("id");
362                 if (id == null || id.equals("")) {
363                         log.error("Credential Resolvers require specification of the attribute \"id\".");
364                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
365                 }
366
367                 String keyStoreType = e.getAttribute("keyStoreType");
368                 if (keyStoreType == null || keyStoreType.equals("")) {
369                         log.debug("Using default store type for credential.");
370                         keyStoreType = "JKS";
371                 }
372
373                 String path = loadPath(e);
374                 String alias = loadAlias(e);
375                 String keyPassword = loadKeyPassword(e);
376                 String keyStorePassword = loadKeyStorePassword(e);
377
378                 try {
379                         KeyStore keyStore = KeyStore.getInstance(keyStoreType);
380
381                         keyStore.load(new ShibResource(path, this.getClass()).getInputStream(), keyStorePassword.toCharArray());
382
383                         PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, keyPassword.toCharArray());
384
385                         if (privateKey == null) {
386                                 throw new CredentialFactoryException("No key entry was found with an alias of (" + alias + ").");
387                         }
388
389                         Certificate[] certificates = keyStore.getCertificateChain(alias);
390                         if (certificates == null) {
391                                 throw new CredentialFactoryException(
392                                         "An error occurred while reading the java keystore: No certificate found with the specified alias ("
393                                                 + alias
394                                                 + ").");
395                         }
396
397                         X509Certificate[] x509Certs = new X509Certificate[certificates.length];
398                         for (int i = 0; i < certificates.length; i++) {
399                                 if (certificates[i] instanceof X509Certificate) {
400                                         x509Certs[i] = (X509Certificate) certificates[i];
401                                 } else {
402                                         throw new CredentialFactoryException(
403                                                 "The KeyStore Credential Resolver can only load X509 certificates.  Found an unsupported certificate of type ("
404                                                         + certificates[i]
405                                                         + ").");
406                                 }
407                         }
408
409                         return new Credential(x509Certs, privateKey);
410
411                 } catch (KeyStoreException kse) {
412                         throw new CredentialFactoryException("An error occurred while accessing the java keystore: " + kse);
413                 } catch (NoSuchAlgorithmException nsae) {
414                         throw new CredentialFactoryException("Appropriate JCE provider not found in the java environment: " + nsae);
415                 } catch (CertificateException ce) {
416                         throw new CredentialFactoryException(
417                                 "The java keystore contained a certificate that could not be loaded: " + ce);
418                 } catch (IOException ioe) {
419                         throw new CredentialFactoryException("An error occurred while reading the java keystore: " + ioe);
420                 } catch (UnrecoverableKeyException uke) {
421                         throw new CredentialFactoryException(
422                                 "An error occurred while attempting to load the key from the java keystore: " + uke);
423                 }
424
425         }
426
427         private String loadPath(Element e) throws CredentialFactoryException {
428
429                 NodeList pathElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
430                 if (pathElements.getLength() < 1) {
431                         log.error("KeyStore path not specified.");
432                         throw new CredentialFactoryException("KeyStore Credential Resolver requires a <Path> specification.");
433                 }
434                 if (pathElements.getLength() > 1) {
435                         log.error("Multiple KeyStore path specifications, using first.");
436                 }
437                 Node tnode = pathElements.item(0).getFirstChild();
438                 String path = null;
439                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
440                         path = tnode.getNodeValue();
441                 }
442                 if (path == null || path.equals("")) {
443                         log.error("KeyStore path not specified.");
444                         throw new CredentialFactoryException("KeyStore Credential Resolver requires a <Path> specification.");
445                 }
446                 return path;
447         }
448
449         private String loadAlias(Element e) throws CredentialFactoryException {
450
451                 NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Alias");
452                 if (aliasElements.getLength() < 1) {
453                         log.error("KeyStore key alias not specified.");
454                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <Alias> specification.");
455                 }
456                 if (aliasElements.getLength() > 1) {
457                         log.error("Multiple KeyStore alias specifications, using first.");
458                 }
459                 Node tnode = aliasElements.item(0).getFirstChild();
460                 String alias = null;
461                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
462                         alias = tnode.getNodeValue();
463                 }
464                 if (alias == null || alias.equals("")) {
465                         log.error("KeyStore key alias not specified.");
466                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <Alias> specification.");
467                 }
468                 return alias;
469         }
470
471         private String loadKeyStorePassword(Element e) throws CredentialFactoryException {
472
473                 NodeList passwordElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyStorePassword");
474                 if (passwordElements.getLength() < 1) {
475                         log.error("KeyStore password not specified.");
476                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyStorePassword> specification.");
477                 }
478                 if (passwordElements.getLength() > 1) {
479                         log.error("Multiple KeyStore password specifications, using first.");
480                 }
481                 Node tnode = passwordElements.item(0).getFirstChild();
482                 String password = null;
483                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
484                         password = tnode.getNodeValue();
485                 }
486                 if (password == null || password.equals("")) {
487                         log.error("KeyStore password not specified.");
488                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyStorePassword> specification.");
489                 }
490                 return password;
491         }
492
493         private String loadKeyPassword(Element e) throws CredentialFactoryException {
494
495                 NodeList passwords = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyPassword");
496                 if (passwords.getLength() < 1) {
497                         log.error("KeyStore key password not specified.");
498                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyPassword> specification.");
499                 }
500                 if (passwords.getLength() > 1) {
501                         log.error("Multiple KeyStore key password specifications, using first.");
502                 }
503                 Node tnode = passwords.item(0).getFirstChild();
504                 String password = null;
505                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
506                         password = tnode.getNodeValue();
507                 }
508                 if (password == null || password.equals("")) {
509                         log.error("KeyStore key password not specified.");
510                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyPassword> specification.");
511                 }
512                 return password;
513         }
514 }
515
516 class CustomCredentialResolver implements CredentialResolver {
517
518         private static Logger log = Logger.getLogger(CustomCredentialResolver.class.getName());
519         private CredentialResolver resolver;
520
521         public Credential loadCredential(Element e) throws CredentialFactoryException {
522
523                 if (!e.getTagName().equals("CustomCredResolver")) {
524                         log.error("Invalid Credential Resolver configuration: expected <CustomCredResolver> .");
525                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
526                 }
527
528                 String id = e.getAttribute("id");
529                 if (id == null || id.equals("")) {
530                         log.error("Credential Resolvers require specification of the attribute \"id\".");
531                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
532                 }
533
534                 String className = e.getAttribute("Class");
535                 if (className == null || className.equals("")) {
536                         log.error("Custom Credential Resolver requires specification of the attribute \"Class\".");
537                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
538                 }
539
540                 try {
541                         return ((CredentialResolver) Class.forName(className).newInstance()).loadCredential(e);
542
543                 } catch (Exception loaderException) {
544                         log.error(
545                                 "Failed to load Custom Credential Resolver implementation class: " + loaderException.getMessage());
546                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
547                 }
548
549         }
550
551 }
552
553 class CredentialFactoryException extends Exception {
554
555         CredentialFactoryException(String message) {
556                 super(message);
557         }
558 }