2bee8907b29f85576bd92b270e14df7986a6eb86
[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:1.0";
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("FileResolver")) {
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("CustomResolver")) {
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("FileResolver")) {
168                         log.error("Invalid Credential Resolver configuration: expected <FileResolver> .");
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("format");
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("storeType");
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 certAlias = loadCertAlias(e, alias);
376                 String keyPassword = loadKeyPassword(e);
377                 String keyStorePassword = loadKeyStorePassword(e);
378
379                 try {
380                         KeyStore keyStore = KeyStore.getInstance(keyStoreType);
381
382                         keyStore.load(new ShibResource(path, this.getClass()).getInputStream(), keyStorePassword.toCharArray());
383
384                         PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, keyPassword.toCharArray());
385
386                         if (privateKey == null) {
387                                 throw new CredentialFactoryException("No key entry was found with an alias of (" + alias + ").");
388                         }
389
390                         Certificate[] certificates = keyStore.getCertificateChain(certAlias);
391                         if (certificates == null) {
392                                 throw new CredentialFactoryException(
393                                         "An error occurred while reading the java keystore: No certificate found with the specified alias ("
394                                                 + certAlias
395                                                 + ").");
396                         }
397
398                         X509Certificate[] x509Certs = new X509Certificate[certificates.length];
399                         for (int i = 0; i < certificates.length; i++) {
400                                 if (certificates[i] instanceof X509Certificate) {
401                                         x509Certs[i] = (X509Certificate) certificates[i];
402                                 } else {
403                                         throw new CredentialFactoryException(
404                                                 "The KeyStore Credential Resolver can only load X509 certificates.  Found an unsupported certificate of type ("
405                                                         + certificates[i]
406                                                         + ").");
407                                 }
408                         }
409
410                         return new Credential(x509Certs, privateKey);
411
412                 } catch (KeyStoreException kse) {
413                         throw new CredentialFactoryException("An error occurred while accessing the java keystore: " + kse);
414                 } catch (NoSuchAlgorithmException nsae) {
415                         throw new CredentialFactoryException("Appropriate JCE provider not found in the java environment: " + nsae);
416                 } catch (CertificateException ce) {
417                         throw new CredentialFactoryException(
418                                 "The java keystore contained a certificate that could not be loaded: " + ce);
419                 } catch (IOException ioe) {
420                         throw new CredentialFactoryException("An error occurred while reading the java keystore: " + ioe);
421                 } catch (UnrecoverableKeyException uke) {
422                         throw new CredentialFactoryException(
423                                 "An error occurred while attempting to load the key from the java keystore: " + uke);
424                 }
425
426         }
427
428         private String loadPath(Element e) throws CredentialFactoryException {
429
430                 NodeList pathElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
431                 if (pathElements.getLength() < 1) {
432                         log.error("KeyStore path not specified.");
433                         throw new CredentialFactoryException("KeyStore Credential Resolver requires a <Path> specification.");
434                 }
435                 if (pathElements.getLength() > 1) {
436                         log.error("Multiple KeyStore path specifications, using first.");
437                 }
438                 Node tnode = pathElements.item(0).getFirstChild();
439                 String path = null;
440                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
441                         path = tnode.getNodeValue();
442                 }
443                 if (path == null || path.equals("")) {
444                         log.error("KeyStore path not specified.");
445                         throw new CredentialFactoryException("KeyStore Credential Resolver requires a <Path> specification.");
446                 }
447                 return path;
448         }
449
450         private String loadAlias(Element e) throws CredentialFactoryException {
451
452                 NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyAlias");
453                 if (aliasElements.getLength() < 1) {
454                         log.error("KeyStore key alias not specified.");
455                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyAlias> specification.");
456                 }
457                 if (aliasElements.getLength() > 1) {
458                         log.error("Multiple key alias specifications, using first.");
459                 }
460                 Node tnode = aliasElements.item(0).getFirstChild();
461                 String alias = null;
462                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
463                         alias = tnode.getNodeValue();
464                 }
465                 if (alias == null || alias.equals("")) {
466                         log.error("KeyStore key alias not specified.");
467                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyAlias> specification.");
468                 }
469                 return alias;
470         }
471
472         private String loadCertAlias(Element e, String defaultAlias) throws CredentialFactoryException {
473
474                 NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "CertAlias");
475                 if (aliasElements.getLength() < 1) {
476                         log.debug("KeyStore cert alias not specified, defaulting to key alias.");
477                         return defaultAlias;
478                 }
479
480                 if (aliasElements.getLength() > 1) {
481                         log.error("Multiple cert alias specifications, using first.");
482                 }
483
484                 Node tnode = aliasElements.item(0).getFirstChild();
485                 String alias = null;
486                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
487                         alias = tnode.getNodeValue();
488                 }
489                 if (alias == null || alias.equals("")) {
490                         log.debug("KeyStore cert alias not specified, defaulting to key alias.");
491                         return defaultAlias;
492                 }
493                 return alias;
494         }
495
496         private String loadKeyStorePassword(Element e) throws CredentialFactoryException {
497
498                 NodeList passwordElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "StorePassword");
499                 if (passwordElements.getLength() < 1) {
500                         log.error("KeyStore password not specified.");
501                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <StorePassword> specification.");
502                 }
503                 if (passwordElements.getLength() > 1) {
504                         log.error("Multiple KeyStore password specifications, using first.");
505                 }
506                 Node tnode = passwordElements.item(0).getFirstChild();
507                 String password = null;
508                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
509                         password = tnode.getNodeValue();
510                 }
511                 if (password == null || password.equals("")) {
512                         log.error("KeyStore password not specified.");
513                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <StorePassword> specification.");
514                 }
515                 return password;
516         }
517
518         private String loadKeyPassword(Element e) throws CredentialFactoryException {
519
520                 NodeList passwords = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyPassword");
521                 if (passwords.getLength() < 1) {
522                         log.error("KeyStore key password not specified.");
523                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyPassword> specification.");
524                 }
525                 if (passwords.getLength() > 1) {
526                         log.error("Multiple KeyStore key password specifications, using first.");
527                 }
528                 Node tnode = passwords.item(0).getFirstChild();
529                 String password = null;
530                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
531                         password = tnode.getNodeValue();
532                 }
533                 if (password == null || password.equals("")) {
534                         log.error("KeyStore key password not specified.");
535                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyPassword> specification.");
536                 }
537                 return password;
538         }
539 }
540
541 class CustomCredentialResolver implements CredentialResolver {
542
543         private static Logger log = Logger.getLogger(CustomCredentialResolver.class.getName());
544         private CredentialResolver resolver;
545
546         public Credential loadCredential(Element e) throws CredentialFactoryException {
547
548                 if (!e.getTagName().equals("CustomCredResolver")) {
549                         log.error("Invalid Credential Resolver configuration: expected <CustomCredResolver> .");
550                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
551                 }
552
553                 String id = e.getAttribute("id");
554                 if (id == null || id.equals("")) {
555                         log.error("Credential Resolvers require specification of the attribute \"id\".");
556                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
557                 }
558
559                 String className = e.getAttribute("Class");
560                 if (className == null || className.equals("")) {
561                         log.error("Custom Credential Resolver requires specification of the attribute \"Class\".");
562                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
563                 }
564
565                 try {
566                         return ((CredentialResolver) Class.forName(className).newInstance()).loadCredential(e);
567
568                 } catch (Exception loaderException) {
569                         log.error(
570                                 "Failed to load Custom Credential Resolver implementation class: " + loaderException.getMessage());
571                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
572                 }
573
574         }
575
576 }
577
578 class CredentialFactoryException extends Exception {
579
580         CredentialFactoryException(String message) {
581                 super(message);
582         }
583 }