Credentials file resolver will now load rsa keys in DER-encoded pkcs8.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / Credentials.java
index e7be474..fe599ce 100644 (file)
 package edu.internet2.middleware.shibboleth.common;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyFactory;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
 import java.security.UnrecoverableKeyException;
-import java.util.Hashtable;
-
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Collection;
+import java.util.Hashtable;
 
 import org.apache.log4j.Logger;
 import org.w3c.dom.Element;
@@ -60,15 +64,13 @@ import org.w3c.dom.NodeList;
  */
 public class Credentials {
 
-       public static final String credentialsNamespace = "urn:mace:shibboleth:credentials";
+       public static final String credentialsNamespace = "urn:mace:shibboleth:credentials:1.0";
 
        private static Logger log = Logger.getLogger(Credentials.class.getName());
        private Hashtable data = new Hashtable();
 
        public Credentials(Element e) {
 
-               //TODO talk to Scott about the possibility of changing "resolver" to "loader", to avoid confusion with the AR
-
                if (!e.getTagName().equals("Credentials")) {
                        throw new IllegalArgumentException();
                }
@@ -83,9 +85,9 @@ public class Credentials {
                        if (resolverNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
                                try {
 
-                                       String credentialId = ((Element) resolverNodes.item(i)).getAttribute("id");
+                                       String credentialId = ((Element) resolverNodes.item(i)).getAttribute("Id");
                                        if (credentialId == null || credentialId.equals("")) {
-                                               log.error("Found credential that was not labeled with a unique \"id\" attribute. Skipping.");
+                                               log.error("Found credential that was not labeled with a unique \"Id\" attribute. Skipping.");
                                        }
 
                                        if (data.containsKey(credentialId)) {
@@ -121,7 +123,7 @@ public class Credentials {
                                return new KeyInfoCredentialResolver().loadCredential(e);
                        }
 
-                       if (e.getTagName().equals("FileCredResolver")) {
+                       if (e.getTagName().equals("FileResolver")) {
                                return new FileCredentialResolver().loadCredential(e);
                        }
 
@@ -129,7 +131,7 @@ public class Credentials {
                                return new KeystoreCredentialResolver().loadCredential(e);
                        }
 
-                       if (e.getTagName().equals("CustomCredResolver")) {
+                       if (e.getTagName().equals("CustomResolver")) {
                                return new CustomCredentialResolver().loadCredential(e);
                        }
 
@@ -155,13 +157,271 @@ class KeyInfoCredentialResolver implements CredentialResolver {
 
 class FileCredentialResolver implements CredentialResolver {
        private static Logger log = Logger.getLogger(FileCredentialResolver.class.getName());
-       FileCredentialResolver() throws CredentialFactoryException {
-               log.error("Credential Resolver (FileCredentialResolver) not implemented");
-               throw new CredentialFactoryException("Failed to load credential.");
+
+       public Credential loadCredential(Element e) throws CredentialFactoryException {
+
+               if (!e.getTagName().equals("FileResolver")) {
+                       log.error("Invalid Credential Resolver configuration: expected <FileResolver> .");
+                       throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
+               }
+
+               String id = e.getAttribute("Id");
+               if (id == null || id.equals("")) {
+                       log.error("Credential Resolvers require specification of the attribute \"Id\".");
+                       throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
+               }
+
+               String certFormat = getCertFormat(e);
+               String certPath = getCertPath(e);
+
+               log.debug("Certificate Format: (" + certFormat + ").");
+               log.debug("Certificate Path: (" + certPath + ").");
+
+               //TODO provider optional
+               //TODO other kinds of certs?
+               //TODO provide a way to specify a separate CA bundle
+               Collection chain = null;
+               try {
+                       CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+                       chain = certFactory.generateCertificates(new ShibResource(certPath, this.getClass()).getInputStream());
+
+                       //TODO probably want to walk the chain and make sure things are kosher
+                       //TODO need to order the chain
+                       if (chain.isEmpty()) {
+                               log.error("File did not contain any valid certificates.");
+                               throw new CredentialFactoryException("File did not contain any valid certificates.");
+                       }
+
+               } catch (IOException p) {
+                       log.error("Could not load resource from specified location (" + certPath + "): " + p);
+                       throw new CredentialFactoryException("Unable to load certificates.");
+               } catch (CertificateException p) {
+                       log.error("Problem parsing certificate at (" + certPath + "): " + p);
+                       throw new CredentialFactoryException("Unable to load certificates.");
+               }
+               String keyFormat = getKeyFormat(e);
+               String keyPath = getKeyPath(e);
+               log.debug("Key Format: (" + keyFormat + ").");
+               log.debug("Key Path: (" + keyPath + ").");
+
+               String keyAlgorithm = "RSA";
+
+               //TODO providers?
+               //TODO support DER, PEM, DER-PKCS8, and PEM-PKCS8?
+               //TODO DSA
+
+               PrivateKey key = null;
+
+               if (keyAlgorithm.equals("RSA") && keyFormat.equals("DER-PKCS8")) {
+                       try {
+                               key = getRSADERKey(new ShibResource(keyPath, this.getClass()).getInputStream());
+                       } catch (IOException ioe) {
+                               log.error("Could not load resource from specified location (" + keyPath + "): " + e);
+                               throw new CredentialFactoryException("Unable to load private key.");
+                       }
+               } else {
+                       log.error("File credential resolver only supports the RSA keys in DER-encoded PKCS8 format (DER-PKCS8).");
+                       throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
+               }
+
+               return new Credential(((X509Certificate[]) chain.toArray(new X509Certificate[0])), key);
        }
-       public Credential loadCredential(Element e) {
-               return null;
+
+       private PrivateKey getRSADERKey(InputStream inStream) throws CredentialFactoryException {
+
+               try {
+
+                       KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+                       byte[] inputBuffer = new byte[8];
+                       int i;
+                       ByteContainer inputBytes = new ByteContainer(400);
+                       do {
+                               i = inStream.read(inputBuffer);
+                               for (int j = 0; j < i; j++) {
+                                       inputBytes.append(inputBuffer[j]);
+                               }
+                       } while (i > -1);
+
+                       PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(inputBytes.toByteArray());
+
+                       return keyFactory.generatePrivate(keySpec);
+
+               } catch (Exception e) {
+                       log.error("Unable to load private key: " + e);
+                       throw new CredentialFactoryException("Unable to load private key.");
+               }
+
+       }
+
+       private String getCertFormat(Element e) throws CredentialFactoryException {
+
+               NodeList certificateElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Certificate");
+               if (certificateElements.getLength() < 1) {
+                       log.error("Certificate not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Certificate> specification.");
+               }
+               if (certificateElements.getLength() > 1) {
+                       log.error("Multiple Certificate path specifications, using first.");
+               }
+
+               String format = ((Element) certificateElements.item(0)).getAttribute("format");
+               if (format == null || format.equals("")) {
+                       log.debug("No format specified for certificate, using default (PEM) format.");
+                       format = "PEM";
+               }
+
+               if ((!format.equals("PEM")) && (!format.equals("DER"))) {
+                       log.error("File credential resolver only supports the (DER) and (PEM) formats.");
+                       throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
+               }
+
+               return format;
+       }
+
+       private String getKeyFormat(Element e) throws CredentialFactoryException {
+
+               NodeList keyElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Key");
+               if (keyElements.getLength() < 1) {
+                       log.error("Key not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Key> specification.");
+               }
+               if (keyElements.getLength() > 1) {
+                       log.error("Multiple Keyf path specifications, using first.");
+               }
+
+               String format = ((Element) keyElements.item(0)).getAttribute("format");
+               if (format == null || format.equals("")) {
+                       log.debug("No format specified for certificate, using default (PEM) format.");
+                       format = "PEM";
+               }
+
+               if (!format.equals("DER-PKCS8")) {
+                       log.error("File credential resolver currently only supports (DER-PKCS8) format.");
+                       throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
+               }
+
+               return format;
+       }
+
+       private String getCertPath(Element e) throws CredentialFactoryException {
+
+               NodeList certificateElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Certificate");
+               if (certificateElements.getLength() < 1) {
+                       log.error("Certificate not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Certificate> specification.");
+               }
+               if (certificateElements.getLength() > 1) {
+                       log.error("Multiple Certificate path specifications, using first.");
+               }
+
+               NodeList pathElements =
+                       ((Element) certificateElements.item(0)).getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
+               if (pathElements.getLength() < 1) {
+                       log.error("Certificate path not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Certificate><Path/></Certificate> specification.");
+               }
+               if (pathElements.getLength() > 1) {
+                       log.error("Multiple Certificate path specifications, using first.");
+               }
+               Node tnode = pathElements.item(0).getFirstChild();
+               String path = null;
+               if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
+                       path = tnode.getNodeValue();
+               }
+               if (path == null || path.equals("")) {
+                       log.error("Certificate path not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Certificate><Path/></Certificate> specification.");
+               }
+               return path;
        }
+
+       private String getKeyPath(Element e) throws CredentialFactoryException {
+
+               NodeList keyElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Key");
+               if (keyElements.getLength() < 1) {
+                       log.error("Key not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Key> specification.");
+               }
+               if (keyElements.getLength() > 1) {
+                       log.error("Multiple Key path specifications, using first.");
+               }
+
+               NodeList pathElements =
+                       ((Element) keyElements.item(0)).getElementsByTagNameNS(Credentials.credentialsNamespace, "Path");
+               if (pathElements.getLength() < 1) {
+                       log.error("Key path not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Key><Path/></Certificate> specification.");
+               }
+               if (pathElements.getLength() > 1) {
+                       log.error("Multiple Key path specifications, using first.");
+               }
+               Node tnode = pathElements.item(0).getFirstChild();
+               String path = null;
+               if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
+                       path = tnode.getNodeValue();
+               }
+               if (path == null || path.equals("")) {
+                       log.error("Key path not specified.");
+                       throw new CredentialFactoryException("File Credential Resolver requires a <Key><Path/></Certificate> specification.");
+               }
+               return path;
+       }
+
+       /**
+        * Auto-enlarging container for bytes.
+        */
+
+       // Sure makes you wish bytes were first class objects.
+
+       private class ByteContainer {
+
+               private byte[] buffer;
+               private int cushion;
+               private int currentSize = 0;
+
+               private ByteContainer(int cushion) {
+                       buffer = new byte[cushion];
+                       this.cushion = cushion;
+               }
+
+               private void grow() {
+                       log.debug("Growing ByteContainer.");
+                       int newSize = currentSize + cushion;
+                       byte[] b = new byte[newSize];
+                       int toCopy = Math.min(currentSize, newSize);
+                       int i;
+                       for (i = 0; i < toCopy; i++) {
+                               b[i] = buffer[i];
+                       }
+                       buffer = b;
+               }
+
+               /** 
+                * Returns an array of the bytes in the container. <p>
+                */
+
+               private byte[] toByteArray() {
+                       byte[] b = new byte[currentSize];
+                       for (int i = 0; i < currentSize; i++) {
+                               b[i] = buffer[i];
+                       }
+                       return b;
+               }
+
+               /** 
+                * Add one byte to the end of the container.
+                */
+
+               private void append(byte b) {
+                       if (currentSize == buffer.length) {
+                               grow();
+                       }
+                       buffer[currentSize] = b;
+                       currentSize++;
+               }
+
+       }
+
 }
 
 class KeystoreCredentialResolver implements CredentialResolver {
@@ -175,13 +435,13 @@ class KeystoreCredentialResolver implements CredentialResolver {
                        throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
                }
 
-               String id = e.getAttribute("id");
+               String id = e.getAttribute("Id");
                if (id == null || id.equals("")) {
-                       log.error("Credential Resolvers require specification of the attribute \"id\".");
+                       log.error("Credential Resolvers require specification of the attribute \"Id\".");
                        throw new CredentialFactoryException("Failed to initialize Credential Resolver.");
                }
 
-               String keyStoreType = e.getAttribute("keyStoreType");
+               String keyStoreType = e.getAttribute("storeType");
                if (keyStoreType == null || keyStoreType.equals("")) {
                        log.debug("Using default store type for credential.");
                        keyStoreType = "JKS";
@@ -189,6 +449,7 @@ class KeystoreCredentialResolver implements CredentialResolver {
 
                String path = loadPath(e);
                String alias = loadAlias(e);
+               String certAlias = loadCertAlias(e, alias);
                String keyPassword = loadKeyPassword(e);
                String keyStorePassword = loadKeyStorePassword(e);
 
@@ -203,11 +464,11 @@ class KeystoreCredentialResolver implements CredentialResolver {
                                throw new CredentialFactoryException("No key entry was found with an alias of (" + alias + ").");
                        }
 
-                       Certificate[] certificates = keyStore.getCertificateChain(alias);
+                       Certificate[] certificates = keyStore.getCertificateChain(certAlias);
                        if (certificates == null) {
                                throw new CredentialFactoryException(
                                        "An error occurred while reading the java keystore: No certificate found with the specified alias ("
-                                               + alias
+                                               + certAlias
                                                + ").");
                        }
 
@@ -265,13 +526,13 @@ class KeystoreCredentialResolver implements CredentialResolver {
 
        private String loadAlias(Element e) throws CredentialFactoryException {
 
-               NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Alias");
+               NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyAlias");
                if (aliasElements.getLength() < 1) {
                        log.error("KeyStore key alias not specified.");
-                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <Alias> specification.");
+                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyAlias> specification.");
                }
                if (aliasElements.getLength() > 1) {
-                       log.error("Multiple KeyStore alias specifications, using first.");
+                       log.error("Multiple key alias specifications, using first.");
                }
                Node tnode = aliasElements.item(0).getFirstChild();
                String alias = null;
@@ -280,17 +541,41 @@ class KeystoreCredentialResolver implements CredentialResolver {
                }
                if (alias == null || alias.equals("")) {
                        log.error("KeyStore key alias not specified.");
-                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <Alias> specification.");
+                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyAlias> specification.");
+               }
+               return alias;
+       }
+
+       private String loadCertAlias(Element e, String defaultAlias) throws CredentialFactoryException {
+
+               NodeList aliasElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "CertAlias");
+               if (aliasElements.getLength() < 1) {
+                       log.debug("KeyStore cert alias not specified, defaulting to key alias.");
+                       return defaultAlias;
+               }
+
+               if (aliasElements.getLength() > 1) {
+                       log.error("Multiple cert alias specifications, using first.");
+               }
+
+               Node tnode = aliasElements.item(0).getFirstChild();
+               String alias = null;
+               if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
+                       alias = tnode.getNodeValue();
+               }
+               if (alias == null || alias.equals("")) {
+                       log.debug("KeyStore cert alias not specified, defaulting to key alias.");
+                       return defaultAlias;
                }
                return alias;
        }
 
        private String loadKeyStorePassword(Element e) throws CredentialFactoryException {
 
-               NodeList passwordElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "KeyStorePassword");
+               NodeList passwordElements = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "StorePassword");
                if (passwordElements.getLength() < 1) {
                        log.error("KeyStore password not specified.");
-                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyStorePassword> specification.");
+                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <StorePassword> specification.");
                }
                if (passwordElements.getLength() > 1) {
                        log.error("Multiple KeyStore password specifications, using first.");
@@ -302,7 +587,7 @@ class KeystoreCredentialResolver implements CredentialResolver {
                }
                if (password == null || password.equals("")) {
                        log.error("KeyStore password not specified.");
-                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <KeyStorePassword> specification.");
+                       throw new CredentialFactoryException("KeyStore Credential Resolver requires an <StorePassword> specification.");
                }
                return password;
        }
@@ -333,7 +618,6 @@ class KeystoreCredentialResolver implements CredentialResolver {
 class CustomCredentialResolver implements CredentialResolver {
 
        private static Logger log = Logger.getLogger(CustomCredentialResolver.class.getName());
-       private CredentialResolver resolver;
 
        public Credential loadCredential(Element e) throws CredentialFactoryException {