/* * Copyright 1997-2007 Sun Microsystems, Inc. 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. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. */ package net.sourceforge.jnlp.tools; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.security.CodeSigner; import java.security.KeyStore; import java.security.cert.CertPath; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Vector; import java.util.jar.JarEntry; import net.sourceforge.jnlp.util.JarFile; import net.sourceforge.jnlp.JARDesc; import net.sourceforge.jnlp.JNLPFile; import net.sourceforge.jnlp.LaunchException; import net.sourceforge.jnlp.cache.ResourceTracker; import net.sourceforge.jnlp.runtime.JNLPRuntime; import net.sourceforge.jnlp.security.AppVerifier; import net.sourceforge.jnlp.security.CertVerifier; import net.sourceforge.jnlp.security.CertificateUtils; import net.sourceforge.jnlp.security.KeyStores; import sun.security.util.DerInputStream; import sun.security.util.DerValue; import sun.security.x509.NetscapeCertTypeExtension; /** *

* The jar certificate verifier utility. * * @author Roland Schemers * @author Jan Luehe */ public class JarCertVerifier implements CertVerifier { private static final String META_INF = "META-INF/"; // 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 static enum VerifyResult { UNSIGNED, SIGNED_OK, SIGNED_NOT_OK } /** All of the jar files that were verified for signing */ private List verifiedJars = new ArrayList(); /** All of the jar files that were not verified */ private List unverifiedJars = new ArrayList(); /** The certificates used for jar verification linked to their respective information */ private Map certs = new HashMap(); /** Temporary cert path hack to be used to keep track of which one a UI dialog is using */ private CertPath currentlyUsed; /** Absolute location to jars and the number of entries which are possibly signable */ private Map jarSignableEntries = new HashMap(); /** The application verifier to use by this instance */ private AppVerifier appVerifier; /** * 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. */ public JarCertVerifier(AppVerifier verifier) { appVerifier = verifier; } /** * Return true if there are no signable entries in the jar. * This will return false if any of verified jars have content more than just META-INF/. */ public boolean isTriviallySigned() { return getTotalJarEntries(jarSignableEntries) <= 0 && certs.size() <= 0; } public boolean getAlreadyTrustPublisher() { boolean allPublishersTrusted = appVerifier.hasAlreadyTrustedPublisher( certs, jarSignableEntries); if (JNLPRuntime.isDebug()) { System.out.println("App already has trusted publisher: " + allPublishersTrusted); } return allPublishersTrusted; } public boolean getRootInCacerts() { boolean allRootCAsTrusted = appVerifier.hasRootInCacerts(certs, jarSignableEntries); if (JNLPRuntime.isDebug()) { System.out.println("App has trusted root CA: " + allRootCAsTrusted); } return allRootCAsTrusted; } public CertPath getCertPath(CertPath cPath) { // Parameter ignored. return currentlyUsed; } public boolean hasSigningIssues(CertPath certPath) { return certs.get(certPath).hasSigningIssues(); } public List getDetails(CertPath certPath) { if (certPath != null) { currentlyUsed = certPath; } return certs.get(currentlyUsed).getDetailsAsStrings(); } /** * Get a list of the cert paths of all signers across the app. * * @return List of CertPath vars representing each of the signers present on any jar. */ public List getCertsList() { return new ArrayList(certs.keySet()); } /** * Find the information the specified cert path has with respect to this application. * * @return All the information the path has with this app. */ 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 (isTriviallySigned()) return true; boolean fullySigned = appVerifier.isFullySigned(certs, jarSignableEntries); if (JNLPRuntime.isDebug()) { System.out.println("App already has trusted publisher: " + fullySigned); } return fullySigned; } /** * 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); } /** * 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) { try { File jarFile = tracker.getCacheFile(jar.getLocation()); // some sort of resource download/cache error. Nothing to add // in that case ... but don't fail here if (jarFile == null) { continue; } String localFile = jarFile.getAbsolutePath(); if (verifiedJars.contains(localFile) || unverifiedJars.contains(localFile)) { continue; } VerifyResult result = verifyJar(localFile); if (result == VerifyResult.UNSIGNED) { unverifiedJars.add(localFile); } else if (result == VerifyResult.SIGNED_NOT_OK) { verifiedJars.add(localFile); } else if (result == VerifyResult.SIGNED_OK) { verifiedJars.add(localFile); } } catch (Exception e) { // We may catch exceptions from using verifyJar() // or from checkTrustedCerts throw e; } } for (CertPath certPath : certs.keySet()) checkTrustedCerts(certPath); } /** * 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 { jarFile = new JarFile(jarName, true); Vector entriesVec = new Vector(); byte[] buffer = new byte[8192]; Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry je = entries.nextElement(); entriesVec.addElement(je); InputStream is = jarFile.getInputStream(je); try { while (is.read(buffer, 0, buffer.length) != -1) { // we just read. this will throw a SecurityException // if a signature/digest check fails. } } finally { if (is != null) { is.close(); } } } return verifyJarEntryCerts(jarName, jarFile.getManifest() != null, entriesVec); } catch (Exception e) { e.printStackTrace(); throw e; } finally { // close the resource if (jarFile != null) { jarFile.close(); } } } /** * 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. */ VerifyResult verifyJarEntryCerts(String jarName, boolean jarHasManifest, Vector entries) throws Exception { // Contains number of entries the cert with this CertPath has signed. Map 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); } } } // 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(); } } } } // 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 (numSignableEntriesInJar == 0) { // Allow jars with no signable entries to simply be considered signed. // There should be no security risk in doing so. result = VerifyResult.SIGNED_OK; } else 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; } /** * 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. */ 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 { return null; } } else { return null; } } 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 { return null; } } else { return null; } } /** * 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 */ static boolean isMetaInfFile(String name) { String ucName = name.toUpperCase(); return ucName.startsWith(META_INF); } /** * 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() */ void checkCertUsage(CertPath certPath, X509Certificate userCert, boolean[] bad) { // Can act as a signer? // 1. if KeyUsage, then [0] should be true // 2. if ExtendedKeyUsage, then should contains ANY or CODE_SIGNING // 3. if NetscapeCertType, then should contains OBJECT_SIGNING // 1,2,3 must be true if (bad != null) { bad[0] = bad[1] = bad[2] = false; } boolean[] keyUsage = userCert.getKeyUsage(); if (keyUsage != null) { if (keyUsage.length < 1 || !keyUsage[0]) { if (bad != null) { bad[0] = true; } else { certs.get(certPath).setBadKeyUsage(); } } } try { List xKeyUsage = userCert.getExtendedKeyUsage(); if (xKeyUsage != null) { if (!xKeyUsage.contains("2.5.29.37.0") // anyExtendedKeyUsage && !xKeyUsage.contains("1.3.6.1.5.5.7.3.3")) { // codeSigning if (bad != null) { bad[1] = true; } else { certs.get(certPath).setBadExtendedKeyUsage(); } } } } catch (java.security.cert.CertificateParsingException e) { // shouldn't happen } try { // OID_NETSCAPE_CERT_TYPE 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); Boolean val = (Boolean) extn .get(NetscapeCertTypeExtension.OBJECT_SIGNING); if (!val) { if (bad != null) { bad[2] = true; } else { certs.get(certPath).setBadNetscapeCertType(); } } } } catch (IOException e) { // } } /** * 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 Collections.unmodifiableMap(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; } }