/* * 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.zip.*; import java.util.jar.*; import java.text.Collator; import java.text.MessageFormat; 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 Collator collator = Collator.getInstance(); static { // this is for case insensitive string comparisions collator.setStrength(Collator.PRIMARY); } 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 final String VERSION = "1.0"; static final int IN_KEYSTORE = 0x01; static final int IN_SCOPE = 0x02; static enum verifyResult { UNSIGNED, SIGNED_OK, SIGNED_NOT_OK } // signer's certificate chain (when composing) X509Certificate[] certChain; /* * private key */ PrivateKey privateKey; KeyStore store; String keystore; // key store file boolean nullStream = false; // null keystore input stream (NONE) boolean token = false; // token-based keystore String jarfile; // jar file to sign String alias; // alias to sign jar with char[] storepass; // keystore password boolean protectedPath; // protected authentication path String storetype; // keystore type String providerName; // provider name Vector providers = null; // list of providers HashMap providerArgs = new HashMap(); // arguments for provider constructors char[] keypass; // private key password String sigfile; // name of .SF file String sigalg; // name of signature algorithm String digestalg = "SHA1"; // name of digest algorithm String signedjar; // output filename String tsaUrl; // location of the Timestamping Authority String tsaAlias; // alias for the Timestamping Authority's certificate boolean verify = false; // verify the jar boolean verbose = false; // verbose output when signing/verifying boolean showcerts = false; // show certs when verifying boolean debug = false; // debug boolean signManifest = true; // "sign" the whole manifest boolean externalSF = true; // leave the .SF out of the PKCS7 block 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 ArrayList certs = null; /** details of this signing */ private ArrayList details = new ArrayList(); /* (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 certs; } public void verifyJars(List jars, ResourceTracker tracker) throws Exception { certs = new ArrayList(); for (int i = 0; i < jars.size(); i++) { JARDesc jar = jars.get(i); verifiedJars = new ArrayList(); unverifiedJars = new ArrayList(); 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) { return; } 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; } } } public verifyResult verifyJar(String jarName) throws Exception { boolean anySigned = false; boolean hasUnsignedEntry = false; JarFile jarFile = null; // certs could be uninitialized if one calls this method directly if (certs == null) certs = new ArrayList(); 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; hasUnsignedEntry |= !je.isDirectory() && !isSigned && !signatureRelated(name); if (isSigned) { // TODO: Perhaps we should check here that // signers.length is only of size 1, and throw an // exception if it's not? for (int i = 0; i < signers.length; i++) { CertPath certPath = signers[i].getSignerCertPath(); if (!certs.contains(certPath)) certs.add(certPath); //we really only want the first certPath if (!certPath.equals(this.certPath)) { this.certPath = certPath; } Certificate cert = signers[i].getSignerCertPath() .getCertificates().get(0); if (cert instanceof X509Certificate) { checkCertUsage((X509Certificate) cert, null); if (!showcerts) { long notAfter = ((X509Certificate) cert) .getNotAfter().getTime(); if (notAfter < now) { hasExpiredCert = true; } else if (notAfter < now + SIX_MONTHS) { hasExpiringCert = true; } } } } } } //while e has more elements } //if man not null //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(); } } // check if the certs added above are in the trusted path checkTrustedCerts(); //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(); rootInCacerts = CertificateUtils.inKeyStores(root, caKeyStores); } 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 (Certificate) 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 (Certificate) certList.get( certList.size() - 1); } else { return null; } } else { return null; } } private void addToDetails(String detail) { if (!details.contains(detail)) details.add(detail); } Hashtable storeHash = new Hashtable(); /** * 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; } }