FileDocCategorySizeDatePackage
ASJarSigner.javaAPI DocGlassfish v2 API23491Fri May 04 22:34:12 BST 2007com.sun.enterprise.appclient.jws

ASJarSigner.java

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.enterprise.appclient.jws;

import com.sun.enterprise.security.SecurityUtil;
import com.sun.enterprise.util.i18n.StringManager;
import com.sun.logging.LogDomains;
import java.io.File;
import java.security.AccessControlException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.Permission;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import sun.security.tools.JarSigner;

/**
 * Signs a specified JAR file.
 *<p>
 *This implementation searches the available keystores for the signing alias
 *indicated in the domain.xml config or, if not specified, the default alias,
 *the first time it is invoked to sign a JAR file.  After the first requested
 *signing it uses the same alias and provider to sign all JARs.
 *<p>
 *The public interface to this class is the static signJar method.  
 *
 * @author tjquinn
 */
public class ASJarSigner {
    
    /** property name optionally set by the admin in domain.xml to select an alias for signing */
    private static final String USER_SPECIFIED_ALIAS_PROPERTYNAME = "com.sun.aas.jws.signing.alias";

    /** keystore type for JKS keystores */
    private static final String JKS_KEYSTORE_TYPE_VALUE = "jks";
    
    /** default alias for signing if the admin does not specify one */
    private static final String DEFAULT_ALIAS_VALUE = "s1as";

    /** user-specified signing alias */
    private static final String userAlias = System.getProperty(USER_SPECIFIED_ALIAS_PROPERTYNAME);
    
    private static final Logger logger = 
            LogDomains.getLogger(LogDomains.CORE_LOGGER);

    private static final StringManager localStrings = StringManager.getManager(ASJarSigner.class);
    
    /** info used for signing, saved after being looked up during the first request */
    private static SigningInfo signingInfo = null;

    /** the existing, unsigned JAR file */
    private File unsignedJar;
    
    /** the signed JAR file, to be created */
    private File signedJar;
    
    /**
     *Creates a signed jar from the specified unsigned jar.
     *@param unsignedJar the unsigned JAR file
     *@param signedJar the signed JAR to be created
     *@return the elapsed time to sign the JAR (in milliseconds)
     *@throws Exception getting the keystores from SSLUtils fails
     */
    public static long signJar(File unsignedJar, File signedJar) throws Exception {
        /*
         *Make sure the signing information has been initialized.
         */
        synchronized(ASJarSigner.class) {
            if (signingInfo == null) {
                signingInfo = createSigningInfo();
            }
        }
        ASJarSigner signer = new ASJarSigner(unsignedJar, signedJar);
        return signer.sign();
    }
    
    /**
     * Creates a new instance of ASJarSigner
     */
    private ASJarSigner(File unsignedJar, File signedJar) throws Exception {
        this.unsignedJar = unsignedJar;
        this.signedJar = signedJar;
    }
    
    /**
     *Signs the jar file using the current signing info.
     *@return elapsed milliseconds it took to sign the JAR
     *@throws Exception SSLUtils.getKeyStores() cannot do so
     */
    private long sign() throws Exception {
        long startTime = System.currentTimeMillis();
        
        /*
         *Obtain the command-line arguments suitable for signing this JAR
         *based on the signing information already established, which will depend
         *on the keystore type in which the alias was found, etc.
         */
        String[] args = signingInfo.getSigningArgs(unsignedJar, signedJar);

        /*
         *In response to errors, the JarSigner class writes errors directly to 
         *System.out (rather than throw exceptions) and invokes System.exit.  
         *To prevent JarSigner's error handling from forcing the app server to
         *exit establish a security manager that prohibits the use of System.exit,
         *temporarily while JarSigner runs.  
         *
         *Make sure to change the security manager and use the JarSigner
         *class only one thread at a time.
         */
        synchronized(SignedStaticContent.class) {
            
            /*
             *Save the current security manager; restored later.
             */
            SecurityManager mgr = System.getSecurityManager();

            try {
                NoExitSecurityManager noExitMgr = new NoExitSecurityManager(mgr);
                System.setSecurityManager(noExitMgr);

                /*
                 *Run the jar signer.
                 */
                JarSigner.main(args);
            } catch (Throwable t) {
                /*
                 *In case of any problems, make sure there is no ill-formed signed
                 *jar file left behind.
                 */
                signedJar.delete();

                /*
                 *The jar signer will have written some information to System.out
                 *and/or System.err.  Refer the user to those earlier messages.
                 */
                throw new Exception(localStrings.getString("jws.sign.errorSigning", signedJar.getAbsolutePath()), t);
            } finally {
                /*
                 *Restore the saved security manager.
                 */
                System.setSecurityManager(mgr);

                /*
                 *Clear out the args array to hide the password.
                 */
                for (int i = 0; i < args.length; i++) {
                    args[i] = null;
                }
                long duration = System.currentTimeMillis() - startTime;
                logger.fine("Signing " + unsignedJar.getAbsolutePath() + " took " + duration + " ms");
            }
        } 
        
        return System.currentTimeMillis() - startTime;
    }

