diff --git a/netx/net/sourceforge/jnlp/resources/Messages.properties b/netx/net/sourceforge/jnlp/resources/Messages.properties --- a/netx/net/sourceforge/jnlp/resources/Messages.properties +++ b/netx/net/sourceforge/jnlp/resources/Messages.properties @@ -226,7 +226,6 @@ SJNLPFileIsNotSigned=This application co SBadKeyUsage=Resources contain entries whose signer certificate's KeyUsage extension doesn't allow code signing. SBadExtendedKeyUsage=Resources contain entries whose signer certificate's ExtendedKeyUsage extension doesn't allow code signing. SBadNetscapeCertType=Resources contain entries whose signer certificate's NetscapeCertType extension doesn't allow code signing. -SHasUnsignedEntry=Resources contain unsigned entries which have not been integrity-checked. SHasExpiredCert=The digital signature has expired. SHasExpiringCert=Resources contain entries whose signer certificate will expire within six months. SNotYetValidCert=Resources contain entries whose signer certificate is not yet valid. diff --git a/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java b/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java --- a/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java +++ b/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java @@ -78,8 +78,10 @@ import net.sourceforge.jnlp.cache.CacheU import net.sourceforge.jnlp.cache.IllegalResourceDescriptorException; import net.sourceforge.jnlp.cache.ResourceTracker; import net.sourceforge.jnlp.cache.UpdatePolicy; +import net.sourceforge.jnlp.security.AppVerifier; +import net.sourceforge.jnlp.security.JNLPAppVerifier; +import net.sourceforge.jnlp.security.PluginAppVerifier; import net.sourceforge.jnlp.security.SecurityDialogs; -import net.sourceforge.jnlp.security.SecurityDialogs.AccessType; import net.sourceforge.jnlp.tools.JarCertVerifier; import net.sourceforge.jnlp.util.FileUtils; import sun.misc.JarIndex; @@ -152,14 +154,8 @@ public class JNLPClassLoader extends URL /** all jars not yet part of classloader or active */ private List available = new ArrayList(); - /** all of the jar files that were verified */ - private ArrayList verifiedJars = null; - - /** all of the jar files that were not verified */ - private ArrayList unverifiedJars = null; - /** the jar cert verifier tool to verify our jars */ - private JarCertVerifier jcv = null; + private final JarCertVerifier jcv; private boolean signing = false; @@ -222,6 +218,16 @@ public class JNLPClassLoader extends URL this.mainClass = mainName; + AppVerifier verifier; + + if (file instanceof PluginBridge && !((PluginBridge)file).useJNLPHref()) { + verifier = new PluginAppVerifier(); + } else { + verifier = new JNLPAppVerifier(); + } + + jcv = new JarCertVerifier(verifier); + // initialize extensions initializeExtensions(); @@ -603,10 +609,8 @@ public class JNLPClassLoader extends URL if (JNLPRuntime.isVerifying()) { - JarCertVerifier jcv; - try { - jcv = verifyJars(initialJars); + jcv.add(initialJars, tracker); } catch (Exception e) { //we caught an Exception from the JarCertVerifier class. //Note: one of these exceptions could be from not being able @@ -617,7 +621,7 @@ public class JNLPClassLoader extends URL } //Case when at least one jar has some signing - if (jcv.anyJarsSigned() && jcv.isFullySignedByASingleCert()) { + if (jcv.isFullySigned()) { signing = true; if (!jcv.allJarsSigned() && @@ -649,10 +653,10 @@ public class JNLPClassLoader extends URL // 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 (!jcv.getAlreadyTrustPublisher() && !jcv.isTriviallySigned()) { - checkTrustWithUser(jcv); + if (!jcv.isTriviallySigned()) { + checkTrustWithUser(); } else { /** * If the user trusts this publisher (i.e. the publisher's certificate @@ -863,7 +867,6 @@ public class JNLPClassLoader extends URL private void verifySignedJNLP(JARDesc jarDesc, JarFile jarFile) throws LaunchException { - JarCertVerifier signer = new JarCertVerifier(); List desc = new ArrayList(); desc.add(jarDesc); @@ -874,9 +877,9 @@ public class JNLPClassLoader extends URL InputStreamReader jnlpReader = null; try { - signer.verifyJars(desc, tracker); - - if (signer.allJarsSigned()) { // If the jar is signed + // NOTE: verification should have happened by now. In other words, + // calling jcv.verifyJars(desc, tracker) here should have no affect. + if (jcv.isFullySigned()) { Enumeration entries = jarFile.entries(); JarEntry je; @@ -960,7 +963,7 @@ public class JNLPClassLoader extends URL /* * After this exception is caught, it is escaped. If an exception is * thrown while handling the jar file, (mainly for - * JarCertVerifier.verifyJars) it assumes the jar file is unsigned and + * JarCertVerifier.add) it assumes the jar file is unsigned and * skip the check for a signed JNLP file */ @@ -990,28 +993,18 @@ public class JNLPClassLoader extends URL e.printStackTrace(System.err); } } - - private void checkTrustWithUser(JarCertVerifier jcv) throws LaunchException { + + /** + * Prompt the user for trust on all the signers that require approval. + * @throws LaunchException if the user does not approve every dialog prompt. + */ + private void checkTrustWithUser() throws LaunchException { if (JNLPRuntime.isTrustAll()){ return; } - if (!jcv.getRootInCacerts()) { //root cert is not in cacerts - boolean b = SecurityDialogs.showCertWarningDialog( - AccessType.UNVERIFIED, file, jcv); - if (!b) - throw new LaunchException(null, null, R("LSFatal"), - R("LCLaunching"), R("LNotVerified"), ""); - } else if (jcv.getRootInCacerts()) { //root cert is in cacerts - boolean b = false; - if (jcv.noSigningIssues()) - b = SecurityDialogs.showCertWarningDialog( - AccessType.VERIFIED, file, jcv); - else if (!jcv.noSigningIssues()) - b = SecurityDialogs.showCertWarningDialog( - AccessType.SIGNING_ERROR, file, jcv); - if (!b) - throw new LaunchException(null, null, R("LSFatal"), - R("LCLaunching"), R("LCancelOnUserRequest"), ""); + + if (jcv.isFullySigned() && !jcv.getAlreadyTrustPublisher()) { + jcv.checkTrustWithUser(file); } } @@ -1225,15 +1218,25 @@ public class JNLPClassLoader extends URL continue; } - JarCertVerifier signer = new JarCertVerifier(); - List jars = new ArrayList(); - JARDesc jarDesc = new JARDesc(new File(extractedJarLocation).toURL(), null, null, false, false, false, false); - jars.add(jarDesc); tracker.addResource(new File(extractedJarLocation).toURL(), null, null, null); - signer.verifyJars(jars, tracker); - if (signer.anyJarsSigned() && !signer.getAlreadyTrustPublisher()) { - checkTrustWithUser(signer); + URL codebase = file.getCodeBase(); + if (codebase == null) { + //FIXME: codebase should be the codebase of the Main Jar not + //the location. Although, it still works in the current state. + codebase = file.getResources().getMainJAR().getLocation(); + } + + SecurityDesc jarSecurity = null; + if (jcv.isFullySigned()) { + // Already trust application, nested jar should be given + jarSecurity = new SecurityDesc(file, + SecurityDesc.ALL_PERMISSIONS, + codebase.getHost()); + } else { + jarSecurity = new SecurityDesc(file, + SecurityDesc.SANDBOX_PERMISSIONS, + codebase.getHost()); } try { @@ -1243,25 +1246,6 @@ public class JNLPClassLoader extends URL CachedJarFileCallback.getInstance().addMapping(fakeRemote, fileURL); addURL(fakeRemote); - SecurityDesc jarSecurity = file.getSecurity(); - - if (file instanceof PluginBridge) { - - URL codebase = null; - - if (file.getCodeBase() != null) { - codebase = file.getCodeBase(); - } else { - //Fixme: codebase should be the codebase of the Main Jar not - //the location. Although, it still works in the current state. - codebase = file.getResources().getMainJAR().getLocation(); - } - - jarSecurity = new SecurityDesc(file, - SecurityDesc.ALL_PERMISSIONS, - codebase.getHost()); - } - jarLocationSecurityMap.put(fakeRemote, jarSecurity); } catch (MalformedURLException mfue) { @@ -1474,18 +1458,6 @@ public class JNLPClassLoader extends URL } /** - * Verifies code signing of jars to be used. - * - * @param jars the jars to be verified. - */ - private JarCertVerifier verifyJars(List jars) throws Exception { - - jcv = new JarCertVerifier(); - jcv.verifyJars(jars, tracker); - return jcv; - } - - /** * Find the loaded class in this loader or any of its extension loaders. */ protected Class findLoadedClassAll(String name) { @@ -1651,7 +1623,6 @@ public class JNLPClassLoader extends URL // Verify if needed - final JarCertVerifier signer = new JarCertVerifier(); final List jars = new ArrayList(); jars.add(desc); @@ -1663,14 +1634,12 @@ public class JNLPClassLoader extends URL AccessController.doPrivileged(new PrivilegedExceptionAction() { public Void run() throws Exception { - signer.verifyJars(jars, tracker); + jcv.add(jars, tracker); - if (signer.anyJarsSigned() && !signer.getAlreadyTrustPublisher()) { - checkTrustWithUser(signer); - } + checkTrustWithUser(); final SecurityDesc security; - if (signer.anyJarsSigned()) { + if (jcv.isFullySigned()) { security = new SecurityDesc(file, SecurityDesc.ALL_PERMISSIONS, file.getCodeBase().getHost()); diff --git a/netx/net/sourceforge/jnlp/security/AppVerifier.java b/netx/net/sourceforge/jnlp/security/AppVerifier.java new file mode 100644 --- /dev/null +++ b/netx/net/sourceforge/jnlp/security/AppVerifier.java @@ -0,0 +1,91 @@ +/* AppVerifier.java + Copyright (C) 2012 Red Hat, Inc. + +This file is part of IcedTea. + +IcedTea is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 2. + +IcedTea is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IcedTea; see the file COPYING. If not, write to +the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. +*/ + +package net.sourceforge.jnlp.security; + +import java.security.cert.CertPath; +import java.util.HashMap; + +import net.sourceforge.jnlp.JNLPFile; +import net.sourceforge.jnlp.LaunchException; +import net.sourceforge.jnlp.tools.CertInformation; +import net.sourceforge.jnlp.tools.JarCertVerifier; + +/** + * An interface that provides various details about an app's signers. + */ +public interface AppVerifier { + + /** + * Checks if the app has already found trust in its publisher(s). + * @param certs The certs to search through and their cert information + * @param signedJars A map of all the jars of this app and the number of + * signed entries each one has. + * @return True if the app trusts its publishers. + */ + public boolean hasAlreadyTrustedPublisher( + HashMap certs, + HashMap signedJars); + + /** + * Checks if the app has signer(s) whose certs along their chains are in CA certs. + * @param certs The certs to search through and their cert information + * @param signedJars A map of all the jars of this app and the number of + * signed entries each one has. + * @return True if the app has a root in the CA certs store. + */ + public boolean hasRootInCacerts(HashMap certs, + HashMap signedJars); + + /** + * Checks if the app's jars are covered by the provided certificates, enough + * to consider the app fully signed. + * @param certs Any possible signer and their respective information regarding this app. + * @param signedJars A map of all the jars of this app and the number of + * signed entries each one has. + * @return + */ + public boolean isFullySigned(HashMap certs, + HashMap signedJars); + + /** + * Prompt the user with requests for trusting the certificates used by this app + * @throws LaunchException + */ + public void checkTrustWithUser(JarCertVerifier jcv, JNLPFile file) + throws LaunchException; +} diff --git a/netx/net/sourceforge/jnlp/security/CertVerifier.java b/netx/net/sourceforge/jnlp/security/CertVerifier.java --- a/netx/net/sourceforge/jnlp/security/CertVerifier.java +++ b/netx/net/sourceforge/jnlp/security/CertVerifier.java @@ -1,5 +1,5 @@ /* CertVerifier.java - Copyright (C) 2009 Red Hat, Inc. + Copyright (C) 2012 Red Hat, Inc. This file is part of IcedTea. @@ -39,10 +39,10 @@ package net.sourceforge.jnlp.security; import java.security.cert.CertPath; import java.security.cert.Certificate; -import java.util.ArrayList; +import java.util.List; /** - * An interface that provides various details about a certificate + * An interface that provides various details about certificates of an app. */ public interface CertVerifier { @@ -58,36 +58,30 @@ public interface CertVerifier { public boolean getRootInCacerts(); /** - * Return if there are signing issues with the certificate(s) being veried + * Return if there are signing issues with the certificate being verified */ - public boolean hasSigningIssues(); + public boolean hasSigningIssues(CertPath certPath); /** - * Return if there are no signing issues with this cert (!hasSigningIssues()) + * Get the details regarding issue with this certificate */ - public boolean noSigningIssues(); + public List getDetails(CertPath certPath); /** - * Get the details regarding issue(s) with this certificate - */ - public ArrayList getDetails(); - - /** - * Return a valid certificate path to this certificate(s) being verified + * Return a valid certificate path to this certificate being verified * @return The CertPath */ - public CertPath getCertPath(); + public CertPath getCertPath(CertPath certPath); /** * Returns the application's publisher's certificate. */ - public abstract Certificate getPublisher(); + public abstract Certificate getPublisher(CertPath certPath); /** * Returns the application's root's certificate. This - * may return the same certificate as getPublisher() in + * may return the same certificate as getPublisher(CertPath certPath) in * the event that the application is self signed. */ - public abstract Certificate getRoot(); - + public abstract Certificate getRoot(CertPath certPath); } diff --git a/netx/net/sourceforge/jnlp/security/CertWarningPane.java b/netx/net/sourceforge/jnlp/security/CertWarningPane.java --- a/netx/net/sourceforge/jnlp/security/CertWarningPane.java +++ b/netx/net/sourceforge/jnlp/security/CertWarningPane.java @@ -96,7 +96,7 @@ public class CertWarningPane extends Sec private void addComponents() { AccessType type = parent.getAccessType(); JNLPFile file = parent.getFile(); - Certificate c = parent.getCertVerifier().getPublisher(); + Certificate c = parent.getCertVerifier().getPublisher(null); String name = ""; String publisher = ""; @@ -253,7 +253,7 @@ public class CertWarningPane extends Sec if (alwaysTrust != null && alwaysTrust.isSelected()) { try { KeyStore ks = KeyStores.getKeyStore(Level.USER, Type.CERTS); - X509Certificate c = (X509Certificate) parent.getCertVerifier().getPublisher(); + X509Certificate c = (X509Certificate) parent.getCertVerifier().getPublisher(null); CertificateUtils.addToKeyStore(c, ks); File keyStoreFile = new File(KeyStores.getKeyStoreLocation(Level.USER, Type.CERTS)); if (!keyStoreFile.isFile()) { diff --git a/netx/net/sourceforge/jnlp/security/CertsInfoPane.java b/netx/net/sourceforge/jnlp/security/CertsInfoPane.java --- a/netx/net/sourceforge/jnlp/security/CertsInfoPane.java +++ b/netx/net/sourceforge/jnlp/security/CertsInfoPane.java @@ -84,7 +84,7 @@ public class CertsInfoPane extends Secur * Builds the JTree out of CertPaths. */ void buildTree() { - certPath = parent.getCertVerifier().getCertPath(); + certPath = parent.getCertVerifier().getCertPath(null); X509Certificate firstCert = ((X509Certificate) certPath.getCertificates().get(0)); String subjectString = diff --git a/netx/net/sourceforge/jnlp/security/HttpsCertVerifier.java b/netx/net/sourceforge/jnlp/security/HttpsCertVerifier.java --- a/netx/net/sourceforge/jnlp/security/HttpsCertVerifier.java +++ b/netx/net/sourceforge/jnlp/security/HttpsCertVerifier.java @@ -83,7 +83,14 @@ public class HttpsCertVerifier implement return isTrusted; } - public CertPath getCertPath() { + + /* XXX: Most of these methods have a CertPath param that should be passed + * from the UI dialogs. However, this is not implemented yet so most of + * the params are ignored. + */ + + @Override + public CertPath getCertPath(CertPath certPath) { // Parameter ignored. ArrayList list = new ArrayList(); for (int i = 0; i < chain.length; i++) @@ -102,7 +109,8 @@ public class HttpsCertVerifier implement return certPaths.get(0); } - public ArrayList getDetails() { + @Override + public List getDetails(CertPath certPath) { // Parameter ignored. boolean hasExpiredCert = false; boolean hasExpiringCert = false; @@ -195,13 +203,15 @@ public class HttpsCertVerifier implement details.add(detail); } - public Certificate getPublisher() { + @Override + public Certificate getPublisher(CertPath certPath) { // Paramater ignored. if (chain.length > 0) return (Certificate) chain[0]; return null; } - public Certificate getRoot() { + @Override + public Certificate getRoot(CertPath certPath) { // Parameter ignored. if (chain.length > 0) return (Certificate) chain[chain.length - 1]; return null; @@ -210,18 +220,14 @@ public class HttpsCertVerifier implement public boolean getRootInCacerts() { try { KeyStore[] caCertsKeyStores = KeyStores.getCAKeyStores(); - return CertificateUtils.inKeyStores((X509Certificate) getRoot(), caCertsKeyStores); + return CertificateUtils.inKeyStores((X509Certificate) getRoot(null), caCertsKeyStores); } catch (Exception e) { } return false; } - public boolean hasSigningIssues() { + @Override + public boolean hasSigningIssues(CertPath certPath) { return false; } - - public boolean noSigningIssues() { - return false; - } - } diff --git a/netx/net/sourceforge/jnlp/security/JNLPAppVerifier.java b/netx/net/sourceforge/jnlp/security/JNLPAppVerifier.java new file mode 100644 --- /dev/null +++ b/netx/net/sourceforge/jnlp/security/JNLPAppVerifier.java @@ -0,0 +1,143 @@ +/* JNLPAppVerifier.java + Copyright (C) 2012 Red Hat, Inc. + +This file is part of IcedTea. + +IcedTea is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 2. + +IcedTea is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IcedTea; see the file COPYING. If not, write to +the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. + */ + +package net.sourceforge.jnlp.security; + +import static net.sourceforge.jnlp.runtime.Translator.R; + +import java.security.cert.CertPath; +import java.util.HashMap; +import java.util.Map; + +import net.sourceforge.jnlp.JNLPFile; +import net.sourceforge.jnlp.LaunchException; +import net.sourceforge.jnlp.security.SecurityDialogs.AccessType; +import net.sourceforge.jnlp.tools.CertInformation; +import net.sourceforge.jnlp.tools.JarCertVerifier; + +public class JNLPAppVerifier implements AppVerifier { + + @Override + public boolean hasAlreadyTrustedPublisher( + HashMap certs, + HashMap signedJars) { + int sumOfSignableEntries = JarCertVerifier.getTotalJarEntries(signedJars); + for (CertInformation certInfo : certs.values()) { + Map certSignedJars = certInfo.getSignedJars(); + + if (JarCertVerifier.getTotalJarEntries(certSignedJars) == sumOfSignableEntries + && certInfo.isPublisherAlreadyTrusted()) { + return true; + } + } + return false; + } + + @Override + public boolean hasRootInCacerts(HashMap certs, + HashMap signedJars) { + int sumOfSignableEntries = JarCertVerifier.getTotalJarEntries(signedJars); + for (CertInformation certInfo : certs.values()) { + Map certSignedJars = certInfo.getSignedJars(); + + if (JarCertVerifier.getTotalJarEntries(certSignedJars) == sumOfSignableEntries + && certInfo.isRootInCacerts()) { + return true; + } + } + return false; + } + + @Override + public boolean isFullySigned(HashMap certs, + HashMap signedJars) { + int sumOfSignableEntries = JarCertVerifier.getTotalJarEntries(signedJars); + for (CertPath cPath : certs.keySet()) { + // If this cert has signed everything, return true + if (hasCompletelySignedApp(certs.get(cPath), sumOfSignableEntries)) { + return true; + } + } + + // No cert found that signed all entries. Return false. + return false; + } + + @Override + public void checkTrustWithUser(JarCertVerifier jcv, JNLPFile file) + throws LaunchException { + + int sumOfSignableEntries = JarCertVerifier.getTotalJarEntries(jcv.getJarSignableEntries()); + for (CertPath cPath : jcv.getCertsList()) { + jcv.setCurrentlyUsedCertPath(cPath); + CertInformation info = jcv.getCertInformation(cPath); + if (hasCompletelySignedApp(info, sumOfSignableEntries)) { + if (info.isPublisherAlreadyTrusted()) { + return; + } + + AccessType dialogType; + if (info.isRootInCacerts() && !info.hasSigningIssues()) { + dialogType = AccessType.VERIFIED; + } else if (info.isRootInCacerts()) { + dialogType = AccessType.SIGNING_ERROR; + } else { + dialogType = AccessType.UNVERIFIED; + } + + boolean wasApproved = SecurityDialogs.showCertWarningDialog( + dialogType, file, jcv); + if (wasApproved) { + return; + } + } + } + + throw new LaunchException(null, null, R("LSFatal"), R("LCLaunching"), + R("LCancelOnUserRequest"), ""); + } + + /** + * Find out if the CertPath with the given info has fully signed the app. + * @param info The information regarding the CertPath in question + * @param sumOfSignableEntries The total number of signable entries in the app. + * @return True if the signer has fully signed this app. + */ + public boolean hasCompletelySignedApp(CertInformation info, int sumOfSignableEntries) { + return JarCertVerifier.getTotalJarEntries(info.getSignedJars()) == sumOfSignableEntries; + } +} diff --git a/netx/net/sourceforge/jnlp/security/MoreInfoPane.java b/netx/net/sourceforge/jnlp/security/MoreInfoPane.java --- a/netx/net/sourceforge/jnlp/security/MoreInfoPane.java +++ b/netx/net/sourceforge/jnlp/security/MoreInfoPane.java @@ -44,7 +44,7 @@ import java.awt.Dimension; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.ArrayList; +import java.util.List; import javax.swing.BorderFactory; import javax.swing.ImageIcon; @@ -73,7 +73,7 @@ public class MoreInfoPane extends Securi * Constructs the GUI components of this panel */ private void addComponents() { - ArrayList details = certVerifier.getDetails(); + List details = certVerifier.getDetails(null); // Show signed JNLP warning if the signed main jar does not have a // signed JNLP file and the launching JNLP file contains special properties diff --git a/netx/net/sourceforge/jnlp/security/PluginAppVerifier.java b/netx/net/sourceforge/jnlp/security/PluginAppVerifier.java new file mode 100644 --- /dev/null +++ b/netx/net/sourceforge/jnlp/security/PluginAppVerifier.java @@ -0,0 +1,224 @@ +/* PluginAppVerifier.java + Copyright (C) 2012 Red Hat, Inc. + +This file is part of IcedTea. + +IcedTea is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 2. + +IcedTea is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IcedTea; see the file COPYING. If not, write to +the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. + */ + +package net.sourceforge.jnlp.security; + +import static net.sourceforge.jnlp.runtime.Translator.R; + +import java.security.cert.CertPath; +import java.util.ArrayList; +import java.util.HashMap; + +import net.sourceforge.jnlp.JNLPFile; +import net.sourceforge.jnlp.LaunchException; +import net.sourceforge.jnlp.security.SecurityDialogs.AccessType; +import net.sourceforge.jnlp.tools.CertInformation; +import net.sourceforge.jnlp.tools.JarCertVerifier; + +public class PluginAppVerifier implements AppVerifier { + + @Override + public boolean hasAlreadyTrustedPublisher( + HashMap certs, + HashMap signedJars) { + + boolean allPublishersTrusted = true; + + for(String jarName : signedJars.keySet()) { + int numbSignableEntries = signedJars.get(jarName); + boolean publisherTrusted = false; + + for (CertInformation certInfo : certs.values()) { + if(certInfo.isSignerOfJar(jarName) + && numbSignableEntries == certInfo.getNumJarEntriesSigned(jarName) + && certInfo.isPublisherAlreadyTrusted()) { + publisherTrusted = true; + break; + } + } + + allPublishersTrusted &= publisherTrusted; + } + return allPublishersTrusted; + } + + @Override + public boolean hasRootInCacerts(HashMap certs, + HashMap signedJars) { + + boolean allRootCAsTrusted = true; + + for(String jarName : signedJars.keySet()) { + int numbSignableEntries = signedJars.get(jarName); + boolean rootCATrusted = false; + + for (CertInformation certInfo : certs.values()) { + if(certInfo.isSignerOfJar(jarName) + && numbSignableEntries == certInfo.getNumJarEntriesSigned(jarName) + && certInfo.isRootInCacerts()) { + rootCATrusted = true; + break; + } + } + + allRootCAsTrusted &= rootCATrusted; + } + return allRootCAsTrusted; + } + + @Override + public boolean isFullySigned(HashMap certs, + HashMap signedJars) { + + boolean isFullySigned = true; + + for(String jarName : signedJars.keySet()) { + int numbSignableEntries = signedJars.get(jarName); + boolean isSigned = false; + + for (CertInformation certInfo : certs.values()) { + if(certInfo.isSignerOfJar(jarName) + && numbSignableEntries == certInfo.getNumJarEntriesSigned(jarName)) { + isSigned = true; + break; + } + } + + isFullySigned &= isSigned; + } + + return isFullySigned; + } + + @Override + public void checkTrustWithUser(JarCertVerifier jcv, JNLPFile file) + throws LaunchException { + ArrayList certPaths = buildCertPathsList(jcv); + ArrayList alreadyApprovedByUser = new ArrayList(); + for (String jarName : jcv.getJarSignableEntries().keySet()) { + boolean trustFoundOrApproved = false; + for (CertPath cPathApproved : alreadyApprovedByUser) { + jcv.setCurrentlyUsedCertPath(cPathApproved); + CertInformation info = jcv.getCertInformation(cPathApproved); + if (info.isSignerOfJar(jarName) + && alreadyApprovedByUser.contains(cPathApproved)) { + trustFoundOrApproved = true; + break; + } + } + + if (trustFoundOrApproved) { + continue; + } + + for (CertPath cPath : certPaths) { + jcv.setCurrentlyUsedCertPath(cPath); + CertInformation info = jcv.getCertInformation(cPath); + if (info.isSignerOfJar(jarName)) { + if (info.isPublisherAlreadyTrusted()) { + trustFoundOrApproved = true; + alreadyApprovedByUser.add(cPath); + break; + } + + AccessType dialogType; + if (info.isRootInCacerts() && !info.hasSigningIssues()) { + dialogType = AccessType.VERIFIED; + } else if (info.isRootInCacerts()) { + dialogType = AccessType.SIGNING_ERROR; + } else { + dialogType = AccessType.UNVERIFIED; + } + + boolean wasApproved = SecurityDialogs.showCertWarningDialog( + dialogType, file, jcv); + if (wasApproved) { + alreadyApprovedByUser.add(cPath); + trustFoundOrApproved = true; + break; + } + } + } + if (!trustFoundOrApproved) { + throw new LaunchException(null, null, R("LSFatal"), + R("LCLaunching"), R("LCancelOnUserRequest"), ""); + } + } + } + + /** + * Build a list of all the CertPaths that were detected in the provided + * JCV, placing them in the most trusted possible order. + * @param jcv The verifier containing the CertPaths to examine. + * @return A list of CertPaths sorted in the following order: Signers with + * 1. Already trusted publishers + * 2. Roots in the CA store and have no signing issues + * 3. Roots in the CA store but have signing issues + * 4. Everything else + */ + public ArrayList buildCertPathsList(JarCertVerifier jcv) { + ArrayList certPathsList = jcv.getCertsList(); + ArrayList returnList = new ArrayList(); + + for (CertPath cPath : certPathsList) { + if (!returnList.contains(cPath) + && jcv.getCertInformation(cPath).isPublisherAlreadyTrusted()) + returnList.add(cPath); + } + + for (CertPath cPath : certPathsList) { + if (!returnList.contains(cPath) + && jcv.getCertInformation(cPath).isRootInCacerts() + && !jcv.getCertInformation(cPath).hasSigningIssues()) + returnList.add(cPath); + } + + for (CertPath cPath : certPathsList) { + if (!returnList.contains(cPath) + && jcv.getCertInformation(cPath).isRootInCacerts() + && jcv.getCertInformation(cPath).hasSigningIssues()) + returnList.add(cPath); + } + + for (CertPath cPath : certPathsList) { + if (!returnList.contains(cPath)) + returnList.add(cPath); + } + + return returnList; + } +} diff --git a/netx/net/sourceforge/jnlp/tools/CertInformation.java b/netx/net/sourceforge/jnlp/tools/CertInformation.java new file mode 100644 --- /dev/null +++ b/netx/net/sourceforge/jnlp/tools/CertInformation.java @@ -0,0 +1,292 @@ +/* CertInformation.java + Copyright (C) 2012 Red Hat, Inc. + +This file is part of IcedTea. + +IcedTea is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 2. + +IcedTea is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IcedTea; see the file COPYING. If not, write to +the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. + */ + +package net.sourceforge.jnlp.tools; + +import static net.sourceforge.jnlp.runtime.Translator.R; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.sourceforge.jnlp.runtime.JNLPRuntime; + +/** + * Maintains information about a CertPath that has signed at least one of the + * entries provided by a jar of the app. + */ +public class CertInformation { + + private boolean hasExpiredCert = false; + private boolean hasExpiringCert = false; + + private boolean isNotYetValidCert = false; + + /* Code signer properties of the certificate. */ + private boolean hasBadKeyUsage = false; + private boolean hasBadExtendedKeyUsage = false; + private boolean hasBadNetscapeCertType = false; + + private boolean alreadyTrustPublisher = false; + private boolean rootInCacerts = false; + + static enum Detail { + TRUSTED (R("STrustedCertificate")), + UNTRUSTED (R("SUntrustedCertificate")), + RUN_WITHOUT_RESTRICTIONS(R("SRunWithoutRestrictions")), + EXPIRED (R("SHasExpiredCert")), + EXPIRING (R("SHasExpiringCert")), + NOT_YET_VALID (R("SNotYetValidCert")), + BAD_KEY_USAGE (R("SBadKeyUsage")), + BAT_EXTENDED_KEY_USAGE (R("SBadExtendedKeyUsage")), + BAD_NETSCAPE_CERT_TYPE (R("SBadNetscapeCertType")); + + private final String message; + Detail(String issue) { + message = issue; + } + + public String message() { + return message; + } + } + + private EnumSet details = EnumSet.noneOf(Detail.class); + + /** The jars and their number of entries this cert has signed. */ + private HashMap signedJars = new HashMap(); + + /** + * Return if there are signing issues with this certificate. + * @return true if there are any issues with expiry, validity or bad key usage. + */ + public boolean hasSigningIssues() { + return hasExpiredCert || isNotYetValidCert || hasBadKeyUsage + || hasBadExtendedKeyUsage || hasBadNetscapeCertType; + } + + /** + * Return whether or not the publisher is already trusted. + * + * @return True if the publisher is trusted already. + */ + public boolean isPublisherAlreadyTrusted() { + return alreadyTrustPublisher; + } + + /** + * Set whether or not the publisher is already trusted. + * + */ + public void setAlreadyTrustPublisher() { + alreadyTrustPublisher = true; + } + + /** + * Return whether or not the root is in the list of trusted CA certificates. + * + * @return True if the root is in the list of CA certificates. + */ + public boolean isRootInCacerts() { + return rootInCacerts; + } + + /** + * Set that this cert's root CA is to be trusted. + */ + public void setRootInCacerts() { + rootInCacerts = true; + details.add(Detail.TRUSTED); + } + + /** + * Resets any trust of the root and publisher. Also removes unnecessary + * details from the list of issues. + */ + public void resetForReverification() { + alreadyTrustPublisher = false; + rootInCacerts = false; + removeFromDetails(Detail.UNTRUSTED); + removeFromDetails(Detail.TRUSTED); + } + /** + * Check if this cert is the signer of a jar. + * @param jarName The absolute path of the jar this certificate has signed. + * @return true if this cert has signed the jar found at jarName. + */ + public boolean isSignerOfJar(String jarName) { + return signedJars.containsKey(jarName); + } + + /** + * Add a jar to the list of jars this certificate has signed along with the + * number of entries it has signed in the jar. + * + * @param jarName The absolute path of the jar this certificate has signed. + * @param signedEntriesCount The number of entries this cert has signed in jarName. + */ + public void setNumJarEntriesSigned(String jarName, int signedEntriesCount) { + if (signedJars.containsKey(jarName)) { + if (JNLPRuntime.isDebug()) + System.err.println("WARNING: A jar that has already been " + + "verified is being yet again verified: " + jarName); + } else { + signedJars.put(jarName, signedEntriesCount); + } + } + + /** + * Find the number of entries this cert has signed in the specified jar. + * @param jarName The absolute path of the jar this certificate has signed. + * @return The number of entries this cert has signed in jarName. + */ + public int getNumJarEntriesSigned(String jarName) { + return signedJars.get(jarName); + } + + /** + * Get all the jars this cert has signed along with the number of entries + * in each jar. + * @return + */ + public Map getSignedJars() { + return signedJars; + } + + /** + * Get the details regarding issue(s) with this certificate. + * + * @return A list of all the details/issues with this app. + */ + public List getDetailsAsStrings() { + List detailsToStr = new ArrayList(); + for (Detail issue : details) { + detailsToStr.add(issue.message()); + } + return detailsToStr; + } + + /** + * Remove an issue from the list of details of issues with this certificate. + * List is unchanged if detail was not present. + * + * @param detail The issue to be removed regarding this certificate. + */ + private void removeFromDetails(Detail detail) { + details.remove(detail); + } + + /** + * Set that this cert is expired and add this issue to the list of details. + */ + public void setHasExpiredCert() { + hasExpiredCert = true; + details.add(Detail.RUN_WITHOUT_RESTRICTIONS); + details.add(Detail.EXPIRED); + } + + /** + * Set that this cert is expiring within 6 months and add this issue to + * the list of details. + */ + public void setHasExpiringCert() { + hasExpiringCert = true; + details.add(Detail.RUN_WITHOUT_RESTRICTIONS); + details.add(Detail.EXPIRING); + } + + /** + * Get whether or not this cert will expire within 6 months. + * @return true if the cert will be expired after 6 months. + */ + public boolean hasExpiringCert() { + return hasExpiringCert; + } + + /** + * Set that this cert is not yet valid + * and add this issue to the list of details. + */ + public void setNotYetValidCert() { + isNotYetValidCert = true; + details.add(Detail.RUN_WITHOUT_RESTRICTIONS); + details.add(Detail.NOT_YET_VALID); + } + + + /** + * Set that this cert has bad key usage + * and add this issue to the list of details. + */ + public void setBadKeyUsage() { + hasBadKeyUsage = true; + details.add(Detail.RUN_WITHOUT_RESTRICTIONS); + details.add(Detail.BAD_KEY_USAGE); + } + + + /** + * Set that this cert has bad extended key usage + * and add this issue to the list of details. + */ + public void setBadExtendedKeyUsage() { + hasBadExtendedKeyUsage = true; + details.add(Detail.RUN_WITHOUT_RESTRICTIONS); + details.add(Detail.BAT_EXTENDED_KEY_USAGE); + } + + + /** + * Set that this cert has a bad netscape cert type + * and add this issue to the list of details. + */ + public void setBadNetscapeCertType() { + hasBadNetscapeCertType = true; + details.add(Detail.RUN_WITHOUT_RESTRICTIONS); + details.add(Detail.BAD_NETSCAPE_CERT_TYPE); + } + + /** + * Set that this cert and all of its CAs are untrusted so far. + */ + public void setUntrusted() { + details.add(Detail.UNTRUSTED); + + } +} diff --git a/netx/net/sourceforge/jnlp/tools/JarCertVerifier.java b/netx/net/sourceforge/jnlp/tools/JarCertVerifier.java --- a/netx/net/sourceforge/jnlp/tools/JarCertVerifier.java +++ b/netx/net/sourceforge/jnlp/tools/JarCertVerifier.java @@ -25,8 +25,6 @@ package net.sourceforge.jnlp.tools; -import static net.sourceforge.jnlp.runtime.Translator.R; - import java.io.*; import java.util.*; import java.util.jar.*; @@ -39,11 +37,13 @@ import sun.security.util.*; import net.sourceforge.jnlp.*; import net.sourceforge.jnlp.cache.*; +import net.sourceforge.jnlp.runtime.JNLPRuntime; import net.sourceforge.jnlp.security.*; /** - *

