/* * 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 static net.sourceforge.jnlp.runtime.Translator.R; import java.io.*; import java.util.*; import java.util.jar.*; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.security.cert.CertPath; import java.security.*; import sun.security.x509.*; import sun.security.util.*; import net.sourceforge.jnlp.*; import net.sourceforge.jnlp.cache.*; import net.sourceforge.jnlp.security.*; /** *

The jarsigner utility. * * @author Roland Schemers * @author Jan Luehe */ public class JarSigner 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 } // signer's certificate chain (when composing) X509Certificate[] certChain; boolean verbose = false; // verbose output when signing/verifying boolean showcerts = false; // show certs when verifying private boolean hasExpiredCert = false; private boolean hasExpiringCert = false; private boolean notYetValidCert = false; private boolean badKeyUsage = false; private boolean badExtendedKeyUsage = false; private boolean badNetscapeCertType = false; private boolean alreadyTrustPublisher = false; private boolean rootInCacerts = false; /** * 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. */ 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; /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#getAlreadyTrustPublisher() */ public boolean getAlreadyTrustPublisher() { return alreadyTrustPublisher; } /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#getRootInCacerts() */ public boolean getRootInCacerts() { return rootInCacerts; } public CertPath getCertPath() { return certPath; } /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#hasSigningIssues() */ public boolean hasSigningIssues() { return hasExpiredCert || notYetValidCert || badKeyUsage || badExtendedKeyUsage || badNetscapeCertType; } /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#noSigningIssues() */ public boolean noSigningIssues() { return noSigningIssues; } public boolean anyJarsSigned() { return anyJarsSigned; } /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#getDetails() */ public ArrayList getDetails() { return details; } /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#getCerts() */ public ArrayList getCerts() { 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 */ public boolean isFullySignedByASingleCert() { for (CertPath cPath : certs.keySet()) { // If this cert has signed everything, return true if (certs.get(cPath) == totalSignableEntries) return true; } // No cert found that signed all entries. Return false. return false; } public void verifyJars(List jars, ResourceTracker tracker) throws Exception { verifiedJars = new ArrayList(); unverifiedJars = new ArrayList(); for (int i = 0; i < jars.size(); i++) { JARDesc jar = jars.get(i); 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(); verifyResult result = verifyJar(localFile); if (result == verifyResult.UNSIGNED) { unverifiedJars.add(localFile); } else if (result == verifyResult.SIGNED_NOT_OK) { noSigningIssues = false; 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; } } //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; } } private verifyResult verifyJar(String jarName) throws Exception { boolean anySigned = false; boolean hasUnsignedEntry = false; JarFile jarFile = null; try { jarFile = new JarFile(jarName, true); Vector entriesVec = new Vector(); byte[] buffer = new byte[8192]; JarEntry je; Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { je = entries.nextElement(); entriesVec.addElement(je); InputStream is = jarFile.getInputStream(je); try { int n; while ((n = 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(); } } } if (jarFile.getManifest() != null) { if (verbose) System.out.println(); Enumeration e = entriesVec.elements(); long now = System.currentTimeMillis(); while (e.hasMoreElements()) { je = e.nextElement(); String name = je.getName(); CodeSigner[] signers = je.getCodeSigners(); boolean isSigned = (signers != null); anySigned |= isSigned; boolean shouldHaveSignature = !je.isDirectory() && !signatureRelated(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; } finally { // close the resource if (jarFile != null) { 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. */ 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; } } } catch (Exception e) { // TODO: Warn user about not being able to // look through their cacerts/trusted.certs // file depending on exception. throw e; } if (!rootInCacerts) addToDetails(R("SUntrustedCertificate")); else addToDetails(R("STrustedCertificate")); } } /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#getPublisher() */ public Certificate getPublisher() { if (certPath != null) { List certList = certPath.getCertificates(); if (certList.size() > 0) { return certList.get(0); } else { return null; } } else { return null; } } /* (non-Javadoc) * @see net.sourceforge.jnlp.tools.CertVerifier2#getRoot() */ public Certificate getRoot() { if (certPath != null) { List certList = certPath.getCertificates(); if (certList.size() > 0) { return certList.get(certList.size() - 1); } else { return null; } } else { return null; } } private void addToDetails(String detail) { if (!details.contains(detail)) details.add(detail); } /** * signature-related files include: * . META-INF/MANIFEST.MF * . META-INF/SIG-* * . META-INF/*.SF * . META-INF/*.DSA * . META-INF/*.RSA * * Required for verifyJar() */ private boolean signatureRelated(String name) { String ucName = name.toUpperCase(); if (ucName.equals(JarFile.MANIFEST_NAME) || ucName.equals(META_INF) || (ucName.startsWith(SIG_PREFIX) && ucName.indexOf("/") == ucName.lastIndexOf("/"))) { return true; } if (ucName.startsWith(META_INF) && SignatureFileVerifier.isBlockOrSF(ucName)) { // .SF/.DSA/.RSA files in META-INF subdirs // are not considered signature-related return (ucName.indexOf("/") == ucName.lastIndexOf("/")); } return false; } /** * 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(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 { badKeyUsage = true; } } } 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 { badExtendedKeyUsage = true; } } } } 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 { badNetscapeCertType = true; } } } } 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; } }