Add support to the File Credential Resolver for loading multiple apache style cert...
[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.BufferedReader;
41 import java.io.ByteArrayInputStream;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.InputStreamReader;
45 import java.security.KeyFactory;
46 import java.security.KeyStore;
47 import java.security.KeyStoreException;
48 import java.security.NoSuchAlgorithmException;
49 import java.security.PrivateKey;
50 import java.security.PublicKey;
51 import java.security.Signature;
52 import java.security.UnrecoverableKeyException;
53 import java.security.cert.Certificate;
54 import java.security.cert.CertificateException;
55 import java.security.cert.CertificateFactory;
56 import java.security.cert.X509Certificate;
57 import java.security.spec.PKCS8EncodedKeySpec;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.Hashtable;
61
62 import org.apache.log4j.Logger;
63 import org.w3c.dom.Element;
64 import org.w3c.dom.Node;
65 import org.w3c.dom.NodeList;
66
67 /**
68  * @author Walter Hoehn
69  *  
70  */
71 public class Credentials {
72
73         public static final String credentialsNamespace = "urn:mace:shibboleth:credentials:1.0";
74
75         private static Logger log = Logger.getLogger(Credentials.class.getName());
76         private Hashtable data = new Hashtable();
77
78         public Credentials(Element e) {
79
80                 if (!e.getTagName().equals("Credentials")) {
81                         throw new IllegalArgumentException();
82                 }
83
84                 NodeList resolverNodes = e.getChildNodes();
85                 if (resolverNodes.getLength() <= 0) {
86                         log.error("Credentials configuration inclues no Credential Resolver definitions.");
87                         throw new IllegalArgumentException("Cannot load credentials.");
88                 }
89
90                 for (int i = 0; resolverNodes.getLength() > i; i++) {
91                         if (resolverNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
92                                 try {
93
94                                         String credentialId = ((Element) resolverNodes.item(i)).getAttribute("Id");
95                                         if (credentialId == null || credentialId.equals("")) {
96                                                 log.error("Found credential that was not labeled with a unique \"Id\" attribute. Skipping.");
97                                         }
98
99                                         if (data.containsKey(credentialId)) {
100                                                 log.error("Duplicate credential id (" + credentialId + ") found. Skipping");
101                                         }
102
103                                         log.info("Found credential (" + credentialId + "). Loading...");
104                                         data.put(credentialId, CredentialFactory.loadCredential((Element) resolverNodes.item(i)));
105
106                                 } catch (CredentialFactoryException cfe) {
107                                         log.error("Could not load credential, skipping: " + cfe.getMessage());
108                                 } catch (ClassCastException cce) {
109                                         log.error("Problem realizing credential configuration" + cce.getMessage());
110                                 }
111                         }
112                 }
113         }
114
115         public boolean containsCredential(String identifier) {
116                 return data.containsKey(identifier);
117         }
118
119         public Credential getCredential(String identifier) {
120                 return (Credential) data.get(identifier);
121         }
122
123         static class CredentialFactory {
124
125                 private static Logger log = Logger.getLogger(CredentialFactory.class.getName());
126
127                 public static Credential loadCredential(Element e) throws CredentialFactoryException {
128                         if (e.getTagName().equals("KeyInfo")) {
129                                 return new KeyInfoCredentialResolver().loadCredential(e);
130                         }
131
132                         if (e.getTagName().equals("FileResolver")) {
133                                 return new FileCredentialResolver().loadCredential(e);
134                         }
135
136                         if (e.getTagName().equals("KeyStoreResolver")) {
137                                 return new KeystoreCredentialResolver().loadCredential(e);
138                         }
139
140                         if (e.getTagName().equals("CustomResolver")) {
141                                 return new CustomCredentialResolver().loadCredential(e);
142                         }
143
144                         log.error("Unrecognized Credential Resolver type: " + e.getTagName());
145                         throw new CredentialFactoryException("Failed to load credential.");
146                 }
147
148         }
149
150 }
151
152 class KeyInfoCredentialResolver implements CredentialResolver {
153         private static Logger log = Logger.getLogger(KeyInfoCredentialResolver.class.getName());
154         KeyInfoCredentialResolver() throws CredentialFactoryException {
155                 log.error("Credential Resolver (KeyInfoCredentialResolver) not implemented");
156                 throw new CredentialFactoryException("Failed to load credential.");
157         }
158
159         public Credential loadCredential(Element e) {
160                 return null;
161         }
162 }
163
164 class FileCredentialResolver implements CredentialResolver {
165         private static Logger log = Logger.getLogger(FileCredentialResolver.class.getName());
166
167         public Credential loadCredential(Element e) throws CredentialFactoryException {
168
169                 if (!e.getTagName().equals("FileResolver")) {
170                         log.error("Invalid Credential Resolver configuration: expected <FileResolver> .");
171                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
172                 }
173
174                 String id = e.getAttribute("Id");
175                 if (id == null || id.equals("")) {
176                         log.error("Credential Resolvers require specification of the attribute \"Id\".");
177                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
178                 }
179
180                 //Load the key
181
182                 String keyFormat = getKeyFormat(e);
183                 String keyPath = getKeyPath(e);
184                 log.debug("Key Format: (" + keyFormat + ").");
185                 log.debug("Key Path: (" + keyPath + ").");
186
187                 String keyAlgorithm = "RSA";
188
189                 //TODO providers?
190                 //TODO support DER, PEM, DER-PKCS8, and PEM-PKCS8?
191                 //TODO DSA
192
193                 PrivateKey key = null;
194
195                 if (keyAlgorithm.equals("RSA") && keyFormat.equals("DER-PKCS8")) {
196                         try {
197                                 key = getRSADERKey(new ShibResource(keyPath, this.getClass()).getInputStream());
198                         } catch (IOException ioe) {
199                                 log.error("Could not load resource from specified location (" + keyPath + "): " + e);
200                                 throw new CredentialFactoryException("Unable to load private key.");
201                         }
202                 } else {
203                         log.error("File credential resolver only supports the RSA keys in DER-encoded PKCS8 format (DER-PKCS8).");
204                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
205                 }
206
207                 String certFormat = getCertFormat(e);
208                 String certPath = getCertPath(e);
209                 //A placeholder in case we ever want to make this configurable
210                 String certType = "X.509";
211
212                 log.debug("Certificate Format: (" + certFormat + ").");
213                 log.debug("Certificate Path: (" + certPath + ").");
214
215                 //TODO provider optional
216                 //TODO provide a way to specify a separate CA bundle
217
218                 //The loading code should work for other types, but the chain construction code 
219                 //would break
220                 if (!certType.equals("X.509")) {
221                         log.error("File credential resolver only supports the X.509 certificates.");
222                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
223                 }
224
225                 ArrayList certChain = new ArrayList();
226                 ArrayList allCerts = new ArrayList();
227
228                 try {
229                         Certificate[] certsFromPath =
230                                 loadCertificates(new ShibResource(certPath, this.getClass()).getInputStream(), certType);
231
232                         allCerts.addAll(Arrays.asList(certsFromPath));
233
234                         //Find the end-entity cert first
235                         if (certsFromPath == null || certsFromPath.length == 0) {
236                                 log.error("File at (" + certPath + ") did not contain any valid certificates.");
237                                 throw new CredentialFactoryException("File did not contain any valid certificates.");
238                         }
239
240                         if (certsFromPath.length == 1) {
241                                 log.debug("Certificate file only contains 1 certificate.");
242                                 log.debug("Ensure that it matches the private key.");
243                                 if (!isMatchingKey(certsFromPath[0].getPublicKey(), key)) {
244                                         log.error("Certificate does not match the private key.");
245                                         throw new CredentialFactoryException("File did not contain any valid certificates.");
246                                 }
247                                 certChain.add(certsFromPath[0]);
248                                 log.debug(
249                                         "Successfully identified the end entity cert: "
250                                                 + ((X509Certificate) certChain.get(0)).getSubjectDN());
251
252                         } else {
253                                 log.debug("Certificate file contains multiple certificates.");
254                                 log.debug("Trying to determine the end-entity cert by matching against the private key.");
255                                 for (int i = 0; certsFromPath.length > i; i++) {
256                                         if (isMatchingKey(certsFromPath[i].getPublicKey(), key)) {
257                                                 log.debug("Found matching end cert: " + ((X509Certificate) certsFromPath[i]).getSubjectDN());
258                                                 certChain.add(certsFromPath[i]);
259                                         }
260                                 }
261                                 if (certChain.size() < 1) {
262                                         log.error("No certificate in chain that matches specified private key");
263                                         throw new CredentialFactoryException("No certificate in chain that matches specified private key");
264                                 }
265                                 if (certChain.size() > 1) {
266                                         log.error("More than one certificate in chain that matches specified private key");
267                                         throw new CredentialFactoryException("More than one certificate in chain that matches specified private key");
268                                 }
269                                 log.debug(
270                                         "Successfully identified the end entity cert: "
271                                                 + ((X509Certificate) certChain.get(0)).getSubjectDN());
272                         }
273
274                         //Now load additional certs and construct a chain
275                         String[] caPaths = getCAPaths(e);
276                         if (caPaths != null && caPaths.length > 0) {
277                                 log.debug("Attempting to load certificates from (" + caPaths.length + ") CA certificate files.");
278                                 for (int i = 0; i < caPaths.length; i++) {
279                                         allCerts.addAll(
280                                                 Arrays.asList(
281                                                         loadCertificates(
282                                                                 new ShibResource(caPaths[i], this.getClass()).getInputStream(),
283                                                                 certType)));
284                                 }
285                         }
286
287                         //TODO probably don't want to require a full chain
288                         log.debug("Attempting to construct a certificate chain.");
289                         walkChain((X509Certificate[]) allCerts.toArray(new X509Certificate[0]), certChain);
290
291                         log.info("Verifying that each link in the cert chain is signed appropriately");
292                         for (int i = 0; i < certChain.size() - 1; i++) {
293                                 PublicKey pubKey = ((X509Certificate) certChain.get(i + 1)).getPublicKey();
294                                 try {
295                                         ((X509Certificate) certChain.get(i)).verify(pubKey);
296                                 } catch (Exception se) {
297                                         log.error("Certificate chain cannot be verified: " + se);
298                                         throw new CredentialFactoryException("Certificate chain cannot be verified: " + se);
299                                 }
300                         }
301                         log.debug("All signatures verified. Certificate chain creation successful.");
302
303                 } catch (IOException p) {
304                         log.error("Could not load resource from specified location (" + certPath + "): " + p);
305                         throw new CredentialFactoryException("Unable to load certificates.");
306                 }
307
308                 return new Credential(((X509Certificate[]) certChain.toArray(new X509Certificate[0])), key);
309         }
310
311         private PrivateKey getRSADERKey(InputStream inStream) throws CredentialFactoryException {
312
313                 try {
314
315                         KeyFactory keyFactory = KeyFactory.getInstance("RSA");
316                         byte[] inputBuffer = new byte[8];
317                         int i;
318                         ByteContainer inputBytes = new ByteContainer(400);
319                         do {
320                                 i = inStream.read(inputBuffer);
321                                 for (int j = 0; j < i; j++) {
322                                         inputBytes.append(inputBuffer[j]);
323                                 }
324                         } while (i > -1);
325
326                         PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(inputBytes.toByteArray());
327
328                         return keyFactory.generatePrivate(keySpec);
329
330                 } catch (Exception e) {
331                         log.error("Unable to load private key: " + e);
332                         throw new CredentialFactoryException("Unable to load private key.");
333                 }
334
335         }
336
337         private String getCertFormat(Element e) throws CredentialFactoryException {
338
339                 NodeList certificateElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Certificate");
340                 if (certificateElements.getLength() < 1) {
341                         log.error("Certificate not specified.");
342                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate> specification.");
343                 }
344                 if (certificateElements.getLength() > 1) {
345                         log.error("Multiple Certificate path specifications, using first.");
346                 }
347
348                 String format = ((Element) certificateElements.item(0)).getAttribute("format");
349                 if (format == null || format.equals("")) {
350                         log.debug("No format specified for certificate, using default (PEM) format.");
351                         format = "PEM";
352                 }
353
354                 if ((!format.equals("PEM")) && (!format.equals("DER"))) {
355                         log.error("File credential resolver only supports the (DER) and (PEM) formats.");
356                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
357                 }
358
359                 return format;
360         }
361
362         private String getKeyFormat(Element e) throws CredentialFactoryException {
363
364                 NodeList keyElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Key");
365                 if (keyElements.getLength() < 1) {
366                         log.error("Key not specified.");
367                         throw new CredentialFactoryException("File Credential Resolver requires a <Key> specification.");
368                 }
369                 if (keyElements.getLength() > 1) {
370                         log.error("Multiple Keyf path specifications, using first.");
371                 }
372
373                 String format = ((Element) keyElements.item(0)).getAttribute("format");
374                 if (format == null || format.equals("")) {
375                         log.debug("No format specified for certificate, using default (PEM) format.");
376                         format = "PEM";
377                 }
378
379                 if (!format.equals("DER-PKCS8")) {
380                         log.error("File credential resolver currently only supports (DER-PKCS8) format.");
381                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
382                 }
383
384                 return format;
385         }
386
387         private String getCertPath(Element e) throws CredentialFactoryException {
388
389                 NodeList certificateElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Certificate");
390                 if (certificateElements.getLength() < 1) {
391                         log.error("Certificate not specified.");
392                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate> specification.");
393                 }
394                 if (certificateElements.getLength() > 1) {
395                         log.error("Multiple Certificate path specifications, using first.");
396                 }
397
398                 NodeList pathElements =
399                         ((Element) certificateElements.item(0)).getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
400                 if (pathElements.getLength() < 1) {
401                         log.error("Certificate path not specified.");
402                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate><Path/></Certificate> specification.");
403                 }
404                 if (pathElements.getLength() > 1) {
405                         log.error("Multiple Certificate path specifications, using first.");
406                 }
407                 Node tnode = pathElements.item(0).getFirstChild();
408                 String path = null;
409                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
410                         path = tnode.getNodeValue();
411                 }
412                 if (path == null || path.equals("")) {
413                         log.error("Certificate path not specified.");
414                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate><Path/></Certificate> specification.");
415                 }
416                 return path;
417         }
418
419         private String[] getCAPaths(Element e) throws CredentialFactoryException {
420
421                 NodeList certificateElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Certificate");
422                 if (certificateElements.getLength() < 1) {
423                         log.error("Certificate not specified.");
424                         throw new CredentialFactoryException("File Credential Resolver requires a <Certificate> specification.");
425                 }
426                 if (certificateElements.getLength() > 1) {
427                         log.error("Multiple Certificate path specifications, using first.");
428                 }
429
430                 NodeList pathElements =
431                         ((Element) certificateElements.item(0)).getElementsByTagNameNS(Credentials.credentialsNamespace, "CAPath");
432                 if (pathElements.getLength() < 1) {
433                         log.debug("No CA Certificate paths specified.");
434                         return null;
435                 }
436                 ArrayList paths = new ArrayList();
437                 for (int i = 0; i < pathElements.getLength(); i++) {
438                         Node tnode = pathElements.item(i).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                                 paths.add(path);
445                         }
446                         if (paths.isEmpty()) {
447                                 log.debug("No CA Certificate paths specified.");
448                         }
449                 }
450                 return (String[]) paths.toArray(new String[0]);
451         }
452
453         private String getKeyPath(Element e) throws CredentialFactoryException {
454
455                 NodeList keyElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Key");
456                 if (keyElements.getLength() < 1) {
457                         log.error("Key not specified.");
458                         throw new CredentialFactoryException("File Credential Resolver requires a <Key> specification.");
459                 }
460                 if (keyElements.getLength() > 1) {
461                         log.error("Multiple Key path specifications, using first.");
462                 }
463
464                 NodeList pathElements =
465                         ((Element) keyElements.item(0)).getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
466                 if (pathElements.getLength() < 1) {
467                         log.error("Key path not specified.");
468                         throw new CredentialFactoryException("File Credential Resolver requires a <Key><Path/></Certificate> specification.");
469                 }
470                 if (pathElements.getLength() > 1) {
471                         log.error("Multiple Key path specifications, using first.");
472                 }
473                 Node tnode = pathElements.item(0).getFirstChild();
474                 String path = null;
475                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
476                         path = tnode.getNodeValue();
477                 }
478                 if (path == null || path.equals("")) {
479                         log.error("Key path not specified.");
480                         throw new CredentialFactoryException("File Credential Resolver requires a <Key><Path/></Certificate> specification.");
481                 }
482                 return path;
483         }
484
485         /**
486          * 
487          * Loads a specified bundle of certs individually and returns an array of 
488          * <code>Certificate</code> objects.  This is needed because the standard 
489          * <code>CertificateFactory.getCertificates(InputStream)</code> method bails 
490          * out when it has trouble loading any cert and cannot handle "comments".
491          */
492         private Certificate[] loadCertificates(InputStream inStream, String certType) throws CredentialFactoryException {
493
494                 ArrayList certificates = new ArrayList();
495
496                 try {
497                         CertificateFactory certFactory = CertificateFactory.getInstance(certType);
498
499                         BufferedReader in = new BufferedReader(new InputStreamReader(inStream));
500                         String str;
501                         boolean insideCert = false;
502                         StringBuffer rawCert = null;
503                         while ((str = in.readLine()) != null) {
504
505                                 if (insideCert) {
506                                         rawCert.append(str);
507                                         rawCert.append(System.getProperty("line.separator"));
508                                         if (str.matches("^.*-----END CERTIFICATE-----.*$")) {
509                                                 insideCert = false;
510                                                 try {
511                                                         Certificate cert =
512                                                                 certFactory.generateCertificate(
513                                                                         new ByteArrayInputStream(rawCert.toString().getBytes()));
514                                                         certificates.add(cert);
515                                                 } catch (CertificateException ce) {
516                                                         log.warn("Failed to load a certificate from the certificate bundle: " + ce);
517                                                         if (log.isDebugEnabled()) {
518                                                                 log.debug(
519                                                                         "Dump of bad certificate: "
520                                                                                 + System.getProperty("line.separator")
521                                                                                 + rawCert.toString());
522                                                         }
523                                                 }
524                                                 continue;
525                                         }
526                                 } else if (str.matches("^.*-----BEGIN CERTIFICATE-----.*$")) {
527                                         insideCert = true;
528                                         rawCert = new StringBuffer();
529                                         rawCert.append(str);
530                                         rawCert.append(System.getProperty("line.separator"));
531                                 }
532                         }
533                         in.close();
534                 } catch (IOException p) {
535                         log.error("Could not load resource from specified location: " + p);
536                         throw new CredentialFactoryException("Unable to load certificates.");
537                 } catch (CertificateException p) {
538                         log.error("Problem loading certificate factory: " + p);
539                         throw new CredentialFactoryException("Unable to load certificates.");
540                 }
541
542                 return (Certificate[]) certificates.toArray(new Certificate[0]);
543         }
544
545         /**
546          * Given an ArrayList containing a base certificate and an array of unordered certificates, 
547          * populates the ArrayList with an ordered certificate chain, based on subject and issuer.
548          * 
549          * @param       chainSource array of certificates to pull from
550          * @param       chainDest ArrayList containing base certificate
551          * @throws InvalidCertificateChainException thrown if a chain cannot be constructed from 
552          *                      the specified elements
553          */
554
555         protected void walkChain(X509Certificate[] chainSource, ArrayList chainDest) throws CredentialFactoryException {
556
557                 X509Certificate currentCert = (X509Certificate) chainDest.get(chainDest.size() - 1);
558                 if (currentCert.getSubjectDN().equals(currentCert.getIssuerDN())) {
559                         log.debug("Found self-signed root cert: " + currentCert.getSubjectDN());
560                         return;
561                 } else {
562                         //TODO maybe this should check more than the DN...
563                         for (int i = 0; chainSource.length > i; i++) {
564                                 if (currentCert.getIssuerDN().equals(chainSource[i].getSubjectDN())) {
565                                         chainDest.add(chainSource[i]);
566                                         walkChain(chainSource, chainDest);
567                                         return;
568                                 }
569                         }
570                         log.error("Incomplete certificate chain.");
571                         throw new CredentialFactoryException("Incomplete cerficate chain.");
572                 }
573         }
574
575         /**
576          * Boolean indication of whether a given private key and public key form a valid keypair.
577          * 
578          * @param pubKey the public key
579          * @param privKey the private key
580          */
581
582         protected boolean isMatchingKey(PublicKey pubKey, PrivateKey privKey) {
583
584                 try {
585                         String controlString = "asdf";
586                         log.debug("Checking for matching private key/public key pair");
587                         Signature signature = null;
588                         try {
589                                 signature = Signature.getInstance(privKey.getAlgorithm());
590                         } catch (NoSuchAlgorithmException nsae) {
591                                 log.debug("No provider for (RSA) signature, attempting (MD5withRSA).");
592                                 if (privKey.getAlgorithm().equals("RSA")) {
593                                         signature = Signature.getInstance("MD5withRSA");
594                                 } else {
595                                         throw nsae;
596                                 }
597                         }
598                         signature.initSign(privKey);
599                         signature.update(controlString.getBytes());
600                         byte[] sigBytes = signature.sign();
601                         signature.initVerify(pubKey);
602                         signature.update(controlString.getBytes());
603                         if (signature.verify(sigBytes)) {
604                                 log.debug("Found match.");
605                                 return true;
606                         }
607                 } catch (Exception e) {
608                         log.warn(e);
609                 }
610                 log.debug("This pair does not match.");
611                 return false;
612         }
613
614         /**
615          * Auto-enlarging container for bytes.
616          */
617
618         // Sure makes you wish bytes were first class objects.
619
620         private class ByteContainer {
621
622                 private byte[] buffer;
623                 private int cushion;
624                 private int currentSize = 0;
625
626                 private ByteContainer(int cushion) {
627                         buffer = new byte[cushion];
628                         this.cushion = cushion;
629                 }
630
631                 private void grow() {
632                         log.debug("Growing ByteContainer.");
633                         int newSize = currentSize + cushion;
634                         byte[] b = new byte[newSize];
635                         int toCopy = Math.min(currentSize, newSize);
636                         int i;
637                         for (i = 0; i < toCopy; i++) {
638                                 b[i] = buffer[i];
639                         }
640                         buffer = b;
641                 }
642
643                 /** 
644                  * Returns an array of the bytes in the container. <p>
645                  */
646
647                 private byte[] toByteArray() {
648                         byte[] b = new byte[currentSize];
649                         for (int i = 0; i < currentSize; i++) {
650                                 b[i] = buffer[i];
651                         }
652                         return b;
653                 }
654
655                 /** 
656                  * Add one byte to the end of the container.
657                  */
658
659                 private void append(byte b) {
660                         if (currentSize == buffer.length) {
661                                 grow();
662                         }
663                         buffer[currentSize] = b;
664                         currentSize++;
665                 }
666
667         }
668
669 }
670
671 class KeystoreCredentialResolver implements CredentialResolver {
672
673         private static Logger log = Logger.getLogger(KeystoreCredentialResolver.class.getName());
674
675         public Credential loadCredential(Element e) throws CredentialFactoryException {
676
677                 if (!e.getTagName().equals("KeyStoreResolver")) {
678                         log.error("Invalid Credential Resolver configuration: expected <KeyStoreResolver> .");
679                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
680                 }
681
682                 String id = e.getAttribute("Id");
683                 if (id == null || id.equals("")) {
684                         log.error("Credential Resolvers require specification of the attribute \"Id\".");
685                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
686                 }
687
688                 String keyStoreType = e.getAttribute("storeType");
689                 if (keyStoreType == null || keyStoreType.equals("")) {
690                         log.debug("Using default store type for credential.");
691                         keyStoreType = "JKS";
692                 }
693
694                 String path = loadPath(e);
695                 String alias = loadAlias(e);
696                 String certAlias = loadCertAlias(e, alias);
697                 String keyPassword = loadKeyPassword(e);
698                 String keyStorePassword = loadKeyStorePassword(e);
699
700                 try {
701                         KeyStore keyStore = KeyStore.getInstance(keyStoreType);
702
703                         keyStore.load(new ShibResource(path, this.getClass()).getInputStream(), keyStorePassword.toCharArray());
704
705                         PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, keyPassword.toCharArray());
706
707                         if (privateKey == null) {
708                                 throw new CredentialFactoryException("No key entry was found with an alias of (" + alias + ").");
709                         }
710
711                         Certificate[] certificates = keyStore.getCertificateChain(certAlias);
712                         if (certificates == null) {
713                                 throw new CredentialFactoryException(
714                                         "An error occurred while reading the java keystore: No certificate found with the specified alias ("
715                                                 + certAlias
716                                                 + ").");
717                         }
718
719                         X509Certificate[] x509Certs = new X509Certificate[certificates.length];
720                         for (int i = 0; i < certificates.length; i++) {
721                                 if (certificates[i] instanceof X509Certificate) {
722                                         x509Certs[i] = (X509Certificate) certificates[i];
723                                 } else {
724                                         throw new CredentialFactoryException(
725                                                 "The KeyStore Credential Resolver can only load X509 certificates.  Found an unsupported certificate of type ("
726                                                         + certificates[i]
727                                                         + ").");
728                                 }
729                         }
730
731                         return new Credential(x509Certs, privateKey);
732
733                 } catch (KeyStoreException kse) {
734                         throw new CredentialFactoryException("An error occurred while accessing the java keystore: " + kse);
735                 } catch (NoSuchAlgorithmException nsae) {
736                         throw new CredentialFactoryException("Appropriate JCE provider not found in the java environment: " + nsae);
737                 } catch (CertificateException ce) {
738                         throw new CredentialFactoryException(
739                                 "The java keystore contained a certificate that could not be loaded: " + ce);
740                 } catch (IOException ioe) {
741                         throw new CredentialFactoryException("An error occurred while reading the java keystore: " + ioe);
742                 } catch (UnrecoverableKeyException uke) {
743                         throw new CredentialFactoryException(
744                                 "An error occurred while attempting to load the key from the java keystore: " + uke);
745                 }
746
747         }
748
749         private String loadPath(Element e) throws CredentialFactoryException {
750
751                 NodeList pathElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
752                 if (pathElements.getLength() < 1) {
753                         log.error("KeyStore path not specified.");
754                         throw new CredentialFactoryException("KeyStore Credential Resolver requires a <Path> specification.");
755                 }
756                 if (pathElements.getLength() > 1) {
757                         log.error("Multiple KeyStore path specifications, using first.");
758                 }
759                 Node tnode = pathElements.item(0).getFirstChild();
760                 String path = null;
761                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
762                         path = tnode.getNodeValue();
763                 }
764                 if (path == null || path.equals("")) {
765                         log.error("KeyStore path not specified.");
766                         throw new CredentialFactoryException("KeyStore Credential Resolver requires a <Path> specification.");
767                 }
768                 return path;
769         }
770
771         private String loadAlias(Element e) throws CredentialFactoryException {
772
773                 NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyAlias");
774                 if (aliasElements.getLength() < 1) {
775                         log.error("KeyStore key alias not specified.");
776                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyAlias> specification.");
777                 }
778                 if (aliasElements.getLength() > 1) {
779                         log.error("Multiple key alias specifications, using first.");
780                 }
781                 Node tnode = aliasElements.item(0).getFirstChild();
782                 String alias = null;
783                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
784                         alias = tnode.getNodeValue();
785                 }
786                 if (alias == null || alias.equals("")) {
787                         log.error("KeyStore key alias not specified.");
788                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyAlias> specification.");
789                 }
790                 return alias;
791         }
792
793         private String loadCertAlias(Element e, String defaultAlias) throws CredentialFactoryException {
794
795                 NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "CertAlias");
796                 if (aliasElements.getLength() < 1) {
797                         log.debug("KeyStore cert alias not specified, defaulting to key alias.");
798                         return defaultAlias;
799                 }
800
801                 if (aliasElements.getLength() > 1) {
802                         log.error("Multiple cert alias specifications, using first.");
803                 }
804
805                 Node tnode = aliasElements.item(0).getFirstChild();
806                 String alias = null;
807                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
808                         alias = tnode.getNodeValue();
809                 }
810                 if (alias == null || alias.equals("")) {
811                         log.debug("KeyStore cert alias not specified, defaulting to key alias.");
812                         return defaultAlias;
813                 }
814                 return alias;
815         }
816
817         private String loadKeyStorePassword(Element e) throws CredentialFactoryException {
818
819                 NodeList passwordElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "StorePassword");
820                 if (passwordElements.getLength() < 1) {
821                         log.error("KeyStore password not specified.");
822                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <StorePassword> specification.");
823                 }
824                 if (passwordElements.getLength() > 1) {
825                         log.error("Multiple KeyStore password specifications, using first.");
826                 }
827                 Node tnode = passwordElements.item(0).getFirstChild();
828                 String password = null;
829                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
830                         password = tnode.getNodeValue();
831                 }
832                 if (password == null || password.equals("")) {
833                         log.error("KeyStore password not specified.");
834                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <StorePassword> specification.");
835                 }
836                 return password;
837         }
838
839         private String loadKeyPassword(Element e) throws CredentialFactoryException {
840
841                 NodeList passwords = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyPassword");
842                 if (passwords.getLength() < 1) {
843                         log.error("KeyStore key password not specified.");
844                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyPassword> specification.");
845                 }
846                 if (passwords.getLength() > 1) {
847                         log.error("Multiple KeyStore key password specifications, using first.");
848                 }
849                 Node tnode = passwords.item(0).getFirstChild();
850                 String password = null;
851                 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
852                         password = tnode.getNodeValue();
853                 }
854                 if (password == null || password.equals("")) {
855                         log.error("KeyStore key password not specified.");
856                         throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyPassword> specification.");
857                 }
858                 return password;
859         }
860 }
861
862 class CustomCredentialResolver implements CredentialResolver {
863
864         private static Logger log = Logger.getLogger(CustomCredentialResolver.class.getName());
865
866         public Credential loadCredential(Element e) throws CredentialFactoryException {
867
868                 if (!e.getTagName().equals("CustomCredResolver")) {
869                         log.error("Invalid Credential Resolver configuration: expected <CustomCredResolver> .");
870                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
871                 }
872
873                 String id = e.getAttribute("id");
874                 if (id == null || id.equals("")) {
875                         log.error("Credential Resolvers require specification of the attribute \"id\".");
876                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
877                 }
878
879                 String className = e.getAttribute("Class");
880                 if (className == null || className.equals("")) {
881                         log.error("Custom Credential Resolver requires specification of the attribute \"Class\".");
882                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
883                 }
884
885                 try {
886                         return ((CredentialResolver) Class.forName(className).newInstance()).loadCredential(e);
887
888                 } catch (Exception loaderException) {
889                         log.error(
890                                 "Failed to load Custom Credential Resolver implementation class: " + loaderException.getMessage());
891                         throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
892                 }
893
894         }
895
896 }
897
898 class CredentialFactoryException extends Exception {
899
900         CredentialFactoryException(String message) {
901                 super(message);
902         }
903 }