    /**
     *Wraps any underlying exception.
     *<p>
     *This is primarily used to insulate calling logic from
     *the large variety of exceptions that can occur during signing
     *from which the caller cannot really recover.
     */
    public static class ASJarSignerException extends Exception {
        public ASJarSignerException(String msg, Throwable t) {
            super(msg, t);
        }
    }
    
    /**
     *Returns the signing info to use, creating it if it is not already
     *created.
     *@return the signing information to use in signing JARs
     */
    private static synchronized SigningInfo getSigningInfo() throws Exception {
        if (signingInfo == null) {
            signingInfo = createSigningInfo();
        }
        return signingInfo;
    }
    
    /**
     *Creates an object containing the signing information provided by the
     *user-specified or default alias from the keystore in which it appears.
     *@return the SigningInfo object containing the information to be used for signing
     */
    private static SigningInfo createSigningInfo() throws Exception {
        
        String[] keystorePWs = SecurityUtil.getSecuritySupport().getKeyStorePasswords();
        String[] tokenNames = SecurityUtil.getSecuritySupport().getTokenNames();

        /*
         *Assemble lists of signing info objects, one list for matches on the 
         *user-specified alias (if specified at all), the other for matches on 
         *the default alias.
         */
        ArrayList<SigningInfo> signingInfoForDefaultAlias = new ArrayList<SigningInfo>();
        ArrayList<SigningInfo> signingInfoForUserAlias = new ArrayList<SigningInfo>();
        
        int keystoreSlot = 0;
        
        for (KeyStore ks : SecurityUtil.getSecuritySupport().getKeyStores()) {
            if (userAlias != null && ks.containsAlias(userAlias)) {
                /*
                 *The user specified an alias and the current keystore contains
                 *it.  Use this keystore and the user's alias.
                 */
                signingInfoForUserAlias.add(SigningInfo.newInstance(
                        userAlias, 
                        keystorePWs[keystoreSlot], 
                        ks,
                        tokenNames[keystoreSlot]));
            }
            
            if (ks.containsAlias(DEFAULT_ALIAS_VALUE)) {
                signingInfoForDefaultAlias.add(SigningInfo.newInstance(
                        DEFAULT_ALIAS_VALUE, 
                        keystorePWs[keystoreSlot], 
                        ks, 
                        tokenNames[keystoreSlot]));
            }
            keystoreSlot++;
        }
        
        /*
         *Choose which signing information object to use based on whether the user 
         *specified an alias, if so whether it was found among the known keystores,
         *etc.
         */
        SigningInfo result = selectSigningInfo(signingInfoForUserAlias, signingInfoForDefaultAlias);
        logger.fine(localStrings.getString("jws.sign.signingInfo", result.toString()));
        
        return result;
    }
    