The jar certificate verifier utility. - * + *

+ * The jar certificate verifier utility. + * * @author Roland Schemers * @author Jan Luehe */ @@ -55,53 +55,39 @@ public class JarCertVerifier implements // prefix for new signature-related files in META-INF directory private static final String SIG_PREFIX = META_INF + "SIG-"; - private static final long SIX_MONTHS = 180 * 24 * 60 * 60 * 1000L; //milliseconds + private static final long SIX_MONTHS = 180 * 24 * 60 * 60 * 1000L; // milliseconds - static enum verifyResult { + static enum VerifyResult { UNSIGNED, SIGNED_OK, SIGNED_NOT_OK } - // signer's certificate chain (when composing) - X509Certificate[] certChain; + /** All of the jar files that were verified for signing */ + private ArrayList verifiedJars = new ArrayList(); - boolean verbose = false; // verbose output when signing/verifying - boolean showcerts = false; // show certs when verifying + /** All of the jar files that were not verified */ + private ArrayList unverifiedJars = new ArrayList(); - private boolean hasExpiredCert = false; - private boolean hasExpiringCert = false; - private boolean notYetValidCert = false; + /** The certificates used for jar verification linked to their respective information */ + private HashMap certs = new HashMap(); - private boolean badKeyUsage = false; - private boolean badExtendedKeyUsage = false; - private boolean badNetscapeCertType = false; + /** Temporary cert path hack to be used to keep track of which one a UI dialog is using */ + private CertPath currentlyUsed; - private boolean alreadyTrustPublisher = false; - private boolean rootInCacerts = false; + /** Absolute location to jars and the number of entries which are possibly signable */ + private HashMap jarSignableEntries = new HashMap(); + + /** The application verifier to use by this instance */ + private AppVerifier appVerifier; /** - * The single certPath used in this JarSiging. We're only keeping - * track of one here, since in practice there's only one signer - * for a JNLP Application. + * Create a new jar certificate verifier utility that uses the provided verifier for its strategy pattern. + * + * @param verifier + * The application verifier to be used by the new instance. */ - private CertPath certPath = null; - - private boolean noSigningIssues = true; - - private boolean anyJarsSigned = false; - - /** all of the jar files that were verified */ - private ArrayList verifiedJars = null; - - /** all of the jar files that were not verified */ - private ArrayList unverifiedJars = null; - - /** the certificates used for jar verification */ - private HashMap certs = new HashMap(); - - /** details of this signing */ - private ArrayList details = new ArrayList(); - - private int totalSignableEntries = 0; + public JarCertVerifier(AppVerifier verifier) { + appVerifier = verifier; + } /** Whether a signable entry was found within jars (jars with content more than just META-INF/*) */ private boolean triviallySigned = false; @@ -113,88 +99,107 @@ public class JarCertVerifier implements return triviallySigned; } - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#getAlreadyTrustPublisher() - */ public boolean getAlreadyTrustPublisher() { - return alreadyTrustPublisher; + boolean allPublishersTrusted = appVerifier.hasAlreadyTrustedPublisher( + certs, jarSignableEntries); + if (JNLPRuntime.isDebug()) { + System.out.println("App already has trusted publisher: " + + allPublishersTrusted); + } + return allPublishersTrusted; } - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#getRootInCacerts() - */ public boolean getRootInCacerts() { - return rootInCacerts; + boolean allRootCAsTrusted = appVerifier.hasRootInCacerts(certs, + jarSignableEntries); + if (JNLPRuntime.isDebug()) { + System.out.println("App has trusted root CA: " + allRootCAsTrusted); + } + return allRootCAsTrusted; } - public CertPath getCertPath() { - return certPath; + public CertPath getCertPath(CertPath cPath) { // Parameter ignored. + return currentlyUsed; } - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#hasSigningIssues() - */ - public boolean hasSigningIssues() { - return hasExpiredCert || notYetValidCert || badKeyUsage - || badExtendedKeyUsage || badNetscapeCertType; + public boolean hasSigningIssues(CertPath certPath) { + return certs.get(certPath).hasSigningIssues(); } - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#noSigningIssues() - */ - public boolean noSigningIssues() { - return noSigningIssues; + public List getDetails(CertPath certPath) { + if (certPath != null) { + currentlyUsed = certPath; + } + return certs.get(currentlyUsed).getDetailsAsStrings(); } - public boolean anyJarsSigned() { - return anyJarsSigned; - } - - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#getDetails() + /** + * Get a list of the cert paths of all signers across the app. + * + * @return ArrayList of CertPath vars representing each of the signers present on any jar. */ - public ArrayList getDetails() { - return details; - } - - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#getCerts() - */ - public ArrayList getCerts() { + public ArrayList getCertsList() { return new ArrayList(certs.keySet()); } /** - * Returns whether or not all entries have a common signer. - * - * It is possible to create jars where only some entries are signed. In - * such cases, we should not prompt the user to accept anything, as the whole - * application must be treated as unsigned. This method should be called by a - * caller before it is about to ask the user to accept a cert and determine - * whether the application is trusted or not. - * - * @return Whether or not all entries have a common signer + * Find the information the specified cert path has with respect to this application. + * + * @return All the information the path has with this app. */ - public boolean isFullySignedByASingleCert() { + public CertInformation getCertInformation(CertPath cPath) { + return certs.get(cPath); + } + /** + * Returns whether or not the app is considered completely signed. + * + * An app using a JNLP is considered signed if all of the entries of its jars are signed by at least one common signer. + * + * An applet on the other hand only needs to have each individual jar be fully signed by a signer. The signers can differ between jars. + * + * @return Whether or not the app is considered signed. + */ + // FIXME: Change javadoc once applets do not need entire jars signed. + public boolean isFullySigned() { if (triviallySigned) return true; - - for (CertPath cPath : certs.keySet()) { - // If this cert has signed everything, return true - if (certs.get(cPath) == totalSignableEntries) - return true; + boolean fullySigned = appVerifier.isFullySigned(certs, + jarSignableEntries); + if (JNLPRuntime.isDebug()) { + System.out.println("App already has trusted publisher: " + + fullySigned); } - - // No cert found that signed all entries. Return false. - return false; + return fullySigned; } - public void verifyJars(List jars, ResourceTracker tracker) + /** + * Update the verifier to consider new jars when verifying. + * + * @param jars + * List of new jars to be verified. + * @param tracker + * Resource tracker used to obtain the the jars from cache + * @throws Exception + * Caused by issues with obtaining the jars' entries or interacting with the tracker. + */ + public void add(List jars, ResourceTracker tracker) throws Exception { + verifyJars(jars, tracker); + } - verifiedJars = new ArrayList(); - unverifiedJars = new ArrayList(); + /** + * Verify the jars provided and update the state of this instance to match the new information. + * + * @param jars + * List of new jars to be verified. + * @param tracker + * Resource tracker used to obtain the the jars from cache + * @throws Exception + * Caused by issues with obtaining the jars' entries or interacting with the tracker. + */ + private void verifyJars(List jars, ResourceTracker tracker) + throws Exception { for (JARDesc jar : jars) { @@ -209,17 +214,22 @@ public class JarCertVerifier implements } String localFile = jarFile.getAbsolutePath(); - verifyResult result = verifyJar(localFile); + if (verifiedJars.contains(localFile) + || unverifiedJars.contains(localFile)) { + continue; + } + + VerifyResult result = verifyJar(localFile); triviallySigned = false; - if (result == verifyResult.UNSIGNED) { + if (result == VerifyResult.UNSIGNED) { unverifiedJars.add(localFile); - } else if (result == verifyResult.SIGNED_NOT_OK) { - noSigningIssues = false; + } else if (result == VerifyResult.SIGNED_NOT_OK) { verifiedJars.add(localFile); - } else if (result == verifyResult.SIGNED_OK) { + } else if (result == VerifyResult.SIGNED_OK) { verifiedJars.add(localFile); - triviallySigned = totalSignableEntries <= 0 && certs.size() <= 0; + triviallySigned = getTotalJarEntries(jarSignableEntries) <= 0 + && certs.size() <= 0; } } catch (Exception e) { // We may catch exceptions from using verifyJar() @@ -228,26 +238,20 @@ public class JarCertVerifier implements } } - //we really only want the first certPath - for (CertPath cPath : certs.keySet()) { - - if (certs.get(cPath) != totalSignableEntries) - continue; - else - certPath = cPath; - - // check if the certs added above are in the trusted path - checkTrustedCerts(); - - if (alreadyTrustPublisher || rootInCacerts) - break; - } - + for (CertPath certPath : certs.keySet()) + checkTrustedCerts(certPath); } - private verifyResult verifyJar(String jarName) throws Exception { - boolean anySigned = false; - boolean hasUnsignedEntry = false; + /** + * Checks through all the jar entries of jarName for signers, storing all the common ones in the certs hash map. + * + * @param jarName + * The absolute path to the jar file. + * @return The return of {@link JarCertVerifier#verifyJarEntryCerts} using the entries found in the jar located at jarName. + * @throws Exception + * Will be thrown if there are any problems with the jar. + */ + private VerifyResult verifyJar(String jarName) throws Exception { JarFile jarFile = null; try { @@ -262,10 +266,9 @@ public class JarCertVerifier implements InputStream is = jarFile.getInputStream(je); try { - int n; - while ((n = is.read(buffer, 0, buffer.length)) != -1) { + while (is.read(buffer, 0, buffer.length) != -1) { // we just read. this will throw a SecurityException - // if a signature/digest check fails. + // if a signature/digest check fails. } } finally { if (is != null) { @@ -273,95 +276,9 @@ public class JarCertVerifier implements } } } + return verifyJarEntryCerts(jarName, jarFile.getManifest() != null, + entriesVec); - if (jarFile.getManifest() != null) { - if (verbose) - System.out.println(); - - long now = System.currentTimeMillis(); - - for (JarEntry je : entriesVec) { - String name = je.getName(); - CodeSigner[] signers = je.getCodeSigners(); - boolean isSigned = (signers != null); - anySigned |= isSigned; - - boolean shouldHaveSignature = !je.isDirectory() - && !isMetaInfFile(name); - - hasUnsignedEntry |= shouldHaveSignature && !isSigned; - - if (shouldHaveSignature) - totalSignableEntries++; - - if (shouldHaveSignature && isSigned) { - for (int i = 0; i < signers.length; i++) { - CertPath certPath = signers[i].getSignerCertPath(); - if (!certs.containsKey(certPath)) - certs.put(certPath, 1); - else - certs.put(certPath, certs.get(certPath) + 1); - - Certificate cert = signers[i].getSignerCertPath() - .getCertificates().get(0); - if (cert instanceof X509Certificate) { - checkCertUsage((X509Certificate) cert, null); - if (!showcerts) { - long notBefore = ((X509Certificate) cert) - .getNotBefore().getTime(); - long notAfter = ((X509Certificate) cert) - .getNotAfter().getTime(); - - if (now < notBefore) { - notYetValidCert = true; - } - - if (notAfter < now) { - hasExpiredCert = true; - } else if (notAfter < now + SIX_MONTHS) { - hasExpiringCert = true; - } - } - } - } - } - } //while e has more elements - } else { //if man not null - - // Else increment totalEntries by 1 so that unsigned jars with - // no manifests can't sneak in - totalSignableEntries++; - } - - //Alert the user if any of the following are true. - if (!anySigned) { - return verifyResult.UNSIGNED; - } else { - anyJarsSigned = true; - - //warnings - if (hasUnsignedEntry || hasExpiredCert || hasExpiringCert || - badKeyUsage || badExtendedKeyUsage || badNetscapeCertType || - notYetValidCert) { - - addToDetails(R("SRunWithoutRestrictions")); - - if (badKeyUsage) - addToDetails(R("SBadKeyUsage")); - if (badExtendedKeyUsage) - addToDetails(R("SBadExtendedKeyUsage")); - if (badNetscapeCertType) - addToDetails(R("SBadNetscapeCertType")); - if (hasUnsignedEntry) - addToDetails(R("SHasUnsignedEntry")); - if (hasExpiredCert) - addToDetails(R("SHasExpiredCert")); - if (hasExpiringCert) - addToDetails(R("SHasExpiringCert")); - if (notYetValidCert) - addToDetails(R("SNotYetValidCert")); - } - } } catch (Exception e) { e.printStackTrace(); throw e; @@ -370,51 +287,175 @@ public class JarCertVerifier implements jarFile.close(); } } - - //anySigned does not guarantee that all files were signed. - return (anySigned && !(hasUnsignedEntry || hasExpiredCert - || badKeyUsage || badExtendedKeyUsage || badNetscapeCertType || notYetValidCert)) ? verifyResult.SIGNED_OK : verifyResult.SIGNED_NOT_OK; } /** - * Checks the user's trusted.certs file and the cacerts file to see - * if a publisher's and/or CA's certificate exists there. + * Checks through all the jar entries for signers, storing all the common ones in the certs hash map. + * + * @param jarName + * The absolute path to the jar file. + * @param jarHasManifest + * Whether or not the associated jar has a manifest. + * @param entries + * The list of entries in the associated jar. + * @return If there is at least one signable entry that is not signed by a common signer, return UNSIGNED. Otherwise every signable entry is signed by at least one common signer. If the signer has no issues, return SIGNED_OK. If there are any signing issues, return SIGNED_NOT_OK. + * @throws Exception + * Will be thrown if there are issues with entries. */ - private void checkTrustedCerts() throws Exception { - if (certPath != null) { - try { - X509Certificate publisher = (X509Certificate) getPublisher(); - KeyStore[] certKeyStores = KeyStores.getCertKeyStores(); - alreadyTrustPublisher = CertificateUtils.inKeyStores(publisher, certKeyStores); - X509Certificate root = (X509Certificate) getRoot(); - KeyStore[] caKeyStores = KeyStores.getCAKeyStores(); - // Check entire cert path for a trusted CA - for (Certificate c : certPath.getCertificates()) { - if ((rootInCacerts = CertificateUtils.inKeyStores( - (X509Certificate) c, caKeyStores))) { - break; + VerifyResult verifyJarEntryCerts(String jarName, boolean jarHasManifest, + Vector entries) throws Exception { + // Contains number of entries the cert with this CertPath has signed. + HashMap jarSignCount = new HashMap(); + int numSignableEntriesInJar = 0; + + // Record current time just before checking the jar begins. + long now = System.currentTimeMillis(); + if (jarHasManifest) { + + for (JarEntry je : entries) { + String name = je.getName(); + CodeSigner[] signers = je.getCodeSigners(); + boolean isSigned = (signers != null); + + boolean shouldHaveSignature = !je.isDirectory() + && !isMetaInfFile(name); + + if (shouldHaveSignature) { + numSignableEntriesInJar++; + } + + if (shouldHaveSignature && isSigned) { + for (int i = 0; i < signers.length; i++) { + CertPath certPath = signers[i].getSignerCertPath(); + + if (!jarSignCount.containsKey(certPath)) + jarSignCount.put(certPath, 1); + else + jarSignCount.put(certPath, + jarSignCount.get(certPath) + 1); } } - } catch (Exception e) { - // TODO: Warn user about not being able to - // look through their cacerts/trusted.certs - // file depending on exception. - throw e; + } // while e has more elements + } else { // if manifest is null + + // Else increment total entries by 1 so that unsigned jars with + // no manifests can't sneak in + numSignableEntriesInJar++; + } + + jarSignableEntries.put(jarName, numSignableEntriesInJar); + + // Find all signers that have signed every signable entry in this jar. + boolean allEntriesSignedBySingleCert = false; + for (CertPath certPath : jarSignCount.keySet()) { + if (jarSignCount.get(certPath) == numSignableEntriesInJar) { + allEntriesSignedBySingleCert = true; + + boolean wasPreviouslyVerified = certs.containsKey(certPath); + if (!wasPreviouslyVerified) + certs.put(certPath, new CertInformation()); + + CertInformation certInfo = certs.get(certPath); + if (wasPreviouslyVerified) + certInfo.resetForReverification(); + + certInfo.setNumJarEntriesSigned(jarName, + numSignableEntriesInJar); + + Certificate cert = certPath.getCertificates().get(0); + if (cert instanceof X509Certificate) { + checkCertUsage(certPath, (X509Certificate) cert, null); + long notBefore = ((X509Certificate) cert).getNotBefore().getTime(); + long notAfter = ((X509Certificate) cert).getNotAfter().getTime(); + if (now < notBefore) { + certInfo.setNotYetValidCert(); + } + + if (notAfter < now) { + certInfo.setHasExpiredCert(); + } else if (notAfter < now + SIX_MONTHS) { + certInfo.setHasExpiringCert(); + } + } } + } - if (!rootInCacerts) - addToDetails(R("SUntrustedCertificate")); - else - addToDetails(R("STrustedCertificate")); + // Every signable entry of this jar needs to be signed by at least + // one signer for the jar to be considered successfully signed. + VerifyResult result = null; + if (allEntriesSignedBySingleCert) { + + // We need to find at least one signer without any issues. + for (CertPath entryCertPath : jarSignCount.keySet()) { + if (certs.containsKey(entryCertPath) + && !hasSigningIssues(entryCertPath)) { + result = VerifyResult.SIGNED_OK; + break; + } + } + if (result == null) { + // All signers had issues + result = VerifyResult.SIGNED_NOT_OK; + } + } else { + result = VerifyResult.UNSIGNED; } + + if (JNLPRuntime.isDebug()) { + System.out.println("Jar found at " + jarName + + "has been verified as " + result); + } + return result; } - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#getPublisher() + /** + * Checks the user's trusted.certs file and the cacerts file to see if a + * publisher's and/or CA's certificate exists there. + * + * @param certPath + * The cert path of the signer being checked for trust. */ - public Certificate getPublisher() { - if (certPath != null) { - List certList = certPath.getCertificates(); + private void checkTrustedCerts(CertPath certPath) throws Exception { + CertInformation info = certs.get(certPath); + try { + X509Certificate publisher = (X509Certificate) getPublisher(certPath); + KeyStore[] certKeyStores = KeyStores.getCertKeyStores(); + if (CertificateUtils.inKeyStores(publisher, certKeyStores)) + info.setAlreadyTrustPublisher(); + KeyStore[] caKeyStores = KeyStores.getCAKeyStores(); + // Check entire cert path for a trusted CA + for (Certificate c : certPath.getCertificates()) { + if (CertificateUtils.inKeyStores((X509Certificate) c, + caKeyStores)) { + info.setRootInCacerts(); + return; + } + } + } catch (Exception e) { + // TODO: Warn user about not being able to + // look through their cacerts/trusted.certs + // file depending on exception. + if (JNLPRuntime.isDebug()) { + System.out.println("WARNING: Unable to read through cert store files."); + } + throw e; + } + + // Otherwise a parent cert was not found to be trusted. + info.setUntrusted(); + } + + public void setCurrentlyUsedCertPath(CertPath cPath) { + currentlyUsed = cPath; + } + + public Certificate getPublisher(CertPath cPath) { + if (cPath != null) { + currentlyUsed = cPath; + } + if (currentlyUsed != null) { + List certList = currentlyUsed + .getCertificates(); if (certList.size() > 0) { return certList.get(0); } else { @@ -425,12 +466,13 @@ public class JarCertVerifier implements } } - /* (non-Javadoc) - * @see net.sourceforge.jnlp.tools.CertVerifier2#getRoot() - */ - public Certificate getRoot() { - if (certPath != null) { - List certList = certPath.getCertificates(); + public Certificate getRoot(CertPath cPath) { + if (cPath != null) { + currentlyUsed = cPath; + } + if (currentlyUsed != null) { + List certList = currentlyUsed + .getCertificates(); if (certList.size() > 0) { return certList.get(certList.size() - 1); } else { @@ -441,20 +483,10 @@ public class JarCertVerifier implements } } - private void addToDetails(String detail) { - if (!details.contains(detail)) - details.add(detail); - } - /** * Returns whether a file is in META-INF, and thus does not require signing. - * - * Signature-related files under META-INF include: - * . META-INF/MANIFEST.MF - * . META-INF/SIG-* - * . META-INF/*.SF - * . META-INF/*.DSA - * . META-INF/*.RSA + * + * Signature-related files under META-INF include: . META-INF/MANIFEST.MF . META-INF/SIG-* . META-INF/*.SF . META-INF/*.DSA . META-INF/*.RSA */ static boolean isMetaInfFile(String name) { String ucName = name.toUpperCase(); @@ -463,15 +495,16 @@ public class JarCertVerifier implements /** * Check if userCert is designed to be a code signer - * @param userCert the certificate to be examined - * @param bad 3 booleans to show if the KeyUsage, ExtendedKeyUsage, - * NetscapeCertType has codeSigning flag turned on. - * If null, the class field badKeyUsage, badExtendedKeyUsage, - * badNetscapeCertType will be set. - * - * Required for verifyJar() + * + * @param userCert + * the certificate to be examined + * @param bad + * 3 booleans to show if the KeyUsage, ExtendedKeyUsage, NetscapeCertType has codeSigning flag turned on. If null, the class field badKeyUsage, badExtendedKeyUsage, badNetscapeCertType will be set. + * + * Required for verifyJar() */ - void checkCertUsage(X509Certificate userCert, boolean[] bad) { + void checkCertUsage(CertPath certPath, X509Certificate userCert, + boolean[] bad) { // Can act as a signer? // 1. if KeyUsage, then [0] should be true @@ -489,7 +522,7 @@ public class JarCertVerifier implements if (bad != null) { bad[0] = true; } else { - badKeyUsage = true; + certs.get(certPath).setBadKeyUsage(); } } } @@ -502,7 +535,7 @@ public class JarCertVerifier implements if (bad != null) { bad[1] = true; } else { - badExtendedKeyUsage = true; + certs.get(certPath).setBadExtendedKeyUsage(); } } } @@ -512,24 +545,24 @@ public class JarCertVerifier implements try { // OID_NETSCAPE_CERT_TYPE - byte[] netscapeEx = userCert.getExtensionValue - ("2.16.840.1.113730.1.1"); + byte[] netscapeEx = userCert + .getExtensionValue("2.16.840.1.113730.1.1"); if (netscapeEx != null) { DerInputStream in = new DerInputStream(netscapeEx); byte[] encoded = in.getOctetString(); encoded = new DerValue(encoded).getUnalignedBitString() .toByteArray(); - NetscapeCertTypeExtension extn = - new NetscapeCertTypeExtension(encoded); + NetscapeCertTypeExtension extn = new NetscapeCertTypeExtension( + encoded); - Boolean val = (Boolean) extn.get( - NetscapeCertTypeExtension.OBJECT_SIGNING); + Boolean val = (Boolean) extn + .get(NetscapeCertTypeExtension.OBJECT_SIGNING); if (!val) { if (bad != null) { bad[2] = true; } else { - badNetscapeCertType = true; + certs.get(certPath).setBadNetscapeCertType(); } } } @@ -540,11 +573,31 @@ public class JarCertVerifier implements /** * Returns if all jars are signed. - * + * * @return True if all jars are signed, false if there are one or more unsigned jars */ public boolean allJarsSigned() { return this.unverifiedJars.size() == 0; } + public void checkTrustWithUser(JNLPFile file) throws LaunchException { + appVerifier.checkTrustWithUser(this, file); + } + + public Map getJarSignableEntries() { + return jarSignableEntries; + } + + /** + * Get the total number of entries in the provided map. + * + * @return The number of entries. + */ + public static int getTotalJarEntries(Map map) { + int sum = 0; + for (int value : map.values()) { + sum += value; + } + return sum; + } } diff --git a/tests/netx/unit/net/sourceforge/jnlp/tools/VerifyJarEntryCertsTest.java b/tests/netx/unit/net/sourceforge/jnlp/tools/VerifyJarEntryCertsTest.java new file mode 100644 --- /dev/null +++ b/tests/netx/unit/net/sourceforge/jnlp/tools/VerifyJarEntryCertsTest.java @@ -0,0 +1,510 @@ +/* CertVerifier.java + Copyright (C) 2012 Red Hat, Inc. + +This file is part of IcedTea. + +IcedTea is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 2. + +IcedTea is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IcedTea; see the file COPYING. If not, write to +the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. +*/ + +package net.sourceforge.jnlp.tools; + +import static net.sourceforge.jnlp.runtime.Translator.R; + +import java.security.CodeSigner; +import java.util.Date; +import java.util.List; +import java.util.Vector; +import java.util.jar.JarEntry; + +import net.sourceforge.jnlp.JARDesc; +import net.sourceforge.jnlp.tools.JarCertVerifier.VerifyResult; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.Assert; + +public class VerifyJarEntryCertsTest { + + class JarCertVerifierEntry extends JarEntry { + CodeSigner[] signers; + public JarCertVerifierEntry(String name, CodeSigner[] codesigners) { + super(name); + signers = codesigners; + } + + public JarCertVerifierEntry(String name) { + this(name, null); + } + + public CodeSigner[] getCodeSigners() { + return signers == null ? null : signers.clone(); + } + } + + // Empty list to be used with JarCertVerifier constructor. + private static final List emptyJARDescList = new Vector(); + + private static final String DNPARTIAL = ", OU=JarCertVerifier Unit Test, O=IcedTea, L=Toronto, ST=Ontario, C=CA"; + private static CodeSigner alphaSigner, betaSigner, charlieSigner, + expiredSigner, expiringSigner, notYetValidSigner, expiringAndNotYetValidSigner; + + @BeforeClass + public static void setUp() throws Exception { + Date currentDate = new Date(); + Date pastDate = new Date(currentDate.getTime() - (1000L * 24L * 60L * 60L) - 1000L); // 1 day and 1 second in the past + Date futureDate = new Date(currentDate.getTime() + (1000L * 24L * 60L * 60L)); // 1 day in the future + alphaSigner = CodeSignerCreator.getOneCodeSigner("CN=Alpha Signer" + DNPARTIAL, currentDate, 365); + betaSigner = CodeSignerCreator.getOneCodeSigner("CN=Beta Signer" + DNPARTIAL, currentDate, 365); + charlieSigner = CodeSignerCreator.getOneCodeSigner("CN=Charlie Signer" + DNPARTIAL, currentDate, 365); + expiredSigner = CodeSignerCreator.getOneCodeSigner("CN=Expired Signer" + DNPARTIAL, pastDate, 1); + expiringSigner = CodeSignerCreator.getOneCodeSigner("CN=Expiring Signer" + DNPARTIAL, currentDate, 1); + notYetValidSigner = CodeSignerCreator.getOneCodeSigner("CN=Not Yet Valid Signer" + DNPARTIAL, futureDate, 365); + expiringAndNotYetValidSigner = CodeSignerCreator.getOneCodeSigner("CN=Expiring and Not Yet Valid Signer" + DNPARTIAL, futureDate, 3); + } + + @Test + public void testNoManifest() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + VerifyResult result = jcv.verifyJarEntryCerts("",false, null); + + Assert.assertEquals("No manifest should be considered unsigned.", + VerifyResult.UNSIGNED, result); + Assert.assertEquals("No manifest means no signers in the verifier.", + 0, jcv.getCertsList().size()); + } + + @Test + public void testNoSignableEntries() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("OneDirEntry/")); + entries.add(new JarCertVerifierEntry("META-INF/MANIFEST.MF")); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("No signable entry (only dirs/manifests) should be considered unsigned.", + VerifyResult.UNSIGNED, result); + Assert.assertEquals("No signable entry (only dirs/manifests) means no signers in the verifier.", + 0, jcv.getCertsList().size()); + } + + @Test + public void testSingleEntryNoSigners() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstEntryWithoutSigner")); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One unsigned entry should be considered unsigned.", + VerifyResult.UNSIGNED, result); + Assert.assertEquals("One unsigned entry means no signers in the verifier.", + 0, jcv.getCertsList().size()); + } + + @Test + public void testManyEntriesNoSigners() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstEntryWithoutSigner")); + entries.add(new JarCertVerifierEntry("secondEntryWithoutSigner")); + entries.add(new JarCertVerifierEntry("thirdEntryWithoutSigner")); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Many unsigned entries should be considered unsigned.", + VerifyResult.UNSIGNED, result); + Assert.assertEquals("Many unsigned entries means no signers in the verifier.", 0, + jcv.getCertsList().size()); + } + + @Test + public void testSingleEntrySingleValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] signers = { alphaSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByOne", signers)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One signed entry should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("One signed entry means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("One signed entry means one signer in the verifier.", + jcv.getCertsList().contains(alphaSigner.getSignerCertPath())); + } + + @Test + public void testManyEntriesSingleValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] signers = { alphaSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByOne", signers)); + entries.add(new JarCertVerifierEntry("secondSignedByOne", signers)); + entries.add(new JarCertVerifierEntry("thirdSignedByOne", signers)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by one signer should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("Three entries signed by one signer means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed by one signer means one signer in the verifier.", + jcv.getCertsList().contains(alphaSigner.getSignerCertPath())); + } + + @Test + public void testSingleEntryMultipleValidSigners() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] signers = { alphaSigner, betaSigner, charlieSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByThree", signers)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One entry signed by three signers should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("One entry signed by three means three signers in the verifier.", + 3, jcv.getCertsList().size()); + Assert.assertTrue("One entry signed by three means three signers in the verifier.", + jcv.getCertsList().contains(alphaSigner.getSignerCertPath()) + && jcv.getCertsList().contains(betaSigner.getSignerCertPath()) + && jcv.getCertsList().contains(charlieSigner.getSignerCertPath())); + } + + @Test + public void testManyEntriesMultipleValidSigners() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] signers = { alphaSigner, betaSigner, charlieSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByThree", signers)); + entries.add(new JarCertVerifierEntry("secondSignedByThree", signers)); + entries.add(new JarCertVerifierEntry("thirdSignedByThree", signers)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by three signers should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("Three entries signed by three means three signers in the verifier.", + 3, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed by three means three signers in the verifier.", + jcv.getCertsList().contains(alphaSigner.getSignerCertPath()) + && jcv.getCertsList().contains(betaSigner.getSignerCertPath()) + && jcv.getCertsList().contains(charlieSigner.getSignerCertPath())); + } + + @Test + public void testOneCommonSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] alphaSigners = { alphaSigner }; + CodeSigner[] betaSigners = { alphaSigner, betaSigner }; + CodeSigner[] charlieSigners = { alphaSigner, charlieSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByOne", alphaSigners)); + entries.add(new JarCertVerifierEntry("secondSignedByTwo", betaSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByTwo", charlieSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by at least one common signer should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("Three entries signed completely by only one signer means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed completely by only one signer means one signer in the verifier.", + jcv.getCertsList().contains(alphaSigner.getSignerCertPath())); + } + + @Test + public void testNoCommonSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] alphaSigners = { alphaSigner }; + CodeSigner[] betaSigners = { betaSigner }; + CodeSigner[] charlieSigners = { charlieSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByAlpha", alphaSigners)); + entries.add(new JarCertVerifierEntry("secondSignedByBeta", betaSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByCharlie", charlieSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by no common signers should be considered unsigned.", + VerifyResult.UNSIGNED, result); + Assert.assertEquals("Three entries signed by no common signers means no signers in the verifier.", + 0, jcv.getCertsList().size()); + } + + @Test + public void testFewButNotAllCommonSigners() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] alphaSigners = { alphaSigner }; + CodeSigner[] betaSigners = { betaSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByAlpha", alphaSigners)); + entries.add(new JarCertVerifierEntry("secondSignedByAlpha", alphaSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByBeta", betaSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("First two entries signed by alpha signer, third entry signed by beta signer should be considered unisgned.", + VerifyResult.UNSIGNED, result); + Assert.assertEquals("Three entries signed by some common signers but not all means no signers in the verifier.", + 0, jcv.getCertsList().size()); + } + + @Test + public void testNotAllEntriesSigned() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] alphaSigners = { alphaSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByAlpha", alphaSigners)); + entries.add(new JarCertVerifierEntry("secondSignedByAlpha", alphaSigners)); + entries.add(new JarCertVerifierEntry("thirdUnsigned")); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("First two entries signed by alpha signer, third entry not signed, should be considered unisgned.", + VerifyResult.UNSIGNED, result); + Assert.assertEquals("First two entries signed by alpha signer, third entry not signed, means no signers in the verifier.", + 0, jcv.getCertsList().size()); + } + + @Test + public void testSingleEntryExpiredSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] expiredSigners = { expiredSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByExpired", expiredSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One entry signed by expired cert, should be considered signed but not okay.", + VerifyResult.SIGNED_NOT_OK, result); + Assert.assertEquals("One entry signed by expired cert means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("One entry signed by expired cert means one signer in the verifier.", + jcv.getCertsList().contains(expiredSigner.getSignerCertPath())); + } + + @Test + public void testManyEntriesExpiredSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] expiredSigners = { expiredSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByExpired", expiredSigners)); + entries.add(new JarCertVerifierEntry("secondSignedBExpired", expiredSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByExpired", expiredSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by expired cert, should be considered signed but not okay.", + VerifyResult.SIGNED_NOT_OK, result); + Assert.assertEquals("Three entries signed by expired cert means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed by expired cert means one signer in the verifier.", + jcv.getCertsList().contains(expiredSigner.getSignerCertPath())); + } + + @Test + public void testSingleEntryExpiringSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] expiringSigners = { expiringSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByExpiring", expiringSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One entry signed by expiring cert, should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("One entry signed by expiring cert means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("One entry signed by expiring cert means one signer in the verifier.", + jcv.getCertsList().contains(expiringSigner.getSignerCertPath())); + } + + @Test + public void testManyEntriesExpiringSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] expiringSigners = { expiringSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByExpiring", expiringSigners)); + entries.add(new JarCertVerifierEntry("secondSignedBExpiring", expiringSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByExpiring", expiringSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by expiring cert, should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("Three entries signed by expiring cert means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed by expiring cert means one signer in the verifier.", + jcv.getCertsList().contains(expiringSigner.getSignerCertPath())); + } + + @Test + public void testSingleEntryNotYetValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] notYetValidSigners = { notYetValidSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByNotYetValid", notYetValidSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One entry signed by cert that is not yet valid, should be considered signed but not okay.", + VerifyResult.SIGNED_NOT_OK, result); + Assert.assertEquals("One entry signed by cert that is not yet valid means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("One entry signed by cert that is not yet valid means one signer in the verifier.", + jcv.getCertsList().contains(notYetValidSigner.getSignerCertPath())); + } + + @Test + public void testManyEntriesNotYetValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] notYetValidSigners = { notYetValidSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByNotYetValid", notYetValidSigners)); + entries.add(new JarCertVerifierEntry("secondSignedByNotYetValid", notYetValidSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByNotYetValid", notYetValidSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by cert that is not yet valid, should be considered signed but not okay.", + VerifyResult.SIGNED_NOT_OK, result); + Assert.assertEquals("Three entries signed by cert that is not yet valid means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed by cert that is not yet valid means one signer in the verifier.", + jcv.getCertsList().contains(notYetValidSigner.getSignerCertPath())); + } + + @Test + public void testSingleEntryExpiringAndNotYetValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] expiringAndNotYetValidSigners = { expiringAndNotYetValidSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByExpiringNotYetValid", expiringAndNotYetValidSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One entry signed by cert that is not yet valid but also expiring, should be considered signed but not okay.", + VerifyResult.SIGNED_NOT_OK, result); + Assert.assertEquals("One entry signed by cert that is not yet valid but also expiring means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("One entry signed by cert that is not yet valid but also expiring means one signer in the verifier.", + jcv.getCertsList().contains(expiringAndNotYetValidSigner.getSignerCertPath())); + } + + @Test + public void testManyEntryExpiringAndNotYetValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + + CodeSigner[] expiringAndNotYetValidSigners = { expiringAndNotYetValidSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByExpiringNotYetValid", expiringAndNotYetValidSigners)); + entries.add(new JarCertVerifierEntry("secondSignedByExpiringNotYetValid", expiringAndNotYetValidSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByExpiringNotYetValid", expiringAndNotYetValidSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by cert that is not yet valid but also expiring, should be considered signed but not okay.", + VerifyResult.SIGNED_NOT_OK, result); + Assert.assertEquals("Three entries signed by cert that is not yet valid but also expiring means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed by cert that is not yet valid but also expiring means one signer in the verifier.", + jcv.getCertsList().contains(expiringAndNotYetValidSigner.getSignerCertPath())); + Assert.assertTrue("Three entries signed by cert that is not yet valid but also expiring means expiring issue should be in details list.", + jcv.getDetails(expiringAndNotYetValidSigner.getSignerCertPath()).contains(R("SHasExpiringCert"))); + } + + @Test + public void testSingleEntryOneExpiredOneValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] oneExpiredOneValidSigner = { expiredSigner, alphaSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByTwo", oneExpiredOneValidSigner)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("One entry signed by one expired cert and another valid cert, should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("One entry signed by one expired cert and another valid cert means two signers in the verifier.", + 2, jcv.getCertsList().size()); + Assert.assertTrue("One entry signed by one expired cert and another valid cert means two signers in the verifier.", + jcv.getCertsList().contains(expiredSigner.getSignerCertPath()) + && jcv.getCertsList().contains(alphaSigner.getSignerCertPath())); + } + + @Test + public void testManyEntriesOneExpiredOneValidSigner() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] oneExpiredOneValidSigner = { expiredSigner, alphaSigner }; + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByTwo", oneExpiredOneValidSigner)); + entries.add(new JarCertVerifierEntry("secondSignedByTwo", oneExpiredOneValidSigner)); + entries.add(new JarCertVerifierEntry("thirdSignedByTwo", oneExpiredOneValidSigner)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries signed by one expired cert and another valid cert, should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("Three entries signed by one expired cert and another valid cert means two signers in the verifier.", + 2, jcv.getCertsList().size()); + Assert.assertTrue("Three entries signed by one expired cert and another valid cert means two signers in the verifier.", + jcv.getCertsList().contains(expiredSigner.getSignerCertPath()) + && jcv.getCertsList().contains(alphaSigner.getSignerCertPath())); + } + + @Test + public void testSomeExpiredEntries() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] oneExpiredOneValidSigners = { expiredSigner, alphaSigner }; + CodeSigner[] expiredSigners = { expiredSigner }; + + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("firstSignedByTwo", oneExpiredOneValidSigners)); + entries.add(new JarCertVerifierEntry("secondSignedByTwo", oneExpiredOneValidSigners)); + entries.add(new JarCertVerifierEntry("thirdSignedByExpired", expiredSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Two entries signed by one expired and one valid cert, third signed by just expired cert, should be considered signed but not okay.", + VerifyResult.SIGNED_NOT_OK, result); + Assert.assertEquals("Two entries signed by one expired and one valid cert, third signed by just expired cert means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Two entries signed by one expired and one valid cert, third signed by just expired cert means one signer in the verifier.", + jcv.getCertsList().contains(expiredSigner.getSignerCertPath())); + } + + @Test + public void testManyInvalidOneValidStillSignedOkay() throws Exception { + JarCertVerifier jcv = new JarCertVerifier(null); + CodeSigner[] oneExpiredOneValidSigners = { alphaSigner, expiredSigner }; + CodeSigner[] oneNotYetValidOneValidSigners = { alphaSigner, notYetValidSigner }; + CodeSigner[] oneExpiringSigners = { alphaSigner, expiringSigner }; + + Vector entries = new Vector(); + entries.add(new JarCertVerifierEntry("META-INF/MANIFEST.MF")); + entries.add(new JarCertVerifierEntry("firstSigned", oneExpiredOneValidSigners)); + entries.add(new JarCertVerifierEntry("secondSigned", oneNotYetValidOneValidSigners)); + entries.add(new JarCertVerifierEntry("thirdSigned", oneExpiringSigners)); + entries.add(new JarCertVerifierEntry("oneDir/")); + entries.add(new JarCertVerifierEntry("oneDir/fourthSigned", oneExpiredOneValidSigners)); + VerifyResult result = jcv.verifyJarEntryCerts("",true, entries); + + Assert.assertEquals("Three entries sharing valid cert and others with issues, should be considered signed and okay.", + VerifyResult.SIGNED_OK, result); + Assert.assertEquals("Three entries sharing valid cert and others with issues means one signer in the verifier.", + 1, jcv.getCertsList().size()); + Assert.assertTrue("Three entries sharing valid cert and others with issues means one signer in the verifier.", + jcv.getCertsList().contains(alphaSigner.getSignerCertPath())); + } + + // TODO: ADD BAD KEY USAGE CHECKS +} diff --git a/tests/test-extensions/net/sourceforge/jnlp/tools/CodeSignerCreator.java b/tests/test-extensions/net/sourceforge/jnlp/tools/CodeSignerCreator.java new file mode 100644 --- /dev/null +++ b/tests/test-extensions/net/sourceforge/jnlp/tools/CodeSignerCreator.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.sourceforge.jnlp.tools; + +import java.security.CodeSigner; +import java.security.PrivateKey; +import java.security.Timestamp; +import java.security.cert.CertPath; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; + +import sun.security.x509.AlgorithmId; +import sun.security.x509.CertAndKeyGen; +import sun.security.x509.CertificateAlgorithmId; +import sun.security.x509.CertificateIssuerName; +import sun.security.x509.CertificateSerialNumber; +import sun.security.x509.CertificateSubjectName; +import sun.security.x509.CertificateValidity; +import sun.security.x509.CertificateVersion; +import sun.security.x509.X500Name; +import sun.security.x509.X509CertImpl; +import sun.security.x509.X509CertInfo; + +public class CodeSignerCreator { + + /** + * Create an X509 Certificate signed using SHA1withRSA with a 2048 bit key. + * @param dname Domain Name to represent the certificate + * @param notBefore The date by which the certificate starts being valid. Cannot be null. + * @param validity The number of days the certificate is valid after notBefore. + * @return An X509 certificate setup with properties using the specified parameters. + * @throws Exception + */ + public static X509Certificate createCert(String dname, Date notBefore, int validity) + throws Exception { + int keysize = 2048; + String keyAlgName = "RSA"; + String sigAlgName = "SHA1withRSA"; + + if (dname == null) + throw new Exception("Required DN is null. Please specify cert Domain Name via dname"); + if (notBefore == null) + throw new Exception("Required start date is null. Please specify the date at which the cert is valid via notBefore"); + if (validity < 0) + throw new Exception("Required validity is negative. Please specify the number of days for which the cert is valid after the start date."); + + // KeyTool#doGenKeyPair + X500Name x500Name = new X500Name(dname); + + CertAndKeyGen keypair = new CertAndKeyGen(keyAlgName, sigAlgName); + + keypair.generate(keysize); + PrivateKey privKey = keypair.getPrivateKey(); + + X509Certificate oldCert = keypair.getSelfCertificate(x500Name, + notBefore, validity * 24L * 60L * 60L); + + // KeyTool#doSelfCert + byte[] encoded = oldCert.getEncoded(); + X509CertImpl certImpl = new X509CertImpl(encoded); + X509CertInfo certInfo = (X509CertInfo) certImpl.get(X509CertImpl.NAME + + "." + X509CertImpl.INFO); + + Date notAfter = new Date(notBefore.getTime() + validity*1000L*24L*60L*60L); + + CertificateValidity interval = new CertificateValidity(notBefore, + notAfter); + + certInfo.set(X509CertInfo.VALIDITY, interval); + certInfo.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber( + new java.util.Random().nextInt() & 0x7fffffff)); + certInfo.set(X509CertInfo.SUBJECT + "." + CertificateSubjectName.DN_NAME, x500Name); + certInfo.set(X509CertInfo.ISSUER + "." + CertificateIssuerName.DN_NAME, x500Name); + + // The inner and outer signature algorithms have to match. + // The way we achieve that is really ugly, but there seems to be no + // other solution: We first sign the cert, then retrieve the + // outer sigalg and use it to set the inner sigalg + X509CertImpl newCert = new X509CertImpl(certInfo); + newCert.sign(privKey, sigAlgName); + AlgorithmId sigAlgid = (AlgorithmId)newCert.get(X509CertImpl.SIG_ALG); + certInfo.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, sigAlgid); + + certInfo.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); + + // FIXME Figure out extensions +// CertificateExtensions ext = createV3Extensions( +// null, +// (CertificateExtensions)certInfo.get(X509CertInfo.EXTENSIONS), +// v3ext, +// oldCert.getPublicKey(), +// null); +// certInfo.set(X509CertInfo.EXTENSIONS, ext); + + newCert = new X509CertImpl(certInfo); + newCert.sign(privKey, sigAlgName); + + return newCert; + } + + /** + * Create a new code signer with the specified information. + * @param domainName Domain Name to represent the certificate + * @param notBefore The date by which the certificate starts being valid. Cannot be null. + * @param validity The number of days the certificate is valid after notBefore. + * @return A code signer with the properties passed through its parameters. + */ + public static CodeSigner getOneCodeSigner(String domainName, Date notBefore, int validity) + throws Exception { + X509Certificate jarEntryCert = createCert(domainName, notBefore, validity); + + ArrayList certs = new ArrayList(1); + certs.add(jarEntryCert); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(certs); + Timestamp certTimestamp = new Timestamp(jarEntryCert.getNotBefore(), certPath); + return new CodeSigner(certPath, certTimestamp); + } +}