changeset 278:bd59947fa857

Checks and verifies a signed JNLP file at the launch of the application. A signed JNLP warning is displayed if appropriate.
author Saad Mohammad <smohammad@redhat.com>
date Mon, 22 Aug 2011 15:09:47 -0400
parents 61e08e67b176
children 334a44162495
files ChangeLog netx/net/sourceforge/jnlp/JNLPFile.java netx/net/sourceforge/jnlp/SecurityDesc.java netx/net/sourceforge/jnlp/resources/Messages.properties netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java netx/net/sourceforge/jnlp/security/MoreInfoPane.java netx/net/sourceforge/jnlp/security/SecurityDialog.java
diffstat 7 files changed, 389 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Wed Aug 17 12:01:19 2011 -0400
+++ b/ChangeLog	Mon Aug 22 15:09:47 2011 -0400
@@ -1,3 +1,46 @@
+2011-08-22  Saad Mohammad  <smohammad@redhat.com>
+	* netx/net/sourceforge/jnlp/JNLPFile.java:
+	(parse): After the file has been parsed, it calls 
+	checkForSpecialProperties() to check if the resources contain any special
+	properties.
+	(checkForSpecialProperties): Scans through resources and checks if it 
+	contains any special properties.
+	(requiresSignedJNLPWarning): Returns a boolean after determining if a signed
+	JNLP warning should be displayed.
+	(setSignedJNLPAsMissing): Informs JNLPFile that a signed JNLP file is
+	missing in the main jar.
+	* netx/net/sourceforge/jnlp/SecurityDesc.java:
+	(getJnlpRIAPermissions): Returns all the names of the basic JNLP system
+	properties accessible by RIAs.
+	* netx/net/sourceforge/jnlp/resources/Messages.properties:
+	Added LSignedJNLPFileDidNotMatch and SJNLPFileIsNotSigned.
+	* netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java:
+	(initializeResources): Locates the jar file that contains the main class
+	and verifies if a signed JNLP file is also located in that jar. This also
+	checks 'lazy' jars if the the main class was not found in 'eager' jars.
+	If the main jar was not found, a LaunchException is thrown which terminates
+	the launch of the application.
+	(checkForMain): A method that goes through each jar and checks to see
+	if it has the main class. If the main class is found, it calls
+	verifySignedJNLP() to verify if a valid signed JNLP file is also found in
+	the jar.
+	(verifySignedJNLP): A method that checks if the jar file contains a valid 
+	signed JNLP file.
+	(closeStream): Closes a stream.
+	(loadClassExt): Added a try/catch block when addNextResource() is called.
+	(addNextResource): If the main jar has not been found, checkForMain() is
+	called to check if the jar contains the main class, and verifies if a signed
+	JNLP file is also located.
+	* netx/net/sourceforge/jnlp/security/MoreInfoPane.java:
+	(addComponents): Displays the signed JNLP warning message if necessary.
+	* netx/net/sourceforge/jnlp/security/SecurityDialog.java:
+	(SecurityDialog): Stores the value of whether a signed JNLP warning should 
+	be displayed.
+	(showMoreInfoDialog): Passes in the associated JNLP file when creating a
+	SecurityDialog object.
+	(requiresSignedJNLPWarning): Returns a boolean after determining if a signed
+	JNLP warning should be displayed.
+
 2011-08-17  Danesh Dadachanji <ddadacha@redhat.com>
 
 	Update UI for SecurityDialog
--- a/netx/net/sourceforge/jnlp/JNLPFile.java	Wed Aug 17 12:01:19 2011 -0400
+++ b/netx/net/sourceforge/jnlp/JNLPFile.java	Mon Aug 22 15:09:47 2011 -0400
@@ -107,7 +107,18 @@
 
     /** the default jvm */
     protected String defaultArch = null;