    /**
     *Selects the signing info instance to be used for signing JAR files.
     *This method may issue warnings if the user-specified alias does not
     *appear in any keystore or if the user-specified or default alias appears
     *in more than one keystore.
     *@param signingInfoForUserAlias signing info for the user-specified alias
     *@param signingInfoForDefaultAlias signing info for the default alias
     *@return the SigningInfo instance to be used for signing JARs
     */
    private static SigningInfo selectSigningInfo(
            ArrayList<SigningInfo> signingInfoForUserAlias,
            ArrayList<SigningInfo> signingInfoForDefaultAlias) {
        /*
         *Use the user-specified info if requested and available. Otherwise use
         *the default info.
         */
        ArrayList<SigningInfo> signingInfoOfInterest;
        String aliasOfInterest;
        
        if (userAlias != null) {
            if (signingInfoForUserAlias.size() == 0) {
                logger.log(Level.WARNING, 
                        localStrings.getString("jws.sign.userAliasAbsent", userAlias));
                signingInfoOfInterest = signingInfoForDefaultAlias;
                aliasOfInterest = DEFAULT_ALIAS_VALUE;
            } else {
                signingInfoOfInterest = signingInfoForUserAlias;
                aliasOfInterest = userAlias;
            }
        } else {
            signingInfoOfInterest = signingInfoForDefaultAlias;
            aliasOfInterest = DEFAULT_ALIAS_VALUE;
        }
            
        /*
         *Make sure whichever list of signing info is now of interest has
         *exactly one entry for the alias of interest.
         *
         *If there is no entry for the alias of interest, then we cannot proceed.
         */
        if (signingInfoOfInterest.size() == 0) {
            throw new IllegalArgumentException(
                    localStrings.getString("jws.sign.aliasNotFound", aliasOfInterest));
        }
        
        if (signingInfoOfInterest.size() > 1) {
            /*
             *Prepare a warning identifying all the keystore providers for
             *which the keystore contains the alias of interest.  
             */
            StringBuilder sb = new StringBuilder();
            for (SigningInfo si : signingInfoOfInterest) {
                if (sb.length() > 0) {
                    sb.append(", ");
                }
                sb.append(si);
            }
            logger.log(Level.WARNING, 
                    localStrings.getString("jws.sign.aliasFoundMult", aliasOfInterest, sb.toString()));
        }

        /*
         *Return the first signing info that matched the alias.
         */
        return signingInfoOfInterest.get(0);
    }
    
    /**
     *Represents the information needed to actually sign a JAR file: the alias,
     *the keystore that contains the certificates associated with that alias,
     *and so forth.
     */
    private abstract static class SigningInfo {

        /** JarSigner command line argument options */
        private static final String SIGNEDJAR_OPTION = "-signedjar";
        private static final String KEYSTORE_OPTION = "-keystore";
        private static final String STOREPASS_OPTION = "-storepass";
        private static final String STORETYPE_OPTION = "-storetype";

        private KeyStore keystore;
        private String alias;
        private String password;
        private PrivateKey key;
        private String token;
        
        /**
         *Factory method that returns a new instance of the correct subtype 
         *of SigningInfo, depending on the type of keystore.
         *@param alias the alias to be used during signing
         *@param password the password for this alias
         *@param keystore the keystore in which this alias was found
         *@param token the token (possibly null) for this keystore
         *@return new instance of a SigningInfo subclass
         */
        static SigningInfo newInstance(String alias, String password,
                KeyStore keystore, String token) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
            if (keystore.getType().equalsIgnoreCase(JKS_KEYSTORE_TYPE_VALUE)) {
                return new JKSSigningInfo(alias, password, keystore, token);
            } else {
                return new PKCS11SigningInfo(alias, password, keystore, token);
            }
        }

        public SigningInfo(String alias, String password, 
                KeyStore keystore, String token) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
            this.keystore = keystore;
            this.alias = alias;
            this.password = password;
            this.token = token;
            key = validateKey();
        }
        
        public String getAlias() {
            return alias;
        }
        
        private PrivateKey validateKey() throws KeyStoreException, NoSuchAlgorithmException, 
                UnrecoverableKeyException {
            Key tempKey = keystore.getKey(alias, password.toCharArray());
            if (tempKey instanceof PrivateKey) {
                return (PrivateKey) tempKey;
            } else {
                throw new IllegalArgumentException(localStrings.getString("jws.sign.keyNotPrivate", alias));
            }
        }
        
        public String getProviderName() {
            return keystore.getProvider().getName();
        }
        
        public String getToken() {
            return token;
        }
        
        public String getPassword() {
            return password;
        }
        
        public String getStoreType() {
            return keystore.getType();
        }
        public X509Certificate[] getCertificateChain() throws KeyStoreException {
            Certificate[] certs = keystore.getCertificateChain(alias);
            X509Certificate[] X509certs = new X509Certificate[certs.length];
            int slot = 0;
            for (Certificate c : certs) {
                if (c instanceof X509Certificate) {
                    X509certs[slot++] = (X509Certificate) c;
                } else {
                    throw new IllegalArgumentException(localStrings.getString("jws.sign.notX509Cert", alias));
                }
            }
            return X509certs;
        }
        
        public String toString() {
            return new StringBuilder().
                    append(getClass().getName()).
                    append(": alias=").append(alias).
                    append("; keystore type=").append(keystore.getType()).
                    append("; provider=").append(keystore.getProvider().getName()).
                    
                    toString();
        }
        
        public KeyStore getKeyStore() {
            return keystore;
        }
        
        public String[] getSigningArgs(File unsignedJar, File signedJar) {
            /*Compose the arguments to pass to the JarSigner class equivalent to
             *this command:
             *jarsigner 
             *  -keystore <jks-file-spec, if type is jks; NONE if pkcs11>
             *  -providerName <pkcs11 provider name, if pkcs11>
             *  -storetype <jks or pkcs11, depending on the keystore>
             *  -storepass <keystore password>
             *  -signedjar <signed JAR file spec>
             *  <unsigned JAR file spec>
             *  <alias>
             *
             *Note that techniques for importing certs into the AS keystore are
             *documented as requiring that the key password and the keystore
             *password be the same.  We rely on that here because there is no
             *provision for extracting the password for an alias from the keystore.
             */
            ArrayList<String> args = new ArrayList<String>();
            
            addKeyStoreTypeSpecificArgs(args);
            
            args.add(STORETYPE_OPTION);
            args.add(getKeyStore().getType());

            args.add(STOREPASS_OPTION);
            int passwordSlot = args.size();
            args.add(signingInfo.getPassword());

            args.add(SIGNEDJAR_OPTION);
            args.add(signedJar.getAbsolutePath());
            
            args.add(unsignedJar.getAbsolutePath());

            args.add(signingInfo.getAlias());
            
            String[] result = args.toArray(new String[args.size()]);
            args.set(passwordSlot,"");
            return result;
        }
        
        /**
         *Appends command-line arguments that depend on the specific type of
         *keystore to the collection of arguments.
         *@param args Collection of command-line argument Strings to be added to
         */
        protected abstract void addKeyStoreTypeSpecificArgs(Collection<String> args);
        
        /**
         *Implements the JKS-specific type of signing information.
         */
        private static class JKSSigningInfo extends SigningInfo {

            /** property name the value of which points to the JKS keystore, if present */
            private static final String KEYSTORE_PATH_PROPERTYNAME = "javax.net.ssl.keyStore";

            /**
             *Returns the absolute path to the keystore.
             *@return path to the keystore
             */
            private static String getJKSKeystoreAbsolutePath() {
                return System.getProperty(KEYSTORE_PATH_PROPERTYNAME);
            }
            
            public JKSSigningInfo(String alias, String password, 
                    KeyStore keystore, String token) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
                super(alias, password, keystore, token);
            }
            
            protected void addKeyStoreTypeSpecificArgs(Collection<String> args) {
                /*
                 *  -keystore <filespec>
                 */
                args.add(KEYSTORE_OPTION);
                args.add(getJKSKeystoreAbsolutePath());
            }
            
        }
        
        /**
         *Implements the PKCS11-specific type of signing information.
         */
        private static class PKCS11SigningInfo extends SigningInfo {

            private static final String PKCS11_PROVIDERNAME_OPTION = "-providerName";
            private static final String PKCS11_KEYSTORE_OPTION_VALUE = "NONE";

            public PKCS11SigningInfo(String alias, String password, 
                    KeyStore keystore, String token) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
                super(alias, password, keystore, token);
            }
            
            protected void addKeyStoreTypeSpecificArgs(Collection<String> args) {
                /*For PKCS11 keystores, use:
                 *
                 *  -keystore NONE
                 *  -providerName <provider name for the keystore>
                 *
                 *The provider name includes the token, so any configuration
                 *information the provider needs to sign the JAR will be 
                 *available to the provider instance.
                 */
                args.add(KEYSTORE_OPTION);
                args.add(PKCS11_KEYSTORE_OPTION_VALUE);
                
                args.add(PKCS11_PROVIDERNAME_OPTION);
                args.add(getKeyStore().getProvider().getName());
           }
        }        
    }
    
    /**
     *A security manager that rejects any attempt to exit the VM.
     */
    private class NoExitSecurityManager extends SecurityManager {
        
        private SecurityManager originalManager;
        
        public NoExitSecurityManager(SecurityManager originalManager) {
            this.originalManager = originalManager;
        }
        
        public void checkExit(int status) {
            /*
             *Always reject attempts to exit the VM.
             */
            throw new AccessControlException("System.exit");
        }
        
        public void checkPermission(Permission p) {
            /*
             *Delegate to the other manager, if any.
             */
            if (originalManager != null) {
                originalManager.checkPermission(p);
            }
        }
    }
}