+    
+    /** A signed JNLP file is missing from the main jar */
+    private boolean missingSignedJNLP = false;
+    
+    /** JNLP file contains special properties */
+    private boolean containsSpecialProperties = false;
 
+    /**
+     * List of acceptable properties (not-special)
+     */
+    private String[] generalProperties = SecurityDesc.getJnlpRIAPermissions();
+    
     { // initialize defaults if security allows
         try {
             defaultLocale = Locale.getDefault();
@@ -608,6 +619,9 @@
             launchType = parser.getLauncher(root);
             component = parser.getComponent(root);
             security = parser.getSecurity(root);
+            
+            checkForSpecialProperties();
+            
         } catch (ParseException ex) {
             throw ex;
         } catch (Exception ex) {
@@ -619,6 +633,30 @@
     }
 
     /**
+     * Inspects the JNLP file to check if it contains any special properties
+     */
+    private void checkForSpecialProperties() {
+
+        for (ResourcesDesc res : resources) {
+            for (PropertyDesc propertyDesc : res.getProperties()) {
+
+                for (int i = 0; i < generalProperties.length; i++) {
+                    String property = propertyDesc.getKey();
+
+                    if (property.equals(generalProperties[i])) {
+                        break;
+                    } else if (!property.equals(generalProperties[i])
+                            && i == generalProperties.length - 1) {
+                        containsSpecialProperties = true;
+                        return;
+                    }
+                }
+
+            }
+        }
+    }
+
+    /**
      *
      * @return true if the JNLP file specifies things that can only be
      * applied on a new vm (eg: different max heap memory)
@@ -690,4 +728,21 @@
         return new DownloadOptions(usePack, useVersion);
     }
 
+    /**
+     * Returns a boolean after determining if a signed JNLP warning should be
+     * displayed in the 'More Information' panel.
+     * 
+     * @return true if a warning should be displayed; otherwise false
+     */
+    public boolean requiresSignedJNLPWarning() {
+        return (missingSignedJNLP && containsSpecialProperties);
+    }
+
+    /**
+     * Informs that a signed JNLP file is missing in the main jar
+     */
+    public void setSignedJNLPAsMissing() {
+        missingSignedJNLP = true;
+    }
+
 }
--- a/netx/net/sourceforge/jnlp/SecurityDesc.java	Wed Aug 17 12:01:19 2011 -0400
+++ b/netx/net/sourceforge/jnlp/SecurityDesc.java	Mon Aug 22 15:09:47 2011 -0400
@@ -244,5 +244,17 @@
 
         return permissions;
     }
+    
+    /**
+     * Returns all the names of the basic JNLP system properties accessible by RIAs
+     */
+    public static String[] getJnlpRIAPermissions() {
+        String[] jnlpPermissions = new String[jnlpRIAPermissions.length];
+
+        for (int i = 0; i < jnlpRIAPermissions.length; i++)
+            jnlpPermissions[i] = jnlpRIAPermissions[i].getName();
+
+        return jnlpPermissions;
+    }
 
 }
--- a/netx/net/sourceforge/jnlp/resources/Messages.properties	Wed Aug 17 12:01:19 2011 -0400
+++ b/netx/net/sourceforge/jnlp/resources/Messages.properties	Mon Aug 22 15:09:47 2011 -0400
@@ -80,6 +80,7 @@
 LUnsignedJarWithSecurityInfo=Application requested security permissions, but jars are not signed.
 LSignedAppJarUsingUnsignedJar=Signed application using unsigned jars.
 LSignedAppJarUsingUnsignedJarInfo=The main application jar is signed, but some of the jars it is using aren't.
+LSignedJNLPFileDidNotMatch=The signed JNLP file did not match the launching JNLP file.
 
 JNotApplet=File is not an applet.
 JNotApplication=File is not an application.
@@ -210,6 +211,7 @@
 SNotAllSignedDetail=This application contains both signed and unsigned code. While signed code is safe if you trust the provider, unsigned code may imply code outside of the trusted provider's control.
 SNotAllSignedQuestion=Do you wish to proceed and run this application anyway?
 SAuthenticationPrompt=The {0} server at {1} is requesting authentication. It says "{2}"
+SJNLPFileIsNotSigned=This application contains a digital signature in which the launching JNLP file is not signed.
 
 # Security - used for the More Information dialog
 SBadKeyUsage=Resources contain entries whose signer certificate's KeyUsage extension doesn't allow code signing.
--- a/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java	Wed Aug 17 12:01:19 2011 -0400
+++ b/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java	Mon Aug 22 15:09:47 2011 -0400
@@ -17,10 +17,13 @@
 
 import static net.sourceforge.jnlp.runtime.Translator.R;
 
+import java.io.Closeable;
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -46,11 +49,14 @@
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
-
+import net.sourceforge.jnlp.AppletDesc;
+import net.sourceforge.jnlp.ApplicationDesc;
 import net.sourceforge.jnlp.DownloadOptions;
 import net.sourceforge.jnlp.ExtensionDesc;
 import net.sourceforge.jnlp.JARDesc;
 import net.sourceforge.jnlp.JNLPFile;
+import net.sourceforge.jnlp.JNLPMatcher;
+import net.sourceforge.jnlp.JNLPMatcherException;
 import net.sourceforge.jnlp.LaunchException;
 import net.sourceforge.jnlp.ParseException;
 import net.sourceforge.jnlp.PluginBridge;
@@ -81,6 +87,13 @@
     // extension classes too so that main file classes can load
     // resources in an extension.
 
+    /** Signed JNLP File and Template */
+    final public static String TEMPLATE = "JNLP-INF/APPLICATION_TEMPLATE.JNLP";
+    final public static String APPLICATION = "JNLP-INF/APPLICATION.JNLP";
+    
+    /** True if the application has a signed JNLP File */
+    private boolean isSignedJNLP = false;
+    
     /** map from JNLPFile url to shared classloader */
     private static Map<String, JNLPClassLoader> urlToLoader =
             new HashMap<String, JNLPClassLoader>(); // never garbage collected!
@@ -153,6 +166,10 @@
     
     /** Loader for codebase (which is a path, rather than a file) */
     private CodeBaseClassLoader codeBaseLoader;
+    
+    /** True if the jar with the main class has been found
+     * */
+    private boolean foundMainJar= false;
 
     /**
      * Create a new JNLPClassLoader from the specified file.
@@ -460,6 +477,26 @@
                                     !SecurityDialogs.showNotAllSignedWarningDialog(file))
                     throw new LaunchException(file, null, R("LSFatal"), R("LCClient"), R("LSignedAppJarUsingUnsignedJar"), R("LSignedAppJarUsingUnsignedJarInfo"));
 
+
+                // Check for main class in the downloaded jars, and check/verify signed JNLP fill
+                checkForMain(initialJars);
+
+                // If jar with main class was not found, check available resources
+                while (!foundMainJar && available != null && available.size() != 0) 
+                    addNextResource();
+
+                // If jar with main class was not found and there are no more
+                // available jars, throw a LaunchException
+                if (!foundMainJar
+                        && (available == null || available.size() == 0))
+                    throw new LaunchException(file, null, R("LSFatal"),
+                            R("LCClient"), R("LCantDetermineMainClass"),
+                            R("LCantDetermineMainClassInfo"));
+
+                // If main jar was found, but a signed JNLP file was not located
+                if (!isSignedJNLP && foundMainJar) 
+                    file.setSignedJNLPAsMissing();
+                
                 //user does not trust this publisher
                 if (!js.getAlreadyTrustPublisher()) {
                     checkTrustWithUser(js);
@@ -518,10 +555,205 @@
                 System.err.println(mfe.getMessage());
             }
         }
-
         activateJars(initialJars);
     }
+    
+    /***
+     * Checks for the jar that contains the main class. If the main class was
+     * found, it checks to see if the jar is signed and whether it contains a
+     * signed JNLP file
+     * 
+     * @param jars Jars that are checked to see if they contain the main class
+     * @throws LaunchException Thrown if the signed JNLP file, within the main jar, fails to be verified or does not match
+     */
+    private void checkForMain(List<JARDesc> jars) throws LaunchException {
 
+        Object obj = file.getLaunchInfo();
+        String mainClass;
+
+        if (obj instanceof ApplicationDesc) {
+            ApplicationDesc ad = (ApplicationDesc) file.getLaunchInfo();
+            mainClass = ad.getMainClass();
+        } else if (obj instanceof AppletDesc) {
+            AppletDesc ad = (AppletDesc) file.getLaunchInfo();
+            mainClass = ad.getMainClass();
+        } else
+            return;
+
+        for (int i = 0; i < jars.size(); i++) {
+
+            try {
+                File localFile = tracker
+                        .getCacheFile(jars.get(i).getLocation());
+
+                if (localFile == null)
+                    throw new NullPointerException(
+                            "Could not locate jar file, returned null");
+
+                JarFile jarFile = new JarFile(localFile);
+                Enumeration<JarEntry> entries = jarFile.entries();
+                JarEntry je;
+
+                while (entries.hasMoreElements()) {
+                    je = entries.nextElement();
+                    String jeName = je.getName().replaceAll("/", ".");
+
+                    if (!jeName.startsWith(mainClass + "$Inner")
+                            && (jeName.startsWith(mainClass) && jeName.endsWith(".class"))) {
+                        foundMainJar = true;
+                        verifySignedJNLP(jars.get(i), jarFile);
+                        break;
+                    }
+                }
+            } catch (IOException e) {
+                /*
+                 * After this exception is caught, it is escaped. This will skip
+                 * the jarFile that may have thrown this exception and move on
+                 * to the next jarFile (if there are any)
+                 */
+            }
+        }
+    }
+
+    /**
+     * Is called by checkForMain() to check if the jar file is signed and if it
+     * contains a signed JNLP file.
+     * 
+     * @param jarDesc JARDesc of jar
+     * @param jarFile the jar file
+     * @throws LaunchException thrown if the signed JNLP file, within the main jar, fails to be verified or does not match
+     */
+    private void verifySignedJNLP(JARDesc jarDesc, JarFile jarFile)
+            throws LaunchException {
+
+        JarSigner signer = new JarSigner();
+        List<JARDesc> desc = new ArrayList<JARDesc>();
+        desc.add(jarDesc);
+
+        // Initialize streams
+        InputStream inStream = null;
+        InputStreamReader inputReader = null;
+        FileReader fr = null;
+        InputStreamReader jnlpReader = null;
+
+        try {
+            signer.verifyJars(desc, tracker);
+
+            if (signer.allJarsSigned()) { // If the jar is signed
+
+                Enumeration<JarEntry> entries = jarFile.entries();
+                JarEntry je;
+
+                while (entries.hasMoreElements()) {
+                    je = entries.nextElement();
+                    String jeName = je.getName().toUpperCase();
+
+                    if (jeName.equals(TEMPLATE) || jeName.equals(APPLICATION)) {
+
+                        if (JNLPRuntime.isDebug())
+                            System.err.println("Creating Jar InputStream from JarEntry");
+
+                        inStream = jarFile.getInputStream(je);
+                        inputReader = new InputStreamReader(inStream);
+
+                        if (JNLPRuntime.isDebug())
+                            System.err.println("Creating File InputStream from lauching JNLP file");
+
+                        JNLPFile jnlp = this.getJNLPFile();
+                        URL url = jnlp.getFileLocation();
+                        File jn = null;
+
+                        // If the file is on the local file system, use original path, otherwise find cached file
+                        if (url.getProtocol().toLowerCase().equals("file"))
+                            jn = new File(url.getPath());
+                        else
+                            jn = CacheUtil.getCacheFile(url, null);
+
+                        fr = new FileReader(jn);
+                        jnlpReader = fr;
+
+                        // Initialize JNLPMatcher class
+                        JNLPMatcher matcher;
+
+                        if (jeName.equals(APPLICATION)) { // If signed application was found
+                            if (JNLPRuntime.isDebug())
+                                System.err.println("APPLICATION.JNLP has been located within signed JAR. Starting verfication...");
+                           
+                            matcher = new JNLPMatcher(inputReader, jnlpReader, false);
+                        } else { // Otherwise template was found
+                            if (JNLPRuntime.isDebug())
+                                System.err.println("APPLICATION_TEMPLATE.JNLP has been located within signed JAR. Starting verfication...");
+                            
+                            matcher = new JNLPMatcher(inputReader, jnlpReader,
+                                    true);
+                        }
+
+                        // If signed JNLP file does not matches launching JNLP file, throw JNLPMatcherException
+                        if (!matcher.isMatch())
+                            throw new JNLPMatcherException("Signed Application did not match launching JNLP File");
+
+                        this.isSignedJNLP = true;
+                        if (JNLPRuntime.isDebug())
+                            System.err.println("Signed Application Verification Successful");
+
+                        break; 
+                    }
+                }
+            }
+        } catch (JNLPMatcherException e) {
+
+            /*
+             * Throws LaunchException if signed JNLP file fails to be verified
+             * or fails to match the launching JNLP file
+             */
+
+            throw new LaunchException(file, null, R("LSFatal"), R("LCClient"),
+                    R("LSignedJNLPFileDidNotMatch"), R(e.getMessage()));
+
+            /*
+             * Throwing this exception will fail to initialize the application
+             * resulting in the termination of the application
+             */
+
+        } catch (Exception e) {
+            
+            if (JNLPRuntime.isDebug())
+                e.printStackTrace(System.err);
+
+            /*
+             * After this exception is caught, it is escaped. If an exception is
+             * thrown while handling the jar file, (mainly for
+             * JarSigner.verifyJars) it assumes the jar file is unsigned and
+             * skip the check for a signed JNLP file
+             */
+            
+        } finally {
+
+            //Close all streams
+            closeStream(inStream);
+            closeStream(inputReader);
+            closeStream(fr);
+            closeStream(jnlpReader);
+        }
+
+        if (JNLPRuntime.isDebug())
+            System.err.println("Ending check for signed JNLP file...");
+    }
+
+    /***
+     * Closes a stream
+     * 
+     * @param stream the stream that will be closed
+     */
+    private void closeStream (Closeable stream) {
+        if (stream != null)
+            try {
+                stream.close();
+            } catch (Exception e) {
+                e.printStackTrace(System.err);
+            }
+    }
+    
     private void checkTrustWithUser(JarSigner js) throws LaunchException {
         if (!js.getRootInCacerts()) { //root cert is not in cacerts
             boolean b = SecurityDialogs.showCertWarningDialog(
@@ -1154,7 +1386,20 @@
 
         // add resources until found
         while (true) {
-            JNLPClassLoader addedTo = addNextResource();
+            JNLPClassLoader addedTo = null;
+            
+            try {
+                addedTo = addNextResource();
+            } catch (LaunchException e) {
+
+                /*
+                 * This method will never handle any search for the main class
+                 * [It is handled in initializeResources()]. Therefore, this
+                 * exception will never be thrown here and is escaped
+                 */
+
+                throw new IllegalStateException(e);
+            }
 
             if (addedTo == null)
                 throw new ClassNotFoundException(name);
@@ -1245,8 +1490,9 @@
      * no more resources to add, the method returns immediately.
      *
      * @return the classloader that resources were added to, or null
+     * @throws LaunchException Thrown if the signed JNLP file, within the main jar, fails to be verified or does not match
      */
-    protected JNLPClassLoader addNextResource() {
+    protected JNLPClassLoader addNextResource() throws LaunchException {
         if (available.size() == 0) {
             for (int i = 1; i < loaders.length; i++) {
                 JNLPClassLoader result = loaders[i].addNextResource();
@@ -1262,6 +1508,7 @@
         jars.add(available.get(0));
 
         fillInPartJars(jars);
+        checkForMain(jars);
         activateJars(jars);
 
         return this;
--- a/netx/net/sourceforge/jnlp/security/MoreInfoPane.java	Wed Aug 17 12:01:19 2011 -0400
+++ b/netx/net/sourceforge/jnlp/security/MoreInfoPane.java	Mon Aug 22 15:09:47 2011 -0400
@@ -61,8 +61,11 @@
  */
 public class MoreInfoPane extends SecurityDialogPanel {
 
+    private boolean showSignedJNLPWarning;
+
     public MoreInfoPane(SecurityDialog x, CertVerifier certVerifier) {
         super(x, certVerifier);
+        showSignedJNLPWarning= x.requiresSignedJNLPWarning();
         addComponents();
     }
 
@@ -72,6 +75,11 @@
     private void addComponents() {
         ArrayList<String> details = certVerifier.getDetails();
 
+        // Show signed JNLP warning if the signed main jar does not have a
+        // signed JNLP file and the launching JNLP file contains special properties
+        if(showSignedJNLPWarning)
+            details.add(R("SJNLPFileIsNotSigned"));
+            
         int numLabels = details.size();
         JPanel errorPanel = new JPanel(new GridLayout(numLabels, 1));
         errorPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
@@ -88,6 +96,11 @@
 
             errorPanel.add(new JLabel(htmlWrap(details.get(i)), icon, SwingConstants.LEFT));
         }
+        
+        // Removes signed JNLP warning after it has been used. This will avoid
+        // any alteration to certVerifier.
+        if(showSignedJNLPWarning)
+            details.remove(details.size()-1);
 
         JPanel buttonsPanel = new JPanel(new BorderLayout());
         JButton certDetails = new JButton(R("SCertificateDetails"));
--- a/netx/net/sourceforge/jnlp/security/SecurityDialog.java	Wed Aug 17 12:01:19 2011 -0400
+++ b/netx/net/sourceforge/jnlp/security/SecurityDialog.java	Mon Aug 22 15:09:47 2011 -0400
@@ -92,6 +92,9 @@
      */
     private Object value;
 
+    /** Should show signed JNLP file warning */
+    private boolean requiresSignedJNLPWarning;
+
     SecurityDialog(DialogType dialogType, AccessType accessType,
                 JNLPFile file, CertVerifier jarSigner, X509Certificate cert, Object[] extras) {
         super();
@@ -103,6 +106,9 @@
         this.extras = extras;
         initialized = true;
 
+        if(file != null)
+            requiresSignedJNLPWarning= file.requiresSignedJNLPWarning();
+
         initDialog();
     }
 
@@ -164,8 +170,9 @@
     public static void showMoreInfoDialog(
                 CertVerifier jarSigner, SecurityDialog parent) {
 
+        JNLPFile file= parent.getFile();
         SecurityDialog dialog =
-                        new SecurityDialog(DialogType.MORE_INFO, null, null,
+                        new SecurityDialog(DialogType.MORE_INFO, null, file,
                                 jarSigner);
         dialog.setModalityType(ModalityType.APPLICATION_MODAL);
         dialog.setVisible(true);
@@ -372,5 +379,10 @@
     public void addActionListener(ActionListener listener) {
         listeners.add(listener);
     }
+    
+    public boolean requiresSignedJNLPWarning()
+    {
+        return requiresSignedJNLPWarning;
+    }
 
